Compare commits
269 Commits
Author | SHA1 | Date | |
---|---|---|---|
7a0718d9cc | |||
a7cc3c7fff | |||
9d313f319d | |||
856044c88c | |||
de1b13a50f | |||
a981b7b8c7 | |||
abe9d82d03 | |||
49a3f08a9a | |||
afaa75fcdc | |||
a5c749412a | |||
e64da40355 | |||
05e7283f9f | |||
bea8e230fb | |||
23c1ece807 | |||
539d340f1a | |||
876486bbf5 | |||
9179851c85 | |||
1e96d7e787 | |||
612478bcc7 | |||
23f84fbde0 | |||
b7b4c57034 | |||
872b058d4c | |||
93ad3e71db | |||
7d824bf61e | |||
06228d9ec9 | |||
f9232ffd96 | |||
ab3c335bf1 | |||
9142cc023f | |||
3133a8d8ad | |||
312f39f7f6 | |||
4ba7bb530d | |||
9caad6b3a5 | |||
5e323db2f0 | |||
18daa28cea | |||
65c358c01b | |||
2d14432efe | |||
19b1cc8856 | |||
883f9a6f0b | |||
e969ba237a | |||
207248e706 | |||
9925dec0f4 | |||
09b2b4be10 | |||
f331de4134 | |||
7760072e8e | |||
626bf592de | |||
d550de0e27 | |||
367efb4b53 | |||
4a82c62958 | |||
891aa66bff | |||
2513b60c1b | |||
31cca481f9 | |||
31999bba9f | |||
73d90662e3 | |||
5f5deb09f3 | |||
b68245f4f0 | |||
34c8537cb5 | |||
6b7c9d461d | |||
d2f09ca592 | |||
919c6a8f10 | |||
dcd7e7cd0e | |||
c545bbab1d | |||
dc6e64e690 | |||
578762db27 | |||
e996a2b41a | |||
38496d9161 | |||
4a0756aa26 | |||
63d0ff22fa | |||
5390642978 | |||
2e59dedda3 | |||
ee649717a2 | |||
078ce277ce | |||
7a3cad83b0 | |||
4d953c373f | |||
da9832552f | |||
5909633a04 | |||
750fd987bd | |||
b87e0d6138 | |||
31cb23923a | |||
71d98bc603 | |||
4b09a0db67 | |||
da6c6bcf68 | |||
84fea3ba35 | |||
33de1cbce9 | |||
04b8a981ef | |||
58c7955c64 | |||
dcf9603da8 | |||
c2b9664b4b | |||
e760d2b022 | |||
1a836d914b | |||
1b0e6eb6c4 | |||
39827ad485 | |||
79c4d4e98f | |||
427bd2f348 | |||
4fefd0e128 | |||
d79060237d | |||
2df7a1c592 | |||
8c4ceb3e4b | |||
bf725ece72 | |||
76992bd4f4 | |||
dccd530475 | |||
f1fa01a0eb | |||
7949142ef6 | |||
af1da8fc0a | |||
39f4237cde | |||
b676ed74cd | |||
bf474877ca | |||
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**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
@ -23,16 +28,6 @@ A clear and concise description of what you expected to happen.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
25
LICENSE.md
@ -30,7 +30,7 @@ THE SOFTWARE.
|
||||
## DownThemAll! uikit
|
||||
|
||||
Copyright © 2016-2019 by Nils Maier
|
||||
The uikit libraries and assets are licened under the MIT license.
|
||||
The uikit libraries and assets are licensed under the MIT license.
|
||||
|
||||
## DownThemAll! interface (.html, .css)
|
||||
|
||||
@ -41,7 +41,8 @@ Licensed under GPL2.0; see [LICENSE.gpl-2.0.txt](LICENSE.gpl-2.0.txt).
|
||||
## DownThemAll! icons, icon-font and graphic assets
|
||||
|
||||
Copyright (C) 2012-2019 by Nils Maier
|
||||
Licensed under Creative Commons Attribution-ShareAlike 4.0 International
|
||||
Licensed under Creative Commons Attribution-ShareAlike 4.0 International.
|
||||
|
||||
The icon font contains icons from Font Awesome.
|
||||
|
||||
See: https://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
@ -54,21 +55,35 @@ Copyright © 2010-2019 by Nils Maier, Stefano Verna.
|
||||
The DownThemAll! name and logo cannot be used without explicit permission
|
||||
in any derivative work, except in credits and license-related notices.
|
||||
|
||||
Using the DownThemAll! logo in personal non-distributed non-commerical
|
||||
Using the DownThemAll! logo in personal non-distributed non-commercial
|
||||
modifications of the software and forks is permitted without explicit
|
||||
permission.
|
||||
|
||||
Distributing official DownThemAll! releases without any modifications is allowed without explicit permission.
|
||||
|
||||
## Font Awesome
|
||||
|
||||
Copyright (C) 2016 by Dave Gandy
|
||||
|
||||
License: SIL ()
|
||||
|
||||
Homepage: http://fortawesome.github.com/Font-Awesome/
|
||||
|
||||
## webextension-polyfill
|
||||
|
||||
Lcensed under the Mozilla Public License 2.0.
|
||||
Licensed under the Mozilla Public License 2.0.
|
||||
|
||||
## PSL (public-suffix-list)
|
||||
|
||||
The list itself is licensed under the Mozilla Public License 2.0.
|
||||
The javascript library accessing it is licensed under the MIT license.
|
||||
The javascript library accessing it is licensed under the MIT license.
|
||||
|
||||
## whatwg-mimetype
|
||||
|
||||
Licensed under MIT
|
||||
|
||||
|
||||
## CDHeaderParser
|
||||
|
||||
Licensed under MPL-2
|
||||
(c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
|
||||
|
55
Readme.md
@ -1,12 +1,14 @@
|
||||
DownThemAll! WE
|
||||
===
|
||||
|
||||

|
||||
|
||||
|
||||
# DownThemAll! WE
|
||||
|
||||
The DownThemAll! WebExtension.
|
||||
|
||||
For those still on supported browser: [Non-WebExtension legacy code](https://github.com/downthemall/downthemall-legacy).
|
||||
|
||||
About
|
||||
---
|
||||
## About
|
||||
|
||||
This is the WebExtension version of DownThemAll!, a complete re-development from scratch.
|
||||
Being a WebExtension it lacks a ton of features the original DownThemAll! had. Sorry, but there is no way around it since Mozilla decided to adopt WebExtensions as the *only* extension type and WebExtensions are extremely limited in what they can do.
|
||||
@ -23,19 +25,48 @@ But it is what it is...
|
||||
|
||||
**What we *can* do and did do is bring the mass selection, organizing (renaming masks, etc) and queueing tools of DownThemAll! over to the WebExtension, so you can easily queue up hundreds or thousands files at once without the downloads going up in flames because the browser tried to download them all at once.**
|
||||
|
||||
## Translations
|
||||
|
||||
If you would like to help out translating DTA, please see our [translation guide](_locales/Readme.md).
|
||||
|
||||
## Development
|
||||
|
||||
Development
|
||||
---
|
||||
|
||||
You will want to `yarn` the development dependencies such as webpack first.
|
||||
|
||||
Afterwards there is two important commands to run
|
||||
Afterwards, you will want to run`yarn watch`.
|
||||
This will run the webpack bundler in watch mode, transpiling the TypeScript to Javascript and updating bundles as you change the source.
|
||||
|
||||
* `yarn watch` - This will run the webpack bundler in watch mode, updating bundles as you change the source.
|
||||
* `yarn webext` - This will run the WebExtension in a development profile using the [`web-ext` tool from mozilla](https://www.npmjs.com/package/web-ext) (which you need to install separately).
|
||||
Please note: You have to run `yarn watch` (at least once) as it builds the actual script bundles.
|
||||
|
||||
### Firefox
|
||||
|
||||
I recommend you install the [`web-ext`](https://www.npmjs.com/package/web-ext) tools from mozilla. It is not listed as a dependency by design at it causes problems with dependency resolution in yarn right now if installed in the same location as the rest of the dependencies.
|
||||
|
||||
If you did, then running `yarn webext` (additionally to `yarn watch`) will run the WebExtension in a development profile. This will use the directory `../dtalite.p` to keep a development profile. You might need to create this directory before you use this command. Furthermore `yarn webext` will watch for changes to the sources and automatically reload the extension.
|
||||
|
||||
Alternative, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox).
|
||||
Alternative, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox with the right about:config preferences).
|
||||
|
||||
Before submitting patches, please make sure you run eslint, if this isn't done automatically, and eslint does not report any open issues. Code contributions should favor typescript code over javascript code. External dependencies that would ship with the final product (including all npm/yarn packages) should be kept to a bare minimum.
|
||||
### Chrome
|
||||
|
||||
The code base is comparatively large for a WebExtension, with over 10K sloc of typescript and over 14K sloc total.
|
||||
You have to build the bundles first, of course.
|
||||
|
||||
Then put your Chrome into Developement Mode on the Extensions page, and Load Unpacked the directory of your downthemall clone.
|
||||
|
||||
### Patches
|
||||
|
||||
Before submitting patches, please make sure you run eslint (if this isn't done automatically in your text editor/IDE), and eslint does not report any open issues. Code contributions should favor typescript code over javascript code. External dependencies that would ship with the final product (including all npm/yarn packages) should be kept to a bare minimum and need justification.
|
||||
|
||||
Please submit your patches as Pull Requests, and rebase your commits onto the current `master` before submitting.
|
||||
|
||||
### Code structure
|
||||
|
||||
The code base is comparatively large for a WebExtension, with over 11K sloc of typescript.
|
||||
It isn't as well organized as it should be in some places; hope you don't mind.
|
||||
|
||||
* `uikit/` - The base User Interface Kit, which currently consists of
|
||||
* the `VirtualTable` implementation, aka that interactive HTML table with columns, columns resizing and hiding, etc you see in the Manager, Select and Preferences windows/tabs
|
||||
* the `ContextMenu` and related classes that drive the HTML-based context menus
|
||||
* `lib/` - The "backend stuff" and assorted library routines and classes.
|
||||
* `windows/` - The "frontend stuff" so all the HTML and corresponding code to make that HTML into something interactive
|
||||
* `style/` - CSS and images
|
||||
|
28
TODO.md
@ -1,33 +1,14 @@
|
||||
TODO
|
||||
---
|
||||
|
||||
aka a lot
|
||||
|
||||
P1
|
||||
===
|
||||
|
||||
Musts.
|
||||
|
||||
* packaging
|
||||
* signing
|
||||
|
||||
P2
|
||||
===
|
||||
|
||||
Planned for later.
|
||||
|
||||
* Investigate using an action popup for the browser action
|
||||
* Soft errors and retry logic
|
||||
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
|
||||
* Delete files (well, as far as the browser allows)
|
||||
* Inter-addon API (basic)
|
||||
* Add downloads
|
||||
* Chrome support
|
||||
* vtable perf: cache column widths
|
||||
* Localizations
|
||||
* Settle on system
|
||||
* Do the de-locale
|
||||
* Enagage translators
|
||||
* Download options
|
||||
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
||||
|
||||
@ -43,12 +24,7 @@ Nice-to-haves.
|
||||
* Manipulate downloads (e.g. rewrite URLs)
|
||||
* Native context menus?
|
||||
* Would require massive reworks incl the need for new icon formats, but potentially feasible.
|
||||
* Import/Export
|
||||
* Download priorities (manual scheduling overrides)
|
||||
* Dark Theme support
|
||||
* os/browser define be default
|
||||
* overwritable
|
||||
* Get and cache system icons (because Firefox doesn't allow moz-icon: for WE, but makes them kinda accessible through the downloads API anyway, essentially copying them via a canvas on a privileged hidden page into a data URL... ikr)
|
||||
* Remove `any` types as possible, and generally improve typescript (new language to me)
|
||||
|
||||
P4
|
||||
@ -60,14 +36,10 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
|
||||
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
|
||||
* Segmented downloads
|
||||
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
|
||||
* Handle errors, 404 and such
|
||||
* The Firefox download manager is too stupid and webRequest does not see Downloads, so cannot be done right now.
|
||||
* Conflicts: ask when a file exists
|
||||
* Not supported by Firefox
|
||||
* Speed limiter
|
||||
* 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?
|
||||
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
|
||||
* 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.
|
24
_locales/all.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"ar": "العربية [ar]",
|
||||
"bg": "Български [bg]",
|
||||
"cs": "Čeština (CZ) [cs]",
|
||||
"da": "Dansk [da]",
|
||||
"de": "Deutsch [de]",
|
||||
"el": "Ελληνικά [el]",
|
||||
"en": "English (US) [en]",
|
||||
"es": "Español (España) [es]",
|
||||
"et": "Eesti keel [et]",
|
||||
"fr": "Français [fr]",
|
||||
"hu": "Magyar (HU) [hu]",
|
||||
"id": "Bahasa Indonesia [id]",
|
||||
"it": "Italiano [it]",
|
||||
"ja": "日本語 (JP) [ja]",
|
||||
"ko": "한국어 [ko]",
|
||||
"lt": "Lietuvių [lt]",
|
||||
"nl": "Nederlands [nl]",
|
||||
"pl": "Polski [pl]",
|
||||
"pt": "Português (Brasil) [pt]",
|
||||
"ru": "Русский [ru]",
|
||||
"zh_CN": "简体中文 [zh_CN]",
|
||||
"zh_TW": "正體中文 [zh_TW]"
|
||||
}
|
1300
_locales/ar/messages.json
Normal file
1300
_locales/bg/messages.json
Normal file
1170
_locales/cs/messages.json
Normal file
1284
_locales/da/messages.json
Normal file
1300
_locales/de/messages.json
Normal file
1264
_locales/el/messages.json
Normal file
1300
_locales/es/messages.json
Normal file
1300
_locales/et/messages.json
Normal file
1300
_locales/fr/messages.json
Normal file
1300
_locales/hu/messages.json
Normal file
1136
_locales/id/messages.json
Normal file
1284
_locales/it/messages.json
Normal file
1300
_locales/ja/messages.json
Normal file
1284
_locales/ko/messages.json
Normal file
1300
_locales/lt/messages.json
Normal file
1300
_locales/nl/messages.json
Normal file
1300
_locales/pl/messages.json
Executable file
1300
_locales/pt/messages.json
Normal file
1284
_locales/ru/messages.json
Executable file
1284
_locales/zh_CN/messages.json
Normal file
1300
_locales/zh_TW/messages.json
Normal file
@ -14,7 +14,7 @@
|
||||
},
|
||||
"deffilter-aud": {
|
||||
"label": "Audio",
|
||||
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape)$/i",
|
||||
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape|opus)$/i",
|
||||
"type": 1,
|
||||
"active": false,
|
||||
"icon": "mp3"
|
||||
@ -35,7 +35,7 @@
|
||||
},
|
||||
"deffilter-img": {
|
||||
"label": "Images",
|
||||
"expr": "/\\.(?:jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico)$/i",
|
||||
"expr": "/\\.(?:jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico|heic|heif|webp|jxr|wdp|dng|cr2|arw)$/i",
|
||||
"type": 3,
|
||||
"active": false,
|
||||
"icon": "jpg"
|
||||
@ -63,7 +63,7 @@
|
||||
},
|
||||
"deffilter-vid": {
|
||||
"label": "Videos",
|
||||
"expr": "/\\.(?:mpeg|ra?m|avi|mp(?:g|e|4)|mov|divx|asf|qt|wmv|m\\dv|rv|vob|asx|ogm|ogv|webm|flv|mkv)$/i",
|
||||
"expr": "/\\.(?:mpeg|ra?m|avi|mp(?:g|e|4)|mov|divx|asf|qt|wmv|m\\dv|rv|vob|asx|ogm|ogv|webm|flv|mkv|f4v|m4v)$/i",
|
||||
"type": 3,
|
||||
"active": true,
|
||||
"icon": "mkv"
|
||||
|
@ -80,7 +80,11 @@
|
||||
"tif",
|
||||
"tiff",
|
||||
"wmf",
|
||||
"webp"
|
||||
"webp",
|
||||
"heic",
|
||||
"heif",
|
||||
"jxr",
|
||||
"wdp"
|
||||
],
|
||||
"video": [
|
||||
"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"
|
||||
}
|
||||
}
|
@ -1,17 +1,23 @@
|
||||
{
|
||||
"global-turbo": false,
|
||||
"button-type": "popup",
|
||||
"manager-in-popup": false,
|
||||
"concurrent": 4,
|
||||
"queue-notification": true,
|
||||
"finish-notification": true,
|
||||
"sounds": true,
|
||||
"open-manager-on-queue": true,
|
||||
"text-links": true,
|
||||
"add-paused": false,
|
||||
"hide-context": false,
|
||||
"conflict-action": "uniquify",
|
||||
"nagging": 0,
|
||||
"nagging-next": 6,
|
||||
"nagging-next": 7,
|
||||
"tooltip": true,
|
||||
"show-urls": false,
|
||||
"remove-missing-on-init": false,
|
||||
"retries": 5,
|
||||
"retry-time": 10,
|
||||
"theme": "default",
|
||||
"limits": [
|
||||
{
|
||||
"domain": "*",
|
||||
|
476
docs/changelog/index.html
Normal file
@ -0,0 +1,476 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!-- License: Creative Commons Attribution-ShareAlike 4.0 International License -->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DownThemAll! Changes</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Reenie Beanie";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: local("Reenie Beanie"), local("ReenieBeanie"),
|
||||
url(res/ReenieBeanie-Regular.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
:root {
|
||||
--content-width: 1000px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Ubuntu",
|
||||
"Helvetica Neue", sans-serif;
|
||||
font-size: 12pt;
|
||||
color: rgb(20, 20, 20);
|
||||
background: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
background: white repeat-x url(res/background-tile.png);
|
||||
grid-template-columns: 1fr [content] auto 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
". header ."
|
||||
". content ."
|
||||
"footer footer footer";
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(30, 30, 70);
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
nav {
|
||||
max-width: var(--content-width);
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding-right: 120px;
|
||||
padding-top: 1em;
|
||||
padding-left: 32px;
|
||||
align-content: center;
|
||||
align-items: baseline;
|
||||
background: no-repeat top right url(res/halo.svg);
|
||||
background-size: 120px;
|
||||
min-height: 100px;
|
||||
text-shadow: rgba(255, 255, 255, 0.8) 1px 3px 0px;
|
||||
font-family: "Reenie Beanie", cursive;
|
||||
}
|
||||
|
||||
nav h1,
|
||||
nav h2 {
|
||||
margin: 0;
|
||||
margin-right: 1ex;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
font-size: 400%;
|
||||
}
|
||||
|
||||
nav h2 {
|
||||
font-size: 250%;
|
||||
}
|
||||
|
||||
article {
|
||||
max-width: var(--content-width);
|
||||
grid-area: content;
|
||||
box-sizing: border-box;
|
||||
padding: 1ex 32px;
|
||||
background: white;
|
||||
border-bottom: 0;
|
||||
border-radius: 20px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
h3 em {
|
||||
display: inline-block;
|
||||
padding-left: 1em;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
#paypal {
|
||||
margin: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto auto;
|
||||
grid-row-gap: 1em;
|
||||
grid-column-gap: 1em;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-column-gap: 2em;
|
||||
padding: 2em 2em 1em 2em;
|
||||
background: transparent center top repeat-x url(res/footerbg.png);
|
||||
font-size: x-small;
|
||||
color: gray;
|
||||
grid-area: footer;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0.3ex 0;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 48px;
|
||||
align-self: center;
|
||||
opacity: 0.7;
|
||||
filter: grayscale(90%);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 750px) {
|
||||
nav {
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
padding-right: 120px;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
font-size: 275%;
|
||||
}
|
||||
|
||||
nav h2 {
|
||||
font-size: 220%;
|
||||
}
|
||||
|
||||
#paypal {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
#homepage {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav>
|
||||
<h1>DownThemAll!</h1>
|
||||
<h2>Changes</h2>
|
||||
</nav>
|
||||
<article>
|
||||
<h3>DownThemAll needs your support!</h3>
|
||||
<section id="paypal">
|
||||
<a href="https://www.paypal.me/NilsMaier/10" title="Donate €10" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width=110>
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" stroke="#003087" stroke-width="1px"
|
||||
aria-label="10">
|
||||
<path
|
||||
d="m173.05 47.333h7.9336v-22.517l-8.1436 1.6801v-6.1135l8.0969-1.6801h8.5403v28.631h7.9336v6.2069h-24.361z" />
|
||||
<path
|
||||
d="m225.67 36.086q0-6.5335-1.2367-9.1936-1.2134-2.6834-4.1068-2.6834t-4.1301 2.6834q-1.2367 2.6601-1.2367 9.1936 0 6.6035 1.2367 9.3103 1.2367 2.7067 4.1301 2.7067 2.8701 0 4.1068-2.7067t1.2367-9.3103zm8.9836 0.07q0 8.6569-3.7334 13.37-3.7334 4.6901-10.594 4.6901-6.8835 0-10.617-4.6901-3.7334-4.7135-3.7334-13.37 0-8.6803 3.7334-13.37 3.7334-4.7135 10.617-4.7135 6.8602 0 10.594 4.7135 3.7334 4.6901 3.7334 13.37z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a href="https://www.paypal.me/NilsMaier/15" title="Donate €15" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" opacity=".75" stroke="#003087"
|
||||
stroke-width="1px" aria-label="15">
|
||||
<path
|
||||
d="m173.05 47.333h7.9336v-22.517l-8.1436 1.6801v-6.1135l8.0969-1.6801h8.5403v28.631h7.9336v6.2069h-24.361z" />
|
||||
<path
|
||||
d="m208.75 18.702h22.331v6.6035h-15.167v5.3902q1.0267-0.28001 2.0534-0.42001 1.05-0.16334 2.1701-0.16334 6.3702 0 9.917 3.1968 3.5468 3.1734 3.5468 8.8669 0 5.6468-3.8734 8.8436-3.8501 3.1968-10.71 3.1968-2.9634 0-5.8802-0.58335-2.8934-0.56002-5.7635-1.7267v-7.0702q2.8468 1.6334 5.3902 2.4501 2.5667 0.81669 4.8302 0.81669 3.2668 0 5.1335-1.5867 1.89-1.61 1.89-4.3401 0-2.7534-1.89-4.3401-1.8667-1.5867-5.1335-1.5867-1.9367 0-4.1301 0.51335-2.1934 0.49002-4.7135 1.54z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.paypal.me/NilsMaier/20" title="Donate €20" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" stroke="#003087" stroke-width="1px"
|
||||
aria-label="20">
|
||||
<path
|
||||
d="m181.22 46.936h15.33v6.6035h-25.317v-6.6035l12.717-11.224q1.7034-1.54 2.5201-3.0101 0.81669-1.47 0.81669-3.0568 0-2.4501-1.6567-3.9435-1.6334-1.4934-4.3635-1.4934-2.1001 0-4.5968 0.91003-2.4968 0.88669-5.3435 2.6601v-7.6536q3.0334-1.0034 5.9968-1.5167 2.9634-0.53668 5.8102-0.53668 6.2535 0 9.707 2.7534 3.4768 2.7534 3.4768 7.6769 0 2.8468-1.47 5.3202-1.47 2.4501-6.1835 6.5802z" />
|
||||
<path
|
||||
d="m225.67 36.086q0-6.5335-1.2367-9.1936-1.2134-2.6834-4.1068-2.6834t-4.1301 2.6834q-1.2367 2.6601-1.2367 9.1936 0 6.6035 1.2367 9.3103 1.2367 2.7067 4.1301 2.7067 2.8701 0 4.1068-2.7067t1.2367-9.3103zm8.9836 0.07q0 8.6569-3.7334 13.37-3.7334 4.6901-10.594 4.6901-6.8835 0-10.617-4.6901-3.7334-4.7135-3.7334-13.37 0-8.6803 3.7334-13.37 3.7334-4.7135 10.617-4.7135 6.8602 0 10.594 4.7135 3.7334 4.6901 3.7334 13.37z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a href="https://www.paypal.me/NilsMaier/30" title="Donate €30" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" stroke="#003087" stroke-width="1px"
|
||||
aria-label="30">
|
||||
<path
|
||||
d="m189.71 34.756q3.5234 0.91003 5.3435 3.1734 1.8434 2.2401 1.8434 5.7168 0 5.1802-3.9668 7.8869-3.9668 2.6834-11.574 2.6834-2.6834 0-5.3902-0.44335-2.6834-0.42001-5.3202-1.2834v-6.9302q2.5201 1.26 4.9935 1.9134 2.4967 0.63002 4.9002 0.63002 3.5701 0 5.4602-1.2367 1.9134-1.2367 1.9134-3.5468 0-2.3801-1.9601-3.5934-1.9367-1.2367-5.7402-1.2367h-3.5934v-5.7868h3.7801q3.3834 0 5.0402-1.05 1.6567-1.0734 1.6567-3.2434 0-2.0067-1.61-3.1034t-4.5501-1.0967q-2.1701 0-4.3868 0.49002t-4.4101 1.4467v-6.5802q2.6601-0.74669 5.2735-1.12 2.6134-0.37334 5.1335-0.37334 6.7902 0 10.15 2.2401 3.3834 2.2167 3.3834 6.6969 0 3.0568-1.61 5.0168-1.61 1.9367-4.7601 2.7301z" />
|
||||
<path
|
||||
d="m225.67 36.086q0-6.5335-1.2367-9.1936-1.2134-2.6834-4.1068-2.6834t-4.1301 2.6834q-1.2367 2.6601-1.2367 9.1936 0 6.6035 1.2367 9.3103 1.2367 2.7067 4.1301 2.7067 2.8701 0 4.1068-2.7067t1.2367-9.3103zm8.9836 0.07q0 8.6569-3.7334 13.37-3.7334 4.6901-10.594 4.6901-6.8835 0-10.617-4.6901-3.7334-4.7135-3.7334-13.37 0-8.6803 3.7334-13.37 3.7334-4.7135 10.617-4.7135 6.8602 0 10.594 4.7135 3.7334 4.6901 3.7334 13.37z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a id="ppanother" href="https://www.paypal.me/NilsMaier" title="Donate another amount" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(19.23)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="translate(0 -.24886)" stroke-width="1.7342px">
|
||||
<g transform="matrix(.57894 0 0 .57432 95.638 27.862)" fill="#009cde" stroke="#003087" aria-label="Amount">
|
||||
<path
|
||||
d="m62.511 47.193h-14.047l-2.2167 6.3469h-9.0303l12.904-34.838h10.71l12.904 34.838h-9.0303zm-11.807-6.4635h9.5436l-4.7601-13.86z" />
|
||||
<path
|
||||
d="m105.19 31.746q1.5867-2.4267 3.7568-3.6868 2.1934-1.2834 4.8068-1.2834 4.5035 0 6.8602 2.7768t2.3567 8.0736v15.914h-8.4002v-13.627q0.0233-0.30334 0.0233-0.63002 0.0233-0.32668 0.0233-0.93336 0-2.7768-0.8167-4.0135-0.81669-1.26-2.6367-1.26-2.3801 0-3.6868 1.9601-1.2834 1.9601-1.33 5.6702v12.834h-8.4002v-13.627q0-4.3401-0.74669-5.5768-0.74669-1.26-2.6601-1.26-2.4034 0-3.7101 1.9834-1.3067 1.9601-1.3067 5.6235v12.857h-8.4003v-26.134h8.4003v3.8268q1.54-2.2167 3.5234-3.3368 2.0067-1.12 4.4101-1.12 2.7067 0 4.7835 1.3067 2.0767 1.3067 3.1501 3.6634z" />
|
||||
<path
|
||||
d="m146.26 32.749q-2.7768 0-4.2468 2.0067-1.4467 1.9834-1.4467 5.7402t1.4467 5.7635q1.47 1.9834 4.2468 1.9834 2.7301 0 4.1768-1.9834 1.4467-2.0067 1.4467-5.7635t-1.4467-5.7402q-1.4467-2.0067-4.1768-2.0067zm0-5.9735q6.7435 0 10.524 3.6401 3.8034 3.6401 3.8034 10.08 0 6.4402-3.8034 10.08-3.7801 3.6401-10.524 3.6401-6.7669 0-10.594-3.6401-3.8034-3.6401-3.8034-10.08 0-6.4402 3.8034-10.08 3.8268-3.6401 10.594-3.6401z" />
|
||||
<path
|
||||
d="m169.41 43.366v-15.96h8.4002v2.6134q0 2.1234-0.0233 5.3435-0.0233 3.1968-0.0233 4.2701 0 3.1501 0.16334 4.5501 0.16334 1.3767 0.56002 2.0067 0.51335 0.81669 1.33 1.26 0.84002 0.44335 1.9134 0.44335 2.6134 0 4.1068-2.0067t1.4934-5.5768v-12.904h8.3536v26.134h-8.3536v-3.7801q-1.8901 2.2867-4.0134 3.3834-2.1001 1.0734-4.6435 1.0734-4.5268 0-6.9069-2.7768-2.3567-2.7768-2.3567-8.0736z" />
|
||||
<path
|
||||
d="m233.04 37.626v15.914h-8.4003v-12.18q0-3.3834-0.16333-4.6668-0.14001-1.2834-0.51335-1.8901-0.49002-0.81669-1.33-1.26-0.84003-0.46668-1.9134-0.46668-2.6134 0-4.1068 2.0301-1.4934 2.0067-1.4934 5.5768v12.857h-8.3536v-26.134h8.3536v3.8268q1.8901-2.2867 4.0135-3.3601 2.1234-1.0967 4.6901-1.0967 4.5268 0 6.8602 2.7768 2.3567 2.7768 2.3567 8.0736z" />
|
||||
<path
|
||||
d="m252.97 19.986v7.4202h8.6103v5.9735h-8.6103v11.084q0 1.8201 0.72335 2.4734 0.72336 0.63002 2.8701 0.63002h4.2935v5.9735h-7.1636q-4.9468 0-7.0236-2.0534-2.0534-2.0767-2.0534-7.0235v-11.084h-4.1535v-5.9735h4.1535v-7.4202z" />
|
||||
</g>
|
||||
<g transform="matrix(.57894 0 0 .57432 95.638 23.862)" fill="#003087" aria-label="Another">
|
||||
<path
|
||||
d="m57.459 11.049h-14.047l-2.2167 6.3469h-9.0303l12.904-34.838h10.71l12.904 34.838h-9.0303zm-11.807-6.4635h9.5436l-4.7601-13.86z" />
|
||||
<path
|
||||
d="m102.19 1.4817v15.914h-8.4003v-12.18q0-3.3834-0.16334-4.6668-0.14-1.2834-0.51335-1.8901-0.49002-0.81669-1.33-1.26-0.84002-0.46668-1.9134-0.46668-2.6134 0-4.1068 2.0301-1.4934 2.0067-1.4934 5.5768v12.857h-8.3536v-26.134h8.3536v3.8268q1.8901-2.2867 4.0135-3.3601 2.1234-1.0967 4.6901-1.0967 4.5268 0 6.8602 2.7768 2.3567 2.7768 2.3567 8.0736z" />
|
||||
<path
|
||||
d="m125.43-3.3951q-2.7768 0-4.2468 2.0067-1.4467 1.9834-1.4467 5.7402t1.4467 5.7635q1.47 1.9834 4.2468 1.9834 2.7301 0 4.1768-1.9834 1.4467-2.0067 1.4467-5.7635t-1.4467-5.7402q-1.4467-2.0067-4.1768-2.0067zm0-5.9735q6.7435 0 10.524 3.6401 3.8034 3.6401 3.8034 10.08 0 6.4402-3.8034 10.08-3.7801 3.6401-10.524 3.6401-6.7669 0-10.594-3.6401-3.8034-3.6401-3.8034-10.08 0-6.4402 3.8034-10.08 3.8268-3.6401 10.594-3.6401z" />
|
||||
<path
|
||||
d="m157.99-16.159v7.4202h8.6103v5.9735h-8.6103v11.084q0 1.8201 0.72336 2.4734 0.72336 0.63002 2.8701 0.63002h4.2935v5.9735h-7.1636q-4.9468 0-7.0236-2.0534-2.0534-2.0767-2.0534-7.0235v-11.084h-4.1535v-5.9735h4.1535v-7.4202z" />
|
||||
<path
|
||||
d="m201.02 1.4817v15.914h-8.4002v-12.134q0-3.4301-0.16334-4.7135-0.14001-1.2834-0.51335-1.8901-0.49002-0.81669-1.33-1.26-0.84003-0.46668-1.9134-0.46668-2.6134 0-4.1068 2.0301-1.4934 2.0067-1.4934 5.5768v12.857h-8.3536v-36.308h8.3536v14q1.89-2.2867 4.0134-3.3601 2.1234-1.0967 4.6901-1.0967 4.5268 0 6.8602 2.7768 2.3567 2.7768 2.3567 8.0736z" />
|
||||
<path
|
||||
d="m237.91 4.2585v2.3801h-19.531q0.30334 2.9401 2.1234 4.4101 1.8201 1.47 5.0868 1.47 2.6367 0 5.3902-0.77002 2.7768-0.79336 5.6935-2.3801v6.4402q-2.9634 1.12-5.9268 1.6801-2.9634 0.58335-5.9268 0.58335-7.0936 0-11.037-3.5934-3.9201-3.6168-3.9201-10.127 0-6.3935 3.8501-10.057 3.8734-3.6634 10.64-3.6634 6.1602 0 9.847 3.7101 3.7101 3.7101 3.7101 9.917zm-8.5869-2.7768q0-2.3801-1.4-3.8268-1.3767-1.47-3.6168-1.47-2.4267 0-3.9434 1.3767-1.5167 1.3534-1.8901 3.9201z" />
|
||||
<path
|
||||
d="m266.64-1.6217q-1.0967-0.51335-2.1934-0.74669-1.0734-0.25667-2.1701-0.25667-3.2201 0-4.9702 2.0767-1.7267 2.0534-1.7267 5.9035v12.04h-8.3536v-26.134h8.3536v4.2935q1.61-2.5667 3.6868-3.7334 2.1001-1.19 5.0168-1.19 0.42001 0 0.91002 0.046668 0.49002 0.023334 1.4234 0.14z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
</section>
|
||||
<p>It requires <strong>a lot of time and some money</strong> to create the extension, keep up with browser
|
||||
changes, read and answer emails, bug reports and feature requests, and actually publish the final versions.
|
||||
<em>Any contribution is appreciated. Thank you!</em></p>
|
||||
|
||||
<h2>Major version changes</h2>
|
||||
<h3>Version 4.2 <em>October 9, 2019</em></h3>
|
||||
<ul>
|
||||
<li>Vastly improved name handling in Chrome. (<a
|
||||
href="https://bugzilla.mozilla.org/show_bug.cgi?id=1245652">Missing in Firefox</a>)</li>
|
||||
<li>Support <code>[a:z]</code> character batches</li>
|
||||
<li>Dark theme support</li>
|
||||
<li>Improved the gathering of images in websites</li>
|
||||
<li>The windows, in particular the Manager window, will now usually remember the size and position correctly.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Version 4.1 <em>September 21, 2019</em></h3>
|
||||
<ul>
|
||||
<li>Subfolder dropdown. While it was previously possible to use masks for subfolders already, based on user
|
||||
feedback having a dedicated subfolder selection dropdown is more convenient and easier to discover.</li>
|
||||
<li>You can configure the browser button to more different actions now</li>
|
||||
<li>Play sounds: When the queue finishes, play a sound (can be enabled/disabled in the preferences)</li>
|
||||
<li>Delete files from within the manager</li>
|
||||
<li>Manager can now configured to be opened in a popup window instead of a tab</li>
|
||||
<li>Automatic retries (This feature relies on Firefox reporting errors correctly to extensions, which it
|
||||
only started doing in Firefox 71, which is currently available as Nightly).</li>
|
||||
</ul>
|
||||
|
||||
<h3>Version 4.0 <em>August 31, 2019</em></h3>
|
||||
<p>DownThemAll! reborn, as a WebExtension</p>
|
||||
</article>
|
||||
|
||||
<footer>
|
||||
<svg id="logo" viewBox="0 0 16 16">
|
||||
<defs>
|
||||
<linearGradient id="b">
|
||||
<stop stop-color="#116597" offset="0" />
|
||||
<stop stop-color="#062638" offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="a">
|
||||
<stop stop-color="#fffe99" offset="0" />
|
||||
<stop stop-color="#e49218" offset=".2" />
|
||||
<stop stop-color="#116597" offset="1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="c" xlink:href="#a" gradientUnits="userSpaceOnUse" cy="17.413" cx="11.75"
|
||||
gradientTransform="matrix(.41233 0 0 1.1548 3.24 -8.74)" r="10.752" />
|
||||
<linearGradient id="h" y2="-.242" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(2.495 -6.342) scale(.6787)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="i" y2="-.242" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(2.495 -1.882) scale(.6787)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="g" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(7.08 4.18) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="f" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(7.657 1.298) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="e" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(1.851 4.18) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="d" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(1.851 1.298) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
</defs>
|
||||
<g stroke="#072739">
|
||||
<g stroke-width=".646">
|
||||
<path
|
||||
d="M4.561 7.134v1.515H1.996v.262l2.949 3.333 2.666 3.05h.99l2.686-3.05 2.848-3.252V8.65h-2.727V7.134H4.561z"
|
||||
fill="url(#c)" />
|
||||
<g stroke-linecap="round" stroke-dashoffset="10" fill-rule="evenodd">
|
||||
<path fill="url(#d)" d="M4.562 1.346h1.649v1.649H4.562z" />
|
||||
<path fill="url(#e)" d="M4.562 4.228h1.649v1.649H4.562z" />
|
||||
<path fill="url(#f)" d="M10.368 1.346h1.649v1.649h-1.649z" />
|
||||
<path fill="url(#g)" d="M9.792 4.228h1.649v1.649H9.792z" />
|
||||
</g>
|
||||
</g>
|
||||
<g fill-rule="evenodd" stroke-linecap="round" stroke-dashoffset="10">
|
||||
<path fill="url(#h)" d="M6.689-6.268H9.24v2.551H6.689z" transform="translate(2.874 4.614) scale(.64633)" />
|
||||
<path fill="url(#i)" d="M6.689-1.808H9.24V.743H6.689z" transform="translate(2.874 4.614) scale(.64633)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<section>
|
||||
<p>Copyright © 2007-2018 Nils Maier, Stefano Verna, Federico Parodi</p>
|
||||
<p>Copyright © 2007-2019 Nils Maier</p>
|
||||
<p>The information on this website is licensed under the
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/">
|
||||
Creative Commons Attribution-ShareAlike 4.0 International License</a>.</p>
|
||||
</section>
|
||||
<a id="homepage" href="https://downthemall.org/">
|
||||
<svg viewBox="0 0 250 32" height="20" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1.1174 0 0 1.122 -217.76 -441.22)">
|
||||
<path
|
||||
d="m277.86 402.4c-2.106 0.594-3.9921 1.3387-5.0625 3.0371-1.8782 2.9802-1.8825 0.50072-5.0625 1.0117-0.075 4.312-1.6927 7.0814-1.0117 12.148 0.251 1.099-0.2614 1.8228 2.0234 2.0254 5.078-2.0169 2.6252-12.605 9.1113-13.162 0.661 5.076 1.325 12.532 3.0371 14.174h2.0254c2.0004 0 2.4284-0.24992 2.0254-4.0449-0.157-1.478-1.5594-2.1928-2.0234-4.0508-0.436-1.744-0.20767-2.7525-1.0137-5.0625 0.20397-2.5435-3.0757-5.9543-4.0488-6.0762z" />
|
||||
<path
|
||||
d="m205.98 396.32c0.71583-0.71583 3.037 0 3.037 0 3.312 2.743 2.107 7.979 3.037 12.148 0.97 4.354 2.863 8.229 3.037 12.149-3.865 0.827-3.919-2.155-5.062-4.05-3.517 0.195-3.843 3.58-7.086 4.05 0 0-3.0927 1.1003-6.074 0-2.2496-1.309-2.6451-6.2394-0.9158-9.8255 1.7293-3.5861 5.5833-5.828 10.027-5.3615 0 0-2.1472-6.9628 0-9.11zm-7.087 21.261c5.626-0.111 9.345-2.129 9.111-8.1-5.62-4-11.843 3.748-9.111 8.1z" />
|
||||
<path
|
||||
d="m262.67 400.37c3.124 1.378 4.2238 9.6188 3.037 14.174-0.21875 0.83965-3.6 4.9793-4.049 5.062-5.6104 1.0329-9.589-0.535-13.161-2.024-3.76 0.627-4.631 4.142-10.124 3.037-2.976-2.578-3.758-15.21 2.025-15.187 0.646 5.261-2.233 8.114 0 12.149 7.523 0.437 4.919-9.254 9.111-12.149 4.209 1.404 0.0379 4.9881 2.024 8.1 0.7405 1.1602 0.55971 3.037 2.025 3.037h5.062c2.561-0.813 3.5092-3.7334 4.049-6.074 1.0218-4.4307-3.744-7.344 1e-3 -10.125z" />
|
||||
<path
|
||||
d="m229.26 404.42c4.2274 1.6428 6.429 7.195 4.049 13.161-6.917 2.7019-16.888 6.2299-17.211-1.012-0.35916-8.0617 4.062-15.685 13.162-12.149zm-9.111 11.136c2.075 3.041 7.753 2.154 10.124 0 2.323-10.366-12.448-10.366-10.124 0z" />
|
||||
<path
|
||||
d="m415.54 393.28c1.635-0.285 1.742 0.957 3.037 1.012 0.117 7.88 0.244 15.768-6.074 17.211 0.518-6.569 1.518-12.656 3.037-18.223z" />
|
||||
<path
|
||||
d="m397.32 396.32c0.901 0.449 1.811 0.89 2.024 2.025 1.436 2.366 1.224 7.424 1.013 12.148-0.182 4.063 1.111 10.063-3.037 10.124-1.656-5.939-1.656-18.356 0-24.297z" />
|
||||
<path
|
||||
d="m405.42 396.32c0.901 0.449 1.811 0.89 2.024 2.025 1.436 2.366 1.225 7.424 1.013 12.148-0.182 4.063 1.112 10.063-3.037 10.124-1.656-5.939-1.656-18.356 0-24.297z" />
|
||||
<path
|
||||
d="m386.18 405.43c1.879 2.172 2.706 5.394 3.037 9.112 0.405 2.632 3.021 3.053 3.037 6.074-3.694 0.657-5.392-0.683-7.086-2.025-1.896 2.882-11.656 3.4133-12.148 0-1.2181-8.4422 3.734-13.813 13.16-13.161zm-10.123 11.136c5.229 3.119 8.32-1.521 8.123-7.087-3.941-4.338-8.175 1.277-8.123 7.087z" />
|
||||
<path d="m412.5 414.54c2.928 0.446 4.614 2.134 5.062 5.062-3.131 3.21-8.628-1.578-5.062-5.062z" />
|
||||
<g fill="#069">
|
||||
<path
|
||||
d="m344.67 399.36c-3.462 3.288-12.431 1.068-14.174 6.074-0.212 4.938 5.378 4.072 10.124 4.05-0.763 3.287-5.684 2.415-8.099 4.05 0.194 6.297 7.9627 1.1977 12.148 3.037 2.1858 0.96059-2.6665 2.6983-4.05 3.037-0.92 0.229-7.2519 3.9961-10.124 1.013-4.022-4.1774-6.0966-12.401-2.0268-19.74 2.1561-3.8884 7.9908-2.3919 12.151-3.5449 2.1724-1.0195 4.0825-0.57137 4.051 2.024z" />
|
||||
<path
|
||||
d="m368.97 398.35c0.91935 1.2579 1.638 4.258 2.024 7.086 0.632 4.615 1.344 11.963-3.037 12.149 0.605-6.006-1.776-9.022-1.012-15.187-2.943 2.12-2.8572 7.4034-6.074 9.112-5.4038 2.8702-7.236-2.214-10.124-4.05-0.162 4.899 0.612 10.735-4.05 11.136-0.788-6.862 1.383-10.766 1.013-17.21 6.881-1.144 5.642 5.832 10.124 7.086 2.854-2.881 4.6308-6.3006 7.086-10.123 0.7296-1.1359 3.2534-1.0889 4.05 1e-3z" />
|
||||
<path
|
||||
d="m299.12 404.42c2.5659-0.14763 2.6804 4.5418 3.037 7.087 0.319 2.835 0.61 6.509-1.012 8.099-3.139 1.088-1.619-3.049-2.025-5.062-0.701-3.477-2.194-6.897 0-10.124z" />
|
||||
<path
|
||||
d="m324 395.29c-4.1284 0.12154 0.92333 9.4224-3.6191 9.1289-3.094-0.731-3.6892 1.0347-6.0742 1.0117-5.024-0.414 3.2846-5.0625 3.2846-5.0625-1.3331-0.17112-4.8554 0.83219-4.9679-2.0635-1.1226-0.18087-1.4286 0.17857-3.0288 0.0563-0.30668 3.1413-2.5989 3.3131-5.6576 3.0448-6.3424-0.5564-15.93 1.6227-20.001 5.0366-0.88745 0.20112-1.9712 0.15704-2.4673-0.0332l0.44194 2.0606c8.225-2.236 14.819-5.3086 26.322-5.0625 0.802 5.947-2.2449 15.743 3.0371 17.211 4.327-0.057 0.29512-5.5363 2.0234-9.0879 1.899-3.9024 2.4597-3.319 5.0625-4.0723 5.9836-1.7317 0.3075 9.815 5.0625 10.123 3.109 0.858 1.9194-3.3815 2.0234-5.0625 0.3-4.805 0.0473-13.108-1.0117-17.211-0.15291-0.0158-0.29651-0.0215-0.42969-0.0176z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
BIN
docs/changelog/res/ReenieBeanie-Regular.woff2
Normal file
BIN
docs/changelog/res/background-tile.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/changelog/res/footerbg.png
Normal file
After Width: | Height: | Size: 10 KiB |
55
docs/changelog/res/halo.svg
Normal file
@ -0,0 +1,55 @@
|
||||
<svg version="1.1" viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<radialGradient id="d" cx="161.46" cy="-144.02" r="160.02" gradientTransform="matrix(1 0 0 .99838 0 -.23281)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#febb00" offset="0"/>
|
||||
<stop stop-color="#ffd571" stop-opacity=".49804" offset=".64286"/>
|
||||
<stop stop-color="#ffe5a7" stop-opacity="0" offset=".95153"/>
|
||||
<stop stop-color="#ffe5a7" stop-opacity="0" offset="1"/>
|
||||
</radialGradient>
|
||||
<filter id="e" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="0.32807532"/>
|
||||
</filter>
|
||||
<radialGradient id="c" cx="161.46" cy="-144.02" r="160.02" gradientTransform="matrix(1 0 0 .99838 0 -.23281)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffd14e" offset="0"/>
|
||||
<stop stop-color="#ffeeb5" stop-opacity=".49804" offset=".71429"/>
|
||||
<stop stop-color="#ffeeb5" stop-opacity=".49804" offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="b" cx="12.389" cy="11.882" r="8.199" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#e69412" offset="0"/>
|
||||
<stop stop-color="#f4c478" stop-opacity="0" offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="a" cx="11.75" cy="17.413" r="10.752" gradientTransform="matrix(3.3776 0 0 9.4595 121.98 -293.39)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fffe99" offset="0"/>
|
||||
<stop stop-color="#f69706" offset=".2"/>
|
||||
<stop stop-color="#136fa7" offset="1"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g transform="translate(-61.457 224.02)">
|
||||
<g transform="matrix(.62494 0 0 .62494 60.557 -51.015)" fill="url(#d)" fill-rule="evenodd">
|
||||
<path d="m161.46-144.02 72.75 144.62-46.61 15.146z"/>
|
||||
<path d="m161.46-144.02 143.86 74.228-28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 160.02-24.507v49.014z"/>
|
||||
<path d="m161.46-144.02 115.05-113.88 28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 26.14-159.76 46.615 15.146z"/>
|
||||
<path d="m161.46-144.02-72.755-144.61 46.615-15.146z"/>
|
||||
<path d="m161.46-144.02-143.86-74.228 28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-160.02 24.507-2.7e-6 -49.014z"/>
|
||||
<path d="m161.46-144.02-115.05 113.89-28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-26.14 159.76-46.618-15.146z"/>
|
||||
</g>
|
||||
<g transform="matrix(.59435 -.19312 .19312 .59435 93.307 -24.24)" fill="url(#c)" fill-rule="evenodd" opacity=".88">
|
||||
<path d="m161.46-144.02 72.75 144.62-46.61 15.146z"/>
|
||||
<path d="m161.46-144.02 143.86 74.228-28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 160.02-24.507v49.014z"/>
|
||||
<path d="m161.46-144.02 115.05-113.88 28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 26.14-159.76 46.615 15.146z"/>
|
||||
<path d="m161.46-144.02-72.755-144.61 46.615-15.146z"/>
|
||||
<path d="m161.46-144.02-143.86-74.228 28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-160.02 24.507-2.7e-6 -49.014z"/>
|
||||
<path d="m161.46-144.02-115.05 113.89-28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-26.14 159.76-46.618-15.146z"/>
|
||||
</g>
|
||||
<path transform="matrix(7.0729 0 0 7.4575 73.647 -229.61)" d="m20.588 11.882a8.199 8.199 0 1 1-16.398 0 8.199 8.199 0 1 1 16.398 0z" fill="url(#b)" filter="url(#e)"/>
|
||||
<path d="m133.31-207.91v55.137h-17.218c-6.602-0.97334-3.1317 2.1508-3.1317 2.1508l23.494 26.472 18.653 21.422c1.9951 2.9976 3.1147 2.8985 6.486 2.8985 3.5137 0.43957 4.2275-0.28033 6.4176-2.757l19.07-21.564 22.666-25.81c3.6888-3.9672-2.8847-2.8126-2.8847-2.8126h-18.954v-54.904c0.65539-9.337-1.0536-9.0538-8.6714-8.7929h-37.608c-7.6308-0.26075-8.3181 0.1211-8.3181 8.5607z" fill="url(#a)" stroke="#0a3b56" stroke-width="5.1532"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
BIN
docs/landing/batches.jpg
Executable file
After Width: | Height: | Size: 42 KiB |
BIN
docs/landing/batches.png
Executable file
After Width: | Height: | Size: 108 KiB |
BIN
docs/landing/batches.webm
Executable file
BIN
docs/landing/first.jpg
Executable file
After Width: | Height: | Size: 47 KiB |
BIN
docs/landing/first.png
Executable file
After Width: | Height: | Size: 105 KiB |
BIN
docs/landing/first.webm
Executable file
569
docs/landing/index.html
Executable file
@ -0,0 +1,569 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!-- License: Creative Commons Attribution-ShareAlike 4.0 International License -->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to DownThemAll!</title>
|
||||
<style crossorigin="anonymous">
|
||||
@font-face {
|
||||
font-family: "Reenie Beanie";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: local("Reenie Beanie"), local("ReenieBeanie"),
|
||||
url(res/ReenieBeanie-Regular.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
:root {
|
||||
--content-width: 1000px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Ubuntu",
|
||||
"Helvetica Neue", sans-serif;
|
||||
font-size: 12pt;
|
||||
color: rgb(20, 20, 20);
|
||||
background: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
background: white repeat-x url(res/background-tile.png);
|
||||
grid-template-columns: 1fr [content] auto 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
". header ."
|
||||
". content ."
|
||||
"footer footer footer";
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(30, 30, 70);
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
nav {
|
||||
max-width: var(--content-width);
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding-right: 120px;
|
||||
padding-top: 1em;
|
||||
padding-left: 32px;
|
||||
align-content: center;
|
||||
align-items: baseline;
|
||||
background: no-repeat top right url(res/halo.svg);
|
||||
background-size: 120px;
|
||||
min-height: 100px;
|
||||
text-shadow: rgba(255, 255, 255, 0.8) 1px 3px 0px;
|
||||
font-family: "Reenie Beanie", cursive;
|
||||
}
|
||||
|
||||
nav h1,
|
||||
nav h2 {
|
||||
margin: 0;
|
||||
margin-right: 1ex;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
font-size: 400%;
|
||||
}
|
||||
|
||||
nav h2 {
|
||||
font-size: 250%;
|
||||
}
|
||||
|
||||
article {
|
||||
max-width: var(--content-width);
|
||||
grid-area: content;
|
||||
box-sizing: border-box;
|
||||
padding: 1ex 32px;
|
||||
background: white;
|
||||
border-bottom: 0;
|
||||
border-radius: 20px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
h3 em {
|
||||
display: inline-block;
|
||||
padding-left: 1em;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
#paypal {
|
||||
margin: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto auto;
|
||||
grid-row-gap: 1em;
|
||||
grid-column-gap: 1em;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-column-gap: 2em;
|
||||
padding: 2em 2em 1em 2em;
|
||||
background: transparent center top repeat-x url(res/footerbg.png);
|
||||
font-size: x-small;
|
||||
color: gray;
|
||||
grid-area: footer;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0.3ex 0;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 48px;
|
||||
align-self: center;
|
||||
opacity: 0.7;
|
||||
filter: grayscale(90%);
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
grid-template-columns: [content] 1fr;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
video {
|
||||
grid-area: content;
|
||||
margin: 0 auto;
|
||||
width: 720px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.play {
|
||||
grid-area: content;
|
||||
z-index: 100;
|
||||
min-height: 64px;
|
||||
min-width: 64px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 20%;
|
||||
background-image: url(res/go.svg);
|
||||
opacity: 0.4;
|
||||
transition: opacity 700ms;
|
||||
}
|
||||
|
||||
.play:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 750px) {
|
||||
nav {
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
padding-right: 120px;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
font-size: 275%;
|
||||
}
|
||||
|
||||
nav h2 {
|
||||
font-size: 220%;
|
||||
}
|
||||
|
||||
#paypal {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
#homepage {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav>
|
||||
<h2>Welcome to</h2>
|
||||
<h1>DownThemAll!</h1>
|
||||
</nav>
|
||||
<article>
|
||||
<h3>DownThemAll needs your support!</h3>
|
||||
<section id="paypal">
|
||||
<a href="https://www.paypal.me/NilsMaier/10" title="Donate €10" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width=110>
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" stroke="#003087" stroke-width="1px"
|
||||
aria-label="10">
|
||||
<path
|
||||
d="m173.05 47.333h7.9336v-22.517l-8.1436 1.6801v-6.1135l8.0969-1.6801h8.5403v28.631h7.9336v6.2069h-24.361z" />
|
||||
<path
|
||||
d="m225.67 36.086q0-6.5335-1.2367-9.1936-1.2134-2.6834-4.1068-2.6834t-4.1301 2.6834q-1.2367 2.6601-1.2367 9.1936 0 6.6035 1.2367 9.3103 1.2367 2.7067 4.1301 2.7067 2.8701 0 4.1068-2.7067t1.2367-9.3103zm8.9836 0.07q0 8.6569-3.7334 13.37-3.7334 4.6901-10.594 4.6901-6.8835 0-10.617-4.6901-3.7334-4.7135-3.7334-13.37 0-8.6803 3.7334-13.37 3.7334-4.7135 10.617-4.7135 6.8602 0 10.594 4.7135 3.7334 4.6901 3.7334 13.37z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a href="https://www.paypal.me/NilsMaier/15" title="Donate €15" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" opacity=".75" stroke="#003087"
|
||||
stroke-width="1px" aria-label="15">
|
||||
<path
|
||||
d="m173.05 47.333h7.9336v-22.517l-8.1436 1.6801v-6.1135l8.0969-1.6801h8.5403v28.631h7.9336v6.2069h-24.361z" />
|
||||
<path
|
||||
d="m208.75 18.702h22.331v6.6035h-15.167v5.3902q1.0267-0.28001 2.0534-0.42001 1.05-0.16334 2.1701-0.16334 6.3702 0 9.917 3.1968 3.5468 3.1734 3.5468 8.8669 0 5.6468-3.8734 8.8436-3.8501 3.1968-10.71 3.1968-2.9634 0-5.8802-0.58335-2.8934-0.56002-5.7635-1.7267v-7.0702q2.8468 1.6334 5.3902 2.4501 2.5667 0.81669 4.8302 0.81669 3.2668 0 5.1335-1.5867 1.89-1.61 1.89-4.3401 0-2.7534-1.89-4.3401-1.8667-1.5867-5.1335-1.5867-1.9367 0-4.1301 0.51335-2.1934 0.49002-4.7135 1.54z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.paypal.me/NilsMaier/20" title="Donate €20" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" stroke="#003087" stroke-width="1px"
|
||||
aria-label="20">
|
||||
<path
|
||||
d="m181.22 46.936h15.33v6.6035h-25.317v-6.6035l12.717-11.224q1.7034-1.54 2.5201-3.0101 0.81669-1.47 0.81669-3.0568 0-2.4501-1.6567-3.9435-1.6334-1.4934-4.3635-1.4934-2.1001 0-4.5968 0.91003-2.4968 0.88669-5.3435 2.6601v-7.6536q3.0334-1.0034 5.9968-1.5167 2.9634-0.53668 5.8102-0.53668 6.2535 0 9.707 2.7534 3.4768 2.7534 3.4768 7.6769 0 2.8468-1.47 5.3202-1.47 2.4501-6.1835 6.5802z" />
|
||||
<path
|
||||
d="m225.67 36.086q0-6.5335-1.2367-9.1936-1.2134-2.6834-4.1068-2.6834t-4.1301 2.6834q-1.2367 2.6601-1.2367 9.1936 0 6.6035 1.2367 9.3103 1.2367 2.7067 4.1301 2.7067 2.8701 0 4.1068-2.7067t1.2367-9.3103zm8.9836 0.07q0 8.6569-3.7334 13.37-3.7334 4.6901-10.594 4.6901-6.8835 0-10.617-4.6901-3.7334-4.7135-3.7334-13.37 0-8.6803 3.7334-13.37 3.7334-4.7135 10.617-4.7135 6.8602 0 10.594 4.7135 3.7334 4.6901 3.7334 13.37z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a href="https://www.paypal.me/NilsMaier/30" title="Donate €30" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(28.855)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#009cde" stroke="#003087" stroke-width="1px"
|
||||
aria-label="30">
|
||||
<path
|
||||
d="m189.71 34.756q3.5234 0.91003 5.3435 3.1734 1.8434 2.2401 1.8434 5.7168 0 5.1802-3.9668 7.8869-3.9668 2.6834-11.574 2.6834-2.6834 0-5.3902-0.44335-2.6834-0.42001-5.3202-1.2834v-6.9302q2.5201 1.26 4.9935 1.9134 2.4967 0.63002 4.9002 0.63002 3.5701 0 5.4602-1.2367 1.9134-1.2367 1.9134-3.5468 0-2.3801-1.9601-3.5934-1.9367-1.2367-5.7402-1.2367h-3.5934v-5.7868h3.7801q3.3834 0 5.0402-1.05 1.6567-1.0734 1.6567-3.2434 0-2.0067-1.61-3.1034t-4.5501-1.0967q-2.1701 0-4.3868 0.49002t-4.4101 1.4467v-6.5802q2.6601-0.74669 5.2735-1.12 2.6134-0.37334 5.1335-0.37334 6.7902 0 10.15 2.2401 3.3834 2.2167 3.3834 6.6969 0 3.0568-1.61 5.0168-1.61 1.9367-4.7601 2.7301z" />
|
||||
<path
|
||||
d="m225.67 36.086q0-6.5335-1.2367-9.1936-1.2134-2.6834-4.1068-2.6834t-4.1301 2.6834q-1.2367 2.6601-1.2367 9.1936 0 6.6035 1.2367 9.3103 1.2367 2.7067 4.1301 2.7067 2.8701 0 4.1068-2.7067t1.2367-9.3103zm8.9836 0.07q0 8.6569-3.7334 13.37-3.7334 4.6901-10.594 4.6901-6.8835 0-10.617-4.6901-3.7334-4.7135-3.7334-13.37 0-8.6803 3.7334-13.37 3.7334-4.7135 10.617-4.7135 6.8602 0 10.594 4.7135 3.7334 4.6901 3.7334 13.37z" />
|
||||
</g>
|
||||
<g transform="matrix(1.004 0 0 .996 4.7854 0)" fill="#003087" aria-label="€">
|
||||
<path
|
||||
d="m153.83 51.627q-2.2167 1.2834-4.6435 1.9367-2.4034 0.65335-5.0168 0.65335-6.1135 0-10.314-3.0334-4.1768-3.0568-5.9268-8.7736h-5.0635l2.0534-4.5501h2.2867q-0.0233-0.32668-0.0467-0.67669 0-0.37334 0-1.0267 0-0.67669 0-1.05 0.0233-0.37334 0.0467-0.72336h-4.3401l2.0534-4.5501h3.0101q1.7967-5.7402 5.9502-8.7503 4.1768-3.0101 10.29-3.0101 2.6134 0 5.0168 0.65335 2.4267 0.65335 4.6435 1.9367v7.2102q-1.8901-1.8201-4.0368-2.7301-2.1467-0.93336-4.4568-0.93336-2.9634 0-5.0635 1.4467-2.0767 1.4234-3.1268 4.1768h12.577l-2.0067 4.5501h-11.504q-0.0467 0.37334-0.07 0.79336 0 0.42001 0 1.26 0 0.30334 0 0.67669 0.0233 0.35001 0.0467 0.74669h10.01l-2.0767 4.5501h-6.9769q1.1667 2.8468 3.1968 4.2701 2.0534 1.4234 4.9935 1.4234 2.3101 0 4.4101-0.91003 2.1234-0.93336 4.0835-2.7768z"
|
||||
fill="#003087" stroke-width="1px" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a id="ppanother" href="https://www.paypal.me/NilsMaier" title="Donate another amount" target="_blank">
|
||||
<svg viewBox="0 0 300 72" width="110">
|
||||
<rect x="1.5" y="1.5" width="297" height="69" ry="34.093" fill="#ffc439" fill-rule="evenodd" stroke="#dadce0"
|
||||
stroke-dashoffset="89.999" stroke-linejoin="round" stroke-width="3" style="paint-order:normal" />
|
||||
<g transform="translate(19.23)">
|
||||
<path
|
||||
d="m42.414 58.373 0.83858-5.284-1.868-0.04295h-8.9197l6.1988-38.991c0.01924-0.1177 0.08177-0.22746 0.17317-0.3054 0.09139-0.07794 0.20844-0.12089 0.3303-0.12089h15.04c4.993 0 8.4387 1.0307 10.238 3.0651 0.84339 0.95437 1.3805 1.9517 1.6403 3.0492 0.27258 1.1516 0.27739 2.5275 0.01122 4.2056l-0.01924 0.12248v1.0753l0.84339 0.474c0.71031 0.37379 1.2747 0.80167 1.7076 1.2916 0.72153 0.81599 1.1881 1.8531 1.3853 3.0826 0.20363 1.2645 0.13629 2.7693-0.19722 4.4728-0.38482 1.9596-1.0069 3.6664-1.8471 5.0629-0.77284 1.2868-1.7573 2.3541-2.9262 3.1812-1.116 0.78576-2.442 1.3822-3.9412 1.764-1.4527 0.37538-3.109 0.56467-4.9257 0.56467h-1.1705c-0.83698 0-1.6499 0.29904-2.2881 0.83507-0.63976 0.54717-1.0631 1.2948-1.1929 2.1123l-0.08819 0.47559-1.4815 9.3131-0.06734 0.34198c-0.01764 0.10816-0.0481 0.16224-0.093 0.19883-0.04008 0.0334-0.09781 0.05567-0.15393 0.05567z"
|
||||
fill="#253b80" />
|
||||
<path
|
||||
d="m67.719 24.195c-0.0449 0.28472-0.0962 0.5758-0.15393 0.87484-1.9834 10.102-8.769 13.592-17.435 13.592h-4.4126c-1.0598 0-1.9529 0.7635-2.1181 1.8006l-2.2592 14.214-0.63976 4.029c-0.10743 0.68078 0.4217 1.2948 1.1144 1.2948h7.8262c0.92677 0 1.714-0.66806 1.8599-1.5747l0.07696-0.39447 1.4735-9.2765 0.0946-0.509c0.14431-0.90983 0.93318-1.5779 1.8599-1.5779h1.1705c7.5825 0 13.518-3.054 15.253-11.891 0.72474-3.6918 0.34954-6.7744-1.5681-8.9424-0.58043-0.65374-1.3004-1.1961-2.1421-1.6383z"
|
||||
fill="#179bd7" />
|
||||
<path
|
||||
d="m65.644 23.374c-0.30304-0.08748-0.61571-0.16702-0.93639-0.23859-0.32228-0.06999-0.65259-0.13202-0.99251-0.1861-1.1897-0.19087-2.4933-0.28154-3.8899-0.28154h-11.788c-0.29022 0-0.566 0.06522-0.81293 0.18292-0.54355 0.25927-0.94761 0.76986-1.0454 1.395l-2.5077 15.757-0.07215 0.45969c0.16515-1.0371 1.0582-1.8006 2.1181-1.8006h4.4126c8.6664 0 15.452-3.4914 17.435-13.592 0.05933-0.29904 0.10903-0.59012 0.15393-0.87484-0.50187-0.26404-1.0454-0.48991-1.6307-0.68237-0.14431-0.04772-0.29342-0.09385-0.44414-0.13838z"
|
||||
fill="#222d65" />
|
||||
<path
|
||||
d="m46.179 24.246c0.09781-0.62511 0.50187-1.1357 1.0454-1.3934 0.24853-0.11771 0.52271-0.18292 0.81293-0.18292h11.788c1.3966 0 2.7001 0.09066 3.8899 0.28154 0.33992 0.05408 0.67022 0.11612 0.99251 0.1861 0.32068 0.07158 0.63334 0.15111 0.93639 0.23859 0.15072 0.04454 0.29984 0.09067 0.44575 0.13679 0.58524 0.19246 1.1288 0.41992 1.6307 0.68237 0.59005-3.7332-0.0048-6.275-2.0395-8.5766-2.2432-2.5338-6.2918-3.6186-11.472-3.6186h-15.04c-1.0582 0-1.961 0.7635-2.1245 1.8022l-6.2645 39.392c-0.12346 0.7794 0.48262 1.4825 1.2747 1.4825h9.2853l2.3314-14.673z"
|
||||
fill="#253b80" />
|
||||
</g>
|
||||
<g transform="translate(0 -.24886)" stroke-width="1.7342px">
|
||||
<g transform="matrix(.57894 0 0 .57432 95.638 27.862)" fill="#009cde" stroke="#003087" aria-label="Amount">
|
||||
<path
|
||||
d="m62.511 47.193h-14.047l-2.2167 6.3469h-9.0303l12.904-34.838h10.71l12.904 34.838h-9.0303zm-11.807-6.4635h9.5436l-4.7601-13.86z" />
|
||||
<path
|
||||
d="m105.19 31.746q1.5867-2.4267 3.7568-3.6868 2.1934-1.2834 4.8068-1.2834 4.5035 0 6.8602 2.7768t2.3567 8.0736v15.914h-8.4002v-13.627q0.0233-0.30334 0.0233-0.63002 0.0233-0.32668 0.0233-0.93336 0-2.7768-0.8167-4.0135-0.81669-1.26-2.6367-1.26-2.3801 0-3.6868 1.9601-1.2834 1.9601-1.33 5.6702v12.834h-8.4002v-13.627q0-4.3401-0.74669-5.5768-0.74669-1.26-2.6601-1.26-2.4034 0-3.7101 1.9834-1.3067 1.9601-1.3067 5.6235v12.857h-8.4003v-26.134h8.4003v3.8268q1.54-2.2167 3.5234-3.3368 2.0067-1.12 4.4101-1.12 2.7067 0 4.7835 1.3067 2.0767 1.3067 3.1501 3.6634z" />
|
||||
<path
|
||||
d="m146.26 32.749q-2.7768 0-4.2468 2.0067-1.4467 1.9834-1.4467 5.7402t1.4467 5.7635q1.47 1.9834 4.2468 1.9834 2.7301 0 4.1768-1.9834 1.4467-2.0067 1.4467-5.7635t-1.4467-5.7402q-1.4467-2.0067-4.1768-2.0067zm0-5.9735q6.7435 0 10.524 3.6401 3.8034 3.6401 3.8034 10.08 0 6.4402-3.8034 10.08-3.7801 3.6401-10.524 3.6401-6.7669 0-10.594-3.6401-3.8034-3.6401-3.8034-10.08 0-6.4402 3.8034-10.08 3.8268-3.6401 10.594-3.6401z" />
|
||||
<path
|
||||
d="m169.41 43.366v-15.96h8.4002v2.6134q0 2.1234-0.0233 5.3435-0.0233 3.1968-0.0233 4.2701 0 3.1501 0.16334 4.5501 0.16334 1.3767 0.56002 2.0067 0.51335 0.81669 1.33 1.26 0.84002 0.44335 1.9134 0.44335 2.6134 0 4.1068-2.0067t1.4934-5.5768v-12.904h8.3536v26.134h-8.3536v-3.7801q-1.8901 2.2867-4.0134 3.3834-2.1001 1.0734-4.6435 1.0734-4.5268 0-6.9069-2.7768-2.3567-2.7768-2.3567-8.0736z" />
|
||||
<path
|
||||
d="m233.04 37.626v15.914h-8.4003v-12.18q0-3.3834-0.16333-4.6668-0.14001-1.2834-0.51335-1.8901-0.49002-0.81669-1.33-1.26-0.84003-0.46668-1.9134-0.46668-2.6134 0-4.1068 2.0301-1.4934 2.0067-1.4934 5.5768v12.857h-8.3536v-26.134h8.3536v3.8268q1.8901-2.2867 4.0135-3.3601 2.1234-1.0967 4.6901-1.0967 4.5268 0 6.8602 2.7768 2.3567 2.7768 2.3567 8.0736z" />
|
||||
<path
|
||||
d="m252.97 19.986v7.4202h8.6103v5.9735h-8.6103v11.084q0 1.8201 0.72335 2.4734 0.72336 0.63002 2.8701 0.63002h4.2935v5.9735h-7.1636q-4.9468 0-7.0236-2.0534-2.0534-2.0767-2.0534-7.0235v-11.084h-4.1535v-5.9735h4.1535v-7.4202z" />
|
||||
</g>
|
||||
<g transform="matrix(.57894 0 0 .57432 95.638 23.862)" fill="#003087" aria-label="Another">
|
||||
<path
|
||||
d="m57.459 11.049h-14.047l-2.2167 6.3469h-9.0303l12.904-34.838h10.71l12.904 34.838h-9.0303zm-11.807-6.4635h9.5436l-4.7601-13.86z" />
|
||||
<path
|
||||
d="m102.19 1.4817v15.914h-8.4003v-12.18q0-3.3834-0.16334-4.6668-0.14-1.2834-0.51335-1.8901-0.49002-0.81669-1.33-1.26-0.84002-0.46668-1.9134-0.46668-2.6134 0-4.1068 2.0301-1.4934 2.0067-1.4934 5.5768v12.857h-8.3536v-26.134h8.3536v3.8268q1.8901-2.2867 4.0135-3.3601 2.1234-1.0967 4.6901-1.0967 4.5268 0 6.8602 2.7768 2.3567 2.7768 2.3567 8.0736z" />
|
||||
<path
|
||||
d="m125.43-3.3951q-2.7768 0-4.2468 2.0067-1.4467 1.9834-1.4467 5.7402t1.4467 5.7635q1.47 1.9834 4.2468 1.9834 2.7301 0 4.1768-1.9834 1.4467-2.0067 1.4467-5.7635t-1.4467-5.7402q-1.4467-2.0067-4.1768-2.0067zm0-5.9735q6.7435 0 10.524 3.6401 3.8034 3.6401 3.8034 10.08 0 6.4402-3.8034 10.08-3.7801 3.6401-10.524 3.6401-6.7669 0-10.594-3.6401-3.8034-3.6401-3.8034-10.08 0-6.4402 3.8034-10.08 3.8268-3.6401 10.594-3.6401z" />
|
||||
<path
|
||||
d="m157.99-16.159v7.4202h8.6103v5.9735h-8.6103v11.084q0 1.8201 0.72336 2.4734 0.72336 0.63002 2.8701 0.63002h4.2935v5.9735h-7.1636q-4.9468 0-7.0236-2.0534-2.0534-2.0767-2.0534-7.0235v-11.084h-4.1535v-5.9735h4.1535v-7.4202z" />
|
||||
<path
|
||||
d="m201.02 1.4817v15.914h-8.4002v-12.134q0-3.4301-0.16334-4.7135-0.14001-1.2834-0.51335-1.8901-0.49002-0.81669-1.33-1.26-0.84003-0.46668-1.9134-0.46668-2.6134 0-4.1068 2.0301-1.4934 2.0067-1.4934 5.5768v12.857h-8.3536v-36.308h8.3536v14q1.89-2.2867 4.0134-3.3601 2.1234-1.0967 4.6901-1.0967 4.5268 0 6.8602 2.7768 2.3567 2.7768 2.3567 8.0736z" />
|
||||
<path
|
||||
d="m237.91 4.2585v2.3801h-19.531q0.30334 2.9401 2.1234 4.4101 1.8201 1.47 5.0868 1.47 2.6367 0 5.3902-0.77002 2.7768-0.79336 5.6935-2.3801v6.4402q-2.9634 1.12-5.9268 1.6801-2.9634 0.58335-5.9268 0.58335-7.0936 0-11.037-3.5934-3.9201-3.6168-3.9201-10.127 0-6.3935 3.8501-10.057 3.8734-3.6634 10.64-3.6634 6.1602 0 9.847 3.7101 3.7101 3.7101 3.7101 9.917zm-8.5869-2.7768q0-2.3801-1.4-3.8268-1.3767-1.47-3.6168-1.47-2.4267 0-3.9434 1.3767-1.5167 1.3534-1.8901 3.9201z" />
|
||||
<path
|
||||
d="m266.64-1.6217q-1.0967-0.51335-2.1934-0.74669-1.0734-0.25667-2.1701-0.25667-3.2201 0-4.9702 2.0767-1.7267 2.0534-1.7267 5.9035v12.04h-8.3536v-26.134h8.3536v4.2935q1.61-2.5667 3.6868-3.7334 2.1001-1.19 5.0168-1.19 0.42001 0 0.91002 0.046668 0.49002 0.023334 1.4234 0.14z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
</section>
|
||||
<p>It requires <strong>a lot of time and some money</strong> to create the extension, keep up with browser
|
||||
changes, read and answer emails, bug reports and feature requests, and actually publish the final versions.
|
||||
<em>Any contribution is appreciated. Thank you!</em></p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>DownThemAll! is an extension designed for people who have to download a lot of files and do not want to click on
|
||||
each individual link.
|
||||
Subfolders and renaming masks also help you sort downloads quickly. Customizable filters will help you to rapidly
|
||||
select what links to download, </p>
|
||||
<p>What's more, DownThemAll! comes with the OneClick feature which will select files using your last used filters
|
||||
and preferences.</p>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
<p>After installing DownThemAll! there will be a new button in your browser toolbar. You can add or remove this
|
||||
button using your browser's customization preferences. The button gives you quick and easy access to DownThemAll!
|
||||
main features.</p>
|
||||
<ul>
|
||||
<li>DownThemAll! + OneClick for the current or all tabs in the current window</li>
|
||||
<li>Manually adding downloads</li>
|
||||
<li>Opening the Manager or the extension preferences</li>
|
||||
</ul>
|
||||
<p>Most user will want to start using the extension with the DownThemAll! action, which brings up a selection window
|
||||
for the current tab where you can select/filter links and media contained in the website, and select the location
|
||||
where to save files (within your Downloads directory).</p>
|
||||
<p>After you queue some downloads, the Manager will open in a new tab by default. You can change that behavior to
|
||||
not have it open automatically, or open in a new window instead of a tab.</p>
|
||||
<p>The Manager will then allow you to start, pause, cancel or remove downloads, open completed files, and more.</p>
|
||||
<p>The following video demonstrates how to add and manage, some downloads:</p>
|
||||
<figure>
|
||||
<video src="first.webm" poster="first.jpg"></video>
|
||||
</figure>
|
||||
|
||||
<p>Once you have configured your preferred settings and filters, you can speed up future queuing by using OneClick!:
|
||||
</p>
|
||||
<figure>
|
||||
<video src="oneclick.webm" poster="oneclick.jpg"></video>
|
||||
</figure>
|
||||
|
||||
|
||||
<h2>Context Menu</h2>
|
||||
<p>DownThemAll! is also available from your context menu (right clicking on elements of a website). Along with the
|
||||
usual actions, the context menus also offers way to queue individual links, images or videos.</p>
|
||||
<figure>
|
||||
<video src="onecontext.webm" poster="onecontext.jpg"></video>
|
||||
</figure>
|
||||
|
||||
<h2>Manually Adding Downloads</h2>
|
||||
<p>DownThemAll! allows to manually add downloads. If further supports batches, where links follow a certain pattern,
|
||||
e.g. "image1" to "image10".</p>
|
||||
<figure>
|
||||
<video src="batches.webm" poster="batches.jpg"></video>
|
||||
</figure>
|
||||
|
||||
<h2>Preferences</h2>
|
||||
<p>The extension also comes with a wide variety of options and customizations, including choosing a light or dark theme, choosing the translation to use, custom filters and custom network limits.</p>
|
||||
<p>All those general settings are available in the Preferences tab.</p>
|
||||
<figure>
|
||||
<video src="prefs.webm" poster="prefs.jpg"></video>
|
||||
</figure>
|
||||
<p>If DownThemAll! is not yet available in your language, consider helping out. Our <a href="https://github.com/downthemall/downthemall/blob/master/_locales/Readme.md">Translation Guide</a> explains how to translate the extension.</a></p>
|
||||
|
||||
<h2>Hope you enjoy DownThemAll!</h2>
|
||||
<p>And thanks for all your support, be it in donations, reporting bugs, helping testing or translating it to your native language!</p>
|
||||
|
||||
</article>
|
||||
|
||||
<footer>
|
||||
<svg id="logo" viewBox="0 0 16 16">
|
||||
<defs>
|
||||
<linearGradient id="b">
|
||||
<stop stop-color="#116597" offset="0" />
|
||||
<stop stop-color="#062638" offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="a">
|
||||
<stop stop-color="#fffe99" offset="0" />
|
||||
<stop stop-color="#e49218" offset=".2" />
|
||||
<stop stop-color="#116597" offset="1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="c" xlink:href="#a" gradientUnits="userSpaceOnUse" cy="17.413" cx="11.75"
|
||||
gradientTransform="matrix(.41233 0 0 1.1548 3.24 -8.74)" r="10.752" />
|
||||
<linearGradient id="h" y2="-.242" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(2.495 -6.342) scale(.6787)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="i" y2="-.242" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(2.495 -1.882) scale(.6787)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="g" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(7.08 4.18) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="f" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(7.657 1.298) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="e" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(1.851 4.18) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
<linearGradient id="d" y2="-.242" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="7.763"
|
||||
gradientTransform="translate(1.851 1.298) scale(.43866)" y1="4.218" x1="8.101" />
|
||||
</defs>
|
||||
<g stroke="#072739">
|
||||
<g stroke-width=".646">
|
||||
<path
|
||||
d="M4.561 7.134v1.515H1.996v.262l2.949 3.333 2.666 3.05h.99l2.686-3.05 2.848-3.252V8.65h-2.727V7.134H4.561z"
|
||||
fill="url(#c)" />
|
||||
<g stroke-linecap="round" stroke-dashoffset="10" fill-rule="evenodd">
|
||||
<path fill="url(#d)" d="M4.562 1.346h1.649v1.649H4.562z" />
|
||||
<path fill="url(#e)" d="M4.562 4.228h1.649v1.649H4.562z" />
|
||||
<path fill="url(#f)" d="M10.368 1.346h1.649v1.649h-1.649z" />
|
||||
<path fill="url(#g)" d="M9.792 4.228h1.649v1.649H9.792z" />
|
||||
</g>
|
||||
</g>
|
||||
<g fill-rule="evenodd" stroke-linecap="round" stroke-dashoffset="10">
|
||||
<path fill="url(#h)" d="M6.689-6.268H9.24v2.551H6.689z" transform="translate(2.874 4.614) scale(.64633)" />
|
||||
<path fill="url(#i)" d="M6.689-1.808H9.24V.743H6.689z" transform="translate(2.874 4.614) scale(.64633)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<section>
|
||||
<p>Copyright © 2007-2018 Nils Maier, Stefano Verna, Federico Parodi</p>
|
||||
<p>Copyright © 2007-2019 Nils Maier</p>
|
||||
<p>The information on this website is licensed under the
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/">
|
||||
Creative Commons Attribution-ShareAlike 4.0 International License</a>.</p>
|
||||
</section>
|
||||
<a id="homepage" href="https://downthemall.org/">
|
||||
<svg viewBox="0 0 250 32" height="20" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1.1174 0 0 1.122 -217.76 -441.22)">
|
||||
<path
|
||||
d="m277.86 402.4c-2.106 0.594-3.9921 1.3387-5.0625 3.0371-1.8782 2.9802-1.8825 0.50072-5.0625 1.0117-0.075 4.312-1.6927 7.0814-1.0117 12.148 0.251 1.099-0.2614 1.8228 2.0234 2.0254 5.078-2.0169 2.6252-12.605 9.1113-13.162 0.661 5.076 1.325 12.532 3.0371 14.174h2.0254c2.0004 0 2.4284-0.24992 2.0254-4.0449-0.157-1.478-1.5594-2.1928-2.0234-4.0508-0.436-1.744-0.20767-2.7525-1.0137-5.0625 0.20397-2.5435-3.0757-5.9543-4.0488-6.0762z" />
|
||||
<path
|
||||
d="m205.98 396.32c0.71583-0.71583 3.037 0 3.037 0 3.312 2.743 2.107 7.979 3.037 12.148 0.97 4.354 2.863 8.229 3.037 12.149-3.865 0.827-3.919-2.155-5.062-4.05-3.517 0.195-3.843 3.58-7.086 4.05 0 0-3.0927 1.1003-6.074 0-2.2496-1.309-2.6451-6.2394-0.9158-9.8255 1.7293-3.5861 5.5833-5.828 10.027-5.3615 0 0-2.1472-6.9628 0-9.11zm-7.087 21.261c5.626-0.111 9.345-2.129 9.111-8.1-5.62-4-11.843 3.748-9.111 8.1z" />
|
||||
<path
|
||||
d="m262.67 400.37c3.124 1.378 4.2238 9.6188 3.037 14.174-0.21875 0.83965-3.6 4.9793-4.049 5.062-5.6104 1.0329-9.589-0.535-13.161-2.024-3.76 0.627-4.631 4.142-10.124 3.037-2.976-2.578-3.758-15.21 2.025-15.187 0.646 5.261-2.233 8.114 0 12.149 7.523 0.437 4.919-9.254 9.111-12.149 4.209 1.404 0.0379 4.9881 2.024 8.1 0.7405 1.1602 0.55971 3.037 2.025 3.037h5.062c2.561-0.813 3.5092-3.7334 4.049-6.074 1.0218-4.4307-3.744-7.344 1e-3 -10.125z" />
|
||||
<path
|
||||
d="m229.26 404.42c4.2274 1.6428 6.429 7.195 4.049 13.161-6.917 2.7019-16.888 6.2299-17.211-1.012-0.35916-8.0617 4.062-15.685 13.162-12.149zm-9.111 11.136c2.075 3.041 7.753 2.154 10.124 0 2.323-10.366-12.448-10.366-10.124 0z" />
|
||||
<path
|
||||
d="m415.54 393.28c1.635-0.285 1.742 0.957 3.037 1.012 0.117 7.88 0.244 15.768-6.074 17.211 0.518-6.569 1.518-12.656 3.037-18.223z" />
|
||||
<path
|
||||
d="m397.32 396.32c0.901 0.449 1.811 0.89 2.024 2.025 1.436 2.366 1.224 7.424 1.013 12.148-0.182 4.063 1.111 10.063-3.037 10.124-1.656-5.939-1.656-18.356 0-24.297z" />
|
||||
<path
|
||||
d="m405.42 396.32c0.901 0.449 1.811 0.89 2.024 2.025 1.436 2.366 1.225 7.424 1.013 12.148-0.182 4.063 1.112 10.063-3.037 10.124-1.656-5.939-1.656-18.356 0-24.297z" />
|
||||
<path
|
||||
d="m386.18 405.43c1.879 2.172 2.706 5.394 3.037 9.112 0.405 2.632 3.021 3.053 3.037 6.074-3.694 0.657-5.392-0.683-7.086-2.025-1.896 2.882-11.656 3.4133-12.148 0-1.2181-8.4422 3.734-13.813 13.16-13.161zm-10.123 11.136c5.229 3.119 8.32-1.521 8.123-7.087-3.941-4.338-8.175 1.277-8.123 7.087z" />
|
||||
<path d="m412.5 414.54c2.928 0.446 4.614 2.134 5.062 5.062-3.131 3.21-8.628-1.578-5.062-5.062z" />
|
||||
<g fill="#069">
|
||||
<path
|
||||
d="m344.67 399.36c-3.462 3.288-12.431 1.068-14.174 6.074-0.212 4.938 5.378 4.072 10.124 4.05-0.763 3.287-5.684 2.415-8.099 4.05 0.194 6.297 7.9627 1.1977 12.148 3.037 2.1858 0.96059-2.6665 2.6983-4.05 3.037-0.92 0.229-7.2519 3.9961-10.124 1.013-4.022-4.1774-6.0966-12.401-2.0268-19.74 2.1561-3.8884 7.9908-2.3919 12.151-3.5449 2.1724-1.0195 4.0825-0.57137 4.051 2.024z" />
|
||||
<path
|
||||
d="m368.97 398.35c0.91935 1.2579 1.638 4.258 2.024 7.086 0.632 4.615 1.344 11.963-3.037 12.149 0.605-6.006-1.776-9.022-1.012-15.187-2.943 2.12-2.8572 7.4034-6.074 9.112-5.4038 2.8702-7.236-2.214-10.124-4.05-0.162 4.899 0.612 10.735-4.05 11.136-0.788-6.862 1.383-10.766 1.013-17.21 6.881-1.144 5.642 5.832 10.124 7.086 2.854-2.881 4.6308-6.3006 7.086-10.123 0.7296-1.1359 3.2534-1.0889 4.05 1e-3z" />
|
||||
<path
|
||||
d="m299.12 404.42c2.5659-0.14763 2.6804 4.5418 3.037 7.087 0.319 2.835 0.61 6.509-1.012 8.099-3.139 1.088-1.619-3.049-2.025-5.062-0.701-3.477-2.194-6.897 0-10.124z" />
|
||||
<path
|
||||
d="m324 395.29c-4.1284 0.12154 0.92333 9.4224-3.6191 9.1289-3.094-0.731-3.6892 1.0347-6.0742 1.0117-5.024-0.414 3.2846-5.0625 3.2846-5.0625-1.3331-0.17112-4.8554 0.83219-4.9679-2.0635-1.1226-0.18087-1.4286 0.17857-3.0288 0.0563-0.30668 3.1413-2.5989 3.3131-5.6576 3.0448-6.3424-0.5564-15.93 1.6227-20.001 5.0366-0.88745 0.20112-1.9712 0.15704-2.4673-0.0332l0.44194 2.0606c8.225-2.236 14.819-5.3086 26.322-5.0625 0.802 5.947-2.2449 15.743 3.0371 17.211 4.327-0.057 0.29512-5.5363 2.0234-9.0879 1.899-3.9024 2.4597-3.319 5.0625-4.0723 5.9836-1.7317 0.3075 9.815 5.0625 10.123 3.109 0.858 1.9194-3.3815 2.0234-5.0625 0.3-4.805 0.0473-13.108-1.0117-17.211-0.15291-0.0158-0.29651-0.0215-0.42969-0.0176z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</footer>
|
||||
<script>
|
||||
for (const video of document.querySelectorAll("video")) {
|
||||
const p = document.createElement("div");
|
||||
p.className = "play";
|
||||
video.parentElement.appendChild(p);
|
||||
video.onclick = () => {
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
p.style.display = "block";
|
||||
}
|
||||
video.onended = () => {
|
||||
p.style.display = "block";
|
||||
video.currentTime = 0;
|
||||
};
|
||||
p.onclick = () => {
|
||||
p.style.display = "none";
|
||||
video.currentTime = 0;
|
||||
video.play();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
BIN
docs/landing/oneclick.jpg
Executable file
After Width: | Height: | Size: 43 KiB |
BIN
docs/landing/oneclick.png
Executable file
After Width: | Height: | Size: 82 KiB |
BIN
docs/landing/oneclick.webm
Executable file
BIN
docs/landing/onecontext.jpg
Executable file
After Width: | Height: | Size: 47 KiB |
BIN
docs/landing/onecontext.png
Executable file
After Width: | Height: | Size: 106 KiB |
BIN
docs/landing/onecontext.webm
Executable file
BIN
docs/landing/prefs.jpg
Executable file
After Width: | Height: | Size: 48 KiB |
BIN
docs/landing/prefs.png
Executable file
After Width: | Height: | Size: 100 KiB |
BIN
docs/landing/prefs.webm
Executable file
18
docs/landing/produce.py
Executable file
@ -0,0 +1,18 @@
|
||||
import sys
|
||||
from path import Path
|
||||
from subprocess import check_call
|
||||
|
||||
options = '-map v:0 -map_metadata -1 -c:v libvpx-vp9 -deadline best -b:v 0 -crf 5 -pass 2 -row-mt 1 -vf scale=720:480:force_original_aspect_ratio=decrease:flags=spline+accurate_rnd+full_chroma_int+full_chroma_inp,pad=720:480:(ow-iw)/2:(oh-ih)/2:color=White'.split(" ")
|
||||
|
||||
for f in Path(".").files("*.mov"):
|
||||
for p in [1, 2]:
|
||||
d = f.namebase + ".webm"
|
||||
final = ["ffmpeg", "-y", "-i", f.name] + options + ["-pass", str(p), d]
|
||||
print(final)
|
||||
check_call(final)
|
||||
for f in Path(".").files("*.webm"):
|
||||
for ext in [".png", ".jpg"]:
|
||||
d = f.namebase + ext
|
||||
final = ["ffmpeg", "-y", "-i", f, "-ss", "1", "-q:v", "2", "-frames", "1", d]
|
||||
print(final)
|
||||
check_call(final)
|
BIN
docs/landing/res/ReenieBeanie-Regular.woff2
Executable file
BIN
docs/landing/res/background-tile.png
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
docs/landing/res/footerbg.png
Executable file
After Width: | Height: | Size: 10 KiB |
3
docs/landing/res/go.svg
Executable file
@ -0,0 +1,3 @@
|
||||
<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-3 3 8 5-8 5v-5-5z" fill="rgb(235, 176, 37)" fill-rule="evenodd"/>
|
||||
</svg>
|
After Width: | Height: | Size: 248 B |
55
docs/landing/res/halo.svg
Executable file
@ -0,0 +1,55 @@
|
||||
<svg version="1.1" viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<radialGradient id="d" cx="161.46" cy="-144.02" r="160.02" gradientTransform="matrix(1 0 0 .99838 0 -.23281)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#febb00" offset="0"/>
|
||||
<stop stop-color="#ffd571" stop-opacity=".49804" offset=".64286"/>
|
||||
<stop stop-color="#ffe5a7" stop-opacity="0" offset=".95153"/>
|
||||
<stop stop-color="#ffe5a7" stop-opacity="0" offset="1"/>
|
||||
</radialGradient>
|
||||
<filter id="e" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="0.32807532"/>
|
||||
</filter>
|
||||
<radialGradient id="c" cx="161.46" cy="-144.02" r="160.02" gradientTransform="matrix(1 0 0 .99838 0 -.23281)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffd14e" offset="0"/>
|
||||
<stop stop-color="#ffeeb5" stop-opacity=".49804" offset=".71429"/>
|
||||
<stop stop-color="#ffeeb5" stop-opacity=".49804" offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="b" cx="12.389" cy="11.882" r="8.199" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#e69412" offset="0"/>
|
||||
<stop stop-color="#f4c478" stop-opacity="0" offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="a" cx="11.75" cy="17.413" r="10.752" gradientTransform="matrix(3.3776 0 0 9.4595 121.98 -293.39)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fffe99" offset="0"/>
|
||||
<stop stop-color="#f69706" offset=".2"/>
|
||||
<stop stop-color="#136fa7" offset="1"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g transform="translate(-61.457 224.02)">
|
||||
<g transform="matrix(.62494 0 0 .62494 60.557 -51.015)" fill="url(#d)" fill-rule="evenodd">
|
||||
<path d="m161.46-144.02 72.75 144.62-46.61 15.146z"/>
|
||||
<path d="m161.46-144.02 143.86 74.228-28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 160.02-24.507v49.014z"/>
|
||||
<path d="m161.46-144.02 115.05-113.88 28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 26.14-159.76 46.615 15.146z"/>
|
||||
<path d="m161.46-144.02-72.755-144.61 46.615-15.146z"/>
|
||||
<path d="m161.46-144.02-143.86-74.228 28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-160.02 24.507-2.7e-6 -49.014z"/>
|
||||
<path d="m161.46-144.02-115.05 113.89-28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-26.14 159.76-46.618-15.146z"/>
|
||||
</g>
|
||||
<g transform="matrix(.59435 -.19312 .19312 .59435 93.307 -24.24)" fill="url(#c)" fill-rule="evenodd" opacity=".88">
|
||||
<path d="m161.46-144.02 72.75 144.62-46.61 15.146z"/>
|
||||
<path d="m161.46-144.02 143.86 74.228-28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 160.02-24.507v49.014z"/>
|
||||
<path d="m161.46-144.02 115.05-113.88 28.81 39.653z"/>
|
||||
<path d="m161.46-144.02 26.14-159.76 46.615 15.146z"/>
|
||||
<path d="m161.46-144.02-72.755-144.61 46.615-15.146z"/>
|
||||
<path d="m161.46-144.02-143.86-74.228 28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-160.02 24.507-2.7e-6 -49.014z"/>
|
||||
<path d="m161.46-144.02-115.05 113.89-28.81-39.653z"/>
|
||||
<path d="m161.46-144.02-26.14 159.76-46.618-15.146z"/>
|
||||
</g>
|
||||
<path transform="matrix(7.0729 0 0 7.4575 73.647 -229.61)" d="m20.588 11.882a8.199 8.199 0 1 1-16.398 0 8.199 8.199 0 1 1 16.398 0z" fill="url(#b)" filter="url(#e)"/>
|
||||
<path d="m133.31-207.91v55.137h-17.218c-6.602-0.97334-3.1317 2.1508-3.1317 2.1508l23.494 26.472 18.653 21.422c1.9951 2.9976 3.1147 2.8985 6.486 2.8985 3.5137 0.43957 4.2275-0.28033 6.4176-2.757l19.07-21.564 22.666-25.81c3.6888-3.9672-2.8847-2.8126-2.8847-2.8126h-18.954v-54.904c0.65539-9.337-1.0536-9.0538-8.6714-8.7929h-37.608c-7.6308-0.26075-8.3181 0.1211-8.3181 8.5607z" fill="url(#a)" stroke="#0a3b56" stroke-width="5.1532"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
79
lib/api.ts
@ -5,25 +5,33 @@ import { TYPE_LINK, TYPE_MEDIA } from "./constants";
|
||||
import { filters } from "./filters";
|
||||
import { Prefs } from "./prefs";
|
||||
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 { select } from "./select";
|
||||
import { single } from "./single";
|
||||
import { Notification } from "./notifications";
|
||||
import { MASK, FASTFILTER } from "./recentlist";
|
||||
import { MASK, FASTFILTER, SUBFOLDER } from "./recentlist";
|
||||
import { openManager } from "./windowutils";
|
||||
import { _ } from "./i18n";
|
||||
|
||||
const MAX_BATCH = 10000;
|
||||
|
||||
export const API = new class {
|
||||
async filter(arr: any, type: number) {
|
||||
return (await filters()).filterItemsByType(arr, type);
|
||||
export interface QueueOptions {
|
||||
mask?: string;
|
||||
subfolder?: string;
|
||||
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) {
|
||||
await MASK.init();
|
||||
async queue(items: BaseItem[], options: QueueOptions) {
|
||||
await Promise.all([MASK.init(), SUBFOLDER.init()]);
|
||||
const {mask = MASK.current} = options;
|
||||
const {subfolder = SUBFOLDER.current} = options;
|
||||
|
||||
const {paused = false} = options;
|
||||
const defaults: any = {
|
||||
@ -36,13 +44,11 @@ export const API = new class {
|
||||
fileName: null,
|
||||
title: "",
|
||||
description: "",
|
||||
fromMetalink: false,
|
||||
startDate: new Date(),
|
||||
hashes: [],
|
||||
private: false,
|
||||
postData: null,
|
||||
cleanRequest: false,
|
||||
mask,
|
||||
subfolder,
|
||||
date: Date.now(),
|
||||
paused
|
||||
};
|
||||
@ -54,7 +60,7 @@ export const API = new class {
|
||||
}
|
||||
return currentBatch;
|
||||
});
|
||||
items = items.map((i: any) => {
|
||||
items = items.map(i => {
|
||||
delete i.idx;
|
||||
return new Item(i, defaults);
|
||||
});
|
||||
@ -79,7 +85,7 @@ export const API = new class {
|
||||
}
|
||||
}
|
||||
|
||||
sanity(links: any[], media: any[]) {
|
||||
sanity(links: BaseItem[], media: BaseItem[]) {
|
||||
if (!links.length && !media.length) {
|
||||
new Notification(null, _("no-links"));
|
||||
return false;
|
||||
@ -87,48 +93,57 @@ export const API = new class {
|
||||
return true;
|
||||
}
|
||||
|
||||
async turbo(links: any[], media: any[]) {
|
||||
async turbo(links: BaseItem[], media: BaseItem[]) {
|
||||
if (!this.sanity(links, media)) {
|
||||
return false;
|
||||
}
|
||||
const selected = makeUniqueItems([
|
||||
await API.filter(links, TYPE_LINK),
|
||||
await API.filter(media, TYPE_MEDIA),
|
||||
]);
|
||||
const type = await Prefs.get("last-type", "links");
|
||||
const items = await (async () => {
|
||||
if (type === "links") {
|
||||
return await API.filter(links, TYPE_LINK);
|
||||
}
|
||||
return await API.filter(media, TYPE_MEDIA);
|
||||
})();
|
||||
const selected = makeUniqueItems([items]);
|
||||
if (!selected.length) {
|
||||
return await this.regular(links, media);
|
||||
}
|
||||
return await this.queue(selected, {paused: await Prefs.get("add-paused")});
|
||||
}
|
||||
|
||||
async regularInternal(selected: any) {
|
||||
if (selected.mask && !selected.maskOnce) {
|
||||
async regularInternal(selected: BaseItem[], options: any) {
|
||||
if (options.mask && !options.maskOnce) {
|
||||
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.push(selected.fast);
|
||||
await FASTFILTER.push(options.fast);
|
||||
}
|
||||
const {items} = selected;
|
||||
delete selected.items;
|
||||
return await this.queue(items, selected);
|
||||
if (typeof options.subfolder === "string" && !options.subfolderOnce) {
|
||||
await SUBFOLDER.init();
|
||||
await SUBFOLDER.push(options.subfolder);
|
||||
}
|
||||
if (typeof options.type === "string") {
|
||||
await Prefs.set("last-type", options.type);
|
||||
}
|
||||
return await this.queue(selected, options);
|
||||
}
|
||||
|
||||
async regular(links: any[], media: any[]) {
|
||||
async regular(links: BaseItem[], media: BaseItem[]) {
|
||||
if (!this.sanity(links, media)) {
|
||||
return false;
|
||||
}
|
||||
const selected = await select(links, media);
|
||||
return this.regularInternal(selected);
|
||||
const {items, options} = await select(links, media);
|
||||
return this.regularInternal(items, options);
|
||||
}
|
||||
|
||||
async singleTurbo(item: any) {
|
||||
async singleTurbo(item: BaseItem) {
|
||||
return await this.queue([item], {paused: await Prefs.get("add-paused")});
|
||||
}
|
||||
|
||||
async singleRegular(item: any) {
|
||||
const selected = await single(item);
|
||||
return this.regularInternal(selected);
|
||||
async singleRegular(item: BaseItem | null) {
|
||||
const {items, options} = await single(item);
|
||||
return this.regularInternal(items, options);
|
||||
}
|
||||
}();
|
||||
|
@ -5,7 +5,7 @@ import { ALLOWED_SCHEMES, TRANSFERABLE_PROPERTIES } from "./constants";
|
||||
import { API } from "./api";
|
||||
import { Finisher, makeUniqueItems } from "./item";
|
||||
import { Prefs } from "./prefs";
|
||||
import { _ } from "./i18n";
|
||||
import { _, locale } from "./i18n";
|
||||
import { openPrefs, openManager } from "./windowutils";
|
||||
import { filters } from "./filters";
|
||||
import { getManager } from "./manager/man";
|
||||
@ -13,14 +13,46 @@ import {
|
||||
browserAction as action,
|
||||
menus as _menus, contextMenus as _cmenus,
|
||||
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,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
OnInstalled,
|
||||
} from "./browser";
|
||||
import { Bus } from "./bus";
|
||||
import { filterInSitu } from "./util";
|
||||
|
||||
|
||||
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 {
|
||||
if (tab && tab.incognito && msg) {
|
||||
msg.private = tab.incognito;
|
||||
}
|
||||
const res = await tabs.executeScript(tab.id, {
|
||||
file,
|
||||
allFrames: true,
|
||||
@ -48,6 +80,14 @@ async function runContentJob(tab: any, file: string, msg: any) {
|
||||
}
|
||||
}
|
||||
|
||||
type SelectionOptions = {
|
||||
selectionOnly: boolean;
|
||||
allTabs: boolean;
|
||||
turbo: boolean;
|
||||
tab: Tab;
|
||||
};
|
||||
|
||||
|
||||
class Handler {
|
||||
async processResults(turbo = false, results: any[]) {
|
||||
const links = this.makeUnique(results, "links");
|
||||
@ -59,305 +99,574 @@ class Handler {
|
||||
return makeUniqueItems(
|
||||
results.filter(e => e[what]).map(e => {
|
||||
const finisher = new Finisher(e);
|
||||
return e[what].
|
||||
map((item: any) => finisher.finish(item)).
|
||||
filter((i: any) => i);
|
||||
return filterInSitu(e[what].
|
||||
map((item: any) => finisher.finish(item)), e => !!e);
|
||||
}));
|
||||
}
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
action.onClicked.addListener(this.onClicked);
|
||||
function getMajor(version?: string) {
|
||||
if (!version) {
|
||||
return "";
|
||||
}
|
||||
|
||||
async onClicked(tab: {id: number}) {
|
||||
if (!tab.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.processResults(
|
||||
await Prefs.get("global-turbo"),
|
||||
await runContentJob(
|
||||
tab, "/bundles/content-gather.js", {
|
||||
type: "DTA:gather",
|
||||
selectionOnly: false,
|
||||
textLinks: await Prefs.get("text-links", true),
|
||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
||||
transferable: TRANSFERABLE_PROPERTIES,
|
||||
}));
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
const match = version.match(/^\d+\.\d+/);
|
||||
if (!match) {
|
||||
return "";
|
||||
}
|
||||
}();
|
||||
return match[0];
|
||||
}
|
||||
|
||||
new class Menus extends Handler {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
menus.create({
|
||||
id: "DTARegular",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular"),
|
||||
runtime.onInstalled.addListener(({reason, previousVersion}: OnInstalled) => {
|
||||
const {version} = runtime.getManifest();
|
||||
const major = getMajor(version);
|
||||
const prevMajor = getMajor(previousVersion);
|
||||
if (reason === "update" && major !== prevMajor) {
|
||||
tabs.create({
|
||||
url: `https://about.downthemall.org/changelog/?cur=${major}&prev=${prevMajor}`,
|
||||
});
|
||||
menus.create({
|
||||
id: "DTATurbo",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTARegularLink",
|
||||
contexts: ["link"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.link"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTATurboLink",
|
||||
contexts: ["link"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.link"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTARegularImage",
|
||||
contexts: ["image"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.image"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTATurboImage",
|
||||
contexts: ["image"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.image"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTARegularMedia",
|
||||
contexts: ["video", "audio"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.media"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTATurboMedia",
|
||||
contexts: ["video", "audio"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.media"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTARegularSelection",
|
||||
contexts: ["selection"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.selection"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTATurboSelection",
|
||||
contexts: ["selection"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.selection"),
|
||||
});
|
||||
menus.create({
|
||||
id: "sep-1",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
type: "separator"
|
||||
});
|
||||
menus.create({
|
||||
id: "DTAManager",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
16: "/style/button-manager.png",
|
||||
32: "/style/button-manager@2x.png",
|
||||
},
|
||||
title: _("manager.short"),
|
||||
});
|
||||
menus.create({
|
||||
id: "DTAPrefs",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
16: "/style/settings.svg",
|
||||
32: "/style/settings.svg",
|
||||
64: "/style/settings.svg",
|
||||
128: "/style/settings.svg",
|
||||
},
|
||||
title: _("prefs.short"),
|
||||
});
|
||||
menus.onClicked.addListener(this.onClicked);
|
||||
}
|
||||
else if (reason === "install") {
|
||||
tabs.create({
|
||||
url: `https://about.downthemall.org/4.0/?cur=${major}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
*makeSingleItemList(url: string, results: any[]) {
|
||||
for (const result of results) {
|
||||
const finisher = new Finisher(result);
|
||||
for (const list of [result.links, result.media]) {
|
||||
for (const e of list) {
|
||||
if (e.url !== url) {
|
||||
continue;
|
||||
locale.then(() => {
|
||||
const menuHandler = new class Menus extends Handler {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
const alls = new Map<string, string[]>();
|
||||
const mcreate = (options: any) => {
|
||||
if (CHROME) {
|
||||
delete options.icons;
|
||||
options.contexts = options.contexts.
|
||||
filter((e: string) => CHROME_CONTEXTS.has(e));
|
||||
if (!options.contexts.length) {
|
||||
return;
|
||||
}
|
||||
const finished = finisher.finish(e);
|
||||
if (!finished) {
|
||||
continue;
|
||||
}
|
||||
if (options.contexts.includes("all")) {
|
||||
alls.set(options.id, options.contexts);
|
||||
}
|
||||
menus.create(options);
|
||||
};
|
||||
mcreate({
|
||||
id: "DTARegularLink",
|
||||
contexts: ["link"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.link"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTATurboLink",
|
||||
contexts: ["link"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.link"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTARegularImage",
|
||||
contexts: ["image"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.image"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTATurboImage",
|
||||
contexts: ["image"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.image"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTARegularMedia",
|
||||
contexts: ["video", "audio"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.media"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTATurboMedia",
|
||||
contexts: ["video", "audio"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.media"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTARegularSelection",
|
||||
contexts: ["selection"],
|
||||
icons: {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
},
|
||||
title: _("dta.regular.selection"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTATurboSelection",
|
||||
contexts: ["selection"],
|
||||
icons: {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
},
|
||||
title: _("dta.turbo.selection"),
|
||||
});
|
||||
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",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
type: "separator"
|
||||
});
|
||||
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",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
16: "/style/button-manager.png",
|
||||
32: "/style/button-manager@2x.png",
|
||||
},
|
||||
title: _("manager.short"),
|
||||
});
|
||||
mcreate({
|
||||
id: "DTAPrefs",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
16: "/style/settings.svg",
|
||||
32: "/style/settings.svg",
|
||||
64: "/style/settings.svg",
|
||||
128: "/style/settings.svg",
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
*makeSingleItemList(url: string, results: any[]) {
|
||||
for (const result of results) {
|
||||
const finisher = new Finisher(result);
|
||||
for (const list of [result.links, result.media]) {
|
||||
for (const e of list) {
|
||||
if (e.url !== url) {
|
||||
continue;
|
||||
}
|
||||
const finished = finisher.finish(e);
|
||||
if (!finished) {
|
||||
continue;
|
||||
}
|
||||
yield finished;
|
||||
}
|
||||
yield finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findSingleItem(tab: any, url: string, turbo = false) {
|
||||
if (!url) {
|
||||
return;
|
||||
async findSingleItem(tab: Tab, url: string, turbo = false) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
const results = await runContentJob(
|
||||
tab, "/bundles/content-gather.js", {
|
||||
type: "DTA:gather",
|
||||
selectionOnly: false,
|
||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
||||
transferable: TRANSFERABLE_PROPERTIES,
|
||||
});
|
||||
const found = Array.from(this.makeSingleItemList(url, results));
|
||||
const unique = makeUniqueItems([found]);
|
||||
if (!unique.length) {
|
||||
return;
|
||||
}
|
||||
const [item] = unique;
|
||||
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
||||
}
|
||||
const results = await runContentJob(
|
||||
tab, "/bundles/content-gather.js", {
|
||||
type: "DTA:gather",
|
||||
selectionOnly: false,
|
||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
||||
transferable: TRANSFERABLE_PROPERTIES,
|
||||
|
||||
onClicked(info: MenuClickInfo, tab: Tab) {
|
||||
if (!tab.id) {
|
||||
return;
|
||||
}
|
||||
const {menuItemId} = info;
|
||||
const {[`onClicked${menuItemId}`]: handler}: any = this;
|
||||
if (!handler) {
|
||||
console.error("Invalid Handler for", menuItemId);
|
||||
return;
|
||||
}
|
||||
const rv: Promise<void> | void = handler.call(this, info, tab);
|
||||
if (rv && rv.catch) {
|
||||
rv.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
async emulate(action: string) {
|
||||
const tab = await tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const found = Array.from(this.makeSingleItemList(url, results));
|
||||
const unique = makeUniqueItems([found]);
|
||||
if (!unique.length) {
|
||||
return;
|
||||
if (!tab || !tab.length) {
|
||||
return;
|
||||
}
|
||||
this.onClicked({
|
||||
menuItemId: action
|
||||
}, tab[0]);
|
||||
}
|
||||
const [item] = unique;
|
||||
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
||||
}
|
||||
|
||||
onClicked(info: any, tab: any) {
|
||||
if (!tab.id) {
|
||||
return;
|
||||
async onClickedDTARegular(info: MenuClickInfo, tab: Tab) {
|
||||
return await this.performSelection({
|
||||
selectionOnly: false,
|
||||
allTabs: false,
|
||||
turbo: false,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
const {menuItemId} = info;
|
||||
const {[`onClicked${menuItemId}`]: handler}: any = this;
|
||||
if (!handler) {
|
||||
console.error("Invalid Handler for", menuItemId);
|
||||
return;
|
||||
|
||||
async onClickedDTARegularAll(info: MenuClickInfo, tab: Tab) {
|
||||
return await this.performSelection({
|
||||
selectionOnly: false,
|
||||
allTabs: true,
|
||||
turbo: false,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
handler.call(this, info, tab).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,
|
||||
}));
|
||||
async onClickedDTARegularSelection(info: MenuClickInfo, tab: Tab) {
|
||||
return await this.performSelection({
|
||||
selectionOnly: true,
|
||||
allTabs: false,
|
||||
turbo: false,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
async onClickedDTATurbo(info: MenuClickInfo, tab: Tab) {
|
||||
return await this.performSelection({
|
||||
selectionOnly: false,
|
||||
allTabs: false,
|
||||
turbo: true,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onClickedDTARegular(info: any, tab: any) {
|
||||
return await this.onClickedDTARegularInternal(false, info, tab);
|
||||
}
|
||||
|
||||
async onClickedDTARegularSelection(info: any, tab: any) {
|
||||
return await this.onClickedDTARegularInternal(true, info, tab);
|
||||
}
|
||||
|
||||
async onClickedDTATurboInternal(selectionOnly: boolean, info: any, tab: any) {
|
||||
try {
|
||||
await this.processResults(
|
||||
true,
|
||||
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,
|
||||
}));
|
||||
async onClickedDTATurboAll(info: MenuClickInfo, tab: Tab) {
|
||||
return await this.performSelection({
|
||||
selectionOnly: false,
|
||||
allTabs: true,
|
||||
turbo: true,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
async onClickedDTATurboSelection(info: MenuClickInfo, tab: Tab) {
|
||||
return await this.performSelection({
|
||||
selectionOnly: true,
|
||||
allTabs: false,
|
||||
turbo: true,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onClickedDTATurbo(info: any, tab: any) {
|
||||
return await this.onClickedDTATurboInternal(false, info, tab);
|
||||
}
|
||||
async onClickedDTARegularLink(info: MenuClickInfo, tab: Tab) {
|
||||
if (!info.linkUrl) {
|
||||
return;
|
||||
}
|
||||
await this.findSingleItem(tab, info.linkUrl, false);
|
||||
}
|
||||
|
||||
async onClickedDTATurboSelection(info: any, tab: any) {
|
||||
return await this.onClickedDTATurboInternal(false, info, tab);
|
||||
}
|
||||
async onClickedDTATurboLink(info: MenuClickInfo, tab: Tab) {
|
||||
if (!info.linkUrl) {
|
||||
return;
|
||||
}
|
||||
await this.findSingleItem(tab, info.linkUrl, true);
|
||||
}
|
||||
|
||||
async onClickedDTARegularLink(info: any, tab: any) {
|
||||
return await this.findSingleItem(tab, info.linkUrl, false);
|
||||
}
|
||||
async onClickedDTARegularImage(info: MenuClickInfo, tab: Tab) {
|
||||
if (!info.srcUrl) {
|
||||
return;
|
||||
}
|
||||
await this.findSingleItem(tab, info.srcUrl, false);
|
||||
}
|
||||
|
||||
async onClickedDTATurboLink(info: any, tab: any) {
|
||||
return await this.findSingleItem(tab, info.linkUrl, true);
|
||||
}
|
||||
async onClickedDTATurboImage(info: MenuClickInfo, tab: Tab) {
|
||||
if (!info.srcUrl) {
|
||||
return;
|
||||
}
|
||||
await this.findSingleItem(tab, info.srcUrl, true);
|
||||
}
|
||||
|
||||
async onClickedDTARegularImage(info: any, tab: any) {
|
||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
||||
}
|
||||
async onClickedDTARegularMedia(info: MenuClickInfo, tab: Tab) {
|
||||
if (!info.srcUrl) {
|
||||
return;
|
||||
}
|
||||
await this.findSingleItem(tab, info.srcUrl, false);
|
||||
}
|
||||
|
||||
async onClickedDTATurboImage(info: any, tab: any) {
|
||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
||||
}
|
||||
async onClickedDTATurboMedia(info: MenuClickInfo, tab: Tab) {
|
||||
if (!info.srcUrl) {
|
||||
return;
|
||||
}
|
||||
await this.findSingleItem(tab, info.srcUrl, true);
|
||||
}
|
||||
|
||||
async onClickedDTARegularMedia(info: any, tab: any) {
|
||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
||||
}
|
||||
onClickedDTAAdd() {
|
||||
API.singleRegular(null);
|
||||
}
|
||||
|
||||
async onClickedDTATurboMedia(info: any, tab: any) {
|
||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
||||
}
|
||||
async onClickedDTAManager() {
|
||||
await openManager();
|
||||
}
|
||||
|
||||
async onClickedDTAManager() {
|
||||
await openManager();
|
||||
}
|
||||
async onClickedDTAPrefs() {
|
||||
await openPrefs();
|
||||
}
|
||||
}();
|
||||
|
||||
async onClickedDTAPrefs() {
|
||||
await openPrefs();
|
||||
}
|
||||
}();
|
||||
new class Action extends Handler {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
action.onClicked.addListener(this.onClicked);
|
||||
Prefs.get("button-type", false).then(v => this.adjust(v));
|
||||
Prefs.on("button-type", (prefs, key, value) => {
|
||||
this.adjust(value);
|
||||
});
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
await Prefs.set("last-run", new Date());
|
||||
await filters();
|
||||
await getManager();
|
||||
})().catch(ex => {
|
||||
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
||||
adjust(type: string) {
|
||||
action.setPopup({
|
||||
popup: type !== "popup" ? "" : "/windows/popup.html"
|
||||
});
|
||||
let icons;
|
||||
switch (type) {
|
||||
case "popup":
|
||||
icons = {
|
||||
16: "/style/icon16.png",
|
||||
32: "/style/icon32.png",
|
||||
48: "/style/icon48.png",
|
||||
64: "/style/icon64.png",
|
||||
96: "/style/icon96.png",
|
||||
128: "/style/icon128.png",
|
||||
256: "/style/icon256.png"
|
||||
};
|
||||
break;
|
||||
|
||||
case "dta":
|
||||
icons = {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
};
|
||||
break;
|
||||
|
||||
case "turbo":
|
||||
icons = {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
};
|
||||
break;
|
||||
|
||||
case "manager":
|
||||
icons = {
|
||||
16: "/style/button-manager.png",
|
||||
32: "/style/button-manager@2x.png",
|
||||
};
|
||||
break;
|
||||
}
|
||||
action.setIcon({path: icons});
|
||||
}
|
||||
|
||||
async onClicked() {
|
||||
switch (await Prefs.get("button-type")) {
|
||||
case "popup":
|
||||
break;
|
||||
|
||||
case "dta":
|
||||
menuHandler.emulate("DTARegular");
|
||||
break;
|
||||
|
||||
case "turbo":
|
||||
menuHandler.emulate("DTATurbo");
|
||||
break;
|
||||
|
||||
case "manager":
|
||||
menuHandler.emulate("DTAManager");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
|
||||
Bus.on("do-regular", () => menuHandler.emulate("DTARegular"));
|
||||
Bus.on("do-regular-all", () => menuHandler.emulate("DTARegularAll"));
|
||||
Bus.on("do-turbo", () => menuHandler.emulate("DTATurbo"));
|
||||
Bus.on("do-turbo-all", () => menuHandler.emulate("DTATurboAll"));
|
||||
Bus.on("do-single", () => API.singleRegular(null));
|
||||
Bus.on("open-manager", () => openManager(true));
|
||||
Bus.on("open-prefs", () => openPrefs());
|
||||
|
||||
(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();
|
||||
}
|
||||
|
||||
await Prefs.set("last-run", new Date());
|
||||
await filters();
|
||||
await getManager();
|
||||
})().catch(ex => {
|
||||
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
||||
});
|
||||
});
|
||||
|
@ -73,7 +73,7 @@ class Numeral implements Generator {
|
||||
this.digits = dir ? rawpieces[0].length : rawpieces[1].length;
|
||||
this.length = Math.floor(
|
||||
(this.stop - this.start + (dir ? 1 : -1)) / this.step);
|
||||
this.preview = this[Symbol.iterator]().next().value;
|
||||
this.preview = this[Symbol.iterator]().next().value as string;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
@ -93,14 +93,68 @@ class Numeral implements Generator {
|
||||
}
|
||||
}
|
||||
|
||||
class Character implements Generator {
|
||||
public readonly start: number;
|
||||
|
||||
public readonly stop: number;
|
||||
|
||||
public readonly step: number;
|
||||
|
||||
public readonly length: number;
|
||||
|
||||
public readonly preview: string;
|
||||
|
||||
constructor(str: string) {
|
||||
const rawpieces = str.split(":").map(e => e.trim());
|
||||
const pieces = rawpieces.map((e, i) => {
|
||||
if (i === 2) {
|
||||
return reallyParseInt(e);
|
||||
}
|
||||
if (e.length > 1) {
|
||||
throw new Error("Malformed Character sequence");
|
||||
}
|
||||
return e.charCodeAt(0);
|
||||
});
|
||||
if (pieces.length < 2) {
|
||||
throw new Error("Invalid input");
|
||||
}
|
||||
const [start, stop, step] = pieces;
|
||||
if (step === 0) {
|
||||
throw new Error("Invalid step");
|
||||
}
|
||||
this.step = !step ? 1 : step;
|
||||
const dir = this.step > 0;
|
||||
if (dir && start > stop) {
|
||||
throw new Error("Invalid sequence");
|
||||
}
|
||||
else if (!dir && start < stop) {
|
||||
throw new Error("Invalid sequence");
|
||||
}
|
||||
this.start = start;
|
||||
this.stop = stop;
|
||||
this.length = Math.floor(
|
||||
(this.stop - this.start + (dir ? 1 : -1)) / this.step);
|
||||
this.preview = this[Symbol.iterator]().next().value as string;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
const {start, stop, step} = this;
|
||||
const dir = step > 0;
|
||||
for (let i = start; (dir ? i <= stop : i >= stop); i += step) {
|
||||
yield String.fromCharCode(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BatchGenerator implements Generator {
|
||||
private readonly gens: Generator[];
|
||||
|
||||
public readonly hasInvalid: boolean;
|
||||
|
||||
public readonly length: any;
|
||||
public readonly length: number;
|
||||
|
||||
public readonly preview: any;
|
||||
public readonly preview: string;
|
||||
|
||||
constructor(str: string) {
|
||||
this.gens = [];
|
||||
@ -120,9 +174,14 @@ export class BatchGenerator implements Generator {
|
||||
try {
|
||||
this.gens.push(new Numeral(tok));
|
||||
}
|
||||
catch (ex) {
|
||||
this.gens.push(new Literal(`[${tok}]`));
|
||||
this.hasInvalid = true;
|
||||
catch {
|
||||
try {
|
||||
this.gens.push(new Character(tok));
|
||||
}
|
||||
catch {
|
||||
this.gens.push(new Literal(`[${tok}]`));
|
||||
this.hasInvalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (str) {
|
||||
|
110
lib/browser.ts
@ -3,15 +3,117 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const polyfill = require("webextension-polyfill");
|
||||
|
||||
export const {i18n} = polyfill;
|
||||
export const {extension} = polyfill;
|
||||
export const {notifications} = polyfill;
|
||||
interface ExtensionListener {
|
||||
addListener: (listener: Function) => void;
|
||||
removeListener: (listener: Function) => void;
|
||||
}
|
||||
|
||||
export interface MessageSender {
|
||||
readonly tab?: Tab;
|
||||
readonly frameId?: number;
|
||||
readonly id?: number;
|
||||
readonly url?: string;
|
||||
readonly tlsChannelId?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Tab {
|
||||
readonly id?: number;
|
||||
readonly incognito?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuClickInfo {
|
||||
readonly menuItemId: string | number;
|
||||
readonly button?: number;
|
||||
readonly linkUrl?: string;
|
||||
readonly srcUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface RawPort {
|
||||
readonly error: any;
|
||||
readonly name: string;
|
||||
readonly sender?: MessageSender;
|
||||
readonly onDisconnect: ExtensionListener;
|
||||
readonly onMessage: ExtensionListener;
|
||||
disconnect: () => void;
|
||||
postMessage: (message: any) => void;
|
||||
}
|
||||
|
||||
interface WebRequestFilter {
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
interface WebRequestListener {
|
||||
addListener(
|
||||
callback: Function,
|
||||
filter: WebRequestFilter,
|
||||
extraInfoSpec: string[]
|
||||
): void;
|
||||
removeListener(callback: Function): void;
|
||||
}
|
||||
|
||||
type Header = {name: string; value: string};
|
||||
|
||||
export interface DownloadOptions {
|
||||
conflictAction: string;
|
||||
filename?: string;
|
||||
saveAs: boolean;
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
incognito?: boolean;
|
||||
headers: Header[];
|
||||
}
|
||||
|
||||
export interface DownloadsQuery {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface Downloads {
|
||||
download(download: DownloadOptions): Promise<number>;
|
||||
open(manId: number): Promise<void>;
|
||||
show(manId: number): Promise<void>;
|
||||
pause(manId: number): Promise<void>;
|
||||
resume(manId: number): Promise<void>;
|
||||
cancel(manId: number): Promise<void>;
|
||||
erase(query: DownloadsQuery): Promise<void>;
|
||||
search(query: DownloadsQuery): Promise<any[]>;
|
||||
getFileIcon(id: number, options?: any): Promise<string>;
|
||||
setShelfEnabled(state: boolean): void;
|
||||
removeFile(manId: number): Promise<void>;
|
||||
readonly onCreated: ExtensionListener;
|
||||
readonly onChanged: ExtensionListener;
|
||||
readonly onErased: ExtensionListener;
|
||||
readonly onDeterminingFilename?: ExtensionListener;
|
||||
}
|
||||
|
||||
interface WebRequest {
|
||||
readonly onBeforeSendHeaders: WebRequestListener;
|
||||
readonly onSendHeaders: WebRequestListener;
|
||||
readonly onHeadersReceived: WebRequestListener;
|
||||
}
|
||||
|
||||
export interface OnInstalled {
|
||||
readonly reason: string;
|
||||
readonly previousVersion?: string;
|
||||
readonly temporary: boolean;
|
||||
}
|
||||
|
||||
export const {browserAction} = polyfill;
|
||||
export const {contextMenus} = polyfill;
|
||||
export const {downloads} = polyfill;
|
||||
export const {downloads}: {downloads: Downloads} = polyfill;
|
||||
export const {extension} = polyfill;
|
||||
export const {history} = polyfill;
|
||||
export const {menus} = polyfill;
|
||||
export const {notifications} = polyfill;
|
||||
export const {runtime} = polyfill;
|
||||
export const {sessions} = polyfill;
|
||||
export const {storage} = polyfill;
|
||||
export const {tabs} = polyfill;
|
||||
export const {webNavigation} = polyfill;
|
||||
export const {webRequest}: {webRequest: WebRequest} = polyfill;
|
||||
export const {windows} = polyfill;
|
||||
export const {theme} = polyfill;
|
||||
|
||||
export const CHROME = navigator.appVersion.includes("Chrome/");
|
||||
|
66
lib/bus.ts
@ -2,50 +2,55 @@
|
||||
// License: MIT
|
||||
|
||||
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 {
|
||||
private port: any;
|
||||
private port: RawPort | null;
|
||||
|
||||
constructor(port: any) {
|
||||
private disconnected = false;
|
||||
|
||||
constructor(port: RawPort) {
|
||||
super();
|
||||
this.port = port;
|
||||
|
||||
let disconnected = false;
|
||||
let tabListener: any;
|
||||
const disconnect = () => {
|
||||
if (tabListener) {
|
||||
tabs.onRemoved.removeListener(tabListener);
|
||||
tabListener = null;
|
||||
}
|
||||
if (disconnected) {
|
||||
return;
|
||||
}
|
||||
disconnected = true;
|
||||
this.port = null; // Break the cycle
|
||||
this.emit("disconnect", this, port);
|
||||
};
|
||||
// Nasty firefox bug, thus listen for tab removal explicitly
|
||||
if (port.sender && port.sender.tab && port.sender.tab.id) {
|
||||
const otherTabId = port.sender.tab.id;
|
||||
const tabListener = function(tabId: number) {
|
||||
const tabListener = (tabId: number) => {
|
||||
if (tabId !== otherTabId) {
|
||||
return;
|
||||
}
|
||||
disconnect();
|
||||
this.disconnect();
|
||||
};
|
||||
tabs.onRemoved.addListener(tabListener);
|
||||
}
|
||||
port.onMessage.addListener(this.onMessage.bind(this));
|
||||
port.onDisconnect.addListener(disconnect);
|
||||
port.onDisconnect.addListener(this.disconnect.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.disconnected) {
|
||||
return;
|
||||
}
|
||||
this.disconnected = true;
|
||||
const {port} = this;
|
||||
this.port = null; // Break the cycle
|
||||
this.emit("disconnect", this, port);
|
||||
}
|
||||
|
||||
get name() {
|
||||
if (!this.port) {
|
||||
return null;
|
||||
}
|
||||
return this.port.name;
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -53,6 +58,9 @@ export class Port extends EventEmitter {
|
||||
}
|
||||
|
||||
post(msg: string, ...data: any[]) {
|
||||
if (!this.port) {
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
this.port.postMessage({msg});
|
||||
return;
|
||||
@ -64,14 +72,17 @@ export class Port extends EventEmitter {
|
||||
}
|
||||
|
||||
onMessage(message: any) {
|
||||
if (Object.keys(message).includes("msg")) {
|
||||
this.emit(message.msg, message);
|
||||
if (!this.port) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(message)) {
|
||||
message.forEach(this.onMessage, this);
|
||||
return;
|
||||
}
|
||||
if (Object.keys(message).includes("msg")) {
|
||||
this.emit(message.msg, message);
|
||||
return;
|
||||
}
|
||||
if (typeof message === "string") {
|
||||
this.emit(message);
|
||||
return;
|
||||
@ -99,7 +110,7 @@ export const Bus = new class extends EventEmitter {
|
||||
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;
|
||||
if (!type) {
|
||||
type = msg;
|
||||
@ -107,11 +118,14 @@ export const Bus = new class extends EventEmitter {
|
||||
this.emit(type, msg, callback);
|
||||
}
|
||||
|
||||
onConnect(port: any) {
|
||||
onConnect(port: RawPort) {
|
||||
if (!port.name) {
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
this.ports.emit(port.name, new Port(port));
|
||||
const wrapped = new Port(port);
|
||||
if (!this.ports.emit(port.name, wrapped)) {
|
||||
wrapped.disconnect();
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
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(/^[^']*'/, ""));
|
||||
}
|
||||
}
|
22
lib/db.ts
@ -1,5 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Download } from "./manager/download";
|
||||
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
|
||||
|
||||
// License: MIT
|
||||
|
||||
const VERSION = 1;
|
||||
@ -40,12 +46,12 @@ export const DB = new class DB {
|
||||
});
|
||||
}
|
||||
|
||||
getAllInternal(resolve: (items: any[]) => void, reject: Function) {
|
||||
getAllInternal(resolve: (items: BaseItem[]) => void, reject: Function) {
|
||||
if (!this.db) {
|
||||
reject(new Error("db closed"));
|
||||
return;
|
||||
}
|
||||
const items: any[] = [];
|
||||
const items: BaseItem[] = [];
|
||||
const transaction = this.db.transaction(STORE, "readonly");
|
||||
transaction.onerror = ex => reject(ex);
|
||||
const store = transaction.objectStore(STORE);
|
||||
@ -66,7 +72,7 @@ export const DB = new class DB {
|
||||
return await new Promise(this.getAllInternal);
|
||||
}
|
||||
|
||||
saveItemsInternal(items: any[], resolve: Function, reject: Function) {
|
||||
saveItemsInternal(items: Download[], resolve: Function, reject: Function) {
|
||||
if (!items || !items.length || !this.db) {
|
||||
resolve();
|
||||
return;
|
||||
@ -80,9 +86,13 @@ export const DB = new class DB {
|
||||
if (item.private) {
|
||||
continue;
|
||||
}
|
||||
const req = store.put(item.toJSON());
|
||||
const json = item.toJSON();
|
||||
if (item.state === RUNNING || item.state === RETRYING) {
|
||||
json.state = QUEUED;
|
||||
}
|
||||
const req = store.put(json);
|
||||
if (!("dbId" in item) || item.dbId < 0) {
|
||||
req.onsuccess = () => item.dbId = req.result;
|
||||
req.onsuccess = () => item.dbId = req.result as number;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,7 +101,7 @@ export const DB = new class DB {
|
||||
}
|
||||
}
|
||||
|
||||
async saveItems(items: any[]) {
|
||||
async saveItems(items: Download[]) {
|
||||
await this.init();
|
||||
return await new Promise(this.saveItemsInternal.bind(this, items));
|
||||
}
|
||||
|
106
lib/filters.ts
@ -4,13 +4,16 @@
|
||||
import uuid from "./uuid";
|
||||
|
||||
import "./objectoverlay";
|
||||
import { storage, i18n } from "./browser";
|
||||
import { storage } from "./browser";
|
||||
import { EventEmitter } from "./events";
|
||||
import { Prefs } from "./prefs";
|
||||
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Overlayable } from "./objectoverlay";
|
||||
import * as DEFAULT_FILTERS from "../data/filters.json";
|
||||
import DEFAULT_FILTERS from "../data/filters.json";
|
||||
import { FASTFILTER } from "./recentlist";
|
||||
import { _, locale } from "./i18n";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
|
||||
const REG_ESCAPE = /[{}()[\]\\^$.]/g;
|
||||
const REG_FNMATCH = /[*?]/;
|
||||
@ -91,7 +94,7 @@ function *parseIntoRegexpInternal(str: string): Iterable<RegExp> {
|
||||
|
||||
// multi-expression
|
||||
if (str.includes(",")) {
|
||||
for (const part in str.split(",")) {
|
||||
for (const part of str.split(",")) {
|
||||
yield *parseIntoRegexpInternal(part);
|
||||
}
|
||||
return;
|
||||
@ -173,25 +176,37 @@ export class Matcher {
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
matchItem(item: any) {
|
||||
matchItem(item: BaseItem) {
|
||||
const {usable = "", title = "", description = "", fileName = ""} = item;
|
||||
return this.match(usable) || this.match(title) ||
|
||||
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 {
|
||||
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 _reg: Matcher;
|
||||
|
||||
constructor(owner: Filters, id: any, raw: any) {
|
||||
constructor(owner: Filters, id: string | symbol, raw: RawFilter) {
|
||||
if (!owner || !id || !raw) {
|
||||
throw new Error("null argument");
|
||||
}
|
||||
@ -203,9 +218,11 @@ export class Filter {
|
||||
|
||||
init() {
|
||||
this._label = this.raw.label;
|
||||
if (this.id !== FAST && this.id.startsWith("deffilter-") &&
|
||||
!this.raw.isOverridden("label")) {
|
||||
this._label = i18n.getMessage(this.id) || this._label;
|
||||
if (typeof this.raw.isOverridden !== "undefined" &&
|
||||
typeof this.id === "string") {
|
||||
if (this.id.startsWith("deffilter-") && !this.raw.isOverridden("label")) {
|
||||
this._label = _(this.id) || this._label;
|
||||
}
|
||||
}
|
||||
this._reg = Matcher.fromExpression(this.expr);
|
||||
Object.seal(this);
|
||||
@ -281,7 +298,7 @@ export class Filter {
|
||||
}
|
||||
|
||||
async reset() {
|
||||
if (this.raw.custom) {
|
||||
if (!this.raw.reset) {
|
||||
throw Error("Cannot reset non-default filter");
|
||||
}
|
||||
this.raw.reset();
|
||||
@ -291,7 +308,10 @@ export class Filter {
|
||||
|
||||
async "delete"() {
|
||||
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);
|
||||
}
|
||||
@ -300,7 +320,7 @@ export class Filter {
|
||||
return this._reg.match(str);
|
||||
}
|
||||
|
||||
matchItem(item: any) {
|
||||
matchItem(item: BaseItem) {
|
||||
return this._reg.matchItem(item);
|
||||
}
|
||||
|
||||
@ -315,8 +335,7 @@ class FastFilter extends Filter {
|
||||
throw new Error("Invalid fast filter value");
|
||||
}
|
||||
super(owner, FAST, {
|
||||
id: FAST,
|
||||
label: FAST,
|
||||
label: "fast",
|
||||
type: TYPE_ALL,
|
||||
active: true,
|
||||
expr: value,
|
||||
@ -351,8 +370,6 @@ class Filters extends EventEmitter {
|
||||
|
||||
private filters: Filter[];
|
||||
|
||||
private fastFilter: string | null;
|
||||
|
||||
ignoreNext: boolean;
|
||||
|
||||
private readonly typeMatchers: Map<number, Matcher>;
|
||||
@ -362,10 +379,8 @@ class Filters extends EventEmitter {
|
||||
this.typeMatchers = new Map();
|
||||
this.loaded = false;
|
||||
this.filters = [];
|
||||
this.fastFilter = null;
|
||||
this.ignoreNext = false;
|
||||
this.regenerate();
|
||||
Prefs.on("fast-filter", this.updateFastFilter.bind(this));
|
||||
storage.onChanged.addListener(async (changes: any) => {
|
||||
if (this.ignoreNext) {
|
||||
this.ignoreNext = false;
|
||||
@ -403,6 +418,7 @@ class Filters extends EventEmitter {
|
||||
const id = `custom-${uuid()}`;
|
||||
const filter = new Filter(this, id, {
|
||||
active: true,
|
||||
custom: true,
|
||||
label,
|
||||
expr,
|
||||
type,
|
||||
@ -411,11 +427,11 @@ class Filters extends EventEmitter {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
"get"(id: any) {
|
||||
"get"(id: string | symbol) {
|
||||
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);
|
||||
if (idx < 0) {
|
||||
return;
|
||||
@ -438,21 +454,12 @@ class Filters extends EventEmitter {
|
||||
return new FastFilter(this, value);
|
||||
}
|
||||
|
||||
getFastFilter() {
|
||||
if (!this.fastFilter) {
|
||||
throw new Error("Nothing stored");
|
||||
async getFastFilter() {
|
||||
await FASTFILTER.init();
|
||||
if (!FASTFILTER.current) {
|
||||
return null;
|
||||
}
|
||||
return new FastFilter(this, this.fastFilter);
|
||||
}
|
||||
|
||||
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();
|
||||
return new FastFilter(this, FASTFILTER.current);
|
||||
}
|
||||
|
||||
regenerate() {
|
||||
@ -480,17 +487,6 @@ class Filters extends EventEmitter {
|
||||
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_LINK, new Matcher(links));
|
||||
this.typeMatchers.set(TYPE_MEDIA, new Matcher(media));
|
||||
@ -498,6 +494,7 @@ class Filters extends EventEmitter {
|
||||
}
|
||||
|
||||
async load() {
|
||||
await locale;
|
||||
const defaultFilters = DEFAULT_FILTERS as any;
|
||||
let savedFilters = (await storage.local.get("userFilters"));
|
||||
if (savedFilters && "userFilters" in savedFilters) {
|
||||
@ -534,14 +531,17 @@ class Filters extends EventEmitter {
|
||||
defaultFilters[filter]);
|
||||
this.filters.push(new Filter(this, filter, current));
|
||||
}
|
||||
this.fastFilter = await Prefs.get("fast-filter", null);
|
||||
this.loaded = true;
|
||||
this.regenerate();
|
||||
}
|
||||
|
||||
filterItemsByType(items: any[], type: number) {
|
||||
async filterItemsByType(items: BaseItem[], type: number) {
|
||||
const matcher = this.typeMatchers.get(type);
|
||||
const fast = await this.getFastFilter();
|
||||
return items.filter(function(item) {
|
||||
if (fast && fast.matchItem(item)) {
|
||||
return true;
|
||||
}
|
||||
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> {
|
||||
if (!_filters) {
|
||||
if (!_loader) {
|
||||
_filters = new Filters();
|
||||
await _filters.load();
|
||||
_loader = _filters.load();
|
||||
}
|
||||
await _loader;
|
||||
return _filters;
|
||||
}
|
||||
|
300
lib/i18n.ts
@ -2,50 +2,239 @@
|
||||
// License: MIT
|
||||
|
||||
import {memoize} from "./memoize";
|
||||
import langs from "../_locales/all.json";
|
||||
import { sorted, naturalCaseCompare } from "./sorting";
|
||||
|
||||
function load() {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const {i18n} = require("webextension-polyfill");
|
||||
|
||||
return i18n;
|
||||
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");
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
|
||||
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 {
|
||||
if (entry.message.includes("$")) {
|
||||
this.strings.set(id, new Entry(entry));
|
||||
}
|
||||
else {
|
||||
this.strings.set(id, entry.message);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const messages = require("../_locales/en/messages.json");
|
||||
|
||||
const map = new Map();
|
||||
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] || "";
|
||||
});
|
||||
}
|
||||
};
|
||||
return new Localization(messages);
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = load();
|
||||
const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
|
||||
type MemoLocalize = (id: string, ...args: any[]) => string;
|
||||
|
||||
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
|
||||
@ -53,22 +242,22 @@ const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
|
||||
* @param {string[]} [subst] Message substituations
|
||||
* @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) {
|
||||
return memoGetMessage(id);
|
||||
return memoLocalize(id);
|
||||
}
|
||||
if (subst.length === 1 && Array.isArray(subst[0])) {
|
||||
subst = subst.pop();
|
||||
}
|
||||
return i18n.getMessage(id, subst);
|
||||
return loc.localize(id, subst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize a DOM
|
||||
* @param {Element} elem DOM to localize
|
||||
* @returns {Element} Passed in element (fluent)
|
||||
*/
|
||||
function localize(elem: HTMLElement) {
|
||||
function localize_<T extends HTMLElement | DocumentFragment>(elem: T): T {
|
||||
for (const tmpl of elem.querySelectorAll<HTMLTemplateElement>("template")) {
|
||||
localize_(tmpl.content);
|
||||
}
|
||||
|
||||
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
|
||||
const {i18n: i} = el.dataset;
|
||||
if (!i) {
|
||||
@ -99,8 +288,25 @@ function localize(elem: HTMLElement) {
|
||||
for (const el of document.querySelectorAll("*[data-l18n]")) {
|
||||
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);
|
||||
}
|
||||
}();
|
247
lib/imex.ts
Normal file
@ -0,0 +1,247 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { getTextLinks } from "./textlinks";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
import { ALLOWED_SCHEMES } from "./constants";
|
||||
|
||||
export const NS_METALINK_RFC5854 = "urn:ietf:params:xml:ns:metalink";
|
||||
export const NS_DTA = "http://www.downthemall.net/properties#";
|
||||
|
||||
function parseNum(
|
||||
file: Element,
|
||||
attr: string,
|
||||
defaultValue: number,
|
||||
ns = NS_METALINK_RFC5854) {
|
||||
const val = file.getAttributeNS(ns, attr);
|
||||
if (!val) {
|
||||
return defaultValue + 1;
|
||||
}
|
||||
const num = parseInt(val, 10);
|
||||
if (isFinite(num)) {
|
||||
return num;
|
||||
}
|
||||
return defaultValue + 1;
|
||||
}
|
||||
|
||||
function importMeta4(data: string) {
|
||||
const parser = new DOMParser();
|
||||
const document = parser.parseFromString(data, "text/xml");
|
||||
const {documentElement} = document;
|
||||
const items: BaseItem[] = [];
|
||||
let batch = 0;
|
||||
for (const file of documentElement.querySelectorAll("file")) {
|
||||
try {
|
||||
const url = Array.from(file.querySelectorAll("url")).map(u => {
|
||||
try {
|
||||
const {textContent} = u;
|
||||
if (!textContent) {
|
||||
return null;
|
||||
}
|
||||
const url = new URL(textContent);
|
||||
if (!ALLOWED_SCHEMES.has(url.protocol)) {
|
||||
return null;
|
||||
}
|
||||
const prio = parseNum(u, "priority", 0);
|
||||
return {
|
||||
url,
|
||||
prio
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}).filter(u => !!u).reduce((p, c) => {
|
||||
if (!c) {
|
||||
return null;
|
||||
}
|
||||
if (!p || p.prio < c.prio) {
|
||||
return c;
|
||||
}
|
||||
return p;
|
||||
});
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
batch = parseNum(file, "num", batch, NS_DTA);
|
||||
const idx = parseNum(file, "idx", 0, NS_DTA);
|
||||
const item: BaseItem = {
|
||||
url: url.url.toString(),
|
||||
usable: decodeURIComponent(url.url.toString()),
|
||||
batch,
|
||||
idx
|
||||
};
|
||||
const ref = file.getAttributeNS(NS_DTA, "referrer");
|
||||
if (ref) {
|
||||
item.referrer = ref;
|
||||
item.usableReferrer = decodeURIComponent(ref);
|
||||
}
|
||||
const mask = file.getAttributeNS(NS_DTA, "mask");
|
||||
if (mask) {
|
||||
item.mask = mask;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("Failed to import file", ex);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function parseKV(current: BaseItem, line: string) {
|
||||
const [k, v] = line.split("=", 2);
|
||||
switch (k.toLocaleLowerCase().trim()) {
|
||||
case "referer": {
|
||||
const rurls = getTextLinks(v);
|
||||
if (rurls && rurls.length) {
|
||||
current.referrer = rurls.pop();
|
||||
current.usableReferrer = decodeURIComponent(current.referrer || "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function importText(data: string) {
|
||||
if (data.includes(NS_METALINK_RFC5854)) {
|
||||
return importMeta4(data);
|
||||
}
|
||||
const splitter = /(.+)\n|(.+)$/g;
|
||||
const spacer = /^\s+/;
|
||||
let match;
|
||||
let current: BaseItem | undefined = undefined;
|
||||
let idx = 0;
|
||||
const items = [];
|
||||
while ((match = splitter.exec(data)) !== null) {
|
||||
try {
|
||||
const line = match[0].trimRight();
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
if (spacer.test(line)) {
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
parseKV(current, line);
|
||||
continue;
|
||||
}
|
||||
const urls = getTextLinks(line);
|
||||
if (!urls || !urls.length) {
|
||||
continue;
|
||||
}
|
||||
current = {
|
||||
url: urls[0],
|
||||
usable: decodeURIComponent(urls[0]),
|
||||
idx: ++idx
|
||||
};
|
||||
items.push(current);
|
||||
}
|
||||
catch (ex) {
|
||||
current = undefined;
|
||||
console.error("Failed to import", ex);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export interface Exporter {
|
||||
fileName: string;
|
||||
getText(items: BaseItem[]): string;
|
||||
}
|
||||
|
||||
class TextExporter {
|
||||
readonly fileName: string;
|
||||
|
||||
constructor() {
|
||||
this.fileName = "links.txt";
|
||||
}
|
||||
|
||||
getText(items: BaseItem[]) {
|
||||
const lines = [];
|
||||
for (const item of items) {
|
||||
lines.push(item.url);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
class Aria2Exporter {
|
||||
readonly fileName: string;
|
||||
|
||||
constructor() {
|
||||
this.fileName = "links.aria2.txt";
|
||||
}
|
||||
|
||||
getText(items: BaseItem[]) {
|
||||
const lines = [];
|
||||
for (const item of items) {
|
||||
lines.push(item.url);
|
||||
if (item.referrer) {
|
||||
lines.push(` referer=${item.referrer}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
class MetalinkExporter {
|
||||
readonly fileName: string;
|
||||
|
||||
constructor() {
|
||||
this.fileName = "links.meta4";
|
||||
}
|
||||
|
||||
getText(items: BaseItem[]) {
|
||||
const document = window.document.implementation.
|
||||
createDocument(NS_METALINK_RFC5854, "metalink", null);
|
||||
const root = document.documentElement;
|
||||
root.setAttributeNS(NS_DTA, "generator", "DownThemAll!");
|
||||
root.appendChild(document.createComment(
|
||||
"metalink as exported by DownThemAll!",
|
||||
));
|
||||
|
||||
for (const item of items) {
|
||||
const aitem = item as any;
|
||||
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
|
||||
f.setAttribute("name", aitem.currentName);
|
||||
if (item.batch) {
|
||||
f.setAttributeNS(NS_DTA, "num", item.batch.toString());
|
||||
}
|
||||
if (item.idx) {
|
||||
f.setAttributeNS(NS_DTA, "idx", item.idx.toString());
|
||||
}
|
||||
if (item.referrer) {
|
||||
f.setAttributeNS(NS_DTA, "referrer", item.referrer);
|
||||
}
|
||||
if (item.mask) {
|
||||
f.setAttributeNS(NS_DTA, "mask", item.mask);
|
||||
}
|
||||
|
||||
if (item.description) {
|
||||
const n = document.createElementNS(NS_METALINK_RFC5854, "description");
|
||||
n.textContent = item.description;
|
||||
f.appendChild(n);
|
||||
}
|
||||
|
||||
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
|
||||
u.textContent = item.url;
|
||||
f.appendChild(u);
|
||||
|
||||
if (aitem.totalSize > 0) {
|
||||
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
|
||||
s.textContent = aitem.totalSize.toString();
|
||||
f.appendChild(s);
|
||||
}
|
||||
root.appendChild(f);
|
||||
}
|
||||
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||
xml += root.outerHTML;
|
||||
return xml;
|
||||
}
|
||||
}
|
||||
|
||||
export const textExporter = new TextExporter();
|
||||
export const aria2Exporter = new Aria2Exporter();
|
||||
export const metalinkExporter = new MetalinkExporter();
|
4
lib/ipreg.ts
Normal file
@ -0,0 +1,4 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
export const IPReg = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$|^(?:(?:(?:[0-9a-fA-F]{1,4}):){7}(?:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){6}(?:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){5}(?::((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,2}|:)|(?:(?:[0-9a-fA-F]{1,4}):){4}(?:(:(?:[0-9a-fA-F]{1,4})){0,1}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,3}|:)|(?:(?:[0-9a-fA-F]{1,4}):){3}(?:(:(?:[0-9a-fA-F]{1,4})){0,2}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,4}|:)|(?:(?:[0-9a-fA-F]{1,4}):){2}(?:(:(?:[0-9a-fA-F]{1,4})){0,3}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,5}|:)|(?:(?:[0-9a-fA-F]{1,4}):){1}(?:(:(?:[0-9a-fA-F]{1,4})){0,4}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,6}|:)|(?::((?::(?:[0-9a-fA-F]{1,4})){0,5}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?::(?:[0-9a-fA-F]{1,4})){1,7}|:)))(%[0-9a-zA-Z]{1,})?$/;
|
28
lib/item.ts
@ -4,18 +4,34 @@
|
||||
import { ALLOWED_SCHEMES } 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;
|
||||
subfolder?: string;
|
||||
startDate?: number;
|
||||
private?: boolean;
|
||||
postData?: string;
|
||||
paused?: boolean;
|
||||
}
|
||||
|
||||
const OPTIONPROPS = Object.freeze([
|
||||
"referrer", "usableReferrer",
|
||||
"description", "title",
|
||||
"fileName",
|
||||
"batch", "idx",
|
||||
"mask",
|
||||
"fromMetalink",
|
||||
"subfolder",
|
||||
"startDate",
|
||||
"hashes",
|
||||
"private",
|
||||
"postData",
|
||||
"cleanRequest",
|
||||
"paused"
|
||||
]);
|
||||
|
||||
@ -34,7 +50,7 @@ function maybeAssign(options: any, what: any) {
|
||||
this[what] = val;
|
||||
}
|
||||
|
||||
export class Item {
|
||||
export class Item implements BaseItem {
|
||||
public url: string;
|
||||
|
||||
public usable: string;
|
||||
@ -43,6 +59,8 @@ export class Item {
|
||||
|
||||
public usableReferrer: string;
|
||||
|
||||
public idx: number;
|
||||
|
||||
constructor(raw: any, options?: any) {
|
||||
Object.assign(this, raw);
|
||||
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
|
||||
@ -99,7 +117,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 unique = [];
|
||||
for (const itemlist of items) {
|
||||
|
@ -5,6 +5,8 @@
|
||||
import { parsePath, URLd } from "../util";
|
||||
import { QUEUED, RUNNING, PAUSED } from "./state";
|
||||
import Renamer from "./renamer";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "../item";
|
||||
|
||||
const SAVEDPROPS = [
|
||||
"state",
|
||||
@ -14,6 +16,7 @@ const SAVEDPROPS = [
|
||||
"usableReferrer",
|
||||
"fileName",
|
||||
"mask",
|
||||
"subfolder",
|
||||
"date",
|
||||
// batches
|
||||
"batch",
|
||||
@ -27,10 +30,11 @@ const SAVEDPROPS = [
|
||||
"written",
|
||||
// server stuff
|
||||
"serverName",
|
||||
"browserName",
|
||||
"mime",
|
||||
"prerolled",
|
||||
// other options
|
||||
"private",
|
||||
"fromMetalink",
|
||||
"cleanRequest",
|
||||
// db
|
||||
"manId",
|
||||
"dbId",
|
||||
@ -41,10 +45,15 @@ const DEFAULTS = {
|
||||
state: QUEUED,
|
||||
error: "",
|
||||
serverName: "",
|
||||
browserName: "",
|
||||
fileName: "",
|
||||
totalSize: 0,
|
||||
written: 0,
|
||||
manId: 0,
|
||||
mime: "",
|
||||
prerolled: false,
|
||||
retries: 0,
|
||||
deadline: 0
|
||||
};
|
||||
|
||||
let sessionId = 0;
|
||||
@ -61,14 +70,26 @@ export class BaseDownload {
|
||||
|
||||
public url: string;
|
||||
|
||||
public usable: string;
|
||||
|
||||
public uReferrer: URLd;
|
||||
|
||||
public referrer: string;
|
||||
|
||||
public usableReferrer: string;
|
||||
|
||||
public startDate: Date;
|
||||
|
||||
public fileName: string;
|
||||
|
||||
public description?: string;
|
||||
|
||||
public title?: string;
|
||||
|
||||
public batch: number;
|
||||
|
||||
public idx: number;
|
||||
|
||||
public error: string;
|
||||
|
||||
public postData: any;
|
||||
@ -81,10 +102,19 @@ export class BaseDownload {
|
||||
|
||||
public serverName: string;
|
||||
|
||||
public browserName: string;
|
||||
|
||||
public mime: string;
|
||||
|
||||
public mask: string;
|
||||
|
||||
public subfolder: string;
|
||||
|
||||
constructor(options: any) {
|
||||
public prerolled: boolean;
|
||||
|
||||
public retries: number;
|
||||
|
||||
constructor(options: BaseItem) {
|
||||
Object.assign(this, DEFAULTS);
|
||||
this.assign(options);
|
||||
if (this.state === RUNNING) {
|
||||
@ -92,14 +122,16 @@ export class BaseDownload {
|
||||
}
|
||||
this.sessionId = ++sessionId;
|
||||
this.renamer = new Renamer(this);
|
||||
this.retries = 0;
|
||||
}
|
||||
|
||||
assign(options: any) {
|
||||
assign(options: BaseItem) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self: any = this;
|
||||
const other: any = options;
|
||||
for (const prop of SAVEDPROPS) {
|
||||
if (prop in options) {
|
||||
self[prop] = options[prop];
|
||||
self[prop] = other[prop];
|
||||
}
|
||||
}
|
||||
this.uURL = new URL(this.url) as URLd;
|
||||
@ -117,6 +149,10 @@ export class BaseDownload {
|
||||
return this.serverName || this.fileName || this.urlName || "index.html";
|
||||
}
|
||||
|
||||
get currentName() {
|
||||
return this.browserName || this.dest.name || this.finalName;
|
||||
}
|
||||
|
||||
get urlName() {
|
||||
const path = parsePath(this.uURL);
|
||||
if (path.name) {
|
||||
@ -154,7 +190,11 @@ export class BaseDownload {
|
||||
rv.destName = dest.name;
|
||||
rv.destPath = dest.path;
|
||||
rv.destFull = dest.full;
|
||||
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||
rv.currentFull = `${dest.path}/${rv.currentName}`;
|
||||
rv.error = this.error;
|
||||
rv.ext = this.renamer.p_ext;
|
||||
rv.retries = this.retries;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,42 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { Prefs } from "../prefs";
|
||||
import { parsePath } from "../util";
|
||||
import {
|
||||
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
|
||||
FORCABLE, PAUSABLE, CANCELABLE,
|
||||
} from "./state";
|
||||
import { BaseDownload } from "./basedownload";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { CHROME, downloads, DownloadOptions } from "../browser";
|
||||
import { Prefs, PrefWatcher } from "../prefs";
|
||||
import { PromiseSerializer } from "../pserializer";
|
||||
import { filterInSitu, parsePath } from "../util";
|
||||
import { BaseDownload } from "./basedownload";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Manager } from "./man";
|
||||
import { downloads } from "../browser";
|
||||
import Renamer from "./renamer";
|
||||
import {
|
||||
CANCELABLE,
|
||||
CANCELED,
|
||||
DONE,
|
||||
FORCABLE,
|
||||
MISSING,
|
||||
PAUSABLE,
|
||||
PAUSED,
|
||||
QUEUED,
|
||||
RUNNING,
|
||||
RETRYING
|
||||
} from "./state";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Preroller, PrerollResults } from "./preroller";
|
||||
|
||||
function isRecoverable(error: string) {
|
||||
switch (error) {
|
||||
case "SERVER_FAILED":
|
||||
return true;
|
||||
|
||||
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||
// ignored
|
||||
};
|
||||
default:
|
||||
return error.startsWith("NETWORK_");
|
||||
}
|
||||
}
|
||||
|
||||
const RETRIES = new PrefWatcher("retries", 5);
|
||||
const RETRY_TIME = new PrefWatcher("retry-time", 5);
|
||||
|
||||
export class Download extends BaseDownload {
|
||||
public manager: Manager;
|
||||
@ -29,6 +49,10 @@ export class Download extends BaseDownload {
|
||||
|
||||
public error: string;
|
||||
|
||||
public dbId: number;
|
||||
|
||||
public deadline: number;
|
||||
|
||||
constructor(manager: Manager, options: any) {
|
||||
super(options);
|
||||
this.manager = manager;
|
||||
@ -38,6 +62,7 @@ export class Download extends BaseDownload {
|
||||
}
|
||||
|
||||
markDirty() {
|
||||
this.renamer = new Renamer(this);
|
||||
this.manager.setDirty(this);
|
||||
}
|
||||
|
||||
@ -59,22 +84,28 @@ export class Download extends BaseDownload {
|
||||
if (this.manId) {
|
||||
const {manId: id} = this;
|
||||
try {
|
||||
const state = await downloads.search({id});
|
||||
if (state[0].state === "in_progress") {
|
||||
const state = (await downloads.search({id})).pop() || {};
|
||||
if (state.state === "in_progress" && !state.error && !state.paused) {
|
||||
this.changeState(RUNNING);
|
||||
this.updateStateFromBrowser();
|
||||
return;
|
||||
}
|
||||
if (!state[0].canResume) {
|
||||
if (state.state === "complete") {
|
||||
this.changeState(DONE);
|
||||
this.updateStateFromBrowser();
|
||||
return;
|
||||
}
|
||||
if (!state.canResume) {
|
||||
throw new Error("Cannot resume");
|
||||
}
|
||||
// Cannot await here
|
||||
// Firefox bug: will not return until download is finished
|
||||
downloads.resume(id).catch(() => {});
|
||||
downloads.resume(id).catch(console.error);
|
||||
this.changeState(RUNNING);
|
||||
return;
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("cannot resume", ex);
|
||||
this.manager.removeManId(this.manId);
|
||||
this.removeFromBrowser();
|
||||
}
|
||||
@ -82,46 +113,66 @@ export class Download extends BaseDownload {
|
||||
if (this.state !== QUEUED) {
|
||||
throw new Error("invalid state");
|
||||
}
|
||||
console.trace("starting", this.toString(), this.dest, this.mask);
|
||||
console.log("starting", this.toString(), this.toMsg());
|
||||
this.changeState(RUNNING);
|
||||
|
||||
// Do NOT await
|
||||
this.reallyStart();
|
||||
}
|
||||
|
||||
private async reallyStart() {
|
||||
try {
|
||||
const options: any = {
|
||||
if (!this.prerolled) {
|
||||
await this.maybePreroll();
|
||||
if (this.state !== RUNNING) {
|
||||
// Aborted by preroll
|
||||
return;
|
||||
}
|
||||
}
|
||||
const options: DownloadOptions = {
|
||||
conflictAction: await Prefs.get("conflict-action"),
|
||||
filename: this.dest.full,
|
||||
saveAs: false,
|
||||
url: this.url,
|
||||
headers: [{
|
||||
name: "X-DTA-Tag",
|
||||
value: this.sessionId.toString(),
|
||||
}],
|
||||
headers: [],
|
||||
};
|
||||
if (!CHROME) {
|
||||
options.filename = this.dest.full;
|
||||
}
|
||||
if (!CHROME && this.private) {
|
||||
options.incognito = true;
|
||||
}
|
||||
if (this.postData) {
|
||||
options.body = this.postData;
|
||||
options.method = "POST";
|
||||
}
|
||||
if (this.private) {
|
||||
options.incognito = true;
|
||||
}
|
||||
/* XXX "forbidden"
|
||||
Cannot be worked around with webRequest either
|
||||
as those do not see downloads.
|
||||
if (this.referrer) {
|
||||
if (!CHROME && this.referrer) {
|
||||
options.headers.push({
|
||||
name: "Referer",
|
||||
value: this.referrer
|
||||
});
|
||||
}
|
||||
*/
|
||||
else if (CHROME) {
|
||||
options.headers.push({
|
||||
name: "X-DTA-ID",
|
||||
value: this.sessionId.toString(),
|
||||
});
|
||||
}
|
||||
if (this.manId) {
|
||||
this.manager.removeManId(this.manId);
|
||||
}
|
||||
setShelfEnabled(false);
|
||||
|
||||
try {
|
||||
this.manager.addManId(
|
||||
this.manId = await downloads.download(options), this);
|
||||
}
|
||||
finally {
|
||||
setShelfEnabled(true);
|
||||
catch (ex) {
|
||||
if (!this.referrer) {
|
||||
throw ex;
|
||||
}
|
||||
// Re-attempt without referrer
|
||||
filterInSitu(options.headers, h => h.name !== "Referer");
|
||||
this.manager.addManId(
|
||||
this.manId = await downloads.download(options), this);
|
||||
}
|
||||
this.markDirty();
|
||||
}
|
||||
@ -132,6 +183,45 @@ 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;
|
||||
}
|
||||
this.adoptPrerollResults(res);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||
}
|
||||
finally {
|
||||
if (this.state === RUNNING) {
|
||||
this.prerolled = true;
|
||||
this.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adoptPrerollResults(res: PrerollResults) {
|
||||
if (res.mime) {
|
||||
this.mime = res.mime;
|
||||
}
|
||||
if (res.name) {
|
||||
this.serverName = res.name;
|
||||
}
|
||||
if (res.error) {
|
||||
this.cancelAccordingToError(res.error);
|
||||
}
|
||||
}
|
||||
|
||||
resume(forced = false) {
|
||||
if (!(FORCABLE & this.state)) {
|
||||
return;
|
||||
@ -144,26 +234,41 @@ export class Download extends BaseDownload {
|
||||
}
|
||||
}
|
||||
|
||||
async pause() {
|
||||
async pause(retry?: boolean) {
|
||||
if (!(PAUSABLE & this.state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!retry) {
|
||||
this.retries = 0;
|
||||
this.deadline = 0;
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
this.deadline = Date.now() + RETRY_TIME.value * 60 * 1000;
|
||||
}
|
||||
|
||||
if (this.state === RUNNING && this.manId) {
|
||||
try {
|
||||
await downloads.pause(this.manId);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("pause", ex.toString(), ex);
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.changeState(PAUSED);
|
||||
|
||||
this.changeState(retry ? RETRYING : PAUSED);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.prerolled = false;
|
||||
this.manId = 0;
|
||||
this.written = this.totalSize = 0;
|
||||
this.serverName = "";
|
||||
this.mime = this.serverName = this.browserName = "";
|
||||
this.retries = 0;
|
||||
this.deadline = 0;
|
||||
}
|
||||
|
||||
async removeFromBrowser() {
|
||||
@ -196,6 +301,17 @@ export class Download extends BaseDownload {
|
||||
this.changeState(CANCELED);
|
||||
}
|
||||
|
||||
async cancelAccordingToError(error: string) {
|
||||
if (!isRecoverable(error) || ++this.retries > RETRIES.value) {
|
||||
this.cancel();
|
||||
this.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.pause(true);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
setMissing() {
|
||||
if (this.manId) {
|
||||
this.manager.removeManId(this.manId);
|
||||
@ -240,14 +356,19 @@ export class Download extends BaseDownload {
|
||||
const state = (await downloads.search({id: this.manId})).pop();
|
||||
const {filename, error} = state;
|
||||
const path = parsePath(filename);
|
||||
this.serverName = path.name;
|
||||
this.browserName = path.name;
|
||||
this.adoptSize(state);
|
||||
if (!this.mime && state.mime) {
|
||||
this.mime = state.mime;
|
||||
}
|
||||
this.markDirty();
|
||||
switch (state.state) {
|
||||
case "in_progress":
|
||||
if (error) {
|
||||
this.cancel();
|
||||
this.error = error;
|
||||
if (state.paused) {
|
||||
this.changeState(PAUSED);
|
||||
}
|
||||
else if (error) {
|
||||
this.cancelAccordingToError(error);
|
||||
}
|
||||
else {
|
||||
this.changeState(RUNNING);
|
||||
@ -258,6 +379,9 @@ export class Download extends BaseDownload {
|
||||
if (state.paused) {
|
||||
this.changeState(PAUSED);
|
||||
}
|
||||
else if (error) {
|
||||
this.cancelAccordingToError(error);
|
||||
}
|
||||
else {
|
||||
this.cancel();
|
||||
this.error = error || "";
|
||||
@ -274,4 +398,27 @@ export class Download extends BaseDownload {
|
||||
this.setMissing();
|
||||
}
|
||||
}
|
||||
|
||||
updatefromSuggestion(state: any) {
|
||||
const res: PrerollResults = {};
|
||||
if (state.mime) {
|
||||
res.mime = state.mime;
|
||||
}
|
||||
if (state.filename) {
|
||||
res.name = state.filename;
|
||||
}
|
||||
if (state.finalUrl) {
|
||||
res.finalURL = state.finalUrl;
|
||||
const detected = Preroller.maybeFindNameFromSearchParams(this, res);
|
||||
if (detected) {
|
||||
res.name = detected;
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.adoptPrerollResults(res);
|
||||
}
|
||||
finally {
|
||||
this.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,30 +4,39 @@
|
||||
import { EventEmitter } from "../events";
|
||||
import { Notification } from "../notifications";
|
||||
import { DB } from "../db";
|
||||
import { QUEUED, CANCELED, RUNNING } from "./state";
|
||||
import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Bus, Port } from "../bus";
|
||||
import { sort } from "../sorting";
|
||||
import { Prefs } from "../prefs";
|
||||
import { Prefs, PrefWatcher } from "../prefs";
|
||||
import { _ } from "../i18n";
|
||||
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
||||
import { PromiseSerializer } from "../pserializer";
|
||||
import {Download} from "./download";
|
||||
import {ManagerPort} from "./port";
|
||||
import {Scheduler} from "./scheduler";
|
||||
import {Limits} from "./limits";
|
||||
import { downloads } from "../browser";
|
||||
import { Download } from "./download";
|
||||
import { ManagerPort } from "./port";
|
||||
import { Scheduler } from "./scheduler";
|
||||
import { Limits } from "./limits";
|
||||
import { downloads, runtime, webRequest, CHROME } from "../browser";
|
||||
|
||||
const US = runtime.getURL("");
|
||||
|
||||
const AUTOSAVE_TIMEOUT = 2000;
|
||||
const DIRTY_TIMEOUT = 100;
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
const MISSING_TIMEOUT = 12 * 1000;
|
||||
const RELOAD_TIMEOUT = 10 * 1000;
|
||||
|
||||
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||
// ignored
|
||||
};
|
||||
|
||||
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
|
||||
const SOUNDS = new PrefWatcher("sounds", false);
|
||||
|
||||
export class Manager extends EventEmitter {
|
||||
private items: Download[];
|
||||
|
||||
private active: boolean;
|
||||
public active: boolean;
|
||||
|
||||
private notifiedFinished: boolean;
|
||||
|
||||
@ -43,27 +52,40 @@ export class Manager extends EventEmitter {
|
||||
|
||||
private readonly running: Set<Download>;
|
||||
|
||||
private readonly retrying: Set<Download>;
|
||||
|
||||
private scheduler: Scheduler | null;
|
||||
|
||||
private shouldReload: boolean;
|
||||
|
||||
private deadlineTimer: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.active = true;
|
||||
this.shouldReload = false;
|
||||
this.notifiedFinished = true;
|
||||
this.items = [];
|
||||
this.saveQueue = new CoalescedUpdate(
|
||||
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
||||
this.dirty = new CoalescedUpdate(
|
||||
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
||||
this.processDeadlines = this.processDeadlines.bind(this);
|
||||
this.sids = new Map();
|
||||
this.manIds = new Map();
|
||||
this.ports = new Set();
|
||||
this.scheduler = null;
|
||||
this.running = new Set();
|
||||
this.retrying = new Set();
|
||||
|
||||
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
||||
|
||||
downloads.onChanged.addListener(this.onChanged.bind(this));
|
||||
downloads.onErased.addListener(this.onErased.bind(this));
|
||||
if (CHROME && downloads.onDeterminingFilename) {
|
||||
downloads.onDeterminingFilename.addListener(
|
||||
this.onDeterminingFilename.bind(this));
|
||||
}
|
||||
|
||||
Bus.onPort("manager", (port: Port) => {
|
||||
const mport = new ManagerPort(this, port);
|
||||
@ -71,10 +93,19 @@ export class Manager extends EventEmitter {
|
||||
this.ports.delete(mport);
|
||||
});
|
||||
this.ports.add(mport);
|
||||
return true;
|
||||
});
|
||||
Limits.on("changed", () => {
|
||||
this.resetScheduler();
|
||||
});
|
||||
|
||||
if (CHROME) {
|
||||
webRequest.onBeforeSendHeaders.addListener(
|
||||
this.stuffReferrer.bind(this),
|
||||
{urls: ["<all_urls>"]},
|
||||
["blocking", "requestHeaders", "extraHeaders"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -88,9 +119,19 @@ export class Manager extends EventEmitter {
|
||||
}
|
||||
this.items.push(rv);
|
||||
});
|
||||
await this.resetScheduler();
|
||||
|
||||
// Do not wait for the scheduler
|
||||
this.resetScheduler();
|
||||
|
||||
this.emit("inited");
|
||||
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
||||
runtime.onUpdateAvailable.addListener(() => {
|
||||
if (this.running.size) {
|
||||
this.shouldReload = true;
|
||||
return;
|
||||
}
|
||||
runtime.reload();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -121,6 +162,20 @@ export class Manager extends EventEmitter {
|
||||
this.manIds.delete(downloadId);
|
||||
}
|
||||
|
||||
onDeterminingFilename(state: any, suggest: Function) {
|
||||
const download = this.manIds.get(state.id);
|
||||
if (!download) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
download.updatefromSuggestion(state);
|
||||
}
|
||||
finally {
|
||||
const suggestion = {filename: download.dest.full};
|
||||
suggest(suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
async resetScheduler() {
|
||||
this.scheduler = null;
|
||||
await this.startNext();
|
||||
@ -136,7 +191,7 @@ export class Manager extends EventEmitter {
|
||||
}
|
||||
const next = await this.scheduler.next(this.running);
|
||||
if (!next) {
|
||||
this.maybeNotifyFinished();
|
||||
this.maybeRunFinishActions();
|
||||
break;
|
||||
}
|
||||
if (this.running.has(next) || next.state !== QUEUED) {
|
||||
@ -147,6 +202,7 @@ export class Manager extends EventEmitter {
|
||||
}
|
||||
catch (ex) {
|
||||
next.changeState(CANCELED);
|
||||
next.error = ex.toString();
|
||||
console.error(ex.toString(), ex);
|
||||
}
|
||||
}
|
||||
@ -155,19 +211,43 @@ export class Manager extends EventEmitter {
|
||||
async startDownload(download: Download) {
|
||||
// Add to running first, so we don't confuse the scheduler and other parts
|
||||
this.running.add(download);
|
||||
setShelfEnabled(false);
|
||||
await download.start();
|
||||
this.notifiedFinished = false;
|
||||
}
|
||||
|
||||
async maybeNotifyFinished() {
|
||||
if (!(await Prefs.get("finish-notification"))) {
|
||||
maybeRunFinishActions() {
|
||||
if (this.running.size) {
|
||||
return;
|
||||
}
|
||||
if (this.notifiedFinished || this.running.size) {
|
||||
this.maybeNotifyFinished();
|
||||
if (this.shouldReload) {
|
||||
this.saveQueue.trigger();
|
||||
setTimeout(() => {
|
||||
if (this.running.size) {
|
||||
return;
|
||||
}
|
||||
runtime.reload();
|
||||
}, RELOAD_TIMEOUT);
|
||||
}
|
||||
setShelfEnabled(true);
|
||||
}
|
||||
|
||||
maybeNotifyFinished() {
|
||||
if (this.notifiedFinished || this.running.size || this.retrying.size) {
|
||||
return;
|
||||
}
|
||||
if (SOUNDS.value) {
|
||||
const audio = new Audio(runtime.getURL("/style/done.opus"));
|
||||
audio.addEventListener("canplaythrough", () => audio.play());
|
||||
audio.addEventListener("ended", () => document.body.removeChild(audio));
|
||||
audio.addEventListener("error", () => document.body.removeChild(audio));
|
||||
document.body.appendChild(audio);
|
||||
}
|
||||
if (FINISH_NOTIFICATION.value) {
|
||||
new Notification(null, _("queue-finished"));
|
||||
}
|
||||
this.notifiedFinished = true;
|
||||
new Notification(null, _("queue-finished"));
|
||||
}
|
||||
|
||||
addManId(id: number, download: Download) {
|
||||
@ -215,7 +295,7 @@ export class Manager extends EventEmitter {
|
||||
this.emit("dirty", items);
|
||||
}
|
||||
|
||||
save(items: Download[]) {
|
||||
private save(items: Download[]) {
|
||||
DB.saveItems(items.filter(i => !i.removed)).
|
||||
catch(console.error);
|
||||
}
|
||||
@ -265,7 +345,10 @@ export class Manager extends EventEmitter {
|
||||
changedState(download: Download, oldState: number, newState: number) {
|
||||
if (oldState === RUNNING) {
|
||||
this.running.delete(download);
|
||||
this.maybeNotifyFinished();
|
||||
}
|
||||
else if (oldState === RETRYING) {
|
||||
this.retrying.delete(download);
|
||||
this.findDeadline();
|
||||
}
|
||||
if (newState === QUEUED) {
|
||||
this.resetScheduler();
|
||||
@ -278,7 +361,53 @@ export class Manager extends EventEmitter {
|
||||
this.running.add(download);
|
||||
}
|
||||
else {
|
||||
this.startNext();
|
||||
if (newState === RETRYING) {
|
||||
this.addRetry(download);
|
||||
}
|
||||
this.startNext().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
addRetry(download: Download) {
|
||||
this.retrying.add(download);
|
||||
this.findDeadline();
|
||||
}
|
||||
|
||||
private findDeadline() {
|
||||
let deadline = Array.from(this.retrying).
|
||||
reduce<number>((deadline, item) => {
|
||||
if (deadline) {
|
||||
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
|
||||
}
|
||||
return item.deadline;
|
||||
}, 0);
|
||||
if (deadline <= 0) {
|
||||
return;
|
||||
}
|
||||
deadline -= Date.now();
|
||||
if (deadline <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.deadlineTimer) {
|
||||
window.clearTimeout(this.deadlineTimer);
|
||||
}
|
||||
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
|
||||
}
|
||||
|
||||
private processDeadlines() {
|
||||
this.deadlineTimer = 0;
|
||||
try {
|
||||
const now = Date.now();
|
||||
this.items.forEach(item => {
|
||||
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
|
||||
this.retrying.delete(item);
|
||||
item.resume(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
finally {
|
||||
this.findDeadline();
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,6 +470,35 @@ export class Manager extends EventEmitter {
|
||||
}
|
||||
this.emit("active", this.active);
|
||||
}
|
||||
|
||||
getMsgItems() {
|
||||
return this.items.map(e => e.toMsg());
|
||||
}
|
||||
|
||||
stuffReferrer(details: any): any {
|
||||
if (details.tabId > 0 && !US.startsWith(details.initiator)) {
|
||||
return undefined;
|
||||
}
|
||||
const sidx = details.requestHeaders.findIndex(
|
||||
(e: any) => e.name.toLowerCase() === "x-dta-id");
|
||||
if (sidx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const sid = parseInt(details.requestHeaders[sidx].value, 10);
|
||||
details.requestHeaders.splice(sidx, 1);
|
||||
const item = this.sids.get(sid);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
details.requestHeaders.push({
|
||||
name: "Referer",
|
||||
value: (item.uReferrer || item.uURL).toString()
|
||||
});
|
||||
const rv: any = {
|
||||
requestHeaders: details.requestHeaders
|
||||
};
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
let inited: Promise<Manager>;
|
||||
|
@ -5,6 +5,10 @@ import { donate, openPrefs } from "../windowutils";
|
||||
import { API } from "../api";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseDownload } from "./basedownload";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Manager } from "./man";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Port } from "../bus";
|
||||
|
||||
type SID = {sid: number};
|
||||
type SIDS = {
|
||||
@ -13,9 +17,9 @@ type SIDS = {
|
||||
};
|
||||
|
||||
export class ManagerPort {
|
||||
private manager: any;
|
||||
private manager: Manager;
|
||||
|
||||
private port: any;
|
||||
private port: Port;
|
||||
|
||||
constructor(manager: any, port: any) {
|
||||
this.manager = manager;
|
||||
@ -61,6 +65,7 @@ export class ManagerPort {
|
||||
delete this.manager;
|
||||
delete this.port;
|
||||
});
|
||||
|
||||
this.port.post("active", this.manager.active);
|
||||
this.sendAll();
|
||||
}
|
||||
@ -78,7 +83,6 @@ export class ManagerPort {
|
||||
}
|
||||
|
||||
sendAll() {
|
||||
this.port.post(
|
||||
"all", this.manager.items.map((e: BaseDownload) => e.toMsg()));
|
||||
this.port.post("all", this.manager.getMsgItems());
|
||||
}
|
||||
}
|
||||
|
252
lib/manager/preroller.ts
Normal file
@ -0,0 +1,252 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import MimeType from "whatwg-mimetype";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Download } from "./download";
|
||||
import { CHROME, webRequest } from "../browser";
|
||||
import { CDHeaderParser } from "../cdheaderparser";
|
||||
import { sanitizePath, parsePath } from "../util";
|
||||
import { MimeDB } from "../mime";
|
||||
|
||||
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
|
||||
const PREROLL_HOSTS = /4cdn|chan/;
|
||||
const PREROLL_TIMEOUT = 10000;
|
||||
const PREROLL_NOPE = new Set<string>();
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
const NOPE_STATUSES = Object.freeze(new Set([
|
||||
400,
|
||||
401,
|
||||
402,
|
||||
405,
|
||||
416,
|
||||
]));
|
||||
/* eslint-enable no-magic-numbers */
|
||||
|
||||
const PREROLL_SEARCHEXTS = Object.freeze(new Set<string>([
|
||||
"php",
|
||||
"asp",
|
||||
"aspx",
|
||||
"inc",
|
||||
"py",
|
||||
"pl",
|
||||
"action",
|
||||
"htm",
|
||||
"html",
|
||||
"shtml"
|
||||
]));
|
||||
const NAME_TESTER = /\.[a-z0-9]{1,5}$/i;
|
||||
const CDPARSER = new CDHeaderParser();
|
||||
|
||||
export interface PrerollResults {
|
||||
error?: string;
|
||||
name?: string;
|
||||
mime?: string;
|
||||
finalURL?: string;
|
||||
}
|
||||
|
||||
export class Preroller {
|
||||
private readonly download: Download
|
||||
|
||||
constructor(download: Download) {
|
||||
this.download = download;
|
||||
}
|
||||
|
||||
get shouldPreroll() {
|
||||
if (CHROME) {
|
||||
return false;
|
||||
}
|
||||
const {uURL, renamer} = this.download;
|
||||
const {pathname, search, host} = uURL;
|
||||
if (PREROLL_NOPE.has(host)) {
|
||||
return false;
|
||||
}
|
||||
if (!renamer.p_ext) {
|
||||
return true;
|
||||
}
|
||||
if (search.length) {
|
||||
return true;
|
||||
}
|
||||
if (uURL.pathname.endsWith("/")) {
|
||||
return true;
|
||||
}
|
||||
if (PREROLL_HEURISTICS.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
if (PREROLL_HOSTS.test(host)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async roll() {
|
||||
try {
|
||||
return await (CHROME ? this.prerollChrome() : this.prerollFirefox());
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async prerollFirefox() {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
const {uURL, uReferrer} = this.download;
|
||||
const res = await fetch(uURL.toString(), {
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Range: "bytes=0-1",
|
||||
}),
|
||||
mode: "same-origin",
|
||||
signal,
|
||||
referrer: (uReferrer || uURL).toString(),
|
||||
});
|
||||
if (res.body) {
|
||||
res.body.cancel();
|
||||
}
|
||||
controller.abort();
|
||||
const {headers} = res;
|
||||
return this.finalize(headers, res);
|
||||
}
|
||||
|
||||
private async prerollChrome() {
|
||||
let rid = "";
|
||||
const {uURL, uReferrer} = this.download;
|
||||
const rurl = uURL.toString();
|
||||
let listener: any;
|
||||
const wr = new Promise<any[]>(resolve => {
|
||||
listener = (details: any) => {
|
||||
const {url, requestId, statusCode} = details;
|
||||
if (rid !== requestId && url !== rurl) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (statusCode >= 300 && statusCode < 400) {
|
||||
// Redirect, continue tracking;
|
||||
rid = requestId;
|
||||
return;
|
||||
}
|
||||
resolve(details.responseHeaders);
|
||||
};
|
||||
webRequest.onHeadersReceived.addListener(
|
||||
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
|
||||
});
|
||||
const p = Promise.race([
|
||||
wr,
|
||||
new Promise<any[]>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
|
||||
]);
|
||||
|
||||
p.finally(() => {
|
||||
webRequest.onHeadersReceived.removeListener(listener);
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
const res = await fetch(rurl, {
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
"Range": "bytes=0-1",
|
||||
"X-DTA-ID": this.download.sessionId.toString(),
|
||||
}),
|
||||
signal,
|
||||
referrer: (uReferrer || uURL).toString(),
|
||||
});
|
||||
if (res.body) {
|
||||
res.body.cancel();
|
||||
}
|
||||
controller.abort();
|
||||
const headers = await p;
|
||||
return this.finalize(
|
||||
new Headers(headers.map(i => [i.name, i.value])), res);
|
||||
}
|
||||
|
||||
private finalize(headers: Headers, res: Response): PrerollResults {
|
||||
const rv: PrerollResults = {};
|
||||
|
||||
const type = MimeType.parse(headers.get("content-type") || "");
|
||||
if (type) {
|
||||
rv.mime = type.essence;
|
||||
}
|
||||
|
||||
const dispHeader = headers.get("content-disposition");
|
||||
if (dispHeader) {
|
||||
const file = CDPARSER.parse(dispHeader);
|
||||
// Sanitize
|
||||
rv.name = sanitizePath(file.replace(/[/\\]+/g, "-"));
|
||||
}
|
||||
else {
|
||||
const detected = Preroller.maybeFindNameFromSearchParams(
|
||||
this.download, rv);
|
||||
if (detected) {
|
||||
rv.name = detected;
|
||||
}
|
||||
}
|
||||
|
||||
rv.finalURL = res.url;
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
const {status} = res;
|
||||
if (status === 404) {
|
||||
rv.error = "SERVER_BAD_CONTENT";
|
||||
}
|
||||
else if (status === 403) {
|
||||
rv.error = "SERVER_FORBIDDEN";
|
||||
}
|
||||
else if (status === 402 || status === 407) {
|
||||
rv.error = "SERVER_UNAUTHORIZED";
|
||||
}
|
||||
else if (NOPE_STATUSES.has(status)) {
|
||||
PREROLL_NOPE.add(this.download.uURL.host);
|
||||
if (PREROLL_NOPE.size > 1000) {
|
||||
PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value);
|
||||
}
|
||||
}
|
||||
else if (status > 400 && status < 500) {
|
||||
rv.error = "SERVER_FAILED";
|
||||
}
|
||||
/* eslint-enable no-magic-numbers */
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
static maybeFindNameFromSearchParams(
|
||||
download: Download, res: PrerollResults) {
|
||||
const {p_ext: ext} = download.renamer;
|
||||
if (ext && !PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) {
|
||||
return undefined;
|
||||
}
|
||||
return Preroller.findNameFromSearchParams(download.uURL, res.mime);
|
||||
}
|
||||
|
||||
static findNameFromSearchParams(url: URL, mimetype?: string) {
|
||||
const {searchParams} = url;
|
||||
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 (mimetype) {
|
||||
const mime = MimeDB.getMime(mimetype);
|
||||
if (mime && !mime.extensions.has(p.ext.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const sanitized = sanitizePath(p.name);
|
||||
if (sanitized.length <= detected.length) {
|
||||
continue;
|
||||
}
|
||||
detected = sanitized;
|
||||
}
|
||||
return detected;
|
||||
}
|
||||
}
|
@ -2,8 +2,12 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { parsePath, sanitizePath } from "../util";
|
||||
import { _ } from "../i18n";
|
||||
import { MimeDB } from "../mime";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { parsePath, PathInfo, sanitizePath } from "../util";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseDownload } from "./basedownload";
|
||||
|
||||
const REPLACE_EXPR = /\*\w+\*/gi;
|
||||
|
||||
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
|
||||
});
|
||||
|
||||
export default class Renamer {
|
||||
private readonly d: any;
|
||||
private readonly d: BaseDownload;
|
||||
|
||||
constructor(download: any) {
|
||||
private readonly nameinfo: PathInfo;
|
||||
|
||||
constructor(download: BaseDownload) {
|
||||
this.d = download;
|
||||
const info = parsePath(this.d.finalName);
|
||||
this.nameinfo = this.fixupExtension(info);
|
||||
}
|
||||
|
||||
get nameinfo() {
|
||||
return parsePath(this.d.finalName);
|
||||
private fixupExtension(info: PathInfo): PathInfo {
|
||||
if (!this.d.mime) {
|
||||
return info;
|
||||
}
|
||||
const mime = MimeDB.getMime(this.d.mime);
|
||||
if (!mime) {
|
||||
return info;
|
||||
}
|
||||
const {ext} = info;
|
||||
if (mime.major === "image" || mime.major === "video") {
|
||||
if (ext && mime.extensions.has(ext.toLowerCase())) {
|
||||
return info;
|
||||
}
|
||||
return new PathInfo(info.base, mime.primary, info.path);
|
||||
}
|
||||
if (ext) {
|
||||
return info;
|
||||
}
|
||||
return new PathInfo(info.base, mime.primary, info.path);
|
||||
}
|
||||
|
||||
get ref() {
|
||||
return this.d.uReferrer;
|
||||
}
|
||||
|
||||
|
||||
get p_name() {
|
||||
return this.nameinfo.base;
|
||||
}
|
||||
@ -169,24 +193,24 @@ export default class Renamer {
|
||||
}
|
||||
|
||||
toString() {
|
||||
const {mask} = this.d;
|
||||
const {mask, subfolder} = this.d;
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self: any = this;
|
||||
// XXX flat
|
||||
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) {
|
||||
const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
|
||||
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
|
||||
let prop = type.substr(1, type.length - 2);
|
||||
const flat = prop.startsWith("flat");
|
||||
if (flat) {
|
||||
prop = prop.substr(4);
|
||||
}
|
||||
prop = `p_${prop}`;
|
||||
const rv = (prop in self) ?
|
||||
let rv = (prop in self) ?
|
||||
(self[prop] || "").trim() :
|
||||
type;
|
||||
if (flat) {
|
||||
return rv.replace(/\/+/g, "-");
|
||||
rv = rv.replace(/[/\\]+/g, "-");
|
||||
}
|
||||
return rv;
|
||||
return rv.replace(/\/{2,}/g, "/");
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ export const PAUSED = 1 << 3;
|
||||
export const DONE = 1 << 4;
|
||||
export const CANCELED = 1 << 5;
|
||||
export const MISSING = 1 << 6;
|
||||
export const RETRYING = 1 << 7;
|
||||
|
||||
export const RESUMABLE = PAUSED | CANCELED;
|
||||
export const FORCABLE = PAUSED | QUEUED | CANCELED;
|
||||
export const PAUSABLE = QUEUED | CANCELED | RUNNING;
|
||||
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING;
|
||||
export const RESUMABLE = PAUSED | CANCELED | RETRYING;
|
||||
export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
|
||||
export const PAUSABLE = QUEUED | CANCELED | RUNNING | RETRYING;
|
||||
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING | RETRYING;
|
||||
|
65
lib/mime.ts
Normal file
@ -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",
|
||||
};
|
||||
|
||||
const TIMEOUT = 4000;
|
||||
|
||||
let gid = 1;
|
||||
|
||||
export class Notification extends EventEmitter {
|
||||
private notification: any;
|
||||
|
||||
private readonly generated: boolean;
|
||||
|
||||
constructor(id: string | null, options = {}) {
|
||||
super();
|
||||
|
||||
id = id || "DownThemAll-notification";
|
||||
this.generated = !id;
|
||||
id = id || `DownThemAll-notification${++gid}`;
|
||||
if (typeof options === "string") {
|
||||
options = {message: options};
|
||||
}
|
||||
@ -39,11 +46,16 @@ export class Notification extends EventEmitter {
|
||||
opened(notification: any) {
|
||||
this.notification = notification;
|
||||
this.emit("opened", this);
|
||||
if (this.generated) {
|
||||
setTimeout(() => {
|
||||
notifications.clear(notification);
|
||||
}, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
clicked(notification: any, button?: number) {
|
||||
// We can only be clicked, when we were opened, at which point the
|
||||
// notification id is availablfalse
|
||||
// notification id is available
|
||||
if (notification !== this.notification) {
|
||||
return;
|
||||
}
|
||||
@ -52,6 +64,7 @@ export class Notification extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
this.emit("clicked", this);
|
||||
console.log("clicked", notification);
|
||||
}
|
||||
|
||||
async closed(notification: any) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import * as DEFAULT_PREFS from "../data/prefs.json";
|
||||
import DEFAULT_PREFS from "../data/prefs.json";
|
||||
import { EventEmitter } from "./events";
|
||||
import {loadOverlay} from "./objectoverlay";
|
||||
import { loadOverlay } from "./objectoverlay";
|
||||
import { storage } from "./browser";
|
||||
|
||||
const PREFS = Symbol("PREFS");
|
||||
@ -99,6 +99,5 @@ export class PrefWatcher {
|
||||
|
||||
changed(prefs: any, key: string, value: any) {
|
||||
this.value = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export class RecentList {
|
||||
this.pref = `savedlist-${pref}`;
|
||||
this.defaults = Array.from(defaults);
|
||||
this[LIST] = [];
|
||||
this.limit = 5;
|
||||
this.limit = 15;
|
||||
}
|
||||
|
||||
get values() {
|
||||
@ -102,8 +102,8 @@ export const MASK = new RecentList("mask", [
|
||||
"*name*.*ext*",
|
||||
"*num*_*name*.*ext*",
|
||||
"*url*-*name*.*ext*",
|
||||
"*name* (*text*).*ext*",
|
||||
"*name* (*hh*-*mm*).*ext*"
|
||||
"downthemall/*y*-*m*/*name*.*ext*",
|
||||
"*name* (*text*).*ext*"
|
||||
]);
|
||||
MASK.init().catch(console.error);
|
||||
|
||||
@ -116,3 +116,9 @@ export const FASTFILTER = new RecentList("fastfilter", [
|
||||
"*.z??, *.css, *.html"
|
||||
]);
|
||||
FASTFILTER.init().catch(console.error);
|
||||
|
||||
export const SUBFOLDER = new RecentList("subfolder", [
|
||||
"",
|
||||
"downthemall",
|
||||
]);
|
||||
SUBFOLDER.init().catch(console.error);
|
||||
|
@ -9,12 +9,26 @@ import { donate, openPrefs, openUrls } from "./windowutils";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { filters, FAST, Filter } from "./filters";
|
||||
import { WindowStateTracker } from "./windowstatetracker";
|
||||
import { windows } from "./browser";
|
||||
import { windows, CHROME } 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) {
|
||||
let ws = items.map((item: any, idx: number) => {
|
||||
item.idx = idx;
|
||||
export interface ItemDelta {
|
||||
idx: number;
|
||||
matched?: string | null;
|
||||
}
|
||||
|
||||
function computeSelection(
|
||||
filters: Filter[],
|
||||
items: BaseMatchedItem[],
|
||||
onlyFast: boolean): ItemDelta[] {
|
||||
let ws = items.map((item, idx: number) => {
|
||||
item.idx = item.idx || idx;
|
||||
const {matched = null} = item;
|
||||
item.prevMatched = matched;
|
||||
item.matched = null;
|
||||
@ -23,14 +37,20 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
||||
for (const filter of filters) {
|
||||
ws = ws.filter(item => {
|
||||
if (filter.matchItem(item)) {
|
||||
item.matched = filter.id === FAST ?
|
||||
"fast" :
|
||||
(onlyFast ? null : filter.id);
|
||||
if (filter.id === FAST) {
|
||||
item.matched = "fast";
|
||||
}
|
||||
else if (!onlyFast && typeof filter.id === "string") {
|
||||
item.matched = filter.id;
|
||||
}
|
||||
else {
|
||||
item.matched = null;
|
||||
}
|
||||
}
|
||||
return !item.matched;
|
||||
});
|
||||
}
|
||||
return items.filter(item => item.prevMatched !== item.matched). map(item => {
|
||||
return items.filter(item => item.prevMatched !== item.matched).map(item => {
|
||||
return {
|
||||
idx: item.idx,
|
||||
matched: item.matched
|
||||
@ -41,6 +61,9 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
||||
function *computeActiveFiltersGen(
|
||||
filters: Filter[], activeOverrides: Map<string, boolean>) {
|
||||
for (const filter of filters) {
|
||||
if (typeof filter.id !== "string") {
|
||||
continue;
|
||||
}
|
||||
const override = activeOverrides.get(filter.id);
|
||||
if (typeof override === "boolean") {
|
||||
if (override) {
|
||||
@ -59,11 +82,11 @@ function computeActiveFilters(
|
||||
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
|
||||
}
|
||||
|
||||
function filtersToDescs(filters: any[]) {
|
||||
function filtersToDescs(filters: Filter[]) {
|
||||
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 tracker = new WindowStateTracker("select", {
|
||||
minWidth: 700,
|
||||
@ -75,9 +98,16 @@ export async function select(links: any[], media: any[]) {
|
||||
type: "popup",
|
||||
});
|
||||
const window = await windows.create(windowOptions);
|
||||
tracker.track(window.id);
|
||||
try {
|
||||
if (!CHROME) {
|
||||
windows.update(window.id, tracker.getOptions({}));
|
||||
}
|
||||
const port = await Promise.race<Port>([
|
||||
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
||||
new Promise<Port>(resolve => Bus.oncePort("select", port => {
|
||||
resolve(port);
|
||||
return true;
|
||||
})),
|
||||
timeout<Port>(5 * 1000)]);
|
||||
if (!port.isSelf) {
|
||||
throw Error("Invalid sender connected");
|
||||
@ -85,26 +115,26 @@ export async function select(links: any[], media: any[]) {
|
||||
tracker.track(window.id, port);
|
||||
|
||||
const overrides = new Map();
|
||||
let fast: any = null;
|
||||
let fast: Filter | null = null;
|
||||
let onlyFast: false;
|
||||
try {
|
||||
fast = fm.getFastFilter();
|
||||
fast = await fm.getFastFilter();
|
||||
}
|
||||
catch (ex) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
const sendFilters = function(delta = false) {
|
||||
let {linkFilters, mediaFilters} = fm;
|
||||
const {linkFilters, mediaFilters} = fm;
|
||||
const alink = computeActiveFilters(linkFilters, overrides);
|
||||
const amedia = computeActiveFilters(mediaFilters, overrides);
|
||||
const sactiveFilters = new Set<any>();
|
||||
[alink, amedia].forEach(
|
||||
a => a.forEach(filter => sactiveFilters.add(filter.id)));
|
||||
const activeFilters = Array.from(sactiveFilters);
|
||||
linkFilters = filtersToDescs(linkFilters);
|
||||
mediaFilters = filtersToDescs(mediaFilters);
|
||||
port.post("filters", {linkFilters, mediaFilters, activeFilters});
|
||||
const linkFilterDescs = filtersToDescs(linkFilters);
|
||||
const mediaFilterDescs = filtersToDescs(mediaFilters);
|
||||
port.post("filters", {linkFilterDescs, mediaFilterDescs, activeFilters});
|
||||
|
||||
if (fast) {
|
||||
alink.unshift(fast);
|
||||
@ -128,9 +158,6 @@ export async function select(links: any[], media: 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);
|
||||
});
|
||||
|
||||
@ -166,8 +193,8 @@ export async function select(links: any[], media: any[]) {
|
||||
openPrefs();
|
||||
});
|
||||
|
||||
port.on("openUrls", ({urls}) => {
|
||||
openUrls(urls);
|
||||
port.on("openUrls", ({urls, incognito}) => {
|
||||
openUrls(urls, incognito);
|
||||
});
|
||||
|
||||
try {
|
||||
@ -175,7 +202,11 @@ export async function select(links: any[], media: any[]) {
|
||||
sendFilters(false);
|
||||
const type = await Prefs.get("last-type", "links");
|
||||
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) {
|
||||
const f = fm.get(filter);
|
||||
if (f) {
|
||||
@ -183,7 +214,7 @@ export async function select(links: any[], media: any[]) {
|
||||
}
|
||||
}
|
||||
await fm.save();
|
||||
return result;
|
||||
return {items: selectedItems, options};
|
||||
}
|
||||
finally {
|
||||
fm.off("changed", sendFilters);
|
||||
|
@ -6,9 +6,11 @@ import { Bus, Port } from "./bus";
|
||||
import { WindowStateTracker } from "./windowstatetracker";
|
||||
import { Promised, timeout } from "./util";
|
||||
import { donate } from "./windowutils";
|
||||
import { windows } from "./browser";
|
||||
import { windows, CHROME } 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", {
|
||||
minWidth: 700,
|
||||
minHeight: 460
|
||||
@ -19,9 +21,16 @@ export async function single(item: any) {
|
||||
type: "popup",
|
||||
});
|
||||
const window = await windows.create(windowOptions);
|
||||
tracker.track(window.id);
|
||||
try {
|
||||
if (!CHROME) {
|
||||
windows.update(window.id, tracker.getOptions({}));
|
||||
}
|
||||
const port: Port = await Promise.race<Port>([
|
||||
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
||||
new Promise<Port>(resolve => Bus.oncePort("single", port => {
|
||||
resolve(port);
|
||||
return true;
|
||||
})),
|
||||
timeout<Port>(5 * 1000)]);
|
||||
if (!port.isSelf) {
|
||||
throw Error("Invalid sender connected");
|
||||
@ -46,7 +55,9 @@ export async function single(item: any) {
|
||||
donate();
|
||||
});
|
||||
|
||||
port.post("item", item);
|
||||
if (item) {
|
||||
port.post("item", {item});
|
||||
}
|
||||
return await done;
|
||||
}
|
||||
finally {
|
||||
|
101
lib/util.ts
@ -2,8 +2,9 @@
|
||||
// License: MIT
|
||||
|
||||
import * as psl from "psl";
|
||||
import {memoize, identity} from "./memoize";
|
||||
export {debounce} from "../uikit/lib/util";
|
||||
import { identity, memoize } from "./memoize";
|
||||
import { IPReg } from "./ipreg";
|
||||
export { debounce } from "../uikit/lib/util";
|
||||
|
||||
export class Promised {
|
||||
private promise: Promise<any>;
|
||||
@ -96,8 +97,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
|
||||
export const sanitizePath = identity(
|
||||
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
|
||||
|
||||
export class PathInfo {
|
||||
private baseField: string;
|
||||
|
||||
private extField: string;
|
||||
|
||||
private pathField: string;
|
||||
|
||||
private nameField: string;
|
||||
|
||||
private fullField: string;
|
||||
|
||||
constructor(base: string, ext: string, path: string) {
|
||||
this.baseField = base;
|
||||
this.extField = ext;
|
||||
this.pathField = path;
|
||||
this.update();
|
||||
}
|
||||
|
||||
get base() {
|
||||
return this.baseField;
|
||||
}
|
||||
|
||||
set base(nv) {
|
||||
this.baseField = sanitizePath(nv);
|
||||
this.update();
|
||||
}
|
||||
|
||||
get ext() {
|
||||
return this.extField;
|
||||
}
|
||||
|
||||
set ext(nv) {
|
||||
this.extField = sanitizePath(nv);
|
||||
this.update();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.nameField;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.pathField;
|
||||
}
|
||||
|
||||
set path(nv) {
|
||||
this.pathField = sanitizePath(nv);
|
||||
this.update();
|
||||
}
|
||||
|
||||
get full() {
|
||||
return this.fullField;
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
|
||||
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new PathInfo(this.baseField, this.extField, this.pathField);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX cleanup + test
|
||||
export const parsePath = memoize(function parsePath(path: string | URL) {
|
||||
export const parsePath = memoize(function parsePath(
|
||||
path: string | URL): PathInfo {
|
||||
if (path instanceof URL) {
|
||||
path = decodeURIComponent(path.pathname);
|
||||
}
|
||||
@ -127,13 +192,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
|
||||
}
|
||||
|
||||
path = pieces.join("/");
|
||||
return {
|
||||
path,
|
||||
name,
|
||||
base,
|
||||
ext,
|
||||
full: path ? `${path}/${name}` : name
|
||||
};
|
||||
return new PathInfo(base, ext, path);
|
||||
});
|
||||
|
||||
export class CoalescedUpdate<T> extends Set<T> {
|
||||
@ -179,7 +238,10 @@ export interface URLd extends URL {
|
||||
Object.defineProperty(URL.prototype, "domain", {
|
||||
get() {
|
||||
try {
|
||||
return hostToDomain(this.host) || this.host;
|
||||
const {hostname} = this;
|
||||
return IPReg.test(hostname) ?
|
||||
hostname :
|
||||
hostToDomain(hostname) || hostname;
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
@ -299,3 +361,20 @@ export function mapFilterInSitu<TRes, T>(
|
||||
export function randint(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
|
||||
export function validateSubFolder(folder: string) {
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
folder = folder.replace(/[/\\]+/g, "/");
|
||||
if (folder.startsWith("/")) {
|
||||
throw new Error("error.noabsolutepath");
|
||||
}
|
||||
if (/^[a-z]:\//i.test(folder)) {
|
||||
throw new Error("error.noabsolutepath");
|
||||
}
|
||||
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
|
||||
throw new Error("error.nodotsinpath");
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
import { Prefs } from "./prefs";
|
||||
import { windows } from "./browser";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Port } from "./bus";
|
||||
|
||||
|
||||
const VALID_WINDOW_STATES = Object.freeze(new Set(["normal", "maximized"]));
|
||||
@ -55,13 +57,15 @@ export class WindowStateTracker {
|
||||
|
||||
getOptions(options: any) {
|
||||
const result = Object.assign(options, {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
state: this.state,
|
||||
});
|
||||
if (this.top >= 0) {
|
||||
result.top = this.top;
|
||||
result.left = this.left;
|
||||
if (result.state !== "maximized") {
|
||||
result.width = this.width;
|
||||
result.height = this.height;
|
||||
if (this.top >= 0) {
|
||||
result.top = this.top;
|
||||
result.left = this.left;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -78,34 +82,48 @@ export class WindowStateTracker {
|
||||
if (!this.windowId) {
|
||||
return;
|
||||
}
|
||||
const window = await windows.get(this.windowId);
|
||||
if (!VALID_WINDOW_STATES.has(window.state)) {
|
||||
return;
|
||||
try {
|
||||
const window = await windows.get(this.windowId);
|
||||
if (!VALID_WINDOW_STATES.has(window.state)) {
|
||||
return;
|
||||
}
|
||||
const previous = JSON.stringify(this);
|
||||
this.width = window.width;
|
||||
this.height = window.height;
|
||||
this.left = window.left;
|
||||
this.top = window.top;
|
||||
this.state = window.state;
|
||||
this.validate();
|
||||
if (previous === JSON.stringify(this)) {
|
||||
// Nothing changed
|
||||
return;
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
const previous = JSON.stringify(this);
|
||||
this.width = window.width;
|
||||
this.height = window.height;
|
||||
this.left = window.left;
|
||||
this.top = window.top;
|
||||
this.state = window.state;
|
||||
this.validate();
|
||||
if (previous === JSON.stringify(this)) {
|
||||
// Nothing changed
|
||||
return;
|
||||
catch {
|
||||
// ignored
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
|
||||
track(windowId: number, port: any) {
|
||||
track(windowId: number, port?: Port) {
|
||||
if (port) {
|
||||
port.on("resized", this.update);
|
||||
port.on("unload", e => this.finalize(e));
|
||||
port.on("disconnect", this.finalize.bind(this));
|
||||
}
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
async finalize() {
|
||||
async finalize(state?: any) {
|
||||
if (state) {
|
||||
this.left = state.left;
|
||||
this.top = state.top;
|
||||
}
|
||||
await this.update();
|
||||
this.windowId = 0;
|
||||
if (state) {
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
@ -1,30 +1,61 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { windows, tabs, runtime } from "../lib/browser";
|
||||
import {getManager} from "./manager/man";
|
||||
import * as DEFAULT_ICONS from "../data/icons.json";
|
||||
import { windows, tabs, runtime, CHROME } from "../lib/browser";
|
||||
import { getManager } from "./manager/man";
|
||||
import DEFAULT_ICONS from "../data/icons.json";
|
||||
import { Prefs } from "./prefs";
|
||||
import { _ } from "./i18n";
|
||||
import { WindowStateTracker } from "./windowstatetracker";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Port, Bus } from "./bus";
|
||||
import { timeout } from "./util";
|
||||
|
||||
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
||||
const DONATE_LANG_URLS = Object.freeze(new Map([
|
||||
["de", "https://www.downthemall.org/howto/donate/spenden/"],
|
||||
]));
|
||||
const MANAGER_URL = "/windows/manager.html";
|
||||
|
||||
const IS_CHROME = navigator && navigator.userAgent.includes("Chrome");
|
||||
|
||||
|
||||
export async function mostRecentBrowser(): Promise<any> {
|
||||
let window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||
filter((w: any) => w.type === "normal").pop();
|
||||
export async function mostRecentBrowser(incognito: boolean): Promise<any> {
|
||||
let window;
|
||||
try {
|
||||
window = await windows.getCurrent();
|
||||
if (window.type !== "normal") {
|
||||
throw new Error("not a normal window");
|
||||
}
|
||||
if (incognito && !window.incognito) {
|
||||
throw new Error("Not incognito");
|
||||
}
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
window = await windows.getlastFocused();
|
||||
if (window.type !== "normal") {
|
||||
throw new Error("not a normal window");
|
||||
}
|
||||
if (incognito && !window.incognito) {
|
||||
throw new Error("Not incognito");
|
||||
}
|
||||
}
|
||||
catch {
|
||||
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||
filter(
|
||||
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
|
||||
pop();
|
||||
}
|
||||
}
|
||||
if (!window) {
|
||||
window = await windows.create({
|
||||
url: DONATE_URL,
|
||||
incognito: !!incognito,
|
||||
type: "normal",
|
||||
});
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
export async function openInTab(url: string) {
|
||||
const window = await mostRecentBrowser();
|
||||
export async function openInTab(url: string, incognito: boolean) {
|
||||
const window = await mostRecentBrowser(incognito);
|
||||
await tabs.create({
|
||||
active: true,
|
||||
url,
|
||||
@ -33,7 +64,7 @@ export async function openInTab(url: string) {
|
||||
await windows.update(window.id, {focused: true});
|
||||
}
|
||||
|
||||
export async function openInTabOrFocus(url: string) {
|
||||
export async function openInTabOrFocus(url: string, incognito: boolean) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
@ -43,21 +74,22 @@ export async function openInTabOrFocus(url: string) {
|
||||
await windows.update(tab.windowId, {focused: true});
|
||||
return;
|
||||
}
|
||||
await openInTab(url);
|
||||
await openInTab(url, incognito);
|
||||
}
|
||||
|
||||
export async function maybeOpenInTab(url: string) {
|
||||
export async function maybeOpenInTab(url: string, incognito: boolean) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
if (etabs.length) {
|
||||
return;
|
||||
}
|
||||
await openInTab(url);
|
||||
await openInTab(url, incognito);
|
||||
}
|
||||
|
||||
export async function donate() {
|
||||
await openInTab(DONATE_URL);
|
||||
const url = DONATE_LANG_URLS.get(_("language_code")) || DONATE_URL;
|
||||
await openInTab(url, false);
|
||||
}
|
||||
|
||||
export async function openPrefs() {
|
||||
@ -71,16 +103,64 @@ export async function openManager(focus = true) {
|
||||
catch (ex) {
|
||||
console.error(ex.toString(), ex);
|
||||
}
|
||||
const url = runtime.getURL(MANAGER_URL);
|
||||
const openInPopup = await Prefs.get("manager-in-popup");
|
||||
if (openInPopup) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
if (etabs.length) {
|
||||
if (!focus) {
|
||||
return;
|
||||
}
|
||||
const tab = etabs.pop();
|
||||
await tabs.update(tab.id, {active: true});
|
||||
await windows.update(tab.windowId, {focused: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const tracker = new WindowStateTracker("manager", {
|
||||
minWidth: 700,
|
||||
minHeight: 500,
|
||||
});
|
||||
await tracker.init();
|
||||
const windowOptions = tracker.getOptions({
|
||||
url,
|
||||
type: "popup",
|
||||
});
|
||||
const window = await windows.create(windowOptions);
|
||||
tracker.track(window.id);
|
||||
try {
|
||||
if (!CHROME) {
|
||||
windows.update(window.id, tracker.getOptions({}));
|
||||
}
|
||||
const port = await Promise.race<Port>([
|
||||
new Promise<Port>(resolve => Bus.oncePort("manager", port => {
|
||||
resolve(port);
|
||||
return true;
|
||||
})),
|
||||
timeout<Port>(5 * 1000)]);
|
||||
if (!port.isSelf) {
|
||||
throw Error("Invalid sender connected");
|
||||
}
|
||||
tracker.track(window.id, port);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("couldn't track manager", ex);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (focus) {
|
||||
await openInTabOrFocus(await runtime.getURL(MANAGER_URL));
|
||||
await openInTabOrFocus(runtime.getURL(MANAGER_URL), false);
|
||||
}
|
||||
else {
|
||||
await maybeOpenInTab(await runtime.getURL(MANAGER_URL));
|
||||
await maybeOpenInTab(runtime.getURL(MANAGER_URL), false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openUrls(urls: string) {
|
||||
const window = await mostRecentBrowser();
|
||||
export async function openUrls(urls: string, incognito: boolean) {
|
||||
const window = await mostRecentBrowser(incognito);
|
||||
for (const url of urls) {
|
||||
try {
|
||||
await tabs.create({
|
||||
@ -106,32 +186,10 @@ const ICONS = Object.freeze((() => {
|
||||
return new Map<string, string>(rv);
|
||||
})());
|
||||
|
||||
let iconForPathPlatform: Function;
|
||||
if (IS_CHROME) {
|
||||
const FOUR = 128;
|
||||
const DOUBLE = 64;
|
||||
iconForPathPlatform = function(icon: string, size: number) {
|
||||
let scale = "1x";
|
||||
if (size > FOUR) {
|
||||
// wishful thinking at this point
|
||||
scale = "4x";
|
||||
}
|
||||
else if (size > DOUBLE) {
|
||||
scale = "2x";
|
||||
}
|
||||
return `chrome://fileicon/${icon}?scale=${scale}`;
|
||||
};
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
iconForPathPlatform = function(icon: string, size: number) {
|
||||
return ICONS.get(icon) || "icon-file-generic";
|
||||
};
|
||||
}
|
||||
export const DEFAULT_ICON_SIZE = 16;
|
||||
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
export function iconForPath(path: string, size = 16) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export function iconForPath(path: string, size = DEFAULT_ICON_SIZE) {
|
||||
const web = /^https?:\/\//.test(path);
|
||||
let file = path.split(/[\\/]/).pop();
|
||||
if (file) {
|
||||
@ -152,7 +210,7 @@ export function iconForPath(path: string, size = 16) {
|
||||
file = "file";
|
||||
}
|
||||
}
|
||||
return iconForPathPlatform(file, size);
|
||||
return ICONS.get(file) || "icon-file-generic";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "DownThemAll!",
|
||||
"version": "4.0.1",
|
||||
"version": "4.2.2",
|
||||
|
||||
"description": "__MSG_extensionDescription__",
|
||||
"homepage_url": "https://downthemall.org/",
|
||||
@ -9,6 +9,8 @@
|
||||
|
||||
"default_locale": "en",
|
||||
|
||||
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: http: https: 'self'; default-src 'self'",
|
||||
|
||||
"icons": {
|
||||
"16": "style/icon16.png",
|
||||
"32": "style/icon32.png",
|
||||
@ -22,14 +24,19 @@
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"contextMenus",
|
||||
"menus",
|
||||
"downloads",
|
||||
"downloads.open",
|
||||
"downloads.shelf",
|
||||
"history",
|
||||
"menus",
|
||||
"notifications",
|
||||
"sessions",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webNavigation"
|
||||
"theme",
|
||||
"webNavigation",
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
],
|
||||
|
||||
"background": {
|
||||
@ -40,7 +47,8 @@
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"browser_style": false,
|
||||
"browser_style": true,
|
||||
"default_popup": "windows/popup.html",
|
||||
"default_icon": {
|
||||
"16": "style/icon16.png",
|
||||
"32": "style/icon32.png",
|
||||
|
26
package.json
@ -18,22 +18,24 @@
|
||||
"author": "Nils Maier",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||
"@typescript-eslint/parser": "^2.0.0",
|
||||
"@types/node": "^12.7.8",
|
||||
"@typescript-eslint/eslint-plugin": "^2.3.2",
|
||||
"@typescript-eslint/parser": "^2.3.2",
|
||||
"chai": "^4.1.2",
|
||||
"eslint": "^6.1.0",
|
||||
"mocha": "^6.2.0",
|
||||
"ts-loader": "^6.0.4",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "^3.5.3",
|
||||
"webpack": "^4.39.2",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"eslint": "^6.5.1",
|
||||
"mocha": "^6.2.1",
|
||||
"ts-loader": "^6.2.0",
|
||||
"ts-node": "^8.4.1",
|
||||
"typescript": "^3.6.3",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-cli": "^3.3.9",
|
||||
"xregexp": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/psl": "^1.1.0",
|
||||
"psl": "^1.3.0",
|
||||
"webextension-polyfill": "^0.4.0"
|
||||
"@types/whatwg-mimetype": "^2.1.0",
|
||||
"psl": "^1.4.0",
|
||||
"webextension-polyfill": "^0.5.0",
|
||||
"whatwg-mimetype": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ return url;
|
||||
}();
|
||||
|
||||
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) {
|
||||
@ -75,6 +77,8 @@ function urlToUsable(e: any, u: string) {
|
||||
}
|
||||
|
||||
class Gatherer {
|
||||
private: boolean;
|
||||
|
||||
textLinks: boolean;
|
||||
|
||||
selectionOnly: boolean;
|
||||
@ -86,53 +90,82 @@ class Gatherer {
|
||||
transferable: string[];
|
||||
|
||||
constructor(options: any) {
|
||||
this.private = !!options.private;
|
||||
this.textLinks = options.textLinks;
|
||||
this.selectionOnly = options.selectionOnly;
|
||||
this.selection = options.selectionOnly ? getSelection() : null;
|
||||
this.schemes = new Set(options.schemes);
|
||||
this.transferable = options.transferable;
|
||||
this.collectLink = this.collectLink.bind(this);
|
||||
this.collectImages = this.collectImages.bind(this);
|
||||
this.collectImage = this.collectImage.bind(this);
|
||||
this.collectMedia = this.collectMedia.bind(this);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
collectLink(a: HTMLAnchorElement) {
|
||||
const item = this.makeItem(a.href, a);
|
||||
if (!item) {
|
||||
try {
|
||||
const item = this.makeItem(a.href, a);
|
||||
if (!item) {
|
||||
return item;
|
||||
}
|
||||
urlToUsable(item, item.url);
|
||||
item.fileName = sanitize(a.getAttribute("download"));
|
||||
item.description = extractDescription(a);
|
||||
return item;
|
||||
}
|
||||
urlToUsable(item, item.url);
|
||||
item.fileName = sanitize(a.getAttribute("download"));
|
||||
item.description = extractDescription(a);
|
||||
return item;
|
||||
catch (ex) {
|
||||
console.error("oopsed link", ex.toString(), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
*collectImagesInternal(img: HTMLImageElement) {
|
||||
const src = img.currentSrc || img.src;
|
||||
const item = this.makeItem(src, img);
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
*collectImageInternal(img: HTMLImageElement) {
|
||||
try {
|
||||
{
|
||||
const {src} = img;
|
||||
const item = this.makeItem(src, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
{
|
||||
const {currentSrc} = img;
|
||||
const item = this.makeItem(currentSrc, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
|
||||
const {srcset} = img;
|
||||
if (!srcset) {
|
||||
return;
|
||||
}
|
||||
const imgs = srcset.split(",").flatMap(e => {
|
||||
const idx = e.lastIndexOf(" ");
|
||||
return (idx > 0 ? e.slice(0, idx) : e).trim();
|
||||
});
|
||||
for (const i of imgs) {
|
||||
const item = this.makeItem(i, img);
|
||||
if (item) {
|
||||
yield item;
|
||||
{
|
||||
const {srcset} = img;
|
||||
if (!srcset) {
|
||||
return;
|
||||
}
|
||||
const imgs = srcset.split(",").flatMap(e => {
|
||||
const idx = e.lastIndexOf(" ");
|
||||
return (idx > 0 ? e.slice(0, idx) : e).trim();
|
||||
});
|
||||
for (const i of imgs) {
|
||||
const item = this.makeItem(i, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("oops image", ex.toString(), ex.stack, ex);
|
||||
}
|
||||
}
|
||||
|
||||
collectImages(img: HTMLImageElement) {
|
||||
return [...this.collectImagesInternal(img)];
|
||||
collectImage(img: HTMLImageElement) {
|
||||
return [...this.collectImageInternal(img)];
|
||||
}
|
||||
|
||||
collectMediaInternal(title: string | undefined | null, el: HTMLMediaElement) {
|
||||
@ -156,13 +189,19 @@ class Gatherer {
|
||||
}
|
||||
|
||||
collectMedia(el: HTMLMediaElement) {
|
||||
const item = this.collectMediaInternal(el.getAttribute("title"), el);
|
||||
const rv = item ? [item] : [];
|
||||
const title: string | undefined = item && item.title ||
|
||||
try {
|
||||
const item = this.collectMediaInternal(el.getAttribute("title"), el);
|
||||
const rv = item ? [item] : [];
|
||||
const title: string | undefined = item && item.title ||
|
||||
el.getAttribute("title");
|
||||
rv.push(...Array.from(el.querySelectorAll("source")).
|
||||
map(this.collectMediaInternal.bind(this, title)));
|
||||
return rv;
|
||||
rv.push(...Array.from(el.querySelectorAll("source")).
|
||||
map(this.collectMediaInternal.bind(this, title)));
|
||||
return rv;
|
||||
}
|
||||
catch (ex) {
|
||||
console.log("oopsed media", ex.toString(), ex);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
*findTexts() {
|
||||
@ -207,8 +246,14 @@ class Gatherer {
|
||||
if (!this.textLinks) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(this.findTextLinks()).
|
||||
map(link => this.makeItem(link.href, link));
|
||||
try {
|
||||
return Array.from(this.findTextLinks()).
|
||||
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 {
|
||||
@ -226,6 +271,7 @@ class Gatherer {
|
||||
return {
|
||||
url: url.href,
|
||||
title,
|
||||
private: this.private
|
||||
};
|
||||
}
|
||||
catch (ex) {
|
||||
@ -266,7 +312,7 @@ class Gatherer {
|
||||
function gather(msg: any, sender: any, callback: Function) {
|
||||
try {
|
||||
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
||||
return;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const gatherer = new Gatherer(msg);
|
||||
const result = {
|
||||
@ -276,7 +322,7 @@ function gather(msg: any, sender: any, callback: Function) {
|
||||
gatherer.collectTextLinks()),
|
||||
media: gatherer.makeUniqueItems(
|
||||
Array.from(document.querySelectorAll("img")).
|
||||
flatMap(gatherer.collectImages),
|
||||
flatMap(gatherer.collectImage),
|
||||
Array.from(document.querySelectorAll("video")).
|
||||
flatMap(gatherer.collectMedia),
|
||||
Array.from(document.querySelectorAll("audio")).
|
||||
@ -284,10 +330,11 @@ function gather(msg: any, sender: any, callback: Function) {
|
||||
),
|
||||
};
|
||||
urlToUsable(result, result.baseURL);
|
||||
callback(result);
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex.toString(), ex.stack, ex);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
sounds/done.wav
Normal file
BIN
sounds/error.wav
Normal file
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 |