Compare commits
171 Commits
Author | SHA1 | Date | |
---|---|---|---|
3deac4cda0 | |||
49a589cb87 | |||
3c644e615d | |||
c540c1fc29 | |||
3e31cb326b | |||
add5b65ff2 | |||
6282f54ac0 | |||
1af01856e1 | |||
3f650be613 | |||
0fa7738031 | |||
ebd1581742 | |||
db1b50bc90 | |||
8ad4d7a59d | |||
5c6cd47485 | |||
dbd596e8ea | |||
0362eaf6e3 | |||
4538066e9d | |||
0da87398b4 | |||
82e7361567 | |||
5c84493a0f | |||
83cb8e32f1 | |||
c49af54532 | |||
eee8c4ea1a | |||
858b8f277e | |||
6841fdcfc8 | |||
1e8e7ad6ec | |||
6ed84b9560 | |||
f739cb789c | |||
0d470a7ce0 | |||
9ec1d46787 | |||
ba283e9221 | |||
e549886532 | |||
d3b7032229 | |||
5586bcb671 | |||
bd72c417d2 | |||
52643e0bec | |||
af59fb60ff | |||
ac2bc8cdfd | |||
c5309a8923 | |||
1370723e6d | |||
4ba827fc15 | |||
b2e20b9875 | |||
ef9cff003d | |||
74b3ce7eb1 | |||
6528e2118e | |||
c901438216 | |||
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 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve DownThemAll!
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@ -8,9 +8,9 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. iOS]
|
- OS: [e.g. Windows 10, macOS, Linux (distribution, desktop environment)]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser and version: [e.g. Firefox 42, Chrome 70, Opera 15, Seamonkey 2.16]
|
||||||
- Version [e.g. 22]
|
- DownThemAll! version: [e.g. 4.2, 3.0, latest]
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for this project
|
about: Suggest a feature or an idea for DownThemAll!
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
@ -81,3 +81,9 @@ The javascript library accessing it is licensed under the MIT license.
|
|||||||
## whatwg-mimetype
|
## whatwg-mimetype
|
||||||
|
|
||||||
Licensed under MIT
|
Licensed under MIT
|
||||||
|
|
||||||
|
|
||||||
|
## CDHeaderParser
|
||||||
|
|
||||||
|
Licensed under MPL-2
|
||||||
|
(c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
|
||||||
|
47
Readme.md
@ -1,3 +1,7 @@
|
|||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
# DownThemAll! WE
|
# DownThemAll! WE
|
||||||
|
|
||||||
The DownThemAll! WebExtension.
|
The DownThemAll! WebExtension.
|
||||||
@ -27,28 +31,59 @@ If you would like to help out translating DTA, please see our [translation guide
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
You will want to `yarn` the development dependencies such as webpack first.
|
- [node](https://nodejs.org/en/)
|
||||||
|
- [yarn](https://yarnpkg.com/)
|
||||||
|
- [python3](https://www.python.org/) >= 3.6 (to build zips)
|
||||||
|
- [web-ext](https://www.npmjs.com/package/web-ext) (for development ease)
|
||||||
|
|
||||||
Afterwards, you will want to run`yarn watch`.
|
### Setup
|
||||||
|
|
||||||
|
You will want to run `yarn` to install the development dependencies such as webpack first.
|
||||||
|
|
||||||
|
### Making changes
|
||||||
|
|
||||||
|
Just use your favorite text editor to edit the files.
|
||||||
|
|
||||||
|
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.
|
This will run the webpack bundler in watch mode, transpiling the TypeScript to Javascript and updating bundles as you change the source.
|
||||||
|
|
||||||
Please note: You have to run `yarn watch` (at least once) as it builds the actual script bundles.
|
Please note: You have to run `yarn watch` or `yarn build` (at least once) as it builds the actual script bundles.
|
||||||
|
|
||||||
### Firefox
|
### Running in 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.
|
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.
|
If you did, then running `yarn webext` (additionally to `yarn watch`) will run the WebExtension in a development profile. This will use the directory `../dtalite.p` to keep a development profile. You might need to create this directory before you use this command. Furthermore `yarn webext` will watch for changes to the sources and automatically reload the extension.
|
||||||
|
|
||||||
Alternative, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox with the right about:config preferences).
|
Alternatively, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox with the right about:config preferences).
|
||||||
|
|
||||||
### Chrome
|
### Running in Chrome/Chromium/etc
|
||||||
|
|
||||||
You have to build the bundles first, of course.
|
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.
|
Then put your Chrome into Developement Mode on the Extensions page, and Load Unpacked the directory of your downthemall clone.
|
||||||
|
|
||||||
|
### Making release zips
|
||||||
|
|
||||||
|
To get a basic unofficial set of zips for Firefox and chrome, run `yarn build`.
|
||||||
|
|
||||||
|
If you want to generate release builds like the ones that are eventually released in the extension stores, use `python3 util/build.py --mode=release`.
|
||||||
|
|
||||||
|
The output is located in `web-ext-artifacts`.
|
||||||
|
|
||||||
|
- `-fx.zip` are Firefox builds
|
||||||
|
- `-crx.zip` are Chrome/Chromium builds
|
||||||
|
- `-opr.zip` are Opera builds (essentially like the Chrome one, but without sounds)
|
||||||
|
|
||||||
|
### The AMO Editors tl;dr guide
|
||||||
|
|
||||||
|
1. Install the requirements.
|
||||||
|
2. `yarn && python3 build/util.py --mode=release`
|
||||||
|
3. Have a look in `web-ext-artifacts/dta-*-fx.zip`
|
||||||
|
|
||||||
|
|
||||||
### Patches
|
### 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.
|
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.
|
||||||
|
12
TODO.md
@ -1,19 +1,13 @@
|
|||||||
TODO
|
TODO
|
||||||
---
|
---
|
||||||
|
|
||||||
aka a lot
|
|
||||||
|
|
||||||
P2
|
P2
|
||||||
===
|
===
|
||||||
|
|
||||||
Planned for later.
|
Planned for later.
|
||||||
|
|
||||||
* Soft errors and retry logic
|
|
||||||
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
|
|
||||||
* Delete files (well, as far as the browser allows)
|
|
||||||
* Inter-addon API (basic)
|
* Inter-addon API (basic)
|
||||||
* Add downloads
|
* Add downloads
|
||||||
* Chrome support
|
|
||||||
* vtable perf: cache column widths
|
* vtable perf: cache column widths
|
||||||
* Download options
|
* Download options
|
||||||
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
||||||
@ -30,11 +24,7 @@ Nice-to-haves.
|
|||||||
* Manipulate downloads (e.g. rewrite URLs)
|
* Manipulate downloads (e.g. rewrite URLs)
|
||||||
* Native context menus?
|
* Native context menus?
|
||||||
* Would require massive reworks incl the need for new icon formats, but potentially feasible.
|
* Would require massive reworks incl the need for new icon formats, but potentially feasible.
|
||||||
* Import/Export
|
|
||||||
* Download priorities (manual scheduling overrides)
|
* Download priorities (manual scheduling overrides)
|
||||||
* Dark Theme support
|
|
||||||
* os/browser define be default
|
|
||||||
* overwritable
|
|
||||||
* Remove `any` types as possible, and generally improve typescript (new language to me)
|
* Remove `any` types as possible, and generally improve typescript (new language to me)
|
||||||
|
|
||||||
P4
|
P4
|
||||||
@ -46,8 +36,6 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
|
|||||||
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
|
* 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
|
* 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.
|
* 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
|
* Conflicts: ask when a file exists
|
||||||
* Not supported by Firefox
|
* Not supported by Firefox
|
||||||
* Speed limiter
|
* Speed limiter
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
{
|
{
|
||||||
|
"ar": "العربية [ar]",
|
||||||
|
"bg": "Български [bg]",
|
||||||
"cs": "Čeština (CZ) [cs]",
|
"cs": "Čeština (CZ) [cs]",
|
||||||
|
"da": "Dansk [da]",
|
||||||
"de": "Deutsch [de]",
|
"de": "Deutsch [de]",
|
||||||
"el": "Ελληνικά [el]",
|
"el": "Ελληνικά [el]",
|
||||||
"en": "English (US) [en]",
|
"en": "English (US) [en]",
|
||||||
"es": "Español (España) [es]",
|
"es": "Español (España) [es]",
|
||||||
"et": "Eesti Keel [et]",
|
"et": "Eesti keel [et]",
|
||||||
"fr": "Français (FR) [fr]",
|
"fr": "Français [fr]",
|
||||||
|
"hu": "Magyar (HU) [hu]",
|
||||||
"id": "Bahasa Indonesia [id]",
|
"id": "Bahasa Indonesia [id]",
|
||||||
|
"it": "Italiano [it]",
|
||||||
|
"ja": "日本語 (JP) [ja]",
|
||||||
"ko": "한국어 [ko]",
|
"ko": "한국어 [ko]",
|
||||||
"lt": "Lietuvių [lt]",
|
"lt": "Lietuvių [lt]",
|
||||||
"nl": "Nederlands [nl]",
|
"nl": "Nederlands [nl]",
|
||||||
"pl": "Polski (PL) [pl]",
|
"pl": "Polski [pl]",
|
||||||
"pt": "Português (Brasil) [pt]",
|
"pt": "Português (Brasil) [pt]",
|
||||||
"ru": "Русский [ru]",
|
"ru": "Русский [ru]",
|
||||||
"zh_CN": "简体中文 [zh_CN]",
|
"sv": "Svenska (SV) [sv]",
|
||||||
"zh_TW": "正體中文 [zh_TW]"
|
"tr": "Türkçe TR) [tr]",
|
||||||
|
"zh_CN": "中文(简体) [zh_CN]",
|
||||||
|
"zh_TW": "正體中文 (TW) [zh_TW]"
|
||||||
}
|
}
|
1300
_locales/ar/messages.json
Normal file
1300
_locales/bg/messages.json
Normal file
@ -191,6 +191,22 @@
|
|||||||
"message": "Smazat",
|
"message": "Smazat",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Odstranit soubory",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Odstranit",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Opravdu chcete odstranit následující soubory?",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Odstranit soubory",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Popis",
|
"message": "Popis",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -216,27 +232,27 @@
|
|||||||
"description": "Download (verb/action); e.g. in single and select buttons"
|
"description": "Download (verb/action); e.g. in single and select buttons"
|
||||||
},
|
},
|
||||||
"dta_regular": {
|
"dta_regular": {
|
||||||
"message": "DownThemAll!",
|
"message": "TraitorousDownloading!",
|
||||||
"description": "Regular dta action; Menu text"
|
"description": "Regular dta action; Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_all": {
|
"dta_regular_all": {
|
||||||
"message": "DownThemAll! - Všechny záložky",
|
"message": "TraitorousDownloading! - Všechny záložky",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_image": {
|
"dta_regular_image": {
|
||||||
"message": "Stáhnout obrázek pomocí DownThemAll!",
|
"message": "Stáhnout obrázek pomocí TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_link": {
|
"dta_regular_link": {
|
||||||
"message": "Stáhnout odkaz pomocí DownThemAll!",
|
"message": "Stáhnout odkaz pomocí TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_media": {
|
"dta_regular_media": {
|
||||||
"message": "Stáhnout média pomocí DownThemAll!",
|
"message": "Stáhnout média pomocí TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_selection": {
|
"dta_regular_selection": {
|
||||||
"message": "Stáhnout vybrané pomocí DownThemAll!",
|
"message": "Stáhnout vybrané pomocí TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo": {
|
"dta_turbo": {
|
||||||
@ -275,13 +291,37 @@
|
|||||||
"message": "Neplatná URL adresa",
|
"message": "Neplatná URL adresa",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Absolutní formát cest pro podsložky není prohlížečem podporovaný",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Tečky (.) v podsložkách nejsou prohlížečem podporované",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Nic není vybráno",
|
"message": "Nic není vybráno",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"message": "Exportovat do souboru",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"message": "Exportovat jako seznam aria2",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"message": "Exportovat jako metalink",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"message": "Exportovat jako text",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Správce stahování pro Váš prohlížeč",
|
"message": "Správce stahování pro Váš prohlížeč",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "TraitorousDownloading! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||||
},
|
},
|
||||||
"fastfiltering": {
|
"fastfiltering": {
|
||||||
"message": "Rychlé filtrování",
|
"message": "Rychlé filtrování",
|
||||||
@ -331,6 +371,10 @@
|
|||||||
"message": "Vynutit spuštění",
|
"message": "Vynutit spuštění",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"message": "Importovat ze souboru",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"message": "Informace",
|
"message": "Informace",
|
||||||
"description": "Used in message boxes"
|
"description": "Used in message boxes"
|
||||||
@ -406,7 +450,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manager_title": {
|
"manager_title": {
|
||||||
"message": "DownThemAll! Manažer",
|
"message": "TraitorousDownloading! Manažer",
|
||||||
"description": "Window/tab title"
|
"description": "Window/tab title"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
@ -442,7 +486,7 @@
|
|||||||
"description": "Action for moving a download up"
|
"description": "Action for moving a download up"
|
||||||
},
|
},
|
||||||
"nagging_message": {
|
"nagging_message": {
|
||||||
"message": "Již bylo stáhnuto $DOWNLOADS$ souborů pomocí DownThemAll! Jako častý uživatel byste mohl/a podpořit další vývoj darem. Děkujeme!",
|
"message": "Již bylo stáhnuto $DOWNLOADS$ souborů pomocí TraitorousDownloading! Jako častý uživatel byste mohl/a podpořit další vývoj darem. Děkujeme!",
|
||||||
"description": "Donation nagging message; displayed as a notification bar in manager",
|
"description": "Donation nagging message; displayed as a notification bar in manager",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -522,13 +566,33 @@
|
|||||||
"description": "Menu text; Preferences"
|
"description": "Menu text; Preferences"
|
||||||
},
|
},
|
||||||
"prefs_title": {
|
"prefs_title": {
|
||||||
"message": "Nastavení DownThemAll!",
|
"message": "Nastavení TraitorousDownloading!",
|
||||||
"description": "Window/tab title; Preferences"
|
"description": "Window/tab title; Preferences"
|
||||||
},
|
},
|
||||||
"pref_add_paused": {
|
"pref_add_paused": {
|
||||||
"message": "Přidávat nová stahování pozastavená",
|
"message": "Přidávat nová stahování pozastavená",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Tlačítko TraitorousDownloading!:",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "TraitorousDownloading! selektor",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Otevřít Manažera",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Nabídka",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"message": "OneClick!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Souběžná stahování",
|
"message": "Souběžná stahování",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -537,10 +601,6 @@
|
|||||||
"message": "Zobrazit oznámení po dokončení fronty stahování",
|
"message": "Zobrazit oznámení po dokončení fronty stahování",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Tlačítho na liště je OneClick!",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Nezobrazovat v kontextové nabídce",
|
"message": "Nezobrazovat v kontextové nabídce",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -549,8 +609,12 @@
|
|||||||
"message": "Manažer",
|
"message": "Manažer",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Otevřít Manažera v novém vyskakovacím okně",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Zobrazovat popisky v záložkách Manažeru",
|
"message": "Zobrazovat popisky v záložkách Manažera",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_netglobal": {
|
"pref_netglobal": {
|
||||||
@ -558,7 +622,7 @@
|
|||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
"pref_open_manager_on_queue": {
|
"pref_open_manager_on_queue": {
|
||||||
"message": "Otevřít záložku Manažeru po zařazení stahování do fronty",
|
"message": "Otevřít záložku Manažera po zařazení stahování do fronty",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_queueing": {
|
"pref_queueing": {
|
||||||
@ -573,14 +637,42 @@
|
|||||||
"message": "Odebrat chybějící stahování po restartu",
|
"message": "Odebrat chybějící stahování po restartu",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Počet pokusů o stažení při dočasných chybách",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Interval opakování (v minutách)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Zobrazit URL adresy místo názvů souborů",
|
"message": "Zobrazit URL adresy místo názvů souborů",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Přehrát zvuky",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Vyhledat odkazy v textu stránky (pomalejší)",
|
"message": "Vyhledat odkazy v textu stránky (pomalejší)",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_theme": {
|
||||||
|
"message": "Motiv:",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"pref_theme_dark": {
|
||||||
|
"message": "Tmavý",
|
||||||
|
"description": "option text"
|
||||||
|
},
|
||||||
|
"pref_theme_default": {
|
||||||
|
"message": "Systémový/Výchozí",
|
||||||
|
"description": "option text"
|
||||||
|
},
|
||||||
|
"pref_theme_light": {
|
||||||
|
"message": "Světlý",
|
||||||
|
"description": "option text"
|
||||||
|
},
|
||||||
"pref_ui": {
|
"pref_ui": {
|
||||||
"message": "Uživatelské rozhraní",
|
"message": "Uživatelské rozhraní",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
@ -895,6 +987,20 @@
|
|||||||
"message": "Pokračovat",
|
"message": "Pokračovat",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Opakuji",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Opakuji - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "Probíhá",
|
"message": "Probíhá",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -920,7 +1026,7 @@
|
|||||||
"description": "Menu text; select context"
|
"description": "Menu text; select context"
|
||||||
},
|
},
|
||||||
"select_title": {
|
"select_title": {
|
||||||
"message": "DownThemAll! - Vyberte soubory ke stažení",
|
"message": "TraitorousDownloading! - Vyberte soubory ke stažení",
|
||||||
"description": "Title of the select window"
|
"description": "Title of the select window"
|
||||||
},
|
},
|
||||||
"SERVER_BAD_CONTENT": {
|
"SERVER_BAD_CONTENT": {
|
||||||
@ -943,6 +1049,18 @@
|
|||||||
"message": "Nastavit masku přejmenování",
|
"message": "Nastavit masku přejmenování",
|
||||||
"description": "Menu text; select window"
|
"description": "Menu text; select window"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Nastavte novou masku přejmenování",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Nastavit odkázání",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Nastavit novou odkazující stránku",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Jsou podporována dávková stahování, například:",
|
"message": "Jsou podporována dávková stahování, například:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -952,7 +1070,7 @@
|
|||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
},
|
},
|
||||||
"single_title": {
|
"single_title": {
|
||||||
"message": "DownThemAll! - Přidat odkaz",
|
"message": "TraitorousDownloading! - Přidat odkaz",
|
||||||
"description": "Title of single window"
|
"description": "Title of single window"
|
||||||
},
|
},
|
||||||
"sizeB": {
|
"sizeB": {
|
||||||
@ -1123,6 +1241,14 @@
|
|||||||
"message": "Nová stahování nebudou spuštěna",
|
"message": "Nová stahování nebudou spuštěna",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"message": "Podsložka:",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Umístí soubory v této podložce uvnitř vaší složky stahování",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Popisek",
|
"message": "Popisek",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1166,5 +1292,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Použít pouze jednou",
|
"message": "Použít pouze jednou",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Zrušeno uživatelem",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
1300
_locales/da/messages.json
Normal file
@ -191,6 +191,22 @@
|
|||||||
"message": "Διαγραφή",
|
"message": "Διαγραφή",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Διαγραφή Αρχείων",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Διαγραφή",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Θέλετε σίγουρα να διαγράψετε τα ακόλουθα αρχεία;",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Διαγραφή Αρχείων",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Περιγραφή",
|
"message": "Περιγραφή",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -275,13 +291,21 @@
|
|||||||
"message": "Μη έγκυρο URL",
|
"message": "Μη έγκυρο URL",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Οι πλήρεις διαδρομές υποφακέλων δεν υποστηρίζονται από τους φυλλομετρητές",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Οι τελείες (.) στους υποφακέλους δεν υποστηρίζονται από τους φυλλομετρητές",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Δεν επιλέχθηκαν αντικείμενα",
|
"message": "Δεν επιλέχθηκαν αντικείμενα",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Ο Διαχειριστής Πολλαπλών Λήψεων για τον φυλλομετρητή σας.",
|
"message": "Ο Διαχειριστής Πολλαπλών Λήψεων για τον φυλλομετρητή σας.",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "TraitorousDownloading! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||||
},
|
},
|
||||||
"fastfiltering": {
|
"fastfiltering": {
|
||||||
"message": "Γρήγορο Φιλτράρισμα",
|
"message": "Γρήγορο Φιλτράρισμα",
|
||||||
@ -529,6 +553,26 @@
|
|||||||
"message": "Προσθήκη νέων λήψεων σε παύση, αντί για άμεση εκκίνησή τους",
|
"message": "Προσθήκη νέων λήψεων σε παύση, αντί για άμεση εκκίνησή τους",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Κουμπί Λήψης Όλων",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "Επιλογή Λήψης Όλων",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Άνοιγμα Διαχειριστή",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Αναδυόμενο μενού",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"message": "ΜονόΚλικ!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Ταυτόχρονες λήψεις",
|
"message": "Ταυτόχρονες λήψεις",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -537,10 +581,6 @@
|
|||||||
"message": "Προβολή ειδοποίησης με την ολοκλήρωση της ουράς λήψεων",
|
"message": "Προβολή ειδοποίησης με την ολοκλήρωση της ουράς λήψεων",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Το κουμπί φυλλομετρητή πρέπει να είναι το ΜονόΚλικ!",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Απόκρυψη αντικειμένων μενού γενικού περιεχομένου",
|
"message": "Απόκρυψη αντικειμένων μενού γενικού περιεχομένου",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -549,6 +589,10 @@
|
|||||||
"message": "Διαχειριστής",
|
"message": "Διαχειριστής",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Άνοιγμα διαχειριστή σε νέο αναδυόμενο παράθυρο",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Προβολή επεξηγήσεων στις καρτέλες Διαχειριστή",
|
"message": "Προβολή επεξηγήσεων στις καρτέλες Διαχειριστή",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -573,10 +617,22 @@
|
|||||||
"message": "Απομάκρυνση αγνοημένων λήψεων μετά την επανεκκίνηση",
|
"message": "Απομάκρυνση αγνοημένων λήψεων μετά την επανεκκίνηση",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Αριθμός επαναλήψεων λήψεων με προσωρινά σφάλματα",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Επανάληψη κάθε (σε λεπτά)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Προβολή URLs αντί για Ονόματα",
|
"message": "Προβολή URLs αντί για Ονόματα",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Αναπαραγωγή ήχων",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Δοκιμή εύρεσης συνδέσμων στο κείμενο της ιστοσελίδας (αργό)",
|
"message": "Δοκιμή εύρεσης συνδέσμων στο κείμενο της ιστοσελίδας (αργό)",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -895,6 +951,20 @@
|
|||||||
"message": "Συνέχιση",
|
"message": "Συνέχιση",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Επανάληψη",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Επανάληψη - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "Σε λειτουργία",
|
"message": "Σε λειτουργία",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -943,6 +1013,18 @@
|
|||||||
"message": "Ορισμός Μάσκας Μετονομασίας",
|
"message": "Ορισμός Μάσκας Μετονομασίας",
|
||||||
"description": "Menu text; select window"
|
"description": "Menu text; select window"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Ορισμός μιας νέας μάσκας μετονομασίας",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Ορισμός Αναφορέα",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Ορισμός νέου αναφορέα",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Οι δέσμες υποστηρίζονται, π.χ.:",
|
"message": "Οι δέσμες υποστηρίζονται, π.χ.:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -1123,6 +1205,14 @@
|
|||||||
"message": "Καμία νέα Λήψη δεν θα εκκινηθεί",
|
"message": "Καμία νέα Λήψη δεν θα εκκινηθεί",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"message": "Υποφάκελος:",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Τοποθέτηση αρχείων σε αυτό τον υποφάκελο μέσα στον φάκελο λήψεών σας",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Τίτλος",
|
"message": "Τίτλος",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1166,5 +1256,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Χρήση μόνο μια φορά",
|
"message": "Χρήση μόνο μια φορά",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Ακύρωση του χρήστη",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
1300
_locales/hu/messages.json
Normal file
@ -24,7 +24,7 @@
|
|||||||
"description": "Checkbox label"
|
"description": "Checkbox label"
|
||||||
},
|
},
|
||||||
"add_paused_question": {
|
"add_paused_question": {
|
||||||
"message": "Apa Anda ingin mengingat keputusan ini dan mulai sekarang tambahkan unduhan dalam kondisi terpause?",
|
"message": "Ingat keputusan ini dan tambahkan unduhan dalam kondisi terpause mulai sekarang?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"add_paused_title": {
|
"add_paused_title": {
|
||||||
@ -175,6 +175,22 @@
|
|||||||
"message": "Hapus",
|
"message": "Hapus",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Hapus Berkas",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Hapus",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Hapus berkas-berkas berikut ini?",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Hapus Berkas",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Keterangan",
|
"message": "Keterangan",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -200,23 +216,23 @@
|
|||||||
"description": "Download (verb/action); e.g. in single and select buttons"
|
"description": "Download (verb/action); e.g. in single and select buttons"
|
||||||
},
|
},
|
||||||
"dta_regular_all": {
|
"dta_regular_all": {
|
||||||
"message": "DownThemAll - Semua Tab",
|
"message": "TraitorousDownloading - Semua Tab",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_image": {
|
"dta_regular_image": {
|
||||||
"message": "Simpan Gambar menggunakan DownThemAll!",
|
"message": "Simpan Gambar menggunakan TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_link": {
|
"dta_regular_link": {
|
||||||
"message": "Simpan Tautan menggunakan DownThemAll!",
|
"message": "Simpan Tautan menggunakan TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_media": {
|
"dta_regular_media": {
|
||||||
"message": "Simpan Media menggunakan DownThemAll!",
|
"message": "Simpan Media menggunakan TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_selection": {
|
"dta_regular_selection": {
|
||||||
"message": "Simpan Pilihan menggunakan DownThemAll!",
|
"message": "Simpan Pilihan menggunakan TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo_all": {
|
"dta_turbo_all": {
|
||||||
@ -251,13 +267,37 @@
|
|||||||
"message": "URL Tidak Benar",
|
"message": "URL Tidak Benar",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Path absolut untuk subfolder tidak didukung oleh peramban",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Titik (.) di subfolder tidak didukung oleh peramban",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Tidak ada item terpilih",
|
"message": "Tidak ada item terpilih",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"message": "Ekspor Ke Berkas",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"message": "Ekspor Sebagai Daftar aria2",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"message": "Ekspor Sebagai Metalink",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"message": "Ekspor Sebagai Teks",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Pengunduh Masal untuk browser anda",
|
"message": "Pengunduh Masal untuk browser anda",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "TraitorousDownloading! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||||
},
|
},
|
||||||
"fastfiltering": {
|
"fastfiltering": {
|
||||||
"message": "Penyaringan Cepat",
|
"message": "Penyaringan Cepat",
|
||||||
@ -280,15 +320,15 @@
|
|||||||
"description": "Message box title"
|
"description": "Message box title"
|
||||||
},
|
},
|
||||||
"filter_expression": {
|
"filter_expression": {
|
||||||
"message": "Ekspres-Filter",
|
"message": "Ekspresi",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_label": {
|
"filter_label": {
|
||||||
"message": "Label-Filter",
|
"message": "Label",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_types": {
|
"filter_types": {
|
||||||
"message": "Tipe-Filter",
|
"message": "Tipe",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_type_link": {
|
"filter_type_link": {
|
||||||
@ -307,6 +347,10 @@
|
|||||||
"message": "Paksa Mulai",
|
"message": "Paksa Mulai",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"message": "Impor Dari Berkas",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"message": "Informasi",
|
"message": "Informasi",
|
||||||
"description": "Used in message boxes"
|
"description": "Used in message boxes"
|
||||||
@ -320,7 +364,7 @@
|
|||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"limited_to": {
|
"limited_to": {
|
||||||
"message": "Terbatas ke",
|
"message": "Batasi ke",
|
||||||
"description": "Label text; used in prefs/network"
|
"description": "Label text; used in prefs/network"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
@ -354,7 +398,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manager_title": {
|
"manager_title": {
|
||||||
"message": "Pengelola DownThemAll!",
|
"message": "Pengelola TraitorousDownloading!",
|
||||||
"description": "Window/tab title"
|
"description": "Window/tab title"
|
||||||
},
|
},
|
||||||
"mask_default": {
|
"mask_default": {
|
||||||
@ -362,11 +406,11 @@
|
|||||||
"description": "Status text; Used in the mask column, select window"
|
"description": "Status text; Used in the mask column, select window"
|
||||||
},
|
},
|
||||||
"missing": {
|
"missing": {
|
||||||
"message": "Tidak Ada",
|
"message": "Hilang",
|
||||||
"description": "Status text in manager"
|
"description": "Status text in manager"
|
||||||
},
|
},
|
||||||
"move_bottom": {
|
"move_bottom": {
|
||||||
"message": "Bawah",
|
"message": "Ke Bawah",
|
||||||
"description": "Action for moving a download to the bottom"
|
"description": "Action for moving a download to the bottom"
|
||||||
},
|
},
|
||||||
"move_down": {
|
"move_down": {
|
||||||
@ -374,7 +418,7 @@
|
|||||||
"description": "Action for moving a download down"
|
"description": "Action for moving a download down"
|
||||||
},
|
},
|
||||||
"move_top": {
|
"move_top": {
|
||||||
"message": "Atas",
|
"message": "Ke Atas",
|
||||||
"description": "Action for moving a download to the top"
|
"description": "Action for moving a download to the top"
|
||||||
},
|
},
|
||||||
"move_up": {
|
"move_up": {
|
||||||
@ -382,7 +426,7 @@
|
|||||||
"description": "Action for moving a download up"
|
"description": "Action for moving a download up"
|
||||||
},
|
},
|
||||||
"nagging_message": {
|
"nagging_message": {
|
||||||
"message": "Sejauh ini Anda telah menambahkan $DOWNLOADS$ unduhan menggunakan DownThemAll! Sebagai penggunan setia, silakan berdonasi untuk mendukung pengembangan lebih lanjut. Terima Kasih!",
|
"message": "Sejauh ini Anda telah menambahkan $DOWNLOADS$ unduhan menggunakan TraitorousDownloading! Sebagai penggunan setia, silakan berdonasi untuk mendukung pengembangan lebih lanjut. Terima Kasih!",
|
||||||
"description": "Donation nagging message; displayed as a notification bar in manager",
|
"description": "Donation nagging message; displayed as a notification bar in manager",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -454,13 +498,29 @@
|
|||||||
"description": "Menu text; Preferences"
|
"description": "Menu text; Preferences"
|
||||||
},
|
},
|
||||||
"prefs_title": {
|
"prefs_title": {
|
||||||
"message": "Preferensi DownThemAll!",
|
"message": "Preferensi TraitorousDownloading!",
|
||||||
"description": "Window/tab title; Preferences"
|
"description": "Window/tab title; Preferences"
|
||||||
},
|
},
|
||||||
"pref_add_paused": {
|
"pref_add_paused": {
|
||||||
"message": "Tambahkan unduhan terpause, alih-alih langsung memulai mengunduh",
|
"message": "Tambahkan unduhan terpause, alih-alih langsung memulai mengunduh",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Tombol TraitorousDownloading!:",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "Pilihan TraitorousDownloading!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Buka Pengelola",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Menu popup",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Unduhan Bersamaan",
|
"message": "Unduhan Bersamaan",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -469,10 +529,6 @@
|
|||||||
"message": "Tampilkan pemberitahuan ketika antrian unduhan selesai",
|
"message": "Tampilkan pemberitahuan ketika antrian unduhan selesai",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Tombol peramban sebaiknya OneClick!",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Jangan perlihatkan item menu general context",
|
"message": "Jangan perlihatkan item menu general context",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -481,6 +537,10 @@
|
|||||||
"message": "Pengelola",
|
"message": "Pengelola",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Buka pengelola di jendela popup baru",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Tampilkan tooltip di tab Pengelola",
|
"message": "Tampilkan tooltip di tab Pengelola",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -505,10 +565,22 @@
|
|||||||
"message": "Setelah restart, hapus unduhan yang tidak ada",
|
"message": "Setelah restart, hapus unduhan yang tidak ada",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Jumlah percobaan ulang ketika gagal mengunduh",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Coba setiap (dalam menit)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Tampilan URL alih-alih Nama",
|
"message": "Tampilan URL alih-alih Nama",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Bunyikan suara",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Coba cari tautan di teks di situs web (lambat)",
|
"message": "Coba cari tautan di teks di situs web (lambat)",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -560,7 +632,7 @@
|
|||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_batch_downloads_question": {
|
"remove_batch_downloads_question": {
|
||||||
"message": "Hapus semua unduhan dari kumpulan yang sama dengan unduhan terpilih?",
|
"message": "Hapus semua unduhan dari batch yang sama dengan unduhan terpilih?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_complete_downloads": {
|
"remove_complete_downloads": {
|
||||||
@ -600,7 +672,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_domain_downloads": {
|
"remove_domain_downloads": {
|
||||||
"message": "Hapus Domain Ini",
|
"message": "Hapus Unduhan Dari Domain Ini",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_domain_downloads_question": {
|
"remove_domain_downloads_question": {
|
||||||
@ -630,7 +702,7 @@
|
|||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_failed_downloads": {
|
"remove_failed_downloads": {
|
||||||
"message": "Gagal Menghapus",
|
"message": "Hapus Unduhan Gagal",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_failed_downloads_question": {
|
"remove_failed_downloads_question": {
|
||||||
@ -648,11 +720,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_missing": {
|
"remove_missing": {
|
||||||
"message": "Hapus Unduhan Yang Tidak Ada",
|
"message": "Hapus Unduhan Yang Hilang",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_missing_downloads_question": {
|
"remove_missing_downloads_question": {
|
||||||
"message": "Hapus semua unduhan yang tidak ada?",
|
"message": "Hapus semua unduhan yang hilang?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_paused_downloads": {
|
"remove_paused_downloads": {
|
||||||
@ -664,7 +736,7 @@
|
|||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_selected_complete_downloads": {
|
"remove_selected_complete_downloads": {
|
||||||
"message": "Hapus Yang Selesai Di Pilihan",
|
"message": "Hapus Yang Selesai Dari Unduhan Terpilih",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_selected_complete_downloads_question": {
|
"remove_selected_complete_downloads_question": {
|
||||||
@ -767,6 +839,10 @@
|
|||||||
"message": "Tanggal Ditambahkan - Detik",
|
"message": "Tanggal Ditambahkan - Detik",
|
||||||
"description": "Mask text; see mask button"
|
"description": "Mask text; see mask button"
|
||||||
},
|
},
|
||||||
|
"renamer_tags": {
|
||||||
|
"message": "Penanda Mask Penamaan",
|
||||||
|
"description": "Mask text; see mask button"
|
||||||
|
},
|
||||||
"renamer_text": {
|
"renamer_text": {
|
||||||
"message": "Teks Deskripsi",
|
"message": "Teks Deskripsi",
|
||||||
"description": "Mask text; see mask button"
|
"description": "Mask text; see mask button"
|
||||||
@ -783,6 +859,10 @@
|
|||||||
"message": "Tanggal Ditambahkan - Tahun",
|
"message": "Tanggal Ditambahkan - Tahun",
|
||||||
"description": "Mask text; see mask button"
|
"description": "Mask text; see mask button"
|
||||||
},
|
},
|
||||||
|
"renmask": {
|
||||||
|
"message": "Mask penamaan",
|
||||||
|
"description": "Renaming mask (long)"
|
||||||
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"message": "Atur Ulang",
|
"message": "Atur Ulang",
|
||||||
"description": "Button text; pref window"
|
"description": "Button text; pref window"
|
||||||
@ -807,6 +887,20 @@
|
|||||||
"message": "Lanjutkan",
|
"message": "Lanjutkan",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Mengulang",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Mengulang - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "Berjalan",
|
"message": "Berjalan",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -832,7 +926,7 @@
|
|||||||
"description": "Menu text; select context"
|
"description": "Menu text; select context"
|
||||||
},
|
},
|
||||||
"select_title": {
|
"select_title": {
|
||||||
"message": "DownThemAll! - Pilih Unduhan Anda",
|
"message": "TraitorousDownloading! - Pilih Unduhan Anda",
|
||||||
"description": "Title of the select window"
|
"description": "Title of the select window"
|
||||||
},
|
},
|
||||||
"SERVER_BAD_CONTENT": {
|
"SERVER_BAD_CONTENT": {
|
||||||
@ -851,6 +945,22 @@
|
|||||||
"message": "Tidak Diizinkan",
|
"message": "Tidak Diizinkan",
|
||||||
"description": "Error message"
|
"description": "Error message"
|
||||||
},
|
},
|
||||||
|
"set_mask": {
|
||||||
|
"message": "Set Mask Penamaan",
|
||||||
|
"description": "Menu text; select window"
|
||||||
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Tentukan mask penamaan baru",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Tentukan Referrer",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Tentukan referrer baru",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Batch bisa digunakan, misalnya:",
|
"message": "Batch bisa digunakan, misalnya:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -860,7 +970,7 @@
|
|||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
},
|
},
|
||||||
"single_title": {
|
"single_title": {
|
||||||
"message": "DownThemAll! - Tambah tautan",
|
"message": "TraitorousDownloading! - Tambah tautan",
|
||||||
"description": "Title of single window"
|
"description": "Title of single window"
|
||||||
},
|
},
|
||||||
"sizes_huge": {
|
"sizes_huge": {
|
||||||
@ -971,6 +1081,10 @@
|
|||||||
"message": "Tidak ada Unduhan baru yang akan dimulai",
|
"message": "Tidak ada Unduhan baru yang akan dimulai",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Simpan berkas di subfolder berikut di dalam direktori unduhan Anda",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Judul",
|
"message": "Judul",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1014,5 +1128,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Gunakan Sekali",
|
"message": "Gunakan Sekali",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Dibatalkan Pengguna",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
1284
_locales/it/messages.json
Normal file
1300
_locales/ja/messages.json
Normal file
@ -191,6 +191,22 @@
|
|||||||
"message": "삭제",
|
"message": "삭제",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "파일 삭제",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "삭제",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "정말 다음 파일(들)을 삭제하시겠습니까?",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "파일 삭제",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "설명",
|
"message": "설명",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -216,27 +232,27 @@
|
|||||||
"description": "Download (verb/action); e.g. in single and select buttons"
|
"description": "Download (verb/action); e.g. in single and select buttons"
|
||||||
},
|
},
|
||||||
"dta_regular": {
|
"dta_regular": {
|
||||||
"message": "DownThemAll!",
|
"message": "TraitorousDownloading!",
|
||||||
"description": "Regular dta action; Menu text"
|
"description": "Regular dta action; Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_all": {
|
"dta_regular_all": {
|
||||||
"message": "DownThemAll! - 모든 탭",
|
"message": "TraitorousDownloading! - 모든 탭",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_image": {
|
"dta_regular_image": {
|
||||||
"message": "이미지를 DownThemAll!로 저장",
|
"message": "이미지를 TraitorousDownloading!로 저장",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_link": {
|
"dta_regular_link": {
|
||||||
"message": "링크를 DownThemAll!로 저장",
|
"message": "링크를 TraitorousDownloading!로 저장",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_media": {
|
"dta_regular_media": {
|
||||||
"message": "미디어를 DownThemAll!로 저장",
|
"message": "미디어를 TraitorousDownloading!로 저장",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_selection": {
|
"dta_regular_selection": {
|
||||||
"message": "선택영역을 DownThemAll!로 저장",
|
"message": "선택영역을 TraitorousDownloading!로 저장",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo": {
|
"dta_turbo": {
|
||||||
@ -275,13 +291,37 @@
|
|||||||
"message": "잘못된 URL",
|
"message": "잘못된 URL",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "하위 폴더의 절대 경로는 브라우저에서 지원되지 않습니다",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "하위 폴더의 점(.)은 브라우저에서 지원되지 않습니다",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "선택된 항목 없음",
|
"message": "선택된 항목 없음",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"message": "파일로 내보내기",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"message": "aria2 리스트로 내보내기",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"message": "메타링크로 내보내기",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"message": "텍스트로 내보내기",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "브라우저용 대량 다운로더",
|
"message": "브라우저용 대량 다운로더",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "TraitorousDownloading! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||||
},
|
},
|
||||||
"fastfiltering": {
|
"fastfiltering": {
|
||||||
"message": "빠른 필터링",
|
"message": "빠른 필터링",
|
||||||
@ -331,6 +371,10 @@
|
|||||||
"message": "강제 시작",
|
"message": "강제 시작",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"message": "파일에서 가져오기",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"message": "정보",
|
"message": "정보",
|
||||||
"description": "Used in message boxes"
|
"description": "Used in message boxes"
|
||||||
@ -406,7 +450,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manager_title": {
|
"manager_title": {
|
||||||
"message": "DownThemAll! 관리자",
|
"message": "TraitorousDownloading! 관리자",
|
||||||
"description": "Window/tab title"
|
"description": "Window/tab title"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
@ -442,7 +486,7 @@
|
|||||||
"description": "Action for moving a download up"
|
"description": "Action for moving a download up"
|
||||||
},
|
},
|
||||||
"nagging_message": {
|
"nagging_message": {
|
||||||
"message": "지금까지 DownThemAll!로 $DOWNLOADS$ 다운로드를 추가하셨습니다. 일반 사용자로서 추가 개발을 지원하기 위해 기부를 고려할 수 있습니다. 감사합니다!",
|
"message": "지금까지 TraitorousDownloading!로 $DOWNLOADS$ 다운로드를 추가하셨습니다. 일반 사용자로서 추가 개발을 지원하기 위해 기부를 고려할 수 있습니다. 감사합니다!",
|
||||||
"description": "Donation nagging message; displayed as a notification bar in manager",
|
"description": "Donation nagging message; displayed as a notification bar in manager",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -522,13 +566,33 @@
|
|||||||
"description": "Menu text; Preferences"
|
"description": "Menu text; Preferences"
|
||||||
},
|
},
|
||||||
"prefs_title": {
|
"prefs_title": {
|
||||||
"message": "DownThemAll! 설정",
|
"message": "TraitorousDownloading! 설정",
|
||||||
"description": "Window/tab title; Preferences"
|
"description": "Window/tab title; Preferences"
|
||||||
},
|
},
|
||||||
"pref_add_paused": {
|
"pref_add_paused": {
|
||||||
"message": "새 다운로드를 즉시 시작하는 대신 일시중지로 추가",
|
"message": "새 다운로드를 즉시 시작하는 대신 일시중지로 추가",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "TraitorousDownloading! 버튼:",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "TraitorousDownloading! 선택",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "관리자 열기",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "팝업 메뉴",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"message": "OneClick!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "동시 다운로드",
|
"message": "동시 다운로드",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -537,10 +601,6 @@
|
|||||||
"message": "다운로드 대기열이 끝나면 알림 표시",
|
"message": "다운로드 대기열이 끝나면 알림 표시",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "브라우저 버튼을 OneClick!으로 작동",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "일반 컨텍스트 메뉴 항목 표시 안 함",
|
"message": "일반 컨텍스트 메뉴 항목 표시 안 함",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -549,6 +609,10 @@
|
|||||||
"message": "관리자",
|
"message": "관리자",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "새 팝업 창에 관리자 열기",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "관리자 탭에서 툴팁 표시",
|
"message": "관리자 탭에서 툴팁 표시",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -573,10 +637,22 @@
|
|||||||
"message": "다시 시작한 후 누락된 다운로드 제거",
|
"message": "다시 시작한 후 누락된 다운로드 제거",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "일시적인 오류에 대한 다운로드 재시도 횟수",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "재시도 간격 (분)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "이름 대신 URL 표시",
|
"message": "이름 대신 URL 표시",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "소리 재생",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "웹 사이트 텍스트에서 링크를 찾도록 시도 (느림)",
|
"message": "웹 사이트 텍스트에서 링크를 찾도록 시도 (느림)",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -895,6 +971,20 @@
|
|||||||
"message": "계속",
|
"message": "계속",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "재시도중",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "재시도중 - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "실행중",
|
"message": "실행중",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -920,7 +1010,7 @@
|
|||||||
"description": "Menu text; select context"
|
"description": "Menu text; select context"
|
||||||
},
|
},
|
||||||
"select_title": {
|
"select_title": {
|
||||||
"message": "DownThemAll! - 다운로드 선택",
|
"message": "TraitorousDownloading! - 다운로드 선택",
|
||||||
"description": "Title of the select window"
|
"description": "Title of the select window"
|
||||||
},
|
},
|
||||||
"SERVER_BAD_CONTENT": {
|
"SERVER_BAD_CONTENT": {
|
||||||
@ -943,6 +1033,18 @@
|
|||||||
"message": "이름 바꾸기 마스크 설정",
|
"message": "이름 바꾸기 마스크 설정",
|
||||||
"description": "Menu text; select window"
|
"description": "Menu text; select window"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "새 이름 바꾸기 마스크 설정",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "참조 페이지 설정",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "새 참조 페이지 설정",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "일괄 처리가 지원됩니다, 예:",
|
"message": "일괄 처리가 지원됩니다, 예:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -952,7 +1054,7 @@
|
|||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
},
|
},
|
||||||
"single_title": {
|
"single_title": {
|
||||||
"message": "DownThemAll! - 링크 추가",
|
"message": "TraitorousDownloading! - 링크 추가",
|
||||||
"description": "Title of single window"
|
"description": "Title of single window"
|
||||||
},
|
},
|
||||||
"sizeB": {
|
"sizeB": {
|
||||||
@ -1123,6 +1225,14 @@
|
|||||||
"message": "새 다운로드가 시작되지 않습니다",
|
"message": "새 다운로드가 시작되지 않습니다",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"message": "하위 폴더",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "다운로드 디렉토리 내의 이 하위 폴더에 파일을 저장합니다",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "제목",
|
"message": "제목",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1166,5 +1276,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "한번만",
|
"message": "한번만",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "사용자 취소",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
164
_locales/ru/messages.json
Normal file → Executable file
@ -12,7 +12,7 @@
|
|||||||
"description": "Action: Add paused"
|
"description": "Action: Add paused"
|
||||||
},
|
},
|
||||||
"add_download": {
|
"add_download": {
|
||||||
"message": "Добавить закачку",
|
"message": "добавить закачку",
|
||||||
"description": "Action for adding a download"
|
"description": "Action for adding a download"
|
||||||
},
|
},
|
||||||
"add_new": {
|
"add_new": {
|
||||||
@ -191,6 +191,22 @@
|
|||||||
"message": "Удалить",
|
"message": "Удалить",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Удалить файлы",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Удалить",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Точно удалить эти файлы?",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Удаление файлов",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Описание",
|
"message": "Описание",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -216,27 +232,27 @@
|
|||||||
"description": "Download (verb/action); e.g. in single and select buttons"
|
"description": "Download (verb/action); e.g. in single and select buttons"
|
||||||
},
|
},
|
||||||
"dta_regular": {
|
"dta_regular": {
|
||||||
"message": "DownThemAll!",
|
"message": "TraitorousDownloading!",
|
||||||
"description": "Regular dta action; Menu text"
|
"description": "Regular dta action; Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_all": {
|
"dta_regular_all": {
|
||||||
"message": "DownThemAll! - Все вкладки",
|
"message": "TraitorousDownloading! - Все вкладки",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_image": {
|
"dta_regular_image": {
|
||||||
"message": "Закачать изображение через DownThemAll!",
|
"message": "Закачать изображение через TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_link": {
|
"dta_regular_link": {
|
||||||
"message": "Закачать ссылку через DownThemAll!",
|
"message": "Закачать ссылку через TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_media": {
|
"dta_regular_media": {
|
||||||
"message": "Закачать медиа через DownThemAll!",
|
"message": "Закачать медиа через TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_regular_selection": {
|
"dta_regular_selection": {
|
||||||
"message": "Закачать выделенное через DownThemAll!",
|
"message": "Закачать выделенное через TraitorousDownloading!",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo": {
|
"dta_turbo": {
|
||||||
@ -275,13 +291,37 @@
|
|||||||
"message": "Неправильная ссылка",
|
"message": "Неправильная ссылка",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Полные пути к папкам не поддерживаются браузерами",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Точки (.) в названии папок не поддерживаются браузерами",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Ничего не выбрано",
|
"message": "Ничего не выбрано",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"message": "Экспортировать в файл",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"message": "Экспортировать как список для aria2",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"message": "Экспортировать как Metalink",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"message": "Экспортировать как текст",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Универсальная качалка для вашего браузера",
|
"message": "Универсальная качалка для вашего браузера",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "TraitorousDownloading! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
||||||
},
|
},
|
||||||
"fastfiltering": {
|
"fastfiltering": {
|
||||||
"message": "Быстрый фильтр",
|
"message": "Быстрый фильтр",
|
||||||
@ -331,6 +371,10 @@
|
|||||||
"message": "Принудительный старт",
|
"message": "Принудительный старт",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"message": "Импортировать из файла",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"message": "Информация",
|
"message": "Информация",
|
||||||
"description": "Used in message boxes"
|
"description": "Used in message boxes"
|
||||||
@ -406,7 +450,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manager_title": {
|
"manager_title": {
|
||||||
"message": "DownThemAll! Менеджер",
|
"message": "TraitorousDownloading! Менеджер",
|
||||||
"description": "Window/tab title"
|
"description": "Window/tab title"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
@ -442,7 +486,7 @@
|
|||||||
"description": "Action for moving a download up"
|
"description": "Action for moving a download up"
|
||||||
},
|
},
|
||||||
"nagging_message": {
|
"nagging_message": {
|
||||||
"message": "Добавлено $DOWNLOADS$ закачек через DownThemAll! на текущий момент! Как постоянный пользователь подумайте над тем чтобы поддержать дальнейшую разработку. Спасибо!",
|
"message": "Добавлено $DOWNLOADS$ закачек через TraitorousDownloading! на текущий момент! Как постоянный пользователь подумайте над тем чтобы поддержать дальнейшую разработку. Спасибо!",
|
||||||
"description": "Donation nagging message; displayed as a notification bar in manager",
|
"description": "Donation nagging message; displayed as a notification bar in manager",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -522,13 +566,33 @@
|
|||||||
"description": "Menu text; Preferences"
|
"description": "Menu text; Preferences"
|
||||||
},
|
},
|
||||||
"prefs_title": {
|
"prefs_title": {
|
||||||
"message": "Настройки DownThemAll!",
|
"message": "Настройки TraitorousDownloading!",
|
||||||
"description": "Window/tab title; Preferences"
|
"description": "Window/tab title; Preferences"
|
||||||
},
|
},
|
||||||
"pref_add_paused": {
|
"pref_add_paused": {
|
||||||
"message": "Добавлять закачки приостановленными, вместо того чтобы качать сразу",
|
"message": "Добавлять закачки приостановленными, вместо того чтобы качать сразу",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Кнопка TraitorousDownloading!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "TraitorousDownloading! выделенного",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Открыть менеджер",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Всплывающее меню",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"message": "OneClick!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Настройки/Сеть",
|
"message": "Настройки/Сеть",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -537,10 +601,6 @@
|
|||||||
"message": "Показывать уведомление когда список закачек завешен",
|
"message": "Показывать уведомление когда список закачек завешен",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Использовать OneClick! по умолчанию на кнопке в браузере",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Не добавлять пункты в общее контекстное меню",
|
"message": "Не добавлять пункты в общее контекстное меню",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -549,6 +609,10 @@
|
|||||||
"message": "Менеджер",
|
"message": "Менеджер",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Открывать менеджер в новом всплывающем окне",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Показывать подсказки на вкладках менеджера",
|
"message": "Показывать подсказки на вкладках менеджера",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -573,14 +637,42 @@
|
|||||||
"message": "Удалять неудавшиеся закачки после перезапуска",
|
"message": "Удалять неудавшиеся закачки после перезапуска",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Сколько раз пробовать перезапустить закачку при некритичных сбоях",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Как часто пробовать перезапустить закачку (в минутах)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Показывать ссылки вместо имён",
|
"message": "Показывать ссылки вместо имён",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Проигрывать звуки",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Пытаться обнаружить ссылки в тексте на сайте",
|
"message": "Пытаться обнаружить ссылки в тексте на сайте",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_theme": {
|
||||||
|
"message": "Тема:",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"pref_theme_dark": {
|
||||||
|
"message": "Тёмная",
|
||||||
|
"description": "option text"
|
||||||
|
},
|
||||||
|
"pref_theme_default": {
|
||||||
|
"message": "Система/Браузер",
|
||||||
|
"description": "option text"
|
||||||
|
},
|
||||||
|
"pref_theme_light": {
|
||||||
|
"message": "Светлая",
|
||||||
|
"description": "option text"
|
||||||
|
},
|
||||||
"pref_ui": {
|
"pref_ui": {
|
||||||
"message": "Интерфейс пользователя",
|
"message": "Интерфейс пользователя",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
@ -895,6 +987,20 @@
|
|||||||
"message": "Продолжить",
|
"message": "Продолжить",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Перезапуск",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Перезапуск - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "В процессе",
|
"message": "В процессе",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -920,7 +1026,7 @@
|
|||||||
"description": "Menu text; select context"
|
"description": "Menu text; select context"
|
||||||
},
|
},
|
||||||
"select_title": {
|
"select_title": {
|
||||||
"message": "DownThemAll! - Выбрать ваши закачки",
|
"message": "TraitorousDownloading! - Выбрать ваши закачки",
|
||||||
"description": "Title of the select window"
|
"description": "Title of the select window"
|
||||||
},
|
},
|
||||||
"SERVER_BAD_CONTENT": {
|
"SERVER_BAD_CONTENT": {
|
||||||
@ -943,6 +1049,18 @@
|
|||||||
"message": "Задать маску переименования",
|
"message": "Задать маску переименования",
|
||||||
"description": "Menu text; select window"
|
"description": "Menu text; select window"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Задать новую маску переименования",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Задать реферера",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Задать нового реферера",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Можно задать группы, к примеру:",
|
"message": "Можно задать группы, к примеру:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -952,7 +1070,7 @@
|
|||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
},
|
},
|
||||||
"single_title": {
|
"single_title": {
|
||||||
"message": "DownThemAll! - Добавить ссылку",
|
"message": "TraitorousDownloading! - Добавить ссылку",
|
||||||
"description": "Title of single window"
|
"description": "Title of single window"
|
||||||
},
|
},
|
||||||
"sizeB": {
|
"sizeB": {
|
||||||
@ -1123,6 +1241,14 @@
|
|||||||
"message": "Новые закачки запускаться не будут",
|
"message": "Новые закачки запускаться не будут",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"message": "Подпапка",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Размещать файлы в подпапке выбранного пути для закачек",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Заголовок",
|
"message": "Заголовок",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1166,5 +1292,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Только сейчас",
|
"message": "Только сейчас",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Отменено пользователем",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
1224
_locales/sv/messages.json
Normal file
1300
_locales/tr/messages.json
Normal file
@ -15,7 +15,7 @@
|
|||||||
"deffilter-aud": {
|
"deffilter-aud": {
|
||||||
"label": "Audio",
|
"label": "Audio",
|
||||||
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape|opus)$/i",
|
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape|opus)$/i",
|
||||||
"type": 1,
|
"type": 3,
|
||||||
"active": false,
|
"active": false,
|
||||||
"icon": "mp3"
|
"icon": "mp3"
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"global-turbo": false,
|
"button-type": "popup",
|
||||||
|
"manager-in-popup": false,
|
||||||
"concurrent": 4,
|
"concurrent": 4,
|
||||||
"queue-notification": true,
|
"queue-notification": true,
|
||||||
"finish-notification": true,
|
"finish-notification": true,
|
||||||
|
"sounds": true,
|
||||||
"open-manager-on-queue": true,
|
"open-manager-on-queue": true,
|
||||||
"text-links": true,
|
"text-links": true,
|
||||||
"add-paused": false,
|
"add-paused": false,
|
||||||
@ -13,6 +15,9 @@
|
|||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"show-urls": false,
|
"show-urls": false,
|
||||||
"remove-missing-on-init": false,
|
"remove-missing-on-init": false,
|
||||||
|
"retries": 5,
|
||||||
|
"retry-time": 10,
|
||||||
|
"theme": "default",
|
||||||
"limits": [
|
"limits": [
|
||||||
{
|
{
|
||||||
"domain": "*",
|
"domain": "*",
|
||||||
|
25
lib/api.ts
@ -11,7 +11,7 @@ import { getManager } from "./manager/man";
|
|||||||
import { select } from "./select";
|
import { select } from "./select";
|
||||||
import { single } from "./single";
|
import { single } from "./single";
|
||||||
import { Notification } from "./notifications";
|
import { Notification } from "./notifications";
|
||||||
import { MASK, FASTFILTER } from "./recentlist";
|
import { MASK, FASTFILTER, SUBFOLDER, SERVER } from "./recentlist";
|
||||||
import { openManager } from "./windowutils";
|
import { openManager } from "./windowutils";
|
||||||
import { _ } from "./i18n";
|
import { _ } from "./i18n";
|
||||||
|
|
||||||
@ -19,7 +19,10 @@ const MAX_BATCH = 10000;
|
|||||||
|
|
||||||
export interface QueueOptions {
|
export interface QueueOptions {
|
||||||
mask?: string;
|
mask?: string;
|
||||||
|
subfolder?: string;
|
||||||
|
server?: string;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
|
cookies?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const API = new class APIImpl {
|
export const API = new class APIImpl {
|
||||||
@ -28,10 +31,13 @@ export const API = new class APIImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async queue(items: BaseItem[], options: QueueOptions) {
|
async queue(items: BaseItem[], options: QueueOptions) {
|
||||||
await MASK.init();
|
await Promise.all([MASK.init(), SUBFOLDER.init()]);
|
||||||
const {mask = MASK.current} = options;
|
const {mask = MASK.current} = options;
|
||||||
|
const {subfolder = SUBFOLDER.current} = options;
|
||||||
|
const {server = SERVER.current} = options;
|
||||||
|
|
||||||
const {paused = false} = options;
|
const {paused = false} = options;
|
||||||
|
const {cookies = false} = options;
|
||||||
const defaults: any = {
|
const defaults: any = {
|
||||||
_idx: 0,
|
_idx: 0,
|
||||||
get idx() {
|
get idx() {
|
||||||
@ -46,8 +52,11 @@ export const API = new class APIImpl {
|
|||||||
private: false,
|
private: false,
|
||||||
postData: null,
|
postData: null,
|
||||||
mask,
|
mask,
|
||||||
|
subfolder,
|
||||||
|
server,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
paused
|
paused,
|
||||||
|
cookies,
|
||||||
};
|
};
|
||||||
let currentBatch = await Prefs.get("currentBatch", 0);
|
let currentBatch = await Prefs.get("currentBatch", 0);
|
||||||
const initialBatch = currentBatch;
|
const initialBatch = currentBatch;
|
||||||
@ -77,7 +86,7 @@ export const API = new class APIImpl {
|
|||||||
new Notification(null, _("queued-downloads", items.length));
|
new Notification(null, _("queued-downloads", items.length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (await Prefs.get("open-manager-on-queue")) {
|
if (false && await Prefs.get("open-manager-on-queue")) {
|
||||||
await openManager(false);
|
await openManager(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,6 +126,14 @@ export const API = new class APIImpl {
|
|||||||
await FASTFILTER.init();
|
await FASTFILTER.init();
|
||||||
await FASTFILTER.push(options.fast);
|
await FASTFILTER.push(options.fast);
|
||||||
}
|
}
|
||||||
|
if (typeof options.subfolder === "string" && !options.subfolderOnce) {
|
||||||
|
await SUBFOLDER.init();
|
||||||
|
await SUBFOLDER.push(options.subfolder);
|
||||||
|
}
|
||||||
|
if (typeof options.server === "string" && !options.serverOnce) {
|
||||||
|
await SERVER.init();
|
||||||
|
await SERVER.push(options.server);
|
||||||
|
}
|
||||||
if (typeof options.type === "string") {
|
if (typeof options.type === "string") {
|
||||||
await Prefs.set("last-type", options.type);
|
await Prefs.set("last-type", options.type);
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,15 @@ import {
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
MenuClickInfo,
|
MenuClickInfo,
|
||||||
CHROME,
|
CHROME,
|
||||||
|
runtime,
|
||||||
|
history,
|
||||||
|
sessions,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
OnInstalled,
|
||||||
} from "./browser";
|
} from "./browser";
|
||||||
import { Bus } from "./bus";
|
import { Bus } from "./bus";
|
||||||
import { filterInSitu } from "./util";
|
import { filterInSitu } from "./util";
|
||||||
|
import { DB } from "./db";
|
||||||
|
|
||||||
|
|
||||||
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
|
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
|
||||||
@ -45,6 +51,9 @@ const CHROME_CONTEXTS = Object.freeze(new Set([
|
|||||||
|
|
||||||
async function runContentJob(tab: Tab, file: string, msg: any) {
|
async function runContentJob(tab: Tab, file: string, msg: any) {
|
||||||
try {
|
try {
|
||||||
|
if (tab && tab.incognito && msg) {
|
||||||
|
msg.private = tab.incognito;
|
||||||
|
}
|
||||||
const res = await tabs.executeScript(tab.id, {
|
const res = await tabs.executeScript(tab.id, {
|
||||||
file,
|
file,
|
||||||
allFrames: true,
|
allFrames: true,
|
||||||
@ -98,19 +107,19 @@ class Handler {
|
|||||||
|
|
||||||
async performSelection(options: SelectionOptions) {
|
async performSelection(options: SelectionOptions) {
|
||||||
try {
|
try {
|
||||||
const toptions: any = {
|
const tabOptions: any = {
|
||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
discarded: false,
|
discarded: false,
|
||||||
};
|
};
|
||||||
if (!CHROME) {
|
if (!CHROME) {
|
||||||
toptions.hidden = false;
|
tabOptions.hidden = false;
|
||||||
}
|
}
|
||||||
const selectedTabs = options.allTabs ?
|
const selectedTabs = options.allTabs ?
|
||||||
await tabs.query(toptions) as any[] :
|
await tabs.query(tabOptions) as any[] :
|
||||||
[options.tab];
|
[options.tab];
|
||||||
|
|
||||||
const textLinks = await Prefs.get("text-links", true);
|
const textLinks = await Prefs.get("text-links", true);
|
||||||
const goptions = {
|
const gatherOptions = {
|
||||||
type: "DTA:gather",
|
type: "DTA:gather",
|
||||||
selectionOnly: options.selectionOnly,
|
selectionOnly: options.selectionOnly,
|
||||||
textLinks,
|
textLinks,
|
||||||
@ -119,7 +128,7 @@ class Handler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const results = await Promise.all(selectedTabs.
|
const results = await Promise.all(selectedTabs.
|
||||||
map((tab: any) => runContentJob(tab, GATHER, goptions)));
|
map((tab: any) => runContentJob(tab, GATHER, gatherOptions)));
|
||||||
|
|
||||||
await this.processResults(options.turbo, results.flat());
|
await this.processResults(options.turbo, results.flat());
|
||||||
}
|
}
|
||||||
@ -129,42 +138,40 @@ class Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMajor(version?: string) {
|
||||||
|
if (!version) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const match = version.match(/^\d+\.\d+/);
|
||||||
|
if (!match) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
else if (reason === "install") {
|
||||||
|
// tabs.create({
|
||||||
|
// url: `https://about.downthemall.org/4.0/?cur=${major}`,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
locale.then(() => {
|
locale.then(() => {
|
||||||
new class Action extends Handler {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.onClicked = this.onClicked.bind(this);
|
|
||||||
action.onClicked.addListener(this.onClicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClicked(tab: {id: number}) {
|
|
||||||
if (!tab.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.processResults(
|
|
||||||
true,
|
|
||||||
await runContentJob(
|
|
||||||
tab, "/bundles/content-gather.js", {
|
|
||||||
type: "DTA:gather",
|
|
||||||
selectionOnly: false,
|
|
||||||
textLinks: await Prefs.get("text-links", true),
|
|
||||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
|
||||||
transferable: TRANSFERABLE_PROPERTIES,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
const menuHandler = new class Menus extends Handler {
|
const menuHandler = new class Menus extends Handler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onClicked = this.onClicked.bind(this);
|
this.onClicked = this.onClicked.bind(this);
|
||||||
const alls = new Map<string, string[]>();
|
const alls = new Map<string, string[]>();
|
||||||
const mcreate = (options: any) => {
|
const menuCreate = (options: any) => {
|
||||||
if (CHROME) {
|
if (CHROME) {
|
||||||
delete options.icons;
|
delete options.icons;
|
||||||
options.contexts = options.contexts.
|
options.contexts = options.contexts.
|
||||||
@ -178,7 +185,7 @@ locale.then(() => {
|
|||||||
}
|
}
|
||||||
menus.create(options);
|
menus.create(options);
|
||||||
};
|
};
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTARegularLink",
|
id: "DTARegularLink",
|
||||||
contexts: ["link"],
|
contexts: ["link"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -187,7 +194,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.link"),
|
title: _("dta.regular.link"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTATurboLink",
|
id: "DTATurboLink",
|
||||||
contexts: ["link"],
|
contexts: ["link"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -196,7 +203,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.link"),
|
title: _("dta.turbo.link"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTARegularImage",
|
id: "DTARegularImage",
|
||||||
contexts: ["image"],
|
contexts: ["image"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -205,7 +212,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.image"),
|
title: _("dta.regular.image"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTATurboImage",
|
id: "DTATurboImage",
|
||||||
contexts: ["image"],
|
contexts: ["image"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -214,7 +221,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.image"),
|
title: _("dta.turbo.image"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTARegularMedia",
|
id: "DTARegularMedia",
|
||||||
contexts: ["video", "audio"],
|
contexts: ["video", "audio"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -223,7 +230,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.media"),
|
title: _("dta.regular.media"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTATurboMedia",
|
id: "DTATurboMedia",
|
||||||
contexts: ["video", "audio"],
|
contexts: ["video", "audio"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -232,7 +239,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.media"),
|
title: _("dta.turbo.media"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTARegularSelection",
|
id: "DTARegularSelection",
|
||||||
contexts: ["selection"],
|
contexts: ["selection"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -241,7 +248,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.selection"),
|
title: _("dta.regular.selection"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTATurboSelection",
|
id: "DTATurboSelection",
|
||||||
contexts: ["selection"],
|
contexts: ["selection"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -250,7 +257,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.selection"),
|
title: _("dta.turbo.selection"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTARegular",
|
id: "DTARegular",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -259,7 +266,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular"),
|
title: _("dta.regular"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTATurbo",
|
id: "DTATurbo",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -268,12 +275,12 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo"),
|
title: _("dta.turbo"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "sep-1",
|
id: "sep-1",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTARegularAll",
|
id: "DTARegularAll",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -282,7 +289,7 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("dta-regular-all"),
|
title: _("dta-regular-all"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTATurboAll",
|
id: "DTATurboAll",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -294,12 +301,12 @@ locale.then(() => {
|
|||||||
const sep2ctx = menus.ACTION_MENU_TOP_LEVEL_LIMIT === 6 ?
|
const sep2ctx = menus.ACTION_MENU_TOP_LEVEL_LIMIT === 6 ?
|
||||||
["all", "tools_menu"] :
|
["all", "tools_menu"] :
|
||||||
["all", "browser_action", "tools_menu"];
|
["all", "browser_action", "tools_menu"];
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "sep-2",
|
id: "sep-2",
|
||||||
contexts: sep2ctx,
|
contexts: sep2ctx,
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTAAdd",
|
id: "DTAAdd",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -310,12 +317,12 @@ locale.then(() => {
|
|||||||
},
|
},
|
||||||
title: _("add-download"),
|
title: _("add-download"),
|
||||||
});
|
});
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "sep-3",
|
id: "sep-3",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
mcreate({
|
/* menuCreate({
|
||||||
id: "DTAManager",
|
id: "DTAManager",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -323,8 +330,8 @@ locale.then(() => {
|
|||||||
32: "/style/button-manager@2x.png",
|
32: "/style/button-manager@2x.png",
|
||||||
},
|
},
|
||||||
title: _("manager.short"),
|
title: _("manager.short"),
|
||||||
});
|
});*/
|
||||||
mcreate({
|
menuCreate({
|
||||||
id: "DTAPrefs",
|
id: "DTAPrefs",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -415,7 +422,7 @@ locale.then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async enumulate(action: string) {
|
async emulate(action: string) {
|
||||||
const tab = await tabs.query({
|
const tab = await tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
@ -537,40 +544,133 @@ locale.then(() => {
|
|||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
|
new class Action extends Handler {
|
||||||
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
|
constructor() {
|
||||||
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
|
super();
|
||||||
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
|
this.onClicked = this.onClicked.bind(this);
|
||||||
Bus.on("do-single", () => API.singleRegular(null));
|
action.onClicked.addListener(this.onClicked);
|
||||||
Bus.on("open-manager", () => openManager(true));
|
Prefs.get("button-type", false).then(v => this.adjust(v));
|
||||||
Bus.on("open-prefs", () => openPrefs());
|
Prefs.on("button-type", (prefs, key, value) => {
|
||||||
|
this.adjust(value);
|
||||||
function adjustAction(globalTurbo: boolean) {
|
|
||||||
action.setPopup({
|
|
||||||
popup: globalTurbo ? "" : "/windows/popup.html"
|
|
||||||
});
|
});
|
||||||
action.setIcon({
|
}
|
||||||
path: globalTurbo ? {
|
|
||||||
16: "/style/button-turbo.png",
|
adjust(type: string) {
|
||||||
32: "/style/button-turbo@2x.png",
|
action.setPopup({
|
||||||
} : {
|
popup: type !== "popup" ? "" : "/windows/popup.html"
|
||||||
|
});
|
||||||
|
let icons;
|
||||||
|
switch (type) {
|
||||||
|
case "popup":
|
||||||
|
icons = {
|
||||||
16: "/style/icon16.png",
|
16: "/style/icon16.png",
|
||||||
32: "/style/icon32.png",
|
32: "/style/icon32.png",
|
||||||
48: "/style/icon48.png",
|
48: "/style/icon48.png",
|
||||||
64: "/style/icon64.png",
|
64: "/style/icon64.png",
|
||||||
96: "/style/icon96.png",
|
|
||||||
128: "/style/icon128.png",
|
128: "/style/icon128.png",
|
||||||
256: "/style/icon256.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() {
|
(async function init() {
|
||||||
await Prefs.set("last-run", new Date());
|
const urlBase = runtime.getURL("");
|
||||||
Prefs.get("global-turbo", false).then(v => adjustAction(v));
|
history.onVisited.addListener(({url}: {url: string}) => {
|
||||||
Prefs.on("global-turbo", (prefs, key, value) => {
|
if (!url || !url.startsWith(urlBase)) {
|
||||||
adjustAction(value);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DB.init();
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("db init", ex.toString(), ex.message, ex.stack, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Prefs.set("last-run", new Date());
|
||||||
await filters();
|
await filters();
|
||||||
await getManager();
|
await getManager();
|
||||||
})().catch(ex => {
|
})().catch(ex => {
|
||||||
|
@ -73,7 +73,7 @@ class Numeral implements Generator {
|
|||||||
this.digits = dir ? rawpieces[0].length : rawpieces[1].length;
|
this.digits = dir ? rawpieces[0].length : rawpieces[1].length;
|
||||||
this.length = Math.floor(
|
this.length = Math.floor(
|
||||||
(this.stop - this.start + (dir ? 1 : -1)) / this.step);
|
(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);
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +93,60 @@ 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 {
|
export class BatchGenerator implements Generator {
|
||||||
private readonly gens: Generator[];
|
private readonly gens: Generator[];
|
||||||
|
|
||||||
@ -120,11 +174,16 @@ export class BatchGenerator implements Generator {
|
|||||||
try {
|
try {
|
||||||
this.gens.push(new Numeral(tok));
|
this.gens.push(new Numeral(tok));
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch {
|
||||||
|
try {
|
||||||
|
this.gens.push(new Character(tok));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
this.gens.push(new Literal(`[${tok}]`));
|
this.gens.push(new Literal(`[${tok}]`));
|
||||||
this.hasInvalid = true;
|
this.hasInvalid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (str) {
|
if (str) {
|
||||||
this.gens.push(new Literal(str));
|
this.gens.push(new Literal(str));
|
||||||
}
|
}
|
||||||
|
103
lib/browser.ts
@ -9,47 +9,112 @@ interface ExtensionListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageSender {
|
export interface MessageSender {
|
||||||
tab?: Tab;
|
readonly tab?: Tab;
|
||||||
frameId?: number;
|
readonly frameId?: number;
|
||||||
id?: number;
|
readonly id?: number;
|
||||||
url?: string;
|
readonly url?: string;
|
||||||
tlsChannelId?: string;
|
readonly tlsChannelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
id?: number;
|
readonly id?: number;
|
||||||
|
readonly incognito?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuClickInfo {
|
export interface MenuClickInfo {
|
||||||
menuItemId: string | number;
|
readonly menuItemId: string | number;
|
||||||
button?: number;
|
readonly button?: number;
|
||||||
linkUrl?: string;
|
readonly linkUrl?: string;
|
||||||
srcUrl?: string;
|
readonly srcUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface RawPort {
|
export interface RawPort {
|
||||||
error: any;
|
readonly error: any;
|
||||||
name: string;
|
readonly name: string;
|
||||||
onDisconnect: ExtensionListener;
|
readonly sender?: MessageSender;
|
||||||
onMessage: ExtensionListener;
|
readonly onDisconnect: ExtensionListener;
|
||||||
sender?: MessageSender;
|
readonly onMessage: ExtensionListener;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
postMessage: (message: any) => void;
|
postMessage: (message: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {extension} = polyfill;
|
interface WebRequestFilter {
|
||||||
export const {notifications} = polyfill;
|
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 {browserAction} = polyfill;
|
||||||
export const {contextMenus} = 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 {menus} = polyfill;
|
||||||
|
export const {notifications} = polyfill;
|
||||||
export const {runtime} = polyfill;
|
export const {runtime} = polyfill;
|
||||||
|
export const {sessions} = polyfill;
|
||||||
export const {storage} = polyfill;
|
export const {storage} = polyfill;
|
||||||
export const {tabs} = polyfill;
|
export const {tabs} = polyfill;
|
||||||
export const {webNavigation} = polyfill;
|
export const {webNavigation} = polyfill;
|
||||||
export const {webRequest} = polyfill;
|
export const {webRequest}: {webRequest: WebRequest} = polyfill;
|
||||||
export const {windows} = polyfill;
|
export const {windows} = polyfill;
|
||||||
|
export const {theme} = polyfill;
|
||||||
|
|
||||||
export const CHROME = navigator.appVersion.includes("Chrome/");
|
export const CHROME = navigator.appVersion.includes("Chrome/");
|
||||||
|
export const OPERA = navigator.appVersion.includes("OPR/");
|
||||||
|
32
lib/bus.ts
@ -8,32 +8,35 @@ import {runtime, tabs, RawPort, MessageSender} from "./browser";
|
|||||||
export class Port extends EventEmitter {
|
export class Port extends EventEmitter {
|
||||||
private port: RawPort | null;
|
private port: RawPort | null;
|
||||||
|
|
||||||
|
private disconnected = false;
|
||||||
|
|
||||||
constructor(port: RawPort) {
|
constructor(port: RawPort) {
|
||||||
super();
|
super();
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
let disconnected = false;
|
|
||||||
const disconnect = () => {
|
|
||||||
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
|
// Nasty firefox bug, thus listen for tab removal explicitly
|
||||||
if (port.sender && port.sender.tab && port.sender.tab.id) {
|
if (port.sender && port.sender.tab && port.sender.tab.id) {
|
||||||
const otherTabId = port.sender.tab.id;
|
const otherTabId = port.sender.tab.id;
|
||||||
const tabListener = function(tabId: number) {
|
const tabListener = (tabId: number) => {
|
||||||
if (tabId !== otherTabId) {
|
if (tabId !== otherTabId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
disconnect();
|
this.disconnect();
|
||||||
};
|
};
|
||||||
tabs.onRemoved.addListener(tabListener);
|
tabs.onRemoved.addListener(tabListener);
|
||||||
}
|
}
|
||||||
port.onMessage.addListener(this.onMessage.bind(this));
|
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() {
|
get name() {
|
||||||
@ -120,6 +123,9 @@ export const Bus = new class extends EventEmitter {
|
|||||||
port.disconnect();
|
port.disconnect();
|
||||||
return;
|
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(/^[^']*'/, ""));
|
||||||
|
}
|
||||||
|
}
|
147
lib/db.ts
@ -1,14 +1,25 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseItem } from "./item";
|
import { BaseItem } from "./item";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
// License: MIT
|
import { Download } from "./manager/download";
|
||||||
|
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
|
||||||
|
import { storage } from "./browser";
|
||||||
|
import { sort } from "./sorting";
|
||||||
|
|
||||||
const VERSION = 1;
|
const VERSION = 1;
|
||||||
const STORE = "queue";
|
const STORE = "queue";
|
||||||
|
|
||||||
export const DB = new class DB {
|
interface Database {
|
||||||
|
init(): Promise<void>;
|
||||||
|
saveItems(items: Download[]): Promise<unknown>;
|
||||||
|
deleteItems(items: any[]): Promise<void>;
|
||||||
|
getAll(): Promise<BaseItem[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IDB implements Database {
|
||||||
private db?: IDBDatabase;
|
private db?: IDBDatabase;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -69,7 +80,7 @@ export const DB = new class DB {
|
|||||||
return await new Promise(this.getAllInternal);
|
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) {
|
if (!items || !items.length || !this.db) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@ -83,9 +94,13 @@ export const DB = new class DB {
|
|||||||
if (item.private) {
|
if (item.private) {
|
||||||
continue;
|
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) {
|
if (!("dbId" in item) || item.dbId < 0) {
|
||||||
req.onsuccess = () => item.dbId = req.result;
|
req.onsuccess = () => item.dbId = req.result as number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,12 +109,12 @@ export const DB = new class DB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveItems(items: any[]) {
|
async saveItems(items: Download[]) {
|
||||||
await this.init();
|
await this.init();
|
||||||
return await new Promise(this.saveItemsInternal.bind(this, items));
|
return await new Promise(this.saveItemsInternal.bind(this, items));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteItemsInternal(items: any[], resolve: Function, reject: Function) {
|
deleteItemsInternal(items: any[], resolve: () => void, reject: Function) {
|
||||||
if (!items || !items.length || !this.db) {
|
if (!items || !items.length || !this.db) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@ -132,4 +147,120 @@ export const DB = new class DB {
|
|||||||
await this.init();
|
await this.init();
|
||||||
await new Promise(this.deleteItemsInternal.bind(this, items));
|
await new Promise(this.deleteItemsInternal.bind(this, items));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageDB implements Database {
|
||||||
|
private counter = 1;
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
const {db = null} = await storage.local.get("db");
|
||||||
|
if (!db || !db.counter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.counter = db.counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveItems(items: Download[]) {
|
||||||
|
const db: any = {items: []};
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.dbId) {
|
||||||
|
item.dbId = ++this.counter;
|
||||||
|
}
|
||||||
|
db.items.push(item.toJSON());
|
||||||
|
}
|
||||||
|
db.counter = this.counter;
|
||||||
|
await storage.local.set({db});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteItems(items: any[]): Promise<void> {
|
||||||
|
const gone = new Set(items.map(i => i.dbId));
|
||||||
|
const {db = null} = await storage.local.get("db");
|
||||||
|
if (!db) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
db.items = db.items.filter((i: any) => !gone.has(i.dbId));
|
||||||
|
await storage.local.set({db});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
const {db = null} = await storage.local.get("db");
|
||||||
|
if (!db || !Array.isArray(db.items)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return sort(db.items, (i: any) => i.position) as BaseItem[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryDB implements Database {
|
||||||
|
private counter = 1;
|
||||||
|
|
||||||
|
private items = new Map();
|
||||||
|
|
||||||
|
init(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveItems(items: Download[]) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.private) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!item.dbId) {
|
||||||
|
item.dbId = ++this.counter;
|
||||||
|
}
|
||||||
|
this.items.set(item.dbId, item.toJSON());
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItems(items: any[]) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (!("dbId" in item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.items.delete(item.dbId);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Promise<BaseItem[]> {
|
||||||
|
return Promise.resolve(Array.from(this.items.values()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DB = new class DBWrapper implements Database {
|
||||||
|
saveItems(items: Download[]): Promise<unknown> {
|
||||||
|
return this.db.saveItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItems(items: any[]): Promise<void> {
|
||||||
|
return this.db.deleteItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Promise<BaseItem[]> {
|
||||||
|
return this.db.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private db: Database;
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
this.db = new IDB();
|
||||||
|
await this.db.init();
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.warn(
|
||||||
|
"Failed to initialize idb backend, using storage db fallback", ex);
|
||||||
|
try {
|
||||||
|
this.db = new StorageDB();
|
||||||
|
await this.db.init();
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.warn(
|
||||||
|
"Failed to initialize storage backend, using memory db fallback", ex);
|
||||||
|
this.db = new MemoryDB();
|
||||||
|
await this.db.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}();
|
}();
|
||||||
|
41
lib/i18n.ts
@ -4,7 +4,7 @@
|
|||||||
import {memoize} from "./memoize";
|
import {memoize} from "./memoize";
|
||||||
import langs from "../_locales/all.json";
|
import langs from "../_locales/all.json";
|
||||||
import { sorted, naturalCaseCompare } from "./sorting";
|
import { sorted, naturalCaseCompare } from "./sorting";
|
||||||
|
import lf from "localforage";
|
||||||
|
|
||||||
export const ALL_LANGS = Object.freeze(new Map<string, string>(
|
export const ALL_LANGS = Object.freeze(new Map<string, string>(
|
||||||
sorted(Object.entries(langs), e => {
|
sorted(Object.entries(langs), e => {
|
||||||
@ -40,11 +40,11 @@ class Entry {
|
|||||||
this.message = entry.message.replace(/\$[A-Z0-9]+\$/g, (r: string) => {
|
this.message = entry.message.replace(/\$[A-Z0-9]+\$/g, (r: string) => {
|
||||||
hit = true;
|
hit = true;
|
||||||
const id = r.substr(1, r.length - 2).toLocaleLowerCase();
|
const id = r.substr(1, r.length - 2).toLocaleLowerCase();
|
||||||
const pholder = entry.placeholders[id];
|
const placeholder = entry.placeholders[id];
|
||||||
if (!pholder || !pholder.content) {
|
if (!placeholder || !placeholder.content) {
|
||||||
throw new Error(`Invalid placeholder: ${id}`);
|
throw new Error(`Invalid placeholder: ${id}`);
|
||||||
}
|
}
|
||||||
return `${pholder.content}$`;
|
return `${placeholder.content}$`;
|
||||||
});
|
});
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
throw new Error("Not entry-able");
|
throw new Error("Not entry-able");
|
||||||
@ -123,14 +123,17 @@ async function fetchLanguage(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadCached() {
|
async function loadCached(): Promise<any> {
|
||||||
if (document.location.pathname.includes("/windows/")) {
|
const cached = await lf.getItem<string>(CACHE_KEY);
|
||||||
const cached = localStorage.getItem(CACHE_KEY);
|
if (!cached) {
|
||||||
if (cached) {
|
|
||||||
return JSON.parse(cached) as any[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(cached);
|
||||||
|
if (!Array.isArray(parsed) || !parsed[0].CRASH || !parsed[0].CRASH.message) {
|
||||||
|
console.warn("rejecting cached locales", parsed);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRawLocales() {
|
async function loadRawLocales() {
|
||||||
@ -187,16 +190,16 @@ async function load(): Promise<Localization> {
|
|||||||
}
|
}
|
||||||
CURRENT = currentLang;
|
CURRENT = currentLang;
|
||||||
// en is the base locale
|
// en is the base locale
|
||||||
let valid = loadCached();
|
let valid = await loadCached();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
valid = await loadRawLocales();
|
valid = await loadRawLocales();
|
||||||
localStorage.setItem(CACHE_KEY, JSON.stringify(valid));
|
await lf.setItem(CACHE_KEY, JSON.stringify(valid));
|
||||||
}
|
}
|
||||||
if (!valid.length) {
|
if (!valid.length) {
|
||||||
throw new Error("Could not lood ANY of these locales");
|
throw new Error("Could not load ANY of these locales");
|
||||||
}
|
}
|
||||||
|
|
||||||
const custom = localStorage.getItem(CUSTOM_KEY);
|
const custom = await lf.getItem<string>(CUSTOM_KEY);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
try {
|
try {
|
||||||
valid.push(JSON.parse(custom));
|
valid.push(JSON.parse(custom));
|
||||||
@ -239,7 +242,7 @@ locale.then(l => {
|
|||||||
/**
|
/**
|
||||||
* Localize a message
|
* Localize a message
|
||||||
* @param {string} id Identifier of the string to localize
|
* @param {string} id Identifier of the string to localize
|
||||||
* @param {string[]} [subst] Message substituations
|
* @param {string[]} [subst] Message substitutions
|
||||||
* @returns {string} Localized message
|
* @returns {string} Localized message
|
||||||
*/
|
*/
|
||||||
export function _(id: string, ...subst: any[]) {
|
export function _(id: string, ...subst: any[]) {
|
||||||
@ -302,11 +305,11 @@ export async function localize<T extends HTMLElement | DocumentFragment>(
|
|||||||
return localize_(elem);
|
return localize_(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCustomLocale(data?: string) {
|
export async function saveCustomLocale(data?: string) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
localStorage.removeItem(CUSTOM_KEY);
|
await lf.removeItem(CUSTOM_KEY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new Localization(JSON.parse(data));
|
new Localization(JSON.parse(data));
|
||||||
localStorage.setItem(CUSTOM_KEY, data);
|
await localStorage.setItem(CUSTOM_KEY, data);
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
import { downloads, CHROME } from "./browser";
|
import { downloads, CHROME } from "./browser";
|
||||||
import { EventEmitter } from "../uikit/lib/events";
|
import { EventEmitter } from "../uikit/lib/events";
|
||||||
import { PromiseSerializer } from "./pserializer";
|
import { PromiseSerializer } from "./pserializer";
|
||||||
|
import lf from "localforage";
|
||||||
|
|
||||||
|
|
||||||
const VERSION = 1;
|
|
||||||
const STORE = "iconcache";
|
const STORE = "iconcache";
|
||||||
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
// eslint-disable-next-line no-magic-numbers
|
||||||
const CACHE_SIZES = CHROME ? [16, 32] : [16, 32, 64, 127];
|
const CACHE_SIZES = CHROME ? [16, 32] : [16, 32, 64, 127];
|
||||||
|
|
||||||
@ -48,37 +50,17 @@ const SYNONYMS = Object.freeze(new Map<string, string>([
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
export const IconCache = new class IconCache extends EventEmitter {
|
export const IconCache = new class IconCache extends EventEmitter {
|
||||||
private db: Promise<IDBDatabase>;
|
private db = lf.createInstance({name: STORE});
|
||||||
|
|
||||||
private cache: Map<string, string>;
|
private cache: Map<string, string>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.db = this.init();
|
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
this.get = PromiseSerializer.wrapNew(8, this, this.get);
|
this.get = PromiseSerializer.wrapNew(8, this, this.get);
|
||||||
this.set = PromiseSerializer.wrapNew(1, this, this.set);
|
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) {
|
private normalize(ext: string) {
|
||||||
ext = ext.toLocaleLowerCase();
|
ext = ext.toLocaleLowerCase();
|
||||||
return SYNONYMS.get(ext) || ext;
|
return SYNONYMS.get(ext) || ext;
|
||||||
@ -95,36 +77,25 @@ export const IconCache = new class IconCache extends EventEmitter {
|
|||||||
if (rv) {
|
if (rv) {
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
const db = await this.db;
|
|
||||||
rv = this.cache.get(sext);
|
rv = this.cache.get(sext);
|
||||||
if (rv) {
|
if (rv) {
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
return await new Promise<string | undefined>(resolve => {
|
let result = await this.db.getItem<any>(sext);
|
||||||
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) {
|
if (!result) {
|
||||||
resolve(undefined);
|
return this.cache.get(sext);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (typeof req.result !== "string") {
|
rv = this.cache.get(sext);
|
||||||
|
if (rv) {
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
if (typeof result !== "string") {
|
||||||
result = URL.createObjectURL(result).toString();
|
result = URL.createObjectURL(result).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cache.set(sext, result);
|
this.cache.set(sext, result);
|
||||||
this.cache.set(ext, "");
|
this.cache.set(ext, "");
|
||||||
resolve(result);
|
return result;
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(ext: string, manId: number) {
|
async set(ext: string, manId: number) {
|
||||||
@ -145,18 +116,9 @@ export const IconCache = new class IconCache extends EventEmitter {
|
|||||||
}
|
}
|
||||||
for (const {size, icon} of urls) {
|
for (const {size, icon} of urls) {
|
||||||
this.cache.set(`${ext}-${size}`, URL.createObjectURL(icon));
|
this.cache.set(`${ext}-${size}`, URL.createObjectURL(icon));
|
||||||
|
await this.db.setItem(`${ext}-${size}`, icon);
|
||||||
}
|
}
|
||||||
this.cache.set(ext, "");
|
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);
|
this.emit("cached", ext);
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
261
lib/imex.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
"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;
|
||||||
|
}
|
||||||
|
const description = file.querySelector("description");
|
||||||
|
if (description && description.textContent) {
|
||||||
|
item.description = description.textContent.trim();
|
||||||
|
}
|
||||||
|
const title = file.getElementsByTagNameNS(NS_DTA, "title");
|
||||||
|
if (title && title[0] && title[0].textContent) {
|
||||||
|
item.title = title[0].textContent;
|
||||||
|
}
|
||||||
|
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 refererUrls = getTextLinks(v);
|
||||||
|
if (refererUrls && refererUrls.length) {
|
||||||
|
current.referrer = refererUrls.pop();
|
||||||
|
current.usableReferrer = decodeURIComponent(current.referrer || "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importText(data: string) {
|
||||||
|
if (data.includes(NS_METALINK_RFC5854)) {
|
||||||
|
return importMeta4(data);
|
||||||
|
}
|
||||||
|
const splitter = /((?:.|\r)+)\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", "TraitorousDownloading!");
|
||||||
|
root.appendChild(document.createComment(
|
||||||
|
"metalink as exported by TraitorousDownloading!",
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const anyItem = item as any;
|
||||||
|
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
|
||||||
|
f.setAttribute("name", anyItem.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.title) {
|
||||||
|
const n = document.createElementNS(NS_DTA, "title");
|
||||||
|
n.textContent = item.title;
|
||||||
|
f.appendChild(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
|
||||||
|
u.textContent = item.url;
|
||||||
|
f.appendChild(u);
|
||||||
|
|
||||||
|
if (anyItem.totalSize > 0) {
|
||||||
|
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
|
||||||
|
s.textContent = anyItem.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,})?$/;
|
@ -15,6 +15,7 @@ export interface BaseItem {
|
|||||||
batch?: number;
|
batch?: number;
|
||||||
idx: number;
|
idx: number;
|
||||||
mask?: string;
|
mask?: string;
|
||||||
|
subfolder?: string;
|
||||||
startDate?: number;
|
startDate?: number;
|
||||||
private?: boolean;
|
private?: boolean;
|
||||||
postData?: string;
|
postData?: string;
|
||||||
@ -27,10 +28,12 @@ const OPTIONPROPS = Object.freeze([
|
|||||||
"fileName",
|
"fileName",
|
||||||
"batch", "idx",
|
"batch", "idx",
|
||||||
"mask",
|
"mask",
|
||||||
|
"subfolder",
|
||||||
"startDate",
|
"startDate",
|
||||||
"private",
|
"private",
|
||||||
"postData",
|
"postData",
|
||||||
"paused"
|
"paused",
|
||||||
|
"server", "cookies",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function maybeAssign(options: any, what: any) {
|
function maybeAssign(options: any, what: any) {
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import { parsePath, URLd } from "../util";
|
import { parsePath, URLd } from "../util";
|
||||||
import { QUEUED, RUNNING, PAUSED } from "./state";
|
import { QUEUED, RUNNING, PAUSED } from "./state";
|
||||||
import Renamer from "./renamer";
|
import Renamer from "./renamer";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "../item";
|
||||||
|
|
||||||
const SAVEDPROPS = [
|
const SAVEDPROPS = [
|
||||||
"state",
|
"state",
|
||||||
@ -14,6 +16,7 @@ const SAVEDPROPS = [
|
|||||||
"usableReferrer",
|
"usableReferrer",
|
||||||
"fileName",
|
"fileName",
|
||||||
"mask",
|
"mask",
|
||||||
|
"subfolder",
|
||||||
"date",
|
"date",
|
||||||
// batches
|
// batches
|
||||||
"batch",
|
"batch",
|
||||||
@ -48,7 +51,9 @@ const DEFAULTS = {
|
|||||||
written: 0,
|
written: 0,
|
||||||
manId: 0,
|
manId: 0,
|
||||||
mime: "",
|
mime: "",
|
||||||
prerolled: false
|
prerolled: false,
|
||||||
|
retries: 0,
|
||||||
|
deadline: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let sessionId = 0;
|
let sessionId = 0;
|
||||||
@ -103,9 +108,13 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public mask: string;
|
public mask: string;
|
||||||
|
|
||||||
|
public subfolder: string;
|
||||||
|
|
||||||
public prerolled: boolean;
|
public prerolled: boolean;
|
||||||
|
|
||||||
constructor(options: any) {
|
public retries: number;
|
||||||
|
|
||||||
|
constructor(options: BaseItem) {
|
||||||
Object.assign(this, DEFAULTS);
|
Object.assign(this, DEFAULTS);
|
||||||
this.assign(options);
|
this.assign(options);
|
||||||
if (this.state === RUNNING) {
|
if (this.state === RUNNING) {
|
||||||
@ -113,14 +122,16 @@ export class BaseDownload {
|
|||||||
}
|
}
|
||||||
this.sessionId = ++sessionId;
|
this.sessionId = ++sessionId;
|
||||||
this.renamer = new Renamer(this);
|
this.renamer = new Renamer(this);
|
||||||
|
this.retries = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
assign(options: any) {
|
assign(options: BaseItem) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self: any = this;
|
const self: any = this;
|
||||||
|
const other: any = options;
|
||||||
for (const prop of SAVEDPROPS) {
|
for (const prop of SAVEDPROPS) {
|
||||||
if (prop in options) {
|
if (prop in options) {
|
||||||
self[prop] = options[prop];
|
self[prop] = other[prop];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.uURL = new URL(this.url) as URLd;
|
this.uURL = new URL(this.url) as URLd;
|
||||||
@ -180,8 +191,10 @@ export class BaseDownload {
|
|||||||
rv.destPath = dest.path;
|
rv.destPath = dest.path;
|
||||||
rv.destFull = dest.full;
|
rv.destFull = dest.full;
|
||||||
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||||
|
rv.currentFull = `${dest.path}/${rv.currentName}`;
|
||||||
rv.error = this.error;
|
rv.error = this.error;
|
||||||
rv.ext = this.renamer.p_ext;
|
rv.ext = this.renamer.p_ext;
|
||||||
|
rv.retries = this.retries;
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import MimeType from "whatwg-mimetype";
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { CHROME, downloads, webRequest } from "../browser";
|
import { CHROME, downloads, DownloadOptions } from "../browser";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs, PrefWatcher } from "../prefs";
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
import { filterInSitu, parsePath, sanitizePath } from "../util";
|
import { filterInSitu, parsePath } from "../util";
|
||||||
import { BaseDownload } from "./basedownload";
|
import { BaseDownload } from "./basedownload";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Manager } from "./man";
|
import { Manager } from "./man";
|
||||||
@ -16,67 +16,27 @@ import {
|
|||||||
DONE,
|
DONE,
|
||||||
FORCABLE,
|
FORCABLE,
|
||||||
MISSING,
|
MISSING,
|
||||||
PAUSABLE,
|
PAUSEABLE,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
QUEUED,
|
QUEUED,
|
||||||
RUNNING
|
RUNNING,
|
||||||
|
RETRYING
|
||||||
} from "./state";
|
} from "./state";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Preroller, PrerollResults } from "./preroller";
|
||||||
|
|
||||||
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
|
function isRecoverable(error: string) {
|
||||||
const PREROLL_HOSTS = /4cdn|chan/;
|
switch (error) {
|
||||||
const PREROLL_TIMEOUT = 10000;
|
case "SERVER_FAILED":
|
||||||
const PREROLL_NOPE = new Set<string>();
|
return true;
|
||||||
|
|
||||||
function parseDisposition(disp: MimeType) {
|
default:
|
||||||
if (!disp) {
|
return error.startsWith("NETWORK_");
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
let encoding = (disp.parameters.get("charset") || "utf-8").trim();
|
|
||||||
let file = (disp.parameters.get("filename") || "").trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
||||||
if (!file) {
|
|
||||||
const encoded = disp.parameters.get("filename*");
|
|
||||||
if (!encoded) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const pieces = encoded.split("'", 3);
|
|
||||||
if (pieces.length !== 3) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
encoding = pieces[0].trim() || encoding;
|
|
||||||
file = (pieces[3] || "").trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
||||||
}
|
|
||||||
file = file.trim();
|
|
||||||
if (!file) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// And now for the tricky part...
|
|
||||||
// First unescape the string, to get the raw bytes
|
|
||||||
// not utf-8-interpreted bytes
|
|
||||||
// Then convert the string into an uint8[]
|
|
||||||
// Then decode
|
|
||||||
return new TextDecoder(encoding).decode(
|
|
||||||
new Uint8Array(unescape(file).split("").map(e => e.charCodeAt(0)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error("Cannot decode", encoding, file, ex);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Header = {name: string; value: string};
|
const RETRIES = new PrefWatcher("retries", 5);
|
||||||
interface Options {
|
const RETRY_TIME = new PrefWatcher("retry-time", 5);
|
||||||
conflictAction: string;
|
|
||||||
filename: string;
|
|
||||||
saveAs: boolean;
|
|
||||||
url: string;
|
|
||||||
method?: string;
|
|
||||||
body?: string;
|
|
||||||
incognito?: boolean;
|
|
||||||
headers: Header[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Download extends BaseDownload {
|
export class Download extends BaseDownload {
|
||||||
public manager: Manager;
|
public manager: Manager;
|
||||||
@ -89,6 +49,10 @@ export class Download extends BaseDownload {
|
|||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
|
public dbId: number;
|
||||||
|
|
||||||
|
public deadline: number;
|
||||||
|
|
||||||
constructor(manager: Manager, options: any) {
|
constructor(manager: Manager, options: any) {
|
||||||
super(options);
|
super(options);
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -120,27 +84,28 @@ export class Download extends BaseDownload {
|
|||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
const {manId: id} = this;
|
const {manId: id} = this;
|
||||||
try {
|
try {
|
||||||
const state = await downloads.search({id});
|
const state = (await downloads.search({id})).pop() || {};
|
||||||
if (state[0].state === "in_progress") {
|
if (state.state === "in_progress" && !state.error && !state.paused) {
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
this.updateStateFromBrowser();
|
this.updateStateFromBrowser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state[0].state === "complete") {
|
if (state.state === "complete") {
|
||||||
this.changeState(DONE);
|
this.changeState(DONE);
|
||||||
this.updateStateFromBrowser();
|
this.updateStateFromBrowser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!state[0].canResume) {
|
if (!state.canResume) {
|
||||||
throw new Error("Cannot resume");
|
throw new Error("Cannot resume");
|
||||||
}
|
}
|
||||||
// Cannot await here
|
// Cannot await here
|
||||||
// Firefox bug: will not return until download is finished
|
// Firefox bug: will not return until download is finished
|
||||||
downloads.resume(id).catch(() => {});
|
downloads.resume(id).catch(console.error);
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
console.error("cannot resume", ex);
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
this.removeFromBrowser();
|
this.removeFromBrowser();
|
||||||
}
|
}
|
||||||
@ -164,13 +129,15 @@ export class Download extends BaseDownload {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const options: Options = {
|
const options: DownloadOptions = {
|
||||||
conflictAction: await Prefs.get("conflict-action"),
|
conflictAction: await Prefs.get("conflict-action"),
|
||||||
filename: this.dest.full,
|
|
||||||
saveAs: false,
|
saveAs: false,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
headers: [],
|
headers: [],
|
||||||
};
|
};
|
||||||
|
if (!CHROME) {
|
||||||
|
options.filename = this.dest.full;
|
||||||
|
}
|
||||||
if (!CHROME && this.private) {
|
if (!CHROME && this.private) {
|
||||||
options.incognito = true;
|
options.incognito = true;
|
||||||
}
|
}
|
||||||
@ -184,6 +151,12 @@ export class Download extends BaseDownload {
|
|||||||
value: this.referrer
|
value: this.referrer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (CHROME) {
|
||||||
|
options.headers.push({
|
||||||
|
name: "X-DTA-ID",
|
||||||
|
value: this.sessionId.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
}
|
}
|
||||||
@ -210,39 +183,21 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get shouldPreroll() {
|
|
||||||
const {pathname, search, host} = this.uURL;
|
|
||||||
if (PREROLL_NOPE.has(host)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this.renamer.p_ext) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (search.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.uURL.pathname.endsWith("/")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (PREROLL_HEURISTICS.test(pathname)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (PREROLL_HOSTS.test(host)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async maybePreroll() {
|
private async maybePreroll() {
|
||||||
try {
|
try {
|
||||||
if (this.prerolled) {
|
if (this.prerolled) {
|
||||||
// Check again, just in case, async and all
|
// Check again, just in case, async and all
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.shouldPreroll) {
|
const roller = new Preroller(this);
|
||||||
|
if (!roller.shouldPreroll) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await (CHROME ? this.prerollChrome() : this.prerollFirefox());
|
const res = await roller.roll();
|
||||||
|
if (!res) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.adoptPrerollResults(res);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||||
@ -255,99 +210,16 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async prerollFirefox() {
|
adoptPrerollResults(res: PrerollResults) {
|
||||||
const controller = new AbortController();
|
if (res.mime) {
|
||||||
const {signal} = controller;
|
this.mime = res.mime;
|
||||||
const res = await fetch(this.uURL.toString(), {
|
|
||||||
method: "HEAD",
|
|
||||||
mode: "same-origin",
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
controller.abort();
|
|
||||||
const {headers} = res;
|
|
||||||
this.prerollFinialize(headers, res);
|
|
||||||
}
|
}
|
||||||
|
if (res.name) {
|
||||||
async prerollChrome() {
|
this.serverName = res.name;
|
||||||
let rid = "";
|
|
||||||
const rurl = this.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 (res.error) {
|
||||||
if (statusCode >= 300 && statusCode < 400) {
|
this.cancelAccordingToError(res.error);
|
||||||
// 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: "HEAD",
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
controller.abort();
|
|
||||||
const headers = await p;
|
|
||||||
this.prerollFinialize(
|
|
||||||
new Headers(headers.map(i => [i.name, i.value])), res);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private prerollFinialize(headers: Headers, res: Response) {
|
|
||||||
const type = MimeType.parse(headers.get("content-type") || "");
|
|
||||||
const dispHeader = headers.get("content-disposition");
|
|
||||||
let file = "";
|
|
||||||
if (dispHeader) {
|
|
||||||
const disp = new MimeType(`${type && type.toString() || "application/octet-stream"}; ${dispHeader}`);
|
|
||||||
file = parseDisposition(disp);
|
|
||||||
// Sanitize
|
|
||||||
file = sanitizePath(file.replace(/[/\\]+/g, "-"));
|
|
||||||
}
|
|
||||||
if (type) {
|
|
||||||
this.mime = type.essence;
|
|
||||||
}
|
|
||||||
this.serverName = file;
|
|
||||||
this.markDirty();
|
|
||||||
const {status} = res;
|
|
||||||
/* eslint-disable no-magic-numbers */
|
|
||||||
if (status === 404) {
|
|
||||||
this.cancel();
|
|
||||||
this.error = "SERVER_BAD_CONTENT";
|
|
||||||
}
|
|
||||||
else if (status === 403) {
|
|
||||||
this.cancel();
|
|
||||||
this.error = "SERVER_FORBIDDEN";
|
|
||||||
}
|
|
||||||
else if (status === 402 || status === 407) {
|
|
||||||
this.cancel();
|
|
||||||
this.error = "SERVER_UNAUTHORIZED";
|
|
||||||
}
|
|
||||||
else if (status === 400 || status === 405) {
|
|
||||||
PREROLL_NOPE.add(this.uURL.host);
|
|
||||||
}
|
|
||||||
else if (status > 400 && status < 500) {
|
|
||||||
this.cancel();
|
|
||||||
this.error = "SERVER_FAILED";
|
|
||||||
}
|
|
||||||
/* eslint-enable no-magic-numbers */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resume(forced = false) {
|
resume(forced = false) {
|
||||||
@ -362,20 +234,32 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause(retry?: boolean) {
|
||||||
if (!(PAUSABLE & this.state)) {
|
if (!(PAUSEABLE & this.state)) {
|
||||||
return;
|
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) {
|
if (this.state === RUNNING && this.manId) {
|
||||||
try {
|
try {
|
||||||
await downloads.pause(this.manId);
|
await downloads.pause(this.manId);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error("pause", ex.toString(), ex);
|
console.error("pause", ex.toString(), ex);
|
||||||
|
this.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.changeState(PAUSED);
|
|
||||||
|
this.changeState(retry ? RETRYING : PAUSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
@ -383,6 +267,8 @@ export class Download extends BaseDownload {
|
|||||||
this.manId = 0;
|
this.manId = 0;
|
||||||
this.written = this.totalSize = 0;
|
this.written = this.totalSize = 0;
|
||||||
this.mime = this.serverName = this.browserName = "";
|
this.mime = this.serverName = this.browserName = "";
|
||||||
|
this.retries = 0;
|
||||||
|
this.deadline = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromBrowser() {
|
async removeFromBrowser() {
|
||||||
@ -391,7 +277,7 @@ export class Download extends BaseDownload {
|
|||||||
await downloads.cancel(id);
|
await downloads.cancel(id);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
// ingored
|
// ignored
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
try {
|
try {
|
||||||
@ -399,7 +285,7 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(id, ex.toString(), ex);
|
console.error(id, ex.toString(), ex);
|
||||||
// ingored
|
// ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,6 +301,17 @@ export class Download extends BaseDownload {
|
|||||||
this.changeState(CANCELED);
|
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() {
|
setMissing() {
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
@ -467,9 +364,11 @@ export class Download extends BaseDownload {
|
|||||||
this.markDirty();
|
this.markDirty();
|
||||||
switch (state.state) {
|
switch (state.state) {
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
if (error) {
|
if (state.paused) {
|
||||||
this.cancel();
|
this.changeState(PAUSED);
|
||||||
this.error = error;
|
}
|
||||||
|
else if (error) {
|
||||||
|
this.cancelAccordingToError(error);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
@ -480,6 +379,9 @@ export class Download extends BaseDownload {
|
|||||||
if (state.paused) {
|
if (state.paused) {
|
||||||
this.changeState(PAUSED);
|
this.changeState(PAUSED);
|
||||||
}
|
}
|
||||||
|
else if (error) {
|
||||||
|
this.cancelAccordingToError(error);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
this.error = error || "";
|
this.error = error || "";
|
||||||
@ -496,4 +398,27 @@ export class Download extends BaseDownload {
|
|||||||
this.setMissing();
|
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,11 +4,11 @@
|
|||||||
import { EventEmitter } from "../events";
|
import { EventEmitter } from "../events";
|
||||||
import { Notification } from "../notifications";
|
import { Notification } from "../notifications";
|
||||||
import { DB } from "../db";
|
import { DB } from "../db";
|
||||||
import { QUEUED, CANCELED, RUNNING } from "./state";
|
import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Bus, Port } from "../bus";
|
import { Bus, Port } from "../bus";
|
||||||
import { sort } from "../sorting";
|
import { sort } from "../sorting";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs, PrefWatcher } from "../prefs";
|
||||||
import { _ } from "../i18n";
|
import { _ } from "../i18n";
|
||||||
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
@ -16,8 +16,10 @@ import { Download } from "./download";
|
|||||||
import { ManagerPort } from "./port";
|
import { ManagerPort } from "./port";
|
||||||
import { Scheduler } from "./scheduler";
|
import { Scheduler } from "./scheduler";
|
||||||
import { Limits } from "./limits";
|
import { Limits } from "./limits";
|
||||||
import { downloads, runtime } from "../browser";
|
import { downloads, runtime, webRequest, CHROME, OPERA } from "../browser";
|
||||||
|
import { browser } from "webextension-polyfill-ts";
|
||||||
|
|
||||||
|
const US = runtime.getURL("");
|
||||||
|
|
||||||
const AUTOSAVE_TIMEOUT = 2000;
|
const AUTOSAVE_TIMEOUT = 2000;
|
||||||
const DIRTY_TIMEOUT = 100;
|
const DIRTY_TIMEOUT = 100;
|
||||||
@ -29,6 +31,9 @@ const setShelfEnabled = downloads.setShelfEnabled || function() {
|
|||||||
// ignored
|
// ignored
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
|
||||||
|
const SOUNDS = new PrefWatcher("sounds", false);
|
||||||
|
|
||||||
export class Manager extends EventEmitter {
|
export class Manager extends EventEmitter {
|
||||||
private items: Download[];
|
private items: Download[];
|
||||||
|
|
||||||
@ -48,11 +53,18 @@ export class Manager extends EventEmitter {
|
|||||||
|
|
||||||
private readonly running: Set<Download>;
|
private readonly running: Set<Download>;
|
||||||
|
|
||||||
|
private readonly retrying: Set<Download>;
|
||||||
|
|
||||||
private scheduler: Scheduler | null;
|
private scheduler: Scheduler | null;
|
||||||
|
|
||||||
private shouldReload: boolean;
|
private shouldReload: boolean;
|
||||||
|
|
||||||
|
private deadlineTimer: number;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
if (!document.location.href.includes("background")) {
|
||||||
|
throw new Error("Not on background");
|
||||||
|
}
|
||||||
super();
|
super();
|
||||||
this.active = true;
|
this.active = true;
|
||||||
this.shouldReload = false;
|
this.shouldReload = false;
|
||||||
@ -62,27 +74,42 @@ export class Manager extends EventEmitter {
|
|||||||
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
||||||
this.dirty = new CoalescedUpdate(
|
this.dirty = new CoalescedUpdate(
|
||||||
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
||||||
|
this.processDeadlines = this.processDeadlines.bind(this);
|
||||||
this.sids = new Map();
|
this.sids = new Map();
|
||||||
this.manIds = new Map();
|
this.manIds = new Map();
|
||||||
this.ports = new Set();
|
this.ports = new Set();
|
||||||
this.scheduler = null;
|
this.scheduler = null;
|
||||||
this.running = new Set();
|
this.running = new Set();
|
||||||
|
this.retrying = new Set();
|
||||||
|
|
||||||
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
||||||
|
|
||||||
downloads.onChanged.addListener(this.onChanged.bind(this));
|
downloads.onChanged.addListener(this.onChanged.bind(this));
|
||||||
downloads.onErased.addListener(this.onErased.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) => {
|
Bus.onPort("manager", (port: Port) => {
|
||||||
const mport = new ManagerPort(this, port);
|
const managerPort = new ManagerPort(this, port);
|
||||||
port.on("disconnect", () => {
|
port.on("disconnect", () => {
|
||||||
this.ports.delete(mport);
|
this.ports.delete(managerPort);
|
||||||
});
|
});
|
||||||
this.ports.add(mport);
|
this.ports.add(managerPort);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
Limits.on("changed", () => {
|
Limits.on("changed", () => {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (CHROME) {
|
||||||
|
webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
this.stuffReferrer.bind(this),
|
||||||
|
{urls: ["<all_urls>"]},
|
||||||
|
["blocking", "requestHeaders", "extraHeaders"]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -100,7 +127,7 @@ export class Manager extends EventEmitter {
|
|||||||
// Do not wait for the scheduler
|
// Do not wait for the scheduler
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
|
|
||||||
this.emit("inited");
|
this.emit("initialized");
|
||||||
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
||||||
runtime.onUpdateAvailable.addListener(() => {
|
runtime.onUpdateAvailable.addListener(() => {
|
||||||
if (this.running.size) {
|
if (this.running.size) {
|
||||||
@ -139,6 +166,20 @@ export class Manager extends EventEmitter {
|
|||||||
this.manIds.delete(downloadId);
|
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() {
|
async resetScheduler() {
|
||||||
this.scheduler = null;
|
this.scheduler = null;
|
||||||
await this.startNext();
|
await this.startNext();
|
||||||
@ -179,14 +220,11 @@ export class Manager extends EventEmitter {
|
|||||||
this.notifiedFinished = false;
|
this.notifiedFinished = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async maybeRunFinishActions() {
|
maybeRunFinishActions() {
|
||||||
if (this.running.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.maybeNotifyFinished();
|
|
||||||
if (this.running.size) {
|
if (this.running.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.maybeNotifyFinished();
|
||||||
if (this.shouldReload) {
|
if (this.shouldReload) {
|
||||||
this.saveQueue.trigger();
|
this.saveQueue.trigger();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -199,15 +237,21 @@ export class Manager extends EventEmitter {
|
|||||||
setShelfEnabled(true);
|
setShelfEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async maybeNotifyFinished() {
|
maybeNotifyFinished() {
|
||||||
if (!(await Prefs.get("finish-notification"))) {
|
if (this.notifiedFinished || this.running.size || this.retrying.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.notifiedFinished || this.running.size) {
|
if (SOUNDS.value && !OPERA) {
|
||||||
return;
|
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;
|
this.notifiedFinished = true;
|
||||||
new Notification(null, _("queue-finished"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addManId(id: number, download: Download) {
|
addManId(id: number, download: Download) {
|
||||||
@ -218,6 +262,61 @@ export class Manager extends EventEmitter {
|
|||||||
this.manIds.delete(id);
|
this.manIds.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async prepareItems(items: any[]) {
|
||||||
|
var links = new Array();
|
||||||
|
for (var item of items) {
|
||||||
|
var cookiesToSend = Array();
|
||||||
|
var cs = await browser.cookies.getAll({
|
||||||
|
url: item.url,
|
||||||
|
firstPartyDomain: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item.cookies) {
|
||||||
|
for (var c of cs){
|
||||||
|
cookiesToSend.push({
|
||||||
|
name: c.name,
|
||||||
|
value: c.value,
|
||||||
|
domain: c.domain,
|
||||||
|
// expires: new Date(c.expirationDate * 1000) ?? null,
|
||||||
|
path: c.path,
|
||||||
|
secure: c.secure,
|
||||||
|
httponly: c.httpOnly,
|
||||||
|
// samesite: c.sameSite,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = item.paused ? "Paused":"Queue";
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
description: item.description,
|
||||||
|
filename: item.fileName,
|
||||||
|
mask: item.mask,
|
||||||
|
status: status,
|
||||||
|
postData: item.postData,
|
||||||
|
subdir: item.subfolder,
|
||||||
|
title: item.title,
|
||||||
|
url: item.usable,
|
||||||
|
referrer: item.usableReferrer,
|
||||||
|
cookies: cookiesToSend,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewDownloads(items: any[]) {
|
||||||
|
if (!items || !items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.prepareItems(items).then(links => {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", items[0].server);
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/javascript");
|
||||||
|
xhr.send(JSON.stringify(links));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
addNewDownloads(items: any[]) {
|
addNewDownloads(items: any[]) {
|
||||||
if (!items || !items.length) {
|
if (!items || !items.length) {
|
||||||
return;
|
return;
|
||||||
@ -240,6 +339,7 @@ export class Manager extends EventEmitter {
|
|||||||
this.save(items);
|
this.save(items);
|
||||||
this.startNext();
|
this.startNext();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
setDirty(item: Download) {
|
setDirty(item: Download) {
|
||||||
this.dirty.add(item);
|
this.dirty.add(item);
|
||||||
@ -306,35 +406,86 @@ export class Manager extends EventEmitter {
|
|||||||
if (oldState === RUNNING) {
|
if (oldState === RUNNING) {
|
||||||
this.running.delete(download);
|
this.running.delete(download);
|
||||||
}
|
}
|
||||||
|
else if (oldState === RETRYING) {
|
||||||
|
this.retrying.delete(download);
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
if (newState === QUEUED) {
|
if (newState === QUEUED) {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
this.startNext().catch(console.error);
|
this.startNext().catch(console.error);
|
||||||
}
|
}
|
||||||
else if (newState === RUNNING) {
|
else if (newState === RUNNING) {
|
||||||
// Usually we already added it. Buit if a user uses the built-in
|
// Usually we already added it. But if a user uses the built-in
|
||||||
// download manager to resart
|
// download manager to restart
|
||||||
// a download, we have not, so make sure it is added either way
|
// a download, we have not, so make sure it is added either way
|
||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (newState === RETRYING) {
|
||||||
|
this.addRetry(download);
|
||||||
|
}
|
||||||
this.startNext().catch(console.error);
|
this.startNext().catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRetry(download: Download) {
|
||||||
|
this.retrying.add(download);
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private findDeadline() {
|
||||||
|
let deadline = Array.from(this.retrying).
|
||||||
|
reduce<number>((deadline, item) => {
|
||||||
|
if (deadline) {
|
||||||
|
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
|
||||||
|
}
|
||||||
|
return item.deadline;
|
||||||
|
}, 0);
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deadline -= Date.now();
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.deadlineTimer) {
|
||||||
|
window.clearTimeout(this.deadlineTimer);
|
||||||
|
}
|
||||||
|
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processDeadlines() {
|
||||||
|
this.deadlineTimer = 0;
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.items.forEach(item => {
|
||||||
|
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
|
||||||
|
this.retrying.delete(item);
|
||||||
|
item.resume(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sorted(sids: number[]) {
|
sorted(sids: number[]) {
|
||||||
try {
|
try {
|
||||||
// Construct new items
|
// Construct new items
|
||||||
const csids = new Map(this.sids);
|
const currentSids = new Map(this.sids);
|
||||||
let items = mapFilterInSitu(sids, sid => {
|
let items = mapFilterInSitu(sids, sid => {
|
||||||
const item = csids.get(sid);
|
const item = currentSids.get(sid);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
csids.delete(sid);
|
currentSids.delete(sid);
|
||||||
return item;
|
return item;
|
||||||
}, e => !!e);
|
}, e => !!e);
|
||||||
if (csids.size) {
|
if (currentSids.size) {
|
||||||
items = items.concat(sort(Array.from(csids.values()), i => i.position));
|
items = items.concat(
|
||||||
|
sort(Array.from(currentSids.values()), i => i.position));
|
||||||
}
|
}
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.setPositions();
|
this.setPositions();
|
||||||
@ -384,6 +535,31 @@ export class Manager extends EventEmitter {
|
|||||||
getMsgItems() {
|
getMsgItems() {
|
||||||
return this.items.map(e => e.toMsg());
|
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>;
|
let inited: Promise<Manager>;
|
||||||
|
@ -9,6 +9,8 @@ import { BaseDownload } from "./basedownload";
|
|||||||
import { Manager } from "./man";
|
import { Manager } from "./man";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Port } from "../bus";
|
import { Port } from "../bus";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "../item";
|
||||||
|
|
||||||
type SID = {sid: number};
|
type SID = {sid: number};
|
||||||
type SIDS = {
|
type SIDS = {
|
||||||
@ -42,6 +44,9 @@ export class ManagerPort {
|
|||||||
port.on("prefs", () => {
|
port.on("prefs", () => {
|
||||||
openPrefs();
|
openPrefs();
|
||||||
});
|
});
|
||||||
|
port.on("import", ({items}: {items: BaseItem[]}) => {
|
||||||
|
API.regular(items, []);
|
||||||
|
});
|
||||||
port.on("all", () => this.sendAll());
|
port.on("all", () => this.sendAll());
|
||||||
port.on("removeSids", this.onMsgRemoveSids);
|
port.on("removeSids", this.onMsgRemoveSids);
|
||||||
port.on("showSingle", async () => {
|
port.on("showSingle", async () => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -193,24 +193,24 @@ export default class Renamer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
const {mask} = this.d;
|
const {mask, subfolder} = this.d;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self: any = this;
|
const self: any = this;
|
||||||
// XXX flat
|
const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
|
||||||
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) {
|
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
|
||||||
let prop = type.substr(1, type.length - 2);
|
let prop = type.substr(1, type.length - 2);
|
||||||
const flat = prop.startsWith("flat");
|
const flat = prop.startsWith("flat");
|
||||||
if (flat) {
|
if (flat) {
|
||||||
prop = prop.substr(4);
|
prop = prop.substr(4);
|
||||||
}
|
}
|
||||||
prop = `p_${prop}`;
|
prop = `p_${prop}`;
|
||||||
const rv = (prop in self) ?
|
let rv = (prop in self) ?
|
||||||
(self[prop] || "").trim() :
|
(self[prop] || "").trim() :
|
||||||
type;
|
type;
|
||||||
if (flat) {
|
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 DONE = 1 << 4;
|
||||||
export const CANCELED = 1 << 5;
|
export const CANCELED = 1 << 5;
|
||||||
export const MISSING = 1 << 6;
|
export const MISSING = 1 << 6;
|
||||||
|
export const RETRYING = 1 << 7;
|
||||||
|
|
||||||
export const RESUMABLE = PAUSED | CANCELED;
|
export const RESUMABLE = PAUSED | CANCELED | RETRYING;
|
||||||
export const FORCABLE = PAUSED | QUEUED | CANCELED;
|
export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
|
||||||
export const PAUSABLE = QUEUED | CANCELED | RUNNING;
|
export const PAUSEABLE = QUEUED | CANCELED | RUNNING | RETRYING;
|
||||||
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING;
|
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING | RETRYING;
|
||||||
|
12
lib/mime.ts
@ -25,9 +25,11 @@ export class MimeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MimeDB = new class {
|
export const MimeDB = new class MimeDB {
|
||||||
private readonly mimeToExts: Map<string, MimeInfo>;
|
private readonly mimeToExts: Map<string, MimeInfo>;
|
||||||
|
|
||||||
|
private readonly registeredExtensions: Set<string>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const exts = new Map<string, string[]>();
|
const exts = new Map<string, string[]>();
|
||||||
for (const [prim, more] of Object.entries(mime.e)) {
|
for (const [prim, more] of Object.entries(mime.e)) {
|
||||||
@ -42,6 +44,10 @@ export const MimeDB = new class {
|
|||||||
Object.entries(mime.m),
|
Object.entries(mime.m),
|
||||||
([mime, prim]) => [mime, new MimeInfo(mime, exts.get(prim) || [prim])]
|
([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) {
|
getPrimary(mime: string) {
|
||||||
@ -52,4 +58,8 @@ export const MimeDB = new class {
|
|||||||
getMime(mime: string) {
|
getMime(mime: string) {
|
||||||
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
|
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasExtension(ext: string) {
|
||||||
|
return this.registeredExtensions.has(ext.toLowerCase());
|
||||||
|
}
|
||||||
}();
|
}();
|
||||||
|
@ -8,7 +8,7 @@ import {EventEmitter} from "./events";
|
|||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
type: "basic",
|
type: "basic",
|
||||||
iconUrl: extension.getURL("/style/icon64.png"),
|
iconUrl: extension.getURL("/style/icon64.png"),
|
||||||
title: "DownThemAll!",
|
title: "TraitorousDownloading!",
|
||||||
message: "message",
|
message: "message",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export class Notification extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
this.generated = !id;
|
this.generated = !id;
|
||||||
id = id || `DownThemAll-notification${++gid}`;
|
id = id || `TraitorousDownloading-notification${++gid}`;
|
||||||
if (typeof options === "string") {
|
if (typeof options === "string") {
|
||||||
options = {message: options};
|
options = {message: options};
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,5 @@ export class PrefWatcher {
|
|||||||
|
|
||||||
changed(prefs: any, key: string, value: any) {
|
changed(prefs: any, key: string, value: any) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,3 +116,14 @@ export const FASTFILTER = new RecentList("fastfilter", [
|
|||||||
"*.z??, *.css, *.html"
|
"*.z??, *.css, *.html"
|
||||||
]);
|
]);
|
||||||
FASTFILTER.init().catch(console.error);
|
FASTFILTER.init().catch(console.error);
|
||||||
|
|
||||||
|
export const SUBFOLDER = new RecentList("subfolder", [
|
||||||
|
"",
|
||||||
|
"downthemall",
|
||||||
|
]);
|
||||||
|
SUBFOLDER.init().catch(console.error);
|
||||||
|
|
||||||
|
export const SERVER = new RecentList("server", [
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
SERVER.init().catch(console.error);
|
||||||
|
@ -9,11 +9,12 @@ import { donate, openPrefs, openUrls } from "./windowutils";
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { filters, FAST, Filter } from "./filters";
|
import { filters, FAST, Filter } from "./filters";
|
||||||
import { WindowStateTracker } from "./windowstatetracker";
|
import { WindowStateTracker } from "./windowstatetracker";
|
||||||
import { windows } from "./browser";
|
import { windows, CHROME } from "./browser";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseItem } from "./item";
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
interface BaseMatchedItem extends BaseItem {
|
interface BaseMatchedItem extends BaseItem {
|
||||||
|
sidx?: number;
|
||||||
matched?: string | null;
|
matched?: string | null;
|
||||||
prevMatched?: string | null;
|
prevMatched?: string | null;
|
||||||
}
|
}
|
||||||
@ -28,7 +29,8 @@ function computeSelection(
|
|||||||
items: BaseMatchedItem[],
|
items: BaseMatchedItem[],
|
||||||
onlyFast: boolean): ItemDelta[] {
|
onlyFast: boolean): ItemDelta[] {
|
||||||
let ws = items.map((item, idx: number) => {
|
let ws = items.map((item, idx: number) => {
|
||||||
item.idx = idx;
|
item.idx = item.idx || idx;
|
||||||
|
item.sidx = item.sidx || idx;
|
||||||
const {matched = null} = item;
|
const {matched = null} = item;
|
||||||
item.prevMatched = matched;
|
item.prevMatched = matched;
|
||||||
item.matched = null;
|
item.matched = null;
|
||||||
@ -52,7 +54,7 @@ function computeSelection(
|
|||||||
}
|
}
|
||||||
return items.filter(item => item.prevMatched !== item.matched).map(item => {
|
return items.filter(item => item.prevMatched !== item.matched).map(item => {
|
||||||
return {
|
return {
|
||||||
idx: item.idx,
|
idx: item.sidx as number,
|
||||||
matched: item.matched
|
matched: item.matched
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -98,9 +100,16 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id);
|
||||||
try {
|
try {
|
||||||
|
if (!CHROME) {
|
||||||
|
windows.update(window.id, tracker.getOptions({}));
|
||||||
|
}
|
||||||
const port = await Promise.race<Port>([
|
const port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("select", port => {
|
||||||
|
resolve(port);
|
||||||
|
return true;
|
||||||
|
})),
|
||||||
timeout<Port>(5 * 1000)]);
|
timeout<Port>(5 * 1000)]);
|
||||||
if (!port.isSelf) {
|
if (!port.isSelf) {
|
||||||
throw Error("Invalid sender connected");
|
throw Error("Invalid sender connected");
|
||||||
@ -186,8 +195,8 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
|||||||
openPrefs();
|
openPrefs();
|
||||||
});
|
});
|
||||||
|
|
||||||
port.on("openUrls", ({urls}) => {
|
port.on("openUrls", ({urls, incognito}) => {
|
||||||
openUrls(urls);
|
openUrls(urls, incognito);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -6,7 +6,7 @@ import { Bus, Port } from "./bus";
|
|||||||
import { WindowStateTracker } from "./windowstatetracker";
|
import { WindowStateTracker } from "./windowstatetracker";
|
||||||
import { Promised, timeout } from "./util";
|
import { Promised, timeout } from "./util";
|
||||||
import { donate } from "./windowutils";
|
import { donate } from "./windowutils";
|
||||||
import { windows } from "./browser";
|
import { windows, CHROME } from "./browser";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseItem } from "./item";
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
@ -21,9 +21,16 @@ export async function single(item: BaseItem | null) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id);
|
||||||
try {
|
try {
|
||||||
|
if (!CHROME) {
|
||||||
|
windows.update(window.id, tracker.getOptions({}));
|
||||||
|
}
|
||||||
const port: Port = await Promise.race<Port>([
|
const port: Port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("single", port => {
|
||||||
|
resolve(port);
|
||||||
|
return true;
|
||||||
|
})),
|
||||||
timeout<Port>(5 * 1000)]);
|
timeout<Port>(5 * 1000)]);
|
||||||
if (!port.isSelf) {
|
if (!port.isSelf) {
|
||||||
throw Error("Invalid sender connected");
|
throw Error("Invalid sender connected");
|
||||||
|
23
lib/util.ts
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as psl from "psl";
|
import * as psl from "psl";
|
||||||
import { identity, memoize } from "./memoize";
|
import { identity, memoize } from "./memoize";
|
||||||
|
import { IPReg } from "./ipreg";
|
||||||
export { debounce } from "../uikit/lib/util";
|
export { debounce } from "../uikit/lib/util";
|
||||||
|
|
||||||
export class Promised {
|
export class Promised {
|
||||||
@ -237,7 +238,10 @@ export interface URLd extends URL {
|
|||||||
Object.defineProperty(URL.prototype, "domain", {
|
Object.defineProperty(URL.prototype, "domain", {
|
||||||
get() {
|
get() {
|
||||||
try {
|
try {
|
||||||
return hostToDomain(this.host) || this.host;
|
const {hostname} = this;
|
||||||
|
return IPReg.test(hostname) ?
|
||||||
|
hostname :
|
||||||
|
hostToDomain(hostname) || hostname;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
@ -357,3 +361,20 @@ export function mapFilterInSitu<TRes, T>(
|
|||||||
export function randint(min: number, max: number) {
|
export function randint(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
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 { Prefs } from "./prefs";
|
||||||
import { windows } from "./browser";
|
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"]));
|
const VALID_WINDOW_STATES = Object.freeze(new Set(["normal", "maximized"]));
|
||||||
@ -55,14 +57,16 @@ export class WindowStateTracker {
|
|||||||
|
|
||||||
getOptions(options: any) {
|
getOptions(options: any) {
|
||||||
const result = Object.assign(options, {
|
const result = Object.assign(options, {
|
||||||
width: this.width,
|
|
||||||
height: this.height,
|
|
||||||
state: this.state,
|
state: this.state,
|
||||||
});
|
});
|
||||||
|
if (result.state !== "maximized") {
|
||||||
|
result.width = this.width;
|
||||||
|
result.height = this.height;
|
||||||
if (this.top >= 0) {
|
if (this.top >= 0) {
|
||||||
result.top = this.top;
|
result.top = this.top;
|
||||||
result.left = this.left;
|
result.left = this.left;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +82,7 @@ export class WindowStateTracker {
|
|||||||
if (!this.windowId) {
|
if (!this.windowId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const window = await windows.get(this.windowId);
|
const window = await windows.get(this.windowId);
|
||||||
if (!VALID_WINDOW_STATES.has(window.state)) {
|
if (!VALID_WINDOW_STATES.has(window.state)) {
|
||||||
return;
|
return;
|
||||||
@ -95,17 +100,30 @@ export class WindowStateTracker {
|
|||||||
}
|
}
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
track(windowId: number, port: any) {
|
track(windowId: number, port?: Port) {
|
||||||
if (port) {
|
if (port) {
|
||||||
port.on("resized", this.update);
|
port.on("resized", this.update);
|
||||||
|
port.on("unload", e => this.finalize(e));
|
||||||
|
port.on("disconnect", this.finalize.bind(this));
|
||||||
}
|
}
|
||||||
this.windowId = windowId;
|
this.windowId = windowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async finalize() {
|
async finalize(state?: any) {
|
||||||
|
if (state) {
|
||||||
|
this.left = state.left;
|
||||||
|
this.top = state.top;
|
||||||
|
}
|
||||||
await this.update();
|
await this.update();
|
||||||
this.windowId = 0;
|
this.windowId = 0;
|
||||||
|
if (state) {
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
|
@ -1,44 +1,61 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { windows, tabs, runtime } from "../lib/browser";
|
import { windows, tabs, runtime, CHROME } from "../lib/browser";
|
||||||
import {getManager} from "./manager/man";
|
import { getManager } from "./manager/man";
|
||||||
import DEFAULT_ICONS from "../data/icons.json";
|
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_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 MANAGER_URL = "/windows/manager.html";
|
||||||
|
|
||||||
export async function mostRecentBrowser(): Promise<any> {
|
export async function mostRecentBrowser(incognito: boolean): Promise<any> {
|
||||||
let window;
|
let window;
|
||||||
try {
|
try {
|
||||||
window = await windows.getCurrent({windowTypes: ["normal"]});
|
window = await windows.getCurrent();
|
||||||
if (window.type !== "normal") {
|
if (window.type !== "normal") {
|
||||||
throw new Error("not a normal window");
|
throw new Error("not a normal window");
|
||||||
}
|
}
|
||||||
|
if (incognito && !window.incognito) {
|
||||||
|
throw new Error("Not incognito");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
try {
|
try {
|
||||||
window = await windows.getlastFocused({windowTypes: ["normal"]});
|
window = await windows.getLastFocused();
|
||||||
if (window.type !== "normal") {
|
if (window.type !== "normal") {
|
||||||
throw new Error("not a normal window");
|
throw new Error("not a normal window");
|
||||||
}
|
}
|
||||||
|
if (incognito && !window.incognito) {
|
||||||
|
throw new Error("Not incognito");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||||
filter((w: any) => w.type === "normal").pop();
|
filter(
|
||||||
|
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
|
||||||
|
pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!window) {
|
if (!window) {
|
||||||
window = await windows.create({
|
window = await windows.create({
|
||||||
url: DONATE_URL,
|
incognito: !!incognito,
|
||||||
type: "normal",
|
type: "normal",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openInTab(url: string) {
|
export async function openInTab(url: string, incognito: boolean) {
|
||||||
const window = await mostRecentBrowser();
|
const window = await mostRecentBrowser(incognito);
|
||||||
await tabs.create({
|
await tabs.create({
|
||||||
active: true,
|
active: true,
|
||||||
url,
|
url,
|
||||||
@ -47,7 +64,7 @@ export async function openInTab(url: string) {
|
|||||||
await windows.update(window.id, {focused: true});
|
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({
|
const etabs = await tabs.query({
|
||||||
url
|
url
|
||||||
});
|
});
|
||||||
@ -57,21 +74,22 @@ export async function openInTabOrFocus(url: string) {
|
|||||||
await windows.update(tab.windowId, {focused: true});
|
await windows.update(tab.windowId, {focused: true});
|
||||||
return;
|
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({
|
const etabs = await tabs.query({
|
||||||
url
|
url
|
||||||
});
|
});
|
||||||
if (etabs.length) {
|
if (etabs.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await openInTab(url);
|
await openInTab(url, incognito);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function donate() {
|
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() {
|
export async function openPrefs() {
|
||||||
@ -85,16 +103,64 @@ export async function openManager(focus = true) {
|
|||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex.toString(), 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) {
|
if (focus) {
|
||||||
await openInTabOrFocus(await runtime.getURL(MANAGER_URL));
|
await openInTabOrFocus(runtime.getURL(MANAGER_URL), false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await maybeOpenInTab(await runtime.getURL(MANAGER_URL));
|
await maybeOpenInTab(runtime.getURL(MANAGER_URL), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openUrls(urls: string) {
|
export async function openUrls(urls: string, incognito: boolean) {
|
||||||
const window = await mostRecentBrowser();
|
const window = await mostRecentBrowser(incognito);
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
try {
|
try {
|
||||||
await tabs.create({
|
await tabs.create({
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "DownThemAll!",
|
"name": "TraitorousDownloading!",
|
||||||
"version": "4.0.9",
|
"version": "4.2.6",
|
||||||
|
|
||||||
"description": "__MSG_extensionDescription__",
|
"description": "__MSG_extensionDescription__",
|
||||||
"homepage_url": "https://downthemall.org/",
|
"homepage_url": "https://github.com/lordwelch/downthemall",
|
||||||
"author": "Nils Maier",
|
"author": "lordwelch",
|
||||||
|
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
|
|
||||||
@ -16,7 +16,6 @@
|
|||||||
"32": "style/icon32.png",
|
"32": "style/icon32.png",
|
||||||
"48": "style/icon48.png",
|
"48": "style/icon48.png",
|
||||||
"64": "style/icon64.png",
|
"64": "style/icon64.png",
|
||||||
"96": "style/icon96.png",
|
|
||||||
"128": "style/icon128.png",
|
"128": "style/icon128.png",
|
||||||
"256": "style/icon256.png"
|
"256": "style/icon256.png"
|
||||||
},
|
},
|
||||||
@ -24,15 +23,20 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"menus",
|
"cookies",
|
||||||
"downloads",
|
"downloads",
|
||||||
"downloads.open",
|
"downloads.open",
|
||||||
"downloads.shelf",
|
"downloads.shelf",
|
||||||
|
"history",
|
||||||
|
"menus",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"sessions",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
|
"theme",
|
||||||
"webNavigation",
|
"webNavigation",
|
||||||
"webRequest"
|
"webRequest",
|
||||||
|
"webRequestBlocking"
|
||||||
],
|
],
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
@ -50,11 +54,10 @@
|
|||||||
"32": "style/icon32.png",
|
"32": "style/icon32.png",
|
||||||
"48": "style/icon48.png",
|
"48": "style/icon48.png",
|
||||||
"64": "style/icon64.png",
|
"64": "style/icon64.png",
|
||||||
"96": "style/icon96.png",
|
|
||||||
"128": "style/icon128.png",
|
"128": "style/icon128.png",
|
||||||
"256": "style/icon256.png"
|
"256": "style/icon256.png"
|
||||||
},
|
},
|
||||||
"default_title": "DownThemAll!"
|
"default_title": "TraitorousDownloading!"
|
||||||
},
|
},
|
||||||
|
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
@ -64,7 +67,7 @@
|
|||||||
|
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "dtalite@downthemall.org",
|
"id": "downloading@traitorousenterprises.net",
|
||||||
"strict_min_version": "67.0"
|
"strict_min_version": "67.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
package.json
@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "dtalite",
|
"name": "tdl",
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"description": "DownThemAll! lite",
|
"description": "TraitorousDownloading!",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
"lib": "lib"
|
"lib": "lib"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "util/build.py",
|
"build": "util/build.py",
|
||||||
|
"build:cleanup": "rm -rf bundles",
|
||||||
"build:bundles": "webpack",
|
"build:bundles": "webpack",
|
||||||
"build:regexps": "node util/makexregexps.js > data/xregexps.json",
|
"build:regexps": "node util/makexregexps.js > data/xregexps.json",
|
||||||
"stats": "cloc --vcs=git --exclude-lang=Markdown,SVG",
|
"stats": "cloc --vcs=git --exclude-lang=Markdown,SVG",
|
||||||
@ -18,24 +19,26 @@
|
|||||||
"author": "Nils Maier",
|
"author": "Nils Maier",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^12.7.2",
|
"@types/node": "^12.7.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
"@typescript-eslint/eslint-plugin": "^2.3.2",
|
||||||
"@typescript-eslint/parser": "^2.0.0",
|
"@typescript-eslint/parser": "^2.3.2",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"eslint": "^6.2.2",
|
"eslint": "^6.5.1",
|
||||||
"mocha": "^6.2.0",
|
"mocha": "^6.2.1",
|
||||||
"ts-loader": "^6.0.4",
|
"ts-loader": "^6.2.0",
|
||||||
"ts-node": "^8.3.0",
|
"ts-node": "^8.4.1",
|
||||||
"typescript": "^3.5.3",
|
"typescript": "^3.6.3",
|
||||||
"webpack": "^4.39.3",
|
"webpack": "^4.41.0",
|
||||||
"webpack-cli": "^3.3.7",
|
"webpack-cli": "^3.3.9",
|
||||||
"xregexp": "^4.2.4"
|
"xregexp": "^4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/psl": "^1.1.0",
|
"@types/psl": "^1.1.0",
|
||||||
"@types/whatwg-mimetype": "^2.1.0",
|
"@types/whatwg-mimetype": "^2.1.0",
|
||||||
"psl": "^1.3.0",
|
"localforage": "^1.9.0",
|
||||||
"webextension-polyfill": "^0.4.0",
|
"psl": "^1.4.0",
|
||||||
|
"webextension-polyfill": "^0.5.0",
|
||||||
|
"webextension-polyfill-ts": "^0.22.0",
|
||||||
"whatwg-mimetype": "^2.3.0"
|
"whatwg-mimetype": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@ function urlToUsable(e: any, u: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Gatherer {
|
class Gatherer {
|
||||||
|
private: boolean;
|
||||||
|
|
||||||
textLinks: boolean;
|
textLinks: boolean;
|
||||||
|
|
||||||
selectionOnly: boolean;
|
selectionOnly: boolean;
|
||||||
@ -88,6 +90,7 @@ class Gatherer {
|
|||||||
transferable: string[];
|
transferable: string[];
|
||||||
|
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
|
this.private = !!options.private;
|
||||||
this.textLinks = options.textLinks;
|
this.textLinks = options.textLinks;
|
||||||
this.selectionOnly = options.selectionOnly;
|
this.selectionOnly = options.selectionOnly;
|
||||||
this.selection = options.selectionOnly ? getSelection() : null;
|
this.selection = options.selectionOnly ? getSelection() : null;
|
||||||
@ -118,14 +121,26 @@ class Gatherer {
|
|||||||
|
|
||||||
*collectImageInternal(img: HTMLImageElement) {
|
*collectImageInternal(img: HTMLImageElement) {
|
||||||
try {
|
try {
|
||||||
const src = img.currentSrc || img.src;
|
{
|
||||||
|
const {src} = img;
|
||||||
const item = this.makeItem(src, img);
|
const item = this.makeItem(src, img);
|
||||||
if (item) {
|
if (item) {
|
||||||
item.fileName = "";
|
item.fileName = "";
|
||||||
item.description = item.title;
|
item.description = item.title;
|
||||||
yield item;
|
yield item;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const {currentSrc} = img;
|
||||||
|
const item = this.makeItem(currentSrc, img);
|
||||||
|
if (item) {
|
||||||
|
item.fileName = "";
|
||||||
|
item.description = item.title;
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
const {srcset} = img;
|
const {srcset} = img;
|
||||||
if (!srcset) {
|
if (!srcset) {
|
||||||
return;
|
return;
|
||||||
@ -143,6 +158,7 @@ class Gatherer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error("oops image", ex.toString(), ex.stack, ex);
|
console.error("oops image", ex.toString(), ex.stack, ex);
|
||||||
}
|
}
|
||||||
@ -255,6 +271,7 @@ class Gatherer {
|
|||||||
return {
|
return {
|
||||||
url: url.href,
|
url: url.href,
|
||||||
title,
|
title,
|
||||||
|
private: this.private
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
BIN
sounds/done.wav
Normal file
BIN
sounds/error.wav
Normal file
Before Width: | Height: | Size: 763 B After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 711 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
409
style/common.css
@ -2,14 +2,29 @@
|
|||||||
/* License: gpl-v2 */
|
/* License: gpl-v2 */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--general-color: #2a2a2e;
|
||||||
|
--general-bgcolor: rgb(249, 249, 250);
|
||||||
|
--general-border-color: lightgray;
|
||||||
|
--general-input-color: black;
|
||||||
|
--general-input-bgcolor: white;
|
||||||
|
--general-button-color: black;
|
||||||
|
--general-button-bgcolor: rgb(246, 246, 246);
|
||||||
|
--general-button-bgcolor-hover: white;
|
||||||
|
--general-button-shadow: 0px 0px 5px 1px rgba(128, 128, 128, 0.5);
|
||||||
|
--menu-bgcolor: white;
|
||||||
|
--menu-bgcolor-hover: #2283fb;
|
||||||
|
--table-bgcolor: white;
|
||||||
|
--table-head-bgcolor: white;
|
||||||
--toolbar-bg-color: rgb(248, 134, 6);
|
--toolbar-bg-color: rgb(248, 134, 6);
|
||||||
--toolbar-active-border-color: #478de7;
|
--toolbar-active-border-color: #478de7;
|
||||||
--toolbar-hover-border-color: red;
|
--toolbar-hover-border-color: red;
|
||||||
--toolbar-hover-background: rgb(247, 149, 37);
|
--toolbar-hover-background: rgb(247, 149, 37);
|
||||||
--toolbar-border-width: 2px;
|
--toolbar-border-width: 2px;
|
||||||
|
--toolbar-border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
--add-color: navy;
|
--add-color: navy;
|
||||||
--queue-color: gray;
|
--queue-color: gray;
|
||||||
--pause-color: #ffa318;
|
--pause-color: #ffa318;
|
||||||
|
--retry-color: rgb(0, 112, 204);
|
||||||
--error-color: rgb(160, 13, 42);
|
--error-color: rgb(160, 13, 42);
|
||||||
--running-color: #aae061;
|
--running-color: #aae061;
|
||||||
--finishing-color: #57cc12;
|
--finishing-color: #57cc12;
|
||||||
@ -19,20 +34,81 @@
|
|||||||
--maskbutton-color: rgb(236, 185, 16);
|
--maskbutton-color: rgb(236, 185, 16);
|
||||||
--missing-color: rgb(0, 82, 204);
|
--missing-color: rgb(0, 82, 204);
|
||||||
--open-color: rgba(236, 185, 16, 0.8);
|
--open-color: rgba(236, 185, 16, 0.8);
|
||||||
|
--status-icon-color: #363636;
|
||||||
|
--status-icon-color-hover: #6e6d6d;
|
||||||
|
--tile-url: url(tile.png);
|
||||||
|
--file-icon-image-color: rgb(17, 107, 163);
|
||||||
|
--popup-bgcolor: #fff;
|
||||||
|
--popup-color: #0c0c0d;
|
||||||
|
--modal-color: black;
|
||||||
|
--modal-bgcolor: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--add-color: lightblue;
|
||||||
|
--error-color: rgb(130, 3, 22);
|
||||||
|
--running-color: #67a041;
|
||||||
|
--finishing-color: #4bb111;
|
||||||
|
--done-color: #006f00;
|
||||||
|
--pause-color: #cf9308;
|
||||||
|
--general-bgcolor: #2a2a2e;
|
||||||
|
--general-border-color: rgb(85, 85, 85);
|
||||||
|
--general-button-bgcolor-hover: black;
|
||||||
|
--general-button-bgcolor: rgb(36, 36, 36);
|
||||||
|
--general-button-color: white;
|
||||||
|
--general-color: rgb(249, 249, 250);
|
||||||
|
--menu-bgcolor: black;
|
||||||
|
--menu-bgcolor-hover: #1a6bce;
|
||||||
|
--table-bgcolor: #1a1a1e;
|
||||||
|
--table-head-bgcolor: #3a3a3e;
|
||||||
|
--toolbar-bg-color: rgb(202, 108, 0);
|
||||||
|
--status-icon-color: #b9b9b9;
|
||||||
|
--status-icon-color-hover: #e2e2e2;
|
||||||
|
--tile-url: url(tile-dark.png?3);
|
||||||
|
--toolbar-border: 1px solid rgba(30, 30, 30, 0.5);
|
||||||
|
--file-icon-image-color: rgb(21, 130, 197);
|
||||||
|
--popup-bgcolor: #4a4a4f;
|
||||||
|
--popup-color: rgb(249, 249, 250);
|
||||||
|
--general-button-shadow: 0px 0px 7px 1px rgba(128, 128, 128, 0.8);
|
||||||
|
--modal-color: white;
|
||||||
|
--modal-bgcolor: #333;
|
||||||
|
scrollbar-color: rgba(249, 249, 250, 0.4) rgba(20, 20, 25, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark a {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar {
|
||||||
|
background: rgba(20, 20, 25, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(249, 249, 250, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar-corner {
|
||||||
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-platform="mac"] {
|
html[data-platform="mac"] {
|
||||||
--folder-color: rgb(4, 102, 214);
|
--folder-color: rgb(4, 102, 214);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-size: 10pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'downthemall';
|
font-family: "downthemall";
|
||||||
src: url('downthemall.woff2?75791791') format('woff2');
|
src: url("downthemall.woff2?75791791") format("woff2");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
[class^="icon-"]:before,
|
||||||
|
[class*=" icon-"]:before {
|
||||||
font-family: "downthemall";
|
font-family: "downthemall";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@ -49,70 +125,187 @@ html[data-platform="mac"] {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-add:before { content: '\e800'; } /* '' */
|
.icon-add:before {
|
||||||
.icon-addsegment:before { content: '\e801'; } /* '' */
|
content: "\e800";
|
||||||
.icon-bottom:before { content: '\e802'; } /* '' */
|
} /* '' */
|
||||||
.icon-picture:before { content: '\e803'; } /* '' */
|
.icon-addsegment:before {
|
||||||
.icon-circle:before { content: '\e804'; } /* '' */
|
content: "\e801";
|
||||||
.icon-delete:before { content: '\e805'; } /* '' */
|
} /* '' */
|
||||||
.icon-done:before { content: '\e806'; } /* '' */
|
.icon-bottom:before {
|
||||||
.icon-down:before { content: '\e807'; } /* '' */
|
content: "\e802";
|
||||||
.icon-download:before { content: '\e808'; } /* '' */
|
} /* '' */
|
||||||
.icon-dupe:before { content: '\e809'; } /* '' */
|
.icon-picture:before {
|
||||||
.icon-error:before { content: '\e80a'; } /* '' */
|
content: "\e803";
|
||||||
.icon-failed:before { content: '\e80b'; } /* '' */
|
} /* '' */
|
||||||
.icon-file:before { content: '\e80c'; } /* '' */
|
.icon-circle:before {
|
||||||
.icon-find:before { content: '\e80d'; } /* '' */
|
content: "\e804";
|
||||||
.icon-folder:before { content: '\e80e'; } /* '' */
|
} /* '' */
|
||||||
.icon-force:before { content: '\e80f'; } /* '' */
|
.icon-delete:before {
|
||||||
.icon-go:before { content: '\e810'; } /* '' */
|
content: "\e805";
|
||||||
.icon-import:before { content: '\e811'; } /* '' */
|
} /* '' */
|
||||||
.icon-info:before { content: '\e812'; } /* '' */
|
.icon-done:before {
|
||||||
.icon-launch:before { content: '\e813'; } /* '' */
|
content: "\e806";
|
||||||
.icon-missing:before { content: '\e814'; } /* '' */
|
} /* '' */
|
||||||
.icon-network-off:before { content: '\e815'; } /* '' */
|
.icon-down:before {
|
||||||
.icon-network-on:before { content: '\e816'; } /* '' */
|
content: "\e807";
|
||||||
.icon-pause:before { content: '\e817'; } /* '' */
|
} /* '' */
|
||||||
.icon-remsegment:before { content: '\e818'; } /* '' */
|
.icon-download:before {
|
||||||
.icon-rename:before { content: '\e819'; } /* '' */
|
content: "\e808";
|
||||||
.icon-save:before { content: '\e81a'; } /* '' */
|
} /* '' */
|
||||||
.icon-settings:before { content: '\e81b'; } /* '' */
|
.icon-dupe:before {
|
||||||
.icon-top:before { content: '\e81c'; } /* '' */
|
content: "\e809";
|
||||||
.icon-unchecked:before { content: '\e81d'; } /* '' */
|
} /* '' */
|
||||||
.icon-unlimited:before { content: '\e81e'; } /* '' */
|
.icon-error:before {
|
||||||
.icon-link:before { content: '\e81f'; } /* '' */
|
content: "\e80a";
|
||||||
.icon-up:before { content: '\e820'; } /* '' */
|
} /* '' */
|
||||||
.icon-privacy:before { content: '\e821'; } /* '' */
|
.icon-failed:before {
|
||||||
.icon-tags:before { content: '\e822'; } /* '' */
|
content: "\e80b";
|
||||||
.icon-attention:before { content: '\e823'; } /* '' */
|
} /* '' */
|
||||||
.icon-notification:before { content: '\e824'; } /* '' */
|
.icon-file:before {
|
||||||
.icon-file-video:before { content: '\e825'; } /* '' */
|
content: "\e80c";
|
||||||
.icon-file-generic:before { content: '\e826'; } /* '' */
|
} /* '' */
|
||||||
.icon-question-dark:before { content: '\e827'; } /* '' */
|
.icon-find:before {
|
||||||
.icon-filter:before { content: '\f0b0'; } /* '' */
|
content: "\e80d";
|
||||||
.icon-donate:before { content: '\f0d6'; } /* '' */
|
} /* '' */
|
||||||
.icon-file-doc:before { content: '\f0f6'; } /* '' */
|
.icon-folder:before {
|
||||||
.icon-interface:before { content: '\f108'; } /* '' */
|
content: "\e80e";
|
||||||
.icon-folder-1:before { content: '\f115'; } /* '' */
|
} /* '' */
|
||||||
.icon-sort-asc:before { content: '\f15d'; } /* '' */
|
.icon-force:before {
|
||||||
.icon-sort-desc:before { content: '\f15e'; } /* '' */
|
content: "\e80f";
|
||||||
.icon-file-pdf:before { content: '\f1c1'; } /* '' */
|
} /* '' */
|
||||||
.icon-file-word:before { content: '\f1c2'; } /* '' */
|
.icon-go:before {
|
||||||
.icon-file-image:before { content: '\f1c5'; } /* '' */
|
content: "\e810";
|
||||||
.icon-file-archive:before { content: '\f1c6'; } /* '' */
|
} /* '' */
|
||||||
.icon-file-audio:before { content: '\f1c7'; } /* '' */
|
.icon-import:before {
|
||||||
.icon-toggle:before { content: '\f205'; } /* '' */
|
content: "\e811";
|
||||||
.icon-server:before { content: '\f233'; } /* '' */
|
} /* '' */
|
||||||
.icon-question-light:before { content: '\f29c'; } /* '' */
|
.icon-info:before {
|
||||||
|
content: "\e812";
|
||||||
|
} /* '' */
|
||||||
|
.icon-launch:before {
|
||||||
|
content: "\e813";
|
||||||
|
} /* '' */
|
||||||
|
.icon-missing:before {
|
||||||
|
content: "\e814";
|
||||||
|
} /* '' */
|
||||||
|
.icon-network-off:before {
|
||||||
|
content: "\e815";
|
||||||
|
} /* '' */
|
||||||
|
.icon-network-on:before {
|
||||||
|
content: "\e816";
|
||||||
|
} /* '' */
|
||||||
|
.icon-pause:before {
|
||||||
|
content: "\e817";
|
||||||
|
} /* '' */
|
||||||
|
.icon-remsegment:before {
|
||||||
|
content: "\e818";
|
||||||
|
} /* '' */
|
||||||
|
.icon-rename:before {
|
||||||
|
content: "\e819";
|
||||||
|
} /* '' */
|
||||||
|
.icon-save:before {
|
||||||
|
content: "\e81a";
|
||||||
|
} /* '' */
|
||||||
|
.icon-settings:before {
|
||||||
|
content: "\e81b";
|
||||||
|
} /* '' */
|
||||||
|
.icon-top:before {
|
||||||
|
content: "\e81c";
|
||||||
|
} /* '' */
|
||||||
|
.icon-unchecked:before {
|
||||||
|
content: "\e81d";
|
||||||
|
} /* '' */
|
||||||
|
.icon-unlimited:before {
|
||||||
|
content: "\e81e";
|
||||||
|
} /* '' */
|
||||||
|
.icon-link:before {
|
||||||
|
content: "\e81f";
|
||||||
|
} /* '' */
|
||||||
|
.icon-up:before {
|
||||||
|
content: "\e820";
|
||||||
|
} /* '' */
|
||||||
|
.icon-privacy:before {
|
||||||
|
content: "\e821";
|
||||||
|
} /* '' */
|
||||||
|
.icon-tags:before {
|
||||||
|
content: "\e822";
|
||||||
|
} /* '' */
|
||||||
|
.icon-attention:before {
|
||||||
|
content: "\e823";
|
||||||
|
} /* '' */
|
||||||
|
.icon-notification:before {
|
||||||
|
content: "\e824";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-video:before {
|
||||||
|
content: "\e825";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-generic:before {
|
||||||
|
content: "\e826";
|
||||||
|
} /* '' */
|
||||||
|
.icon-question-dark:before {
|
||||||
|
content: "\e827";
|
||||||
|
} /* '' */
|
||||||
|
.icon-forward:before {
|
||||||
|
content: "\e828";
|
||||||
|
} /* '' */
|
||||||
|
.icon-filter:before {
|
||||||
|
content: "\f0b0";
|
||||||
|
} /* '' */
|
||||||
|
.icon-donate:before {
|
||||||
|
content: "\f0d6";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-doc:before {
|
||||||
|
content: "\f0f6";
|
||||||
|
} /* '' */
|
||||||
|
.icon-interface:before {
|
||||||
|
content: "\f108";
|
||||||
|
} /* '' */
|
||||||
|
.icon-folder-1:before {
|
||||||
|
content: "\f115";
|
||||||
|
} /* '' */
|
||||||
|
.icon-sort-asc:before {
|
||||||
|
content: "\f15d";
|
||||||
|
} /* '' */
|
||||||
|
.icon-sort-desc:before {
|
||||||
|
content: "\f15e";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-pdf:before {
|
||||||
|
content: "\f1c1";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-word:before {
|
||||||
|
content: "\f1c2";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-image:before {
|
||||||
|
content: "\f1c5";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-archive:before {
|
||||||
|
content: "\f1c6";
|
||||||
|
} /* '' */
|
||||||
|
.icon-file-audio:before {
|
||||||
|
content: "\f1c7";
|
||||||
|
} /* '' */
|
||||||
|
.icon-toggle-off:before {
|
||||||
|
content: "\f204";
|
||||||
|
} /* '' */
|
||||||
|
.icon-toggle-on:before {
|
||||||
|
content: "\f205";
|
||||||
|
} /* '' */
|
||||||
|
.icon-server:before {
|
||||||
|
content: "\f233";
|
||||||
|
} /* '' */
|
||||||
|
.icon-question-light:before {
|
||||||
|
content: "\f29c";
|
||||||
|
} /* '' */
|
||||||
|
|
||||||
@media (min-resolution: 144dpi) {
|
@media (min-resolution: 144dpi) {
|
||||||
[class^="icon-file-"]:before, [class*=" icon-file-"]:before {
|
[class^="icon-file-"]:before,
|
||||||
|
[class*=" icon-file-"]:before {
|
||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-file-image {
|
.icon-file-image {
|
||||||
color: rgb(17, 107, 163);
|
color: var(--file-icon-image-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-file-pdf,
|
.icon-file-pdf,
|
||||||
@ -133,18 +326,29 @@ html[data-platform="mac"] {
|
|||||||
color: rgb(202, 81, 198);
|
color: rgb(202, 81, 198);
|
||||||
}
|
}
|
||||||
|
|
||||||
body, html {
|
body,
|
||||||
background: #F6F6F8;
|
html {
|
||||||
color: #0C0C0D;
|
|
||||||
font: message-box;
|
font: message-box;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Ubuntu",
|
||||||
|
"Helvetica Neue", sans-serif;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
color: var(--general-color);
|
||||||
|
background: var(--general-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
html#popup,
|
||||||
|
html#popup > body {
|
||||||
|
color: var(--popup-color);
|
||||||
|
background: var(--popup-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
font: caption;
|
font: caption;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@ -184,7 +388,11 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virtualtable-column:active {
|
.virtualtable-column:active {
|
||||||
background-image: linear-gradient(to top, rgba(0,0,0,0.03), rgba(128,128,128,0.1));
|
background-image: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0, 0, 0, 0.03),
|
||||||
|
rgba(128, 128, 128, 0.1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
th.virtualtable {
|
th.virtualtable {
|
||||||
@ -211,8 +419,12 @@ td.virtualtable {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
background: linear-gradient(to bottom, rgba(128,128,128,0.1) 0%,rgba(0,0,0,0) 100%);
|
background: linear-gradient(
|
||||||
border-top: 1px solid rgba(128,128,128,0.6);
|
to bottom,
|
||||||
|
rgba(128, 128, 128, 0.1) 0%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
border-top: 1px solid rgba(128, 128, 128, 0.6);
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 1ex;
|
margin-bottom: 1ex;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@ -239,15 +451,15 @@ td.virtualtable {
|
|||||||
flex-grow: 3;
|
flex-grow: 3;
|
||||||
margin-right: 2ex;
|
margin-right: 2ex;
|
||||||
padding-right: 1ex;
|
padding-right: 1ex;
|
||||||
border-right: 1px dotted rgba(128,128,128,0.6);
|
border-right: 1px dotted rgba(128, 128, 128, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
#statusPrefs {
|
#statusPrefs {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #363636;
|
color: var(--status-icon-color);
|
||||||
}
|
}
|
||||||
#statusPrefs:hover {
|
#statusPrefs:hover {
|
||||||
color: #6e6d6d;
|
color: var(--status-icon-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
@ -265,13 +477,14 @@ td.virtualtable {
|
|||||||
outline: none;
|
outline: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width:100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown input {
|
.dropdown input {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
color: black;
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
border: none;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
@ -294,7 +507,6 @@ td.virtualtable {
|
|||||||
padding-bottom: 1ex;
|
padding-bottom: 1ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@supports (not (-moz-appearance: none)) {
|
@supports (not (-moz-appearance: none)) {
|
||||||
.dropdown select {
|
.dropdown select {
|
||||||
background: white;
|
background: white;
|
||||||
@ -364,3 +576,54 @@ td.virtualtable {
|
|||||||
#maskButton {
|
#maskButton {
|
||||||
color: var(--maskbutton-color);
|
color: var(--maskbutton-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.virtualtable,
|
||||||
|
.virtualtable-body {
|
||||||
|
color: var(--general-color);
|
||||||
|
background: var(--table-bgcolor);
|
||||||
|
}
|
||||||
|
.virtualtable-head,
|
||||||
|
.virtualtable-head > table {
|
||||||
|
background: var(--table-head-bgcolor) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtualtable-column {
|
||||||
|
border-right: 1px solid var(--general-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtualtable-cell {
|
||||||
|
border-right: 1px dotted var(--general-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtualtable-head,
|
||||||
|
.virtualtable-body {
|
||||||
|
border-bottom: 1px solid var(--general-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.context-menu,
|
||||||
|
ul.context-menu ul {
|
||||||
|
color: var(--general-color);
|
||||||
|
background: var(--menu-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover:not(.context-menu-seperator),
|
||||||
|
.context-menu-item:hover:not(.context-menu-seperator) > * {
|
||||||
|
background: var(--menu-bgcolor-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .context-menu-item.disabled,
|
||||||
|
html.dark .context-menu-item.disabled > * {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: var(--general-input-color);
|
||||||
|
background: var(--general-input-bgcolor);
|
||||||
|
border: inherit;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
color: var(--modal-color);
|
||||||
|
background: var(--modal-bgcolor);
|
||||||
|
}
|
||||||
|
BIN
style/done.opus
Normal file
BIN
style/downthemall.woff2
Normal file → Executable file
BIN
style/error.opus
Normal file
BIN
style/icon.ico
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 13 KiB |
BIN
style/icon16.png
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 810 B |
BIN
style/icon24.png
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 34 KiB |
BIN
style/icon32.png
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
style/icon48.png
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
style/icon64.png
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
style/icon96.png
Before Width: | Height: | Size: 6.3 KiB |
@ -16,7 +16,7 @@ body > * {
|
|||||||
#toolbar {
|
#toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--toolbar-bg-color) url(tile.png) repeat-x;
|
background: var(--toolbar-bg-color) var(--tile-url) repeat-x;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolbar .spacer {
|
#toolbar .spacer {
|
||||||
@ -42,9 +42,9 @@ body > * {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0px 0px 5px 1px rgba(128,128,128,0.5);
|
box-shadow: var(--general-button-shadow);
|
||||||
background: rgb(246,246,246);
|
background: var(--general-button-bgcolor);
|
||||||
color: black;
|
color: var(--general-button-color);
|
||||||
transition: box-shadow 0.5s, background 1s;
|
transition: box-shadow 0.5s, background 1s;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
@ -60,7 +60,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#toolbar > .button:hover:not(.disabled) {
|
#toolbar > .button:hover:not(.disabled) {
|
||||||
background: white;
|
background: var(--general-button-bgcolor-hover);
|
||||||
box-shadow: 0px 0px 7px 2px rgba(70,70,70,0.75);
|
box-shadow: 0px 0px 7px 2px rgba(70,70,70,0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,14 +88,14 @@ body > * {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: grid;
|
display: grid;
|
||||||
background: white;
|
background: var(--general-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
#loading {
|
#loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgba(255,255,255,0.9);
|
background: var(--general-button-bgcolor);
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@ -202,6 +202,23 @@ body > * {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-icon {
|
||||||
|
color: var(--retry-color);
|
||||||
|
}
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-progress-bar {
|
||||||
|
background: var(--retry-color);
|
||||||
|
}
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-progress-undetermined {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--retry-color),
|
||||||
|
var(--retry-color) 6px,
|
||||||
|
transparent 6px,
|
||||||
|
transparent 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.missing .virtualtable-column-2 .virtualtable-icon,
|
.missing .virtualtable-column-2 .virtualtable-icon,
|
||||||
.canceled .virtualtable-column-2 .virtualtable-icon {
|
.canceled .virtualtable-column-2 .virtualtable-icon {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
@ -294,7 +311,7 @@ body > * {
|
|||||||
color: crimson;
|
color: crimson;
|
||||||
}
|
}
|
||||||
#statusNetwork.icon-network-on {
|
#statusNetwork.icon-network-on {
|
||||||
color: navy;
|
color: var(--add-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#statusFilter {
|
#statusFilter {
|
||||||
@ -327,6 +344,7 @@ body > * {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
width: calc(100% - 28px);
|
width: calc(100% - 28px);
|
||||||
}
|
}
|
||||||
@ -386,7 +404,7 @@ body > * {
|
|||||||
font-size: 10pt !important;
|
font-size: 10pt !important;
|
||||||
}
|
}
|
||||||
#nagging {
|
#nagging {
|
||||||
border-top: 1px solid lightgray;
|
border-top: 1px solid var(--general-border-color);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
@ -512,3 +530,23 @@ body > * {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--done-color);
|
background: var(--done-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tooltip-eta.single {
|
||||||
|
font-weight: bold;
|
||||||
|
grid-column-end: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deletefiles-list {
|
||||||
|
padding-left: 1ex;
|
||||||
|
padding-right: 1.5ex;
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(128,128,128,0.1);
|
||||||
|
max-height: 8em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.deletefiles-list > li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
/* License: gpl-v2 */
|
/* License: gpl-v2 */
|
||||||
@import 'common.css';
|
@import "common.css";
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
background: transparent !important;
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -19,9 +19,10 @@ article {
|
|||||||
|
|
||||||
#tabs {
|
#tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: url(icon64.png) 1em 50%/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon64.png) 1em 50%/32px 32px no-repeat,
|
||||||
|
var(--tile-url) repeat-x, var(--toolbar-bg-color);
|
||||||
padding-left: calc(2em + 32px);
|
padding-left: calc(2em + 32px);
|
||||||
color: white;
|
color: var(--general-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.tab {
|
input.tab {
|
||||||
@ -54,9 +55,10 @@ input.tab {
|
|||||||
#tabsel-general:checked ~ #tabs #tabel-general,
|
#tabsel-general:checked ~ #tabs #tabel-general,
|
||||||
#tabsel-filters:checked ~ #tabs #tabel-filters,
|
#tabsel-filters:checked ~ #tabs #tabel-filters,
|
||||||
#tabsel-network:checked ~ #tabs #tabel-network {
|
#tabsel-network:checked ~ #tabs #tabel-network {
|
||||||
color: black !important;
|
color: var(--general-color) !important;
|
||||||
background: white;
|
background: var(--general-bgcolor);
|
||||||
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color);
|
border-top: var(--toolbar-border-width) solid
|
||||||
|
var(--toolbar-active-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabs > label {
|
#tabs > label {
|
||||||
@ -64,13 +66,14 @@ input.tab {
|
|||||||
border-top: var(--toolbar-border-width) solid transparent;
|
border-top: var(--toolbar-border-width) solid transparent;
|
||||||
border-left: 1px solid transparent;
|
border-left: 1px solid transparent;
|
||||||
border-right: 1px solid transparent;
|
border-right: 1px solid transparent;
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.5);
|
border-left: var(--toolbar-border);
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.5);
|
border-right: var(--toolbar-border);
|
||||||
background: var(--toolbar-bg-color);
|
background: var(--toolbar-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabs > label:hover:not(:checked) {
|
#tabs > label:hover:not(:checked) {
|
||||||
border-top: var(--toolbar-border-width) solid var(--toolbar-hover-border-color);
|
border-top: var(--toolbar-border-width) solid
|
||||||
|
var(--toolbar-hover-border-color);
|
||||||
background: var(--toolbar-hover-background);
|
background: var(--toolbar-hover-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +105,7 @@ input.tab {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons > button{
|
.buttons > button {
|
||||||
margin: 0 2em;
|
margin: 0 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,15 +116,27 @@ input.tab {
|
|||||||
fieldset {
|
fieldset {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
border: 1px solid lightgray;
|
border: 1px solid var(--general-border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 1px 1px 6px lightgray;
|
box-shadow: 1px 1px 6px var(--general-border-color);
|
||||||
background: rgba(128, 128, 128, 0.05);
|
background: rgba(128, 128, 128, 0.05);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 60em;
|
max-width: 60em;
|
||||||
padding: 1.2em;
|
padding: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.optiongroups,
|
||||||
|
fieldset > label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset > label > input,
|
||||||
|
fieldset > label > select {
|
||||||
|
margin-left: 1ex;
|
||||||
|
margin-right: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
@ -134,8 +149,36 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virtualtable-container {
|
.virtualtable-container {
|
||||||
border: 1px solid lightgray;
|
border: 1px solid var(--general-border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: rgba(128, 128, 128, 0.05);
|
background: rgba(128, 128, 128, 0.05);
|
||||||
box-shadow: 1px 1px 6px lightgray;
|
box-shadow: 1px 1px 6px var(--general-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#network-general {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optiongroups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optiongroups > div,
|
||||||
|
.optiongroups > div > label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.optiongroups input {
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 0.7ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
@ -107,7 +107,7 @@ body > * {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
padding-left: calc(2em + 32px);
|
padding-left: calc(2em + 32px);
|
||||||
color: black;
|
color: black;
|
||||||
background: url(icon32.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon32.png) 1em 0/32px 32px no-repeat, var(--tile-url) repeat-x, var(--toolbar-bg-color);
|
||||||
font: caption;
|
font: caption;
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -117,7 +117,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
||||||
#tabs {
|
#tabs {
|
||||||
background: url(icon64.png) 1em 50%/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon64.png) 1em 50%/32px 32px no-repeat, var(--tile-url) repeat-x, var(--toolbar-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,27 +145,27 @@ body > * {
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background: var(--toolbar-bg-color);
|
background: var(--toolbar-bg-color);
|
||||||
color: white;
|
color: var(--general-color);
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
padding: 1ex;
|
padding: 1ex;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: var(--toolbar-border-width) solid transparent;
|
border-top: var(--toolbar-border-width) solid transparent;
|
||||||
border-left: 1px solid rgba(255,255,255,0.3);
|
border-left: var(--toolbar-border);
|
||||||
border-right: 1px solid rgba(255,255,255,0.3);
|
border-right: var(--toolbar-border);
|
||||||
transition: border 1s;
|
transition: border 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:not(.active):not(.disabled):hover {
|
.tab:not(.active):not(.disabled):hover {
|
||||||
border-top: var(--toolbar-border-width) solid var(--toolbar-hover-border-color);
|
border-top: var(--toolbar-border-width) solid var(--toolbar-hover-border-color);
|
||||||
color: rgb(255, 226, 167);
|
color: var(--general-color);
|
||||||
background: var(--toolbar-hover-background);
|
background: var(--toolbar-hover-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: black;
|
color: var(--general-color);
|
||||||
background: white;
|
background: var(--table-head-bgcolor);
|
||||||
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color);
|
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color);
|
||||||
border-left: 1px solid transparent;
|
border-left: 1px solid transparent;
|
||||||
border-right: 1px solid transparent;
|
border-right: 1px solid transparent;
|
||||||
|
@ -63,7 +63,8 @@ p.example {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#options > #maskOptions {
|
#options > #subfolderOptions,
|
||||||
|
#options > #maskOptions, #options > #serverOptions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr auto auto;
|
grid-template-columns: 2fr auto auto;
|
||||||
}
|
}
|
||||||
|
BIN
style/tile-dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
style/tile.png
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.3 KiB |
10
tests/.editorconfig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
tab_width = 2
|
||||||
|
trim_trailing_whitespace = true
|
@ -113,4 +113,29 @@ describe("BatchGenerator", function() {
|
|||||||
expect(items[0]).to.equal(gen.preview);
|
expect(items[0]).to.equal(gen.preview);
|
||||||
expect(gen.hasInvalid).to.be.true;
|
expect(gen.hasInvalid).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("characters", function() {
|
||||||
|
const gen = new BatchGenerator("abc[a:c].lol[1].b");
|
||||||
|
const items = Array.from(gen);
|
||||||
|
expect(items).to.deep.equal([
|
||||||
|
"abca.lol[1].b",
|
||||||
|
"abcb.lol[1].b",
|
||||||
|
"abcc.lol[1].b",
|
||||||
|
]);
|
||||||
|
expect(items.length).to.equal(gen.length);
|
||||||
|
expect(items[0]).to.equal(gen.preview);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("characters two", function() {
|
||||||
|
const gen = new BatchGenerator("abc[D:G].lol[1].b");
|
||||||
|
const items = Array.from(gen);
|
||||||
|
expect(items).to.deep.equal([
|
||||||
|
"abcD.lol[1].b",
|
||||||
|
"abcE.lol[1].b",
|
||||||
|
"abcF.lol[1].b",
|
||||||
|
"abcG.lol[1].b",
|
||||||
|
]);
|
||||||
|
expect(items.length).to.equal(gen.length);
|
||||||
|
expect(items[0]).to.equal(gen.preview);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
289
tests/test_cdheaderparser.js
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
/* eslint-env node */
|
||||||
|
"use strict";
|
||||||
|
// License: MPL-2
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { CDHeaderParser } = require("../lib/cdheaderparser");
|
||||||
|
|
||||||
|
const parser = new CDHeaderParser();
|
||||||
|
|
||||||
|
function check(header, expected) {
|
||||||
|
expect(parser.parse(header)).to.equal(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nocheck(header, expected) {
|
||||||
|
expect(parser.parse(header)).not.to.equal(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CDHeaderParser", function() {
|
||||||
|
it("parse wget", function() {
|
||||||
|
// From wget, test_parse_content_disposition
|
||||||
|
// http://git.savannah.gnu.org/cgit/wget.git/tree/src/http.c?id=8551ceccfedb4390fbfa82c12f0ff714dab1ac76#n5325
|
||||||
|
check("filename=\"file.ext\"", "file.ext");
|
||||||
|
check("attachment; filename=\"file.ext\"", "file.ext");
|
||||||
|
check("attachment; filename=\"file.ext\"; dummy", "file.ext");
|
||||||
|
check("attachment", ""); // wget uses NULL, we use "".
|
||||||
|
check("attachement; filename*=UTF-8'en-US'hello.txt", "hello.txt");
|
||||||
|
check("attachement; filename*0=\"hello\"; filename*1=\"world.txt\"",
|
||||||
|
"helloworld.txt");
|
||||||
|
check("attachment; filename=\"A.ext\"; filename*=\"B.ext\"", "B.ext");
|
||||||
|
check("attachment; filename*=\"A.ext\"; filename*0=\"B\"; filename*1=\"B.ext\"",
|
||||||
|
"A.ext");
|
||||||
|
// This test is faulty - https://savannah.gnu.org/bugs/index.php?52531
|
||||||
|
//check("filename**0=\"A\"; filename**1=\"A.ext\"; filename*0=\"B\";filename*1=\"B\"", "AA.ext");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parse Firefox", function() {
|
||||||
|
// From Firefox
|
||||||
|
// https://searchfox.org/mozilla-central/rev/45a3df4e6b8f653b0103d18d97c34dd666706358/netwerk/test/unit/test_MIME_params.js
|
||||||
|
// Changed as follows:
|
||||||
|
// - Replace error codes with empty string (we never throw).
|
||||||
|
|
||||||
|
const BS = "\\";
|
||||||
|
const DQUOTE = "\"";
|
||||||
|
// No filename parameter: return nothing
|
||||||
|
check("attachment;", "");
|
||||||
|
// basic
|
||||||
|
check("attachment; filename=basic", "basic");
|
||||||
|
// extended
|
||||||
|
check("attachment; filename*=UTF-8''extended", "extended");
|
||||||
|
// prefer extended to basic (bug 588781)
|
||||||
|
check("attachment; filename=basic; filename*=UTF-8''extended", "extended");
|
||||||
|
// prefer extended to basic (bug 588781)
|
||||||
|
check("attachment; filename*=UTF-8''extended; filename=basic", "extended");
|
||||||
|
// use first basic value (invalid; error recovery)
|
||||||
|
check("attachment; filename=first; filename=wrong", "first");
|
||||||
|
// old school bad HTTP servers: missing 'attachment' or 'inline'
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("filename=old", "old");
|
||||||
|
check("attachment; filename*=UTF-8''extended", "extended");
|
||||||
|
// continuations not part of RFC 5987 (bug 610054)
|
||||||
|
check("attachment; filename*0=foo; filename*1=bar", "foobar");
|
||||||
|
// Return first continuation (invalid; error recovery)
|
||||||
|
check("attachment; filename*0=first; filename*0=wrong; filename=basic", "first");
|
||||||
|
// Only use correctly ordered continuations (invalid; error recovery)
|
||||||
|
check("attachment; filename*0=first; filename*1=second; filename*0=wrong", "firstsecond");
|
||||||
|
// prefer continuation to basic (unless RFC 5987)
|
||||||
|
check("attachment; filename=basic; filename*0=foo; filename*1=bar", "foobar");
|
||||||
|
// Prefer extended to basic and/or (broken or not) continuation
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended", "extended");
|
||||||
|
// RFC 2231 not clear on correct outcome: we prefer non-continued extended
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar", "extended");
|
||||||
|
// Gaps should result in returning only value until gap hit
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename*0=foo; filename*2=bar", "foo");
|
||||||
|
// Don't allow leading 0's (*01) (invalid; error recovery)
|
||||||
|
check("attachment; filename*0=foo; filename*01=bar", "foo");
|
||||||
|
// continuations should prevail over non-extended (unless RFC 5987)
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
|
||||||
|
" filename*1=line;\r\n" +
|
||||||
|
" filename*2*=%20extended",
|
||||||
|
"multiline extended");
|
||||||
|
// Gaps should result in returning only value until gap hit
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
|
||||||
|
" filename*1=line;\r\n" +
|
||||||
|
" filename*3*=%20extended",
|
||||||
|
"multiline");
|
||||||
|
// First series, only please, and don't slurp up higher elements (*2 in this
|
||||||
|
// case) from later series into earlier one (invalid; error recovery)
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
|
||||||
|
" filename*1=line;\r\n" +
|
||||||
|
" filename*0*=UTF-8''wrong;\r\n" +
|
||||||
|
" filename*1=bad;\r\n" +
|
||||||
|
" filename*2=evil",
|
||||||
|
"multiline");
|
||||||
|
// RFC 2231 not clear on correct outcome: we prefer non-continued extended
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename=basic; filename*0=UTF-8''multi\r\n;" +
|
||||||
|
" filename*=UTF-8''extended;\r\n" +
|
||||||
|
" filename*1=line;\r\n" +
|
||||||
|
" filename*2*=%20extended",
|
||||||
|
"extended");
|
||||||
|
// sneaky: if unescaped, make sure we leave UTF-8'' in value
|
||||||
|
check("attachment; filename*0=UTF-8''unescaped;\r\n" +
|
||||||
|
" filename*1*=%20so%20includes%20UTF-8''%20in%20value",
|
||||||
|
"UTF-8''unescaped so includes UTF-8'' in value");
|
||||||
|
// sneaky: if unescaped, make sure we leave UTF-8'' in value
|
||||||
|
check("attachment; filename=basic; filename*0=UTF-8''unescaped;\r\n" +
|
||||||
|
" filename*1*=%20so%20includes%20UTF-8''%20in%20value",
|
||||||
|
"UTF-8''unescaped so includes UTF-8'' in value");
|
||||||
|
// Prefer basic over invalid continuation
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename=basic; filename*1=multi;\r\n" +
|
||||||
|
" filename*2=line;\r\n" +
|
||||||
|
" filename*3*=%20extended",
|
||||||
|
"basic");
|
||||||
|
// support digits over 10
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
|
||||||
|
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
|
||||||
|
" filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n",
|
||||||
|
"0123456789abcdef");
|
||||||
|
// support digits over 10 (detect gaps)
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
|
||||||
|
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
|
||||||
|
" filename*11=b; filename*12=c;filename*14=e\r\n",
|
||||||
|
"0123456789abc");
|
||||||
|
// return nothing: invalid
|
||||||
|
// (invalid; error recovery)
|
||||||
|
check("attachment; filename*1=multi;\r\n" +
|
||||||
|
" filename*2=line;\r\n" +
|
||||||
|
" filename*3*=%20extended",
|
||||||
|
"");
|
||||||
|
// Bug 272541: Empty disposition type treated as "attachment"
|
||||||
|
// sanity check
|
||||||
|
check("attachment; filename=foo.html", "foo.html");
|
||||||
|
// the actual bug
|
||||||
|
check("; filename=foo.html", "foo.html");
|
||||||
|
// regression check, but see bug 671204
|
||||||
|
check("filename=foo.html", "foo.html");
|
||||||
|
// Bug 384571: RFC 2231 parameters not decoded when appearing in reversed order
|
||||||
|
// check ordering
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
|
||||||
|
" filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
|
||||||
|
" filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e;\r\n",
|
||||||
|
"0123456789abcdef");
|
||||||
|
// check non-digits in sequence numbers
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*1a=1\r\n",
|
||||||
|
"0");
|
||||||
|
// check duplicate sequence numbers
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*0=bad; filename*1=1;\r\n",
|
||||||
|
"0");
|
||||||
|
// check overflow
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*11111111111111111111111111111111111111111111111111111111111=1",
|
||||||
|
"0");
|
||||||
|
// check underflow
|
||||||
|
check("attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
|
||||||
|
" filename*-1=1",
|
||||||
|
"0");
|
||||||
|
// check mixed token/quoted-string
|
||||||
|
check("attachment; filename=basic; filename*0=\"0\";\r\n" +
|
||||||
|
" filename*1=1;\r\n" +
|
||||||
|
" filename*2*=%32",
|
||||||
|
"012");
|
||||||
|
// check empty sequence number
|
||||||
|
check("attachment; filename=basic; filename**=UTF-8''0\r\n", "basic");
|
||||||
|
// Bug 419157: ensure that a MIME parameter with no charset information
|
||||||
|
// fallbacks to Latin-1
|
||||||
|
check("attachment;filename=IT839\x04\xB5(m8)2.pdf;", "IT839\u0004\u00b5(m8)2.pdf");
|
||||||
|
// Bug 588389: unescaping backslashes in quoted string parameters
|
||||||
|
// '\"', should be parsed as '"'
|
||||||
|
check(`attachment; filename=${DQUOTE}${BS + DQUOTE}${DQUOTE}`, DQUOTE);
|
||||||
|
// 'a\"b', should be parsed as 'a"b'
|
||||||
|
check(`attachment; filename=${DQUOTE}a${BS + DQUOTE}b${DQUOTE}`, `a${DQUOTE}b`);
|
||||||
|
// '\x', should be parsed as 'x'
|
||||||
|
check(`attachment; filename=${DQUOTE}${BS}x${DQUOTE}`, "x");
|
||||||
|
// test empty param (quoted-string)
|
||||||
|
check(`attachment; filename=${DQUOTE}${DQUOTE}`, "");
|
||||||
|
// test empty param
|
||||||
|
check("attachment; filename=", "");
|
||||||
|
// Bug 601933: RFC 2047 does not apply to parameters (at least in HTTP)
|
||||||
|
check("attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=", "foo-\u00e4.html");
|
||||||
|
check("attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"", "foo-\u00e4.html");
|
||||||
|
// format sent by GMail as of 2012-07-23 (5987 overrides 2047)
|
||||||
|
check("attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"; filename*=UTF-8''5987", "5987");
|
||||||
|
// Bug 651185: double quotes around 2231/5987 encoded param
|
||||||
|
// Change reverted to backwards compat issues with various web services,
|
||||||
|
// such as OWA (Bug 703015), plus similar problems in Thunderbird. If this
|
||||||
|
// is tried again in the future, email probably needs to be special-cased.
|
||||||
|
// sanity check
|
||||||
|
check("attachment; filename*=utf-8''%41", "A");
|
||||||
|
// the actual bug
|
||||||
|
check(`attachment; filename*=${DQUOTE}utf-8''%41${DQUOTE}`, "A");
|
||||||
|
// Bug 670333: Content-Disposition parser does not require presence of "="
|
||||||
|
// in params
|
||||||
|
// sanity check
|
||||||
|
check("attachment; filename*=UTF-8''foo-%41.html", "foo-A.html");
|
||||||
|
// the actual bug
|
||||||
|
check("attachment; filename *=UTF-8''foo-%41.html", "");
|
||||||
|
// the actual bug, without 2231/5987 encoding
|
||||||
|
check("attachment; filename X", "");
|
||||||
|
// sanity check with WS on both sides
|
||||||
|
check("attachment; filename = foo-A.html", "foo-A.html");
|
||||||
|
// Bug 685192: in RFC2231/5987 encoding, a missing charset field should be
|
||||||
|
// treated as error
|
||||||
|
// the actual bug
|
||||||
|
check("attachment; filename*=''foo", "foo");
|
||||||
|
// sanity check
|
||||||
|
check("attachment; filename*=a''foo", "foo");
|
||||||
|
// Bug 692574: RFC2231/5987 decoding should not tolerate missing single
|
||||||
|
// quotes
|
||||||
|
// one missing
|
||||||
|
check("attachment; filename*=UTF-8'foo-%41.html", "foo-A.html");
|
||||||
|
// both missing
|
||||||
|
check("attachment; filename*=foo-%41.html", "foo-A.html");
|
||||||
|
// make sure fallback works
|
||||||
|
check("attachment; filename*=UTF-8'foo-%41.html; filename=bar.html", "foo-A.html");
|
||||||
|
// Bug 693806: RFC2231/5987 encoding: charset information should be treated
|
||||||
|
// as authoritative
|
||||||
|
// UTF-8 labeled ISO-8859-1
|
||||||
|
check("attachment; filename*=ISO-8859-1''%c3%a4", "\u00c3\u00a4");
|
||||||
|
// UTF-8 labeled ISO-8859-1, but with octets not allowed in ISO-8859-1
|
||||||
|
// accepts x82, understands it as Win1252, maps it to Unicode \u20a1
|
||||||
|
check("attachment; filename*=ISO-8859-1''%e2%82%ac", "\u00e2\u201a\u00ac");
|
||||||
|
// defective UTF-8
|
||||||
|
nocheck("attachment; filename*=UTF-8''A%e4B", "");
|
||||||
|
// defective UTF-8, with fallback
|
||||||
|
nocheck("attachment; filename*=UTF-8''A%e4B; filename=fallback", "fallback");
|
||||||
|
// defective UTF-8 (continuations), with fallback
|
||||||
|
nocheck("attachment; filename*0*=UTF-8''A%e4B; filename=fallback", "fallback");
|
||||||
|
// check that charsets aren't mixed up
|
||||||
|
check("attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8859-1''currency-sign%3d%a4", "currency-sign=\u00a4");
|
||||||
|
// same as above, except reversed
|
||||||
|
check("attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=ISO-8859-15''euro-sign%3d%a4", "currency-sign=\u00a4");
|
||||||
|
// Bug 704989: add workaround for broken Outlook Web App (OWA)
|
||||||
|
// attachment handling
|
||||||
|
check("attachment; filename*=\"a%20b\"", "a b");
|
||||||
|
// Bug 717121: crash nsMIMEHeaderParamImpl::DoParameterInternal
|
||||||
|
check("attachment; filename=\"", "");
|
||||||
|
// We used to read past string if last param w/o = and ;
|
||||||
|
// Note: was only detected on windows PGO builds
|
||||||
|
check("attachment; filename=foo; trouble", "foo");
|
||||||
|
// Same, followed by space, hits another case
|
||||||
|
check("attachment; filename=foo; trouble ", "foo");
|
||||||
|
check("attachment", "");
|
||||||
|
// Bug 730574: quoted-string in RFC2231-continuations not handled
|
||||||
|
check("attachment; filename=basic; filename*0=\"foo\"; filename*1=\"\\b\\a\\r.html\"", "foobar.html");
|
||||||
|
// unmatched escape char
|
||||||
|
check("attachment; filename=basic; filename*0=\"foo\"; filename*1=\"\\b\\a\\", "fooba\\");
|
||||||
|
// Bug 732369: Content-Disposition parser does not require presence of ";" between params
|
||||||
|
// optimally, this would not even return the disposition type "attachment"
|
||||||
|
check("attachment; extension=bla filename=foo", "");
|
||||||
|
check("attachment; filename=foo extension=bla", "foo");
|
||||||
|
check("attachment filename=foo", "");
|
||||||
|
// Bug 777687: handling of broken %escapes
|
||||||
|
nocheck("attachment; filename*=UTF-8''f%oo; filename=bar", "bar");
|
||||||
|
nocheck("attachment; filename*=UTF-8''foo%; filename=bar", "bar");
|
||||||
|
// Bug 783502 - xpcshell test netwerk/test/unit/test_MIME_params.js fails on AddressSanitizer
|
||||||
|
check("attachment; filename=\"\\b\\a\\", "ba\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parse extra", function() {
|
||||||
|
// Extra tests, not covered by above tests.
|
||||||
|
check("inline; FILENAME=file.txt", "file.txt");
|
||||||
|
check("INLINE; FILENAME= \"an example.html\"", "an example.html"); // RFC 6266, section 5.
|
||||||
|
check("inline; filename= \"tl;dr.txt\"", "tl;dr.txt");
|
||||||
|
check("INLINE; FILENAME*= \"an example.html\"", "an example.html");
|
||||||
|
check("inline; filename*= \"tl;dr.txt\"", "tl;dr.txt");
|
||||||
|
check("inline; filename*0=\"tl;dr and \"; filename*1=more.txt", "tl;dr and more.txt");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parse issue 26", function() {
|
||||||
|
// https://github.com/Rob--W/open-in-browser/issues/26
|
||||||
|
check("attachment; filename=\xe5\x9c\x8b.pdf", "\u570b.pdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parse issue 35", function() {
|
||||||
|
// https://github.com/Rob--W/open-in-browser/issues/35
|
||||||
|
check("attachment; filename=okre\x9clenia.rtf", "okreœlenia.rtf");
|
||||||
|
});
|
||||||
|
});
|
30
tests/test_mime.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: CC0 1.0
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const {MimeDB} = require("../lib/mime");
|
||||||
|
|
||||||
|
describe("MIME", function() {
|
||||||
|
it("general", function() {
|
||||||
|
expect(MimeDB.getMime("image/jpeg").major).to.equal("image");
|
||||||
|
expect(MimeDB.getMime("image/jpeg").minor).to.equal("jpeg");
|
||||||
|
expect(MimeDB.getMime("iMage/jPeg").major).to.equal("image");
|
||||||
|
expect(MimeDB.getMime("imAge/jpEg").minor).to.equal("jpeg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exts", function() {
|
||||||
|
expect(MimeDB.getMime("image/jpeg").primary).to.equal("jpg");
|
||||||
|
expect(MimeDB.getMime("image/jpeg").primary).to.equal(
|
||||||
|
MimeDB.getPrimary("image/jpeg"));
|
||||||
|
expect(MimeDB.getMime("iMage/jPeg").primary).to.equal("jpg");
|
||||||
|
expect(MimeDB.getMime("imAge/jpEg").primary).to.equal(
|
||||||
|
MimeDB.getPrimary("image/jpeg"));
|
||||||
|
expect(Array.from(MimeDB.getMime("imAge/jpEg").extensions)).to.deep.equal(
|
||||||
|
["jpg", "jpeg", "jpe", "jfif"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("application/octet-stream should not yield results", function() {
|
||||||
|
expect(MimeDB.getPrimary("application/octet-stream")).to.equal("");
|
||||||
|
expect(MimeDB.getMime("application/octet-Stream")).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
43
tests/test_urld.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
"use strict";
|
||||||
|
// License: CC0 1.0
|
||||||
|
|
||||||
|
require("../lib/util");
|
||||||
|
|
||||||
|
describe("URLd", function() {
|
||||||
|
it("basic domain", function() {
|
||||||
|
let u = new URL("https://www.google.de");
|
||||||
|
expect(u.domain).to.equal("google.de");
|
||||||
|
u = new URL("https://www.google.de:8443");
|
||||||
|
expect(u.domain).to.equal("google.de");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("plain basic domain", function() {
|
||||||
|
const u = new URL("https://google.de");
|
||||||
|
expect(u.domain).to.equal("google.de");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("special domain", function() {
|
||||||
|
let u = new URL("https://www.google.co.uk");
|
||||||
|
expect(u.domain).to.equal("google.co.uk");
|
||||||
|
u = new URL("https://google.co.uk");
|
||||||
|
expect(u.domain).to.equal("google.co.uk");
|
||||||
|
u = new URL("https://www.google.co.uk:8443");
|
||||||
|
expect(u.domain).to.equal("google.co.uk");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ipv4", function() {
|
||||||
|
let u = new URL("https://127.0.0.1:8443");
|
||||||
|
expect(u.domain).to.equal("127.0.0.1");
|
||||||
|
u = new URL("https://0.0.0.0:8443");
|
||||||
|
expect(u.domain).to.equal("0.0.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ipv6", function() {
|
||||||
|
let u = new URL("https://[::1]:8443");
|
||||||
|
expect(u.domain).to.equal("[::1]");
|
||||||
|
u = new URL("https://[2a00:1450:4005:800::2003]:8443");
|
||||||
|
expect(u.domain).to.equal("[2a00:1450:4005:800::2003]");
|
||||||
|
});
|
||||||
|
});
|
@ -108,8 +108,13 @@ export class MenuItem extends MenuItemBase {
|
|||||||
super(owner, id, text, options);
|
super(owner, id, text, options);
|
||||||
this.disabled = options.disabled === "true";
|
this.disabled = options.disabled === "true";
|
||||||
this.elem.setAttribute("aria-role", "menuitem");
|
this.elem.setAttribute("aria-role", "menuitem");
|
||||||
this.elem.addEventListener(
|
this.clicked = this.clicked.bind(this);
|
||||||
"click", () => this.owner.emit("clicked", this.id, this.autoHide));
|
this.elem.addEventListener("click", this.clicked);
|
||||||
|
this.elem.addEventListener("contextmenu", this.clicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked() {
|
||||||
|
this.owner.emit("clicked", this.id, this.autoHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
get disabled() {
|
get disabled() {
|
||||||
|
@ -104,7 +104,7 @@ export class EventEmitter {
|
|||||||
for (const e of Array.from(handlers)) {
|
for (const e of Array.from(handlers)) {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line prefer-spread
|
// eslint-disable-next-line prefer-spread
|
||||||
handled = handled || !!e.apply(null, args);
|
handled = !!e.apply(null, args) || handled;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(`Event handler ${e} for ${event} failed`, ex.toString(), ex.stack, ex);
|
console.error(`Event handler ${e} for ${event} failed`, ex.toString(), ex.stack, ex);
|
||||||
|
@ -45,10 +45,10 @@ export class TableEvents extends BaseTable {
|
|||||||
"scroll", debounce(this.scrolled.bind(this), SCROLL_DEBOUNCE), {
|
"scroll", debounce(this.scrolled.bind(this), SCROLL_DEBOUNCE), {
|
||||||
passive: true
|
passive: true
|
||||||
});
|
});
|
||||||
|
body.addEventListener("contextmenu", this.contextmenu.bind(this), true);
|
||||||
|
|
||||||
table.addEventListener("keypress", this.keypressed.bind(this), true);
|
table.addEventListener("keypress", this.keypressed.bind(this), true);
|
||||||
table.addEventListener("keydown", this.keypressed.bind(this), true);
|
table.addEventListener("keydown", this.keypressed.bind(this), true);
|
||||||
table.addEventListener("contextmenu", this.contextmenu.bind(this), true);
|
|
||||||
|
|
||||||
selectionGrippy.addEventListener("click", this.grippyClicked.bind(this));
|
selectionGrippy.addEventListener("click", this.grippyClicked.bind(this));
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ def main():
|
|||||||
if modified:
|
if modified:
|
||||||
try:
|
try:
|
||||||
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
|
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
|
||||||
json.dump(data, outp, sort_keys=True, indent=2)
|
json.dump(data, outp, sort_keys=True, indent=2, ensure_ascii=False)
|
||||||
os.rename("messages.json.tmp", "_locales/en/messages.json")
|
os.rename("messages.json.tmp", "_locales/en/messages.json")
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
@ -20,16 +20,19 @@ FILES = [
|
|||||||
"LICENSE.*",
|
"LICENSE.*",
|
||||||
]
|
]
|
||||||
|
|
||||||
RELEASE_ID = "{DDC359D1-844A-42a7-9AA1-88A850A938A8}"
|
RELEASE_ID = "downloading@traitorousenterprises.net"
|
||||||
|
|
||||||
UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
||||||
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
||||||
IGNORED = set((".DS_Store", "Thumbs.db"))
|
IGNORED = set((".DS_Store", "Thumbs.db"))
|
||||||
|
# XXX: #125
|
||||||
|
IGNORED_OPERA = set(("done.opus", "error.opus"))
|
||||||
|
|
||||||
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest"))
|
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest", "webRequestBlocking"))
|
||||||
PERM_IGNORED_CHROME = set(("menus",))
|
PERM_IGNORED_CHROME = set(("menus", "sessions", "theme"))
|
||||||
|
|
||||||
SCRIPTS = [
|
SCRIPTS = [
|
||||||
|
"yarn build:cleanup",
|
||||||
"yarn build:regexps",
|
"yarn build:regexps",
|
||||||
"yarn build:bundles",
|
"yarn build:bundles",
|
||||||
]
|
]
|
||||||
@ -45,17 +48,17 @@ def check_licenses():
|
|||||||
raise Exception(f"No license in {file}")
|
raise Exception(f"No license in {file}")
|
||||||
|
|
||||||
|
|
||||||
def files():
|
def files(additional_ignored):
|
||||||
p = Path("")
|
p = Path("")
|
||||||
for pattern in FILES:
|
for pattern in FILES:
|
||||||
for file in sorted(p.glob(pattern)):
|
for file in sorted(p.glob(pattern)):
|
||||||
if file.name in IGNORED or not file.is_file():
|
if file.name in IGNORED or file.name in additional_ignored or not file.is_file():
|
||||||
continue
|
continue
|
||||||
yield file
|
yield file
|
||||||
|
|
||||||
def build(out, manifest):
|
def build(out, manifest, additional_ignored=set()):
|
||||||
with ZipFile(out, "w", compression=ZIP_DEFLATED, allowZip64=False, compresslevel=2) as zp:
|
with ZipFile(out, "w", compression=ZIP_DEFLATED, allowZip64=False) as zp:
|
||||||
for file in files():
|
for file in files(additional_ignored):
|
||||||
if str(file) == "manifest.json":
|
if str(file) == "manifest.json":
|
||||||
buf = manifest
|
buf = manifest
|
||||||
else:
|
else:
|
||||||
@ -68,7 +71,7 @@ def build(out, manifest):
|
|||||||
if file.suffix in UNCOMPRESSABLE:
|
if file.suffix in UNCOMPRESSABLE:
|
||||||
zp.writestr(zinfo, buf, compress_type=ZIP_STORED)
|
zp.writestr(zinfo, buf, compress_type=ZIP_STORED)
|
||||||
else:
|
else:
|
||||||
zp.writestr(zinfo, buf, compress_type=ZIP_DEFLATED, compresslevel=2)
|
zp.writestr(zinfo, buf, compress_type=ZIP_DEFLATED)
|
||||||
print(file)
|
print(file)
|
||||||
|
|
||||||
|
|
||||||
@ -85,14 +88,14 @@ def build_firefox(args):
|
|||||||
|
|
||||||
if args.mode != "release":
|
if args.mode != "release":
|
||||||
infos["version_name"] = f"{version}-{args.mode}"
|
infos["version_name"] = f"{version}-{args.mode}"
|
||||||
infos["browser_specific_settings"]["gecko"]["id"] = f"{args.mode}@downthemall.org"
|
infos["browser_specific_settings"]["gecko"]["id"] = f"{args.mode}@traitorousenterprises.net"
|
||||||
infos["short_name"] = infos.get("name")
|
infos["short_name"] = infos.get("name")
|
||||||
infos["name"] = f"{infos.get('name')} {args.mode}"
|
infos["name"] = f"{infos.get('name')} {args.mode}"
|
||||||
else:
|
else:
|
||||||
infos["browser_specific_settings"]["gecko"]["id"] = RELEASE_ID
|
infos["browser_specific_settings"]["gecko"]["id"] = RELEASE_ID
|
||||||
|
|
||||||
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_FX]
|
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_FX]
|
||||||
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-fx.zip"
|
out = Path("web-ext-artifacts") / f"tdl-{version}-{args.mode}-fx.zip"
|
||||||
if not out.parent.exists():
|
if not out.parent.exists():
|
||||||
out.parent.mkdir()
|
out.parent.mkdir()
|
||||||
if out.exists():
|
if out.exists():
|
||||||
@ -101,7 +104,7 @@ def build_firefox(args):
|
|||||||
build(out, json.dumps(infos, indent=2).encode("utf-8"))
|
build(out, json.dumps(infos, indent=2).encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def build_chrome(args):
|
def build_chromium(args, pkg, additional_ignored=set()):
|
||||||
now = datetime.now().strftime("%Y%m%d%H%M%S")
|
now = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
with open("manifest.json") as manip:
|
with open("manifest.json") as manip:
|
||||||
infos = json.load(manip, object_pairs_hook=OrderedDict)
|
infos = json.load(manip, object_pairs_hook=OrderedDict)
|
||||||
@ -119,13 +122,13 @@ def build_chrome(args):
|
|||||||
infos["name"] = f"{infos.get('name')} {args.mode}"
|
infos["name"] = f"{infos.get('name')} {args.mode}"
|
||||||
|
|
||||||
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_CHROME]
|
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_CHROME]
|
||||||
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-crx.zip"
|
out = Path("web-ext-artifacts") / f"tdl-{version}-{args.mode}-{pkg}.zip"
|
||||||
if not out.parent.exists():
|
if not out.parent.exists():
|
||||||
out.parent.mkdir()
|
out.parent.mkdir()
|
||||||
if out.exists():
|
if out.exists():
|
||||||
out.unlink()
|
out.unlink()
|
||||||
print("Output", out)
|
print("Output", out)
|
||||||
build(out, json.dumps(infos, indent=2).encode("utf-8"))
|
build(out, json.dumps(infos, indent=2).encode("utf-8"), additional_ignored=additional_ignored)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
@ -140,7 +143,8 @@ def main():
|
|||||||
else:
|
else:
|
||||||
run([script], shell=True)
|
run([script], shell=True)
|
||||||
build_firefox(args)
|
build_firefox(args)
|
||||||
build_chrome(args)
|
build_chromium(args, "crx")
|
||||||
|
build_chromium(args, "opr", IGNORED_OPERA)
|
||||||
print("DONE.")
|
print("DONE.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -6,7 +6,7 @@ langs = sorted(Path("_locales").glob("**/messages.json"), key=lambda p: p.parent
|
|||||||
all = {}
|
all = {}
|
||||||
for m in langs:
|
for m in langs:
|
||||||
loc = m.parent.name
|
loc = m.parent.name
|
||||||
with m.open("r") as mp:
|
with m.open("r", encoding="utf-8") as mp:
|
||||||
lang = json.load(mp).get("language").get("message")
|
lang = json.load(mp).get("language").get("message")
|
||||||
if not lang:
|
if not lang:
|
||||||
raise Exception(f"{m}: no language")
|
raise Exception(f"{m}: no language")
|
||||||
|