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
|
||||
about: Create a report to help us improve
|
||||
about: Create a report to help us improve DownThemAll!
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
@ -8,9 +8,9 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
- OS: [e.g. Windows 10, macOS, Linux (distribution, desktop environment)]
|
||||
- Browser and version: [e.g. Firefox 42, Chrome 70, Opera 15, Seamonkey 2.16]
|
||||
- DownThemAll! version: [e.g. 4.2, 3.0, latest]
|
||||
|
||||
**Describe the bug**
|
||||
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
|
||||
about: Suggest an idea for this project
|
||||
about: Suggest a feature or an idea for DownThemAll!
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
@ -81,3 +81,9 @@ The javascript library accessing it is licensed under the MIT license.
|
||||
## whatwg-mimetype
|
||||
|
||||
Licensed under MIT
|
||||
|
||||
|
||||
## CDHeaderParser
|
||||
|
||||
Licensed under MPL-2
|
||||
(c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
|
||||
|
47
Readme.md
@ -1,3 +1,7 @@
|
||||
|
||||

|
||||
|
||||
|
||||
# DownThemAll! WE
|
||||
|
||||
The DownThemAll! WebExtension.
|
||||
@ -27,28 +31,59 @@ If you would like to help out translating DTA, please see our [translation guide
|
||||
|
||||
## 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)
|
||||
|
||||
### Setup
|
||||
|
||||
Afterwards, you will want to run`yarn watch`.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
aka a lot
|
||||
|
||||
P2
|
||||
===
|
||||
|
||||
Planned for later.
|
||||
|
||||
* Soft errors and retry logic
|
||||
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
|
||||
* Delete files (well, as far as the browser allows)
|
||||
* Inter-addon API (basic)
|
||||
* Add downloads
|
||||
* Chrome support
|
||||
* vtable perf: cache column widths
|
||||
* 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.
|
||||
@ -30,11 +24,7 @@ Nice-to-haves.
|
||||
* Manipulate downloads (e.g. rewrite URLs)
|
||||
* Native context menus?
|
||||
* Would require massive reworks incl the need for new icon formats, but potentially feasible.
|
||||
* Import/Export
|
||||
* Download priorities (manual scheduling overrides)
|
||||
* Dark Theme support
|
||||
* os/browser define be default
|
||||
* overwritable
|
||||
* Remove `any` types as possible, and generally improve typescript (new language to me)
|
||||
|
||||
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.
|
||||
* Segmented downloads
|
||||
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
|
||||
* Handle errors, 404 and such
|
||||
* The Firefox download manager is too stupid and webRequest does not see Downloads, so cannot be done right now.
|
||||
* Conflicts: ask when a file exists
|
||||
* Not supported by Firefox
|
||||
* Speed limiter
|
||||
|
@ -1,18 +1,26 @@
|
||||
{
|
||||
"ar": "العربية [ar]",
|
||||
"bg": "Български [bg]",
|
||||
"cs": "Čeština (CZ) [cs]",
|
||||
"da": "Dansk [da]",
|
||||
"de": "Deutsch [de]",
|
||||
"el": "Ελληνικά [el]",
|
||||
"en": "English (US) [en]",
|
||||
"es": "Español (España) [es]",
|
||||
"et": "Eesti Keel [et]",
|
||||
"fr": "Français (FR) [fr]",
|
||||
"et": "Eesti keel [et]",
|
||||
"fr": "Français [fr]",
|
||||
"hu": "Magyar (HU) [hu]",
|
||||
"id": "Bahasa Indonesia [id]",
|
||||
"it": "Italiano [it]",
|
||||
"ja": "日本語 (JP) [ja]",
|
||||
"ko": "한국어 [ko]",
|
||||
"lt": "Lietuvių [lt]",
|
||||
"nl": "Nederlands [nl]",
|
||||
"pl": "Polski (PL) [pl]",
|
||||
"pl": "Polski [pl]",
|
||||
"pt": "Português (Brasil) [pt]",
|
||||
"ru": "Русский [ru]",
|
||||
"zh_CN": "简体中文 [zh_CN]",
|
||||
"zh_TW": "正體中文 [zh_TW]"
|
||||
"sv": "Svenska (SV) [sv]",
|
||||
"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",
|
||||
"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": {
|
||||
"message": "Popis",
|
||||
"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"
|
||||
},
|
||||
"dta_regular": {
|
||||
"message": "DownThemAll!",
|
||||
"message": "TraitorousDownloading!",
|
||||
"description": "Regular dta action; Menu text"
|
||||
},
|
||||
"dta_regular_all": {
|
||||
"message": "DownThemAll! - Všechny záložky",
|
||||
"message": "TraitorousDownloading! - Všechny záložky",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_image": {
|
||||
"message": "Stáhnout obrázek pomocí DownThemAll!",
|
||||
"message": "Stáhnout obrázek pomocí TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_link": {
|
||||
"message": "Stáhnout odkaz pomocí DownThemAll!",
|
||||
"message": "Stáhnout odkaz pomocí TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_media": {
|
||||
"message": "Stáhnout média pomocí DownThemAll!",
|
||||
"message": "Stáhnout média pomocí TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_selection": {
|
||||
"message": "Stáhnout vybrané pomocí DownThemAll!",
|
||||
"message": "Stáhnout vybrané pomocí TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_turbo": {
|
||||
@ -275,13 +291,37 @@
|
||||
"message": "Neplatná URL adresa",
|
||||
"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": {
|
||||
"message": "Nic není vybráno",
|
||||
"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": {
|
||||
"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": {
|
||||
"message": "Rychlé filtrování",
|
||||
@ -331,6 +371,10 @@
|
||||
"message": "Vynutit spuštění",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"import": {
|
||||
"message": "Importovat ze souboru",
|
||||
"description": "menu text"
|
||||
},
|
||||
"information_title": {
|
||||
"message": "Informace",
|
||||
"description": "Used in message boxes"
|
||||
@ -406,7 +450,7 @@
|
||||
}
|
||||
},
|
||||
"manager_title": {
|
||||
"message": "DownThemAll! Manažer",
|
||||
"message": "TraitorousDownloading! Manažer",
|
||||
"description": "Window/tab title"
|
||||
},
|
||||
"mask": {
|
||||
@ -442,7 +486,7 @@
|
||||
"description": "Action for moving a download up"
|
||||
},
|
||||
"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",
|
||||
"placeholders": {
|
||||
"downloads": {
|
||||
@ -522,13 +566,33 @@
|
||||
"description": "Menu text; Preferences"
|
||||
},
|
||||
"prefs_title": {
|
||||
"message": "Nastavení DownThemAll!",
|
||||
"message": "Nastavení TraitorousDownloading!",
|
||||
"description": "Window/tab title; Preferences"
|
||||
},
|
||||
"pref_add_paused": {
|
||||
"message": "Přidávat nová stahování pozastavená",
|
||||
"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": {
|
||||
"message": "Souběžná stahování",
|
||||
"description": "Preferences/Network"
|
||||
@ -537,10 +601,6 @@
|
||||
"message": "Zobrazit oznámení po dokončení fronty stahování",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_global_turbo": {
|
||||
"message": "Tlačítho na liště je OneClick!",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_hide_context": {
|
||||
"message": "Nezobrazovat v kontextové nabídce",
|
||||
"description": "Preferences/General"
|
||||
@ -549,8 +609,12 @@
|
||||
"message": "Manažer",
|
||||
"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": {
|
||||
"message": "Zobrazovat popisky v záložkách Manažeru",
|
||||
"message": "Zobrazovat popisky v záložkách Manažera",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_netglobal": {
|
||||
@ -558,7 +622,7 @@
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"pref_queueing": {
|
||||
@ -573,14 +637,42 @@
|
||||
"message": "Odebrat chybějící stahování po restartu",
|
||||
"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": {
|
||||
"message": "Zobrazit URL adresy místo názvů souborů",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_sounds": {
|
||||
"message": "Přehrát zvuky",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_text_links": {
|
||||
"message": "Vyhledat odkazy v textu stránky (pomalejší)",
|
||||
"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": {
|
||||
"message": "Uživatelské rozhraní",
|
||||
"description": "Preferences/General; group text"
|
||||
@ -895,6 +987,20 @@
|
||||
"message": "Pokračovat",
|
||||
"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": {
|
||||
"message": "Probíhá",
|
||||
"description": "Status text"
|
||||
@ -920,7 +1026,7 @@
|
||||
"description": "Menu text; select context"
|
||||
},
|
||||
"select_title": {
|
||||
"message": "DownThemAll! - Vyberte soubory ke stažení",
|
||||
"message": "TraitorousDownloading! - Vyberte soubory ke stažení",
|
||||
"description": "Title of the select window"
|
||||
},
|
||||
"SERVER_BAD_CONTENT": {
|
||||
@ -943,6 +1049,18 @@
|
||||
"message": "Nastavit masku přejmenování",
|
||||
"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": {
|
||||
"message": "Jsou podporována dávková stahování, například:",
|
||||
"description": "Header text; single window"
|
||||
@ -952,7 +1070,7 @@
|
||||
"description": "Header text; single window"
|
||||
},
|
||||
"single_title": {
|
||||
"message": "DownThemAll! - Přidat odkaz",
|
||||
"message": "TraitorousDownloading! - Přidat odkaz",
|
||||
"description": "Title of single window"
|
||||
},
|
||||
"sizeB": {
|
||||
@ -1123,6 +1241,14 @@
|
||||
"message": "Nová stahování nebudou spuštěna",
|
||||
"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": {
|
||||
"message": "Popisek",
|
||||
"description": "Column text; Title label (short)"
|
||||
@ -1166,5 +1292,9 @@
|
||||
"useonlyonce": {
|
||||
"message": "Použít pouze jednou",
|
||||
"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": "Διαγραφή",
|
||||
"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": {
|
||||
"message": "Περιγραφή",
|
||||
"description": "Description (keep it short); e.g. the description column in select"
|
||||
@ -275,13 +291,21 @@
|
||||
"message": "Μη έγκυρο URL",
|
||||
"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": {
|
||||
"message": "Δεν επιλέχθηκαν αντικείμενα",
|
||||
"description": "Error Message; select window"
|
||||
},
|
||||
"extensionDescription": {
|
||||
"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": {
|
||||
"message": "Γρήγορο Φιλτράρισμα",
|
||||
@ -529,6 +553,26 @@
|
||||
"message": "Προσθήκη νέων λήψεων σε παύση, αντί για άμεση εκκίνησή τους",
|
||||
"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": {
|
||||
"message": "Ταυτόχρονες λήψεις",
|
||||
"description": "Preferences/Network"
|
||||
@ -537,10 +581,6 @@
|
||||
"message": "Προβολή ειδοποίησης με την ολοκλήρωση της ουράς λήψεων",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_global_turbo": {
|
||||
"message": "Το κουμπί φυλλομετρητή πρέπει να είναι το ΜονόΚλικ!",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_hide_context": {
|
||||
"message": "Απόκρυψη αντικειμένων μενού γενικού περιεχομένου",
|
||||
"description": "Preferences/General"
|
||||
@ -549,6 +589,10 @@
|
||||
"message": "Διαχειριστής",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_manager_in_popup": {
|
||||
"message": "Άνοιγμα διαχειριστή σε νέο αναδυόμενο παράθυρο",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_manager_tooltip": {
|
||||
"message": "Προβολή επεξηγήσεων στις καρτέλες Διαχειριστή",
|
||||
"description": "Preferences/General"
|
||||
@ -573,10 +617,22 @@
|
||||
"message": "Απομάκρυνση αγνοημένων λήψεων μετά την επανεκκίνηση",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_retries": {
|
||||
"message": "Αριθμός επαναλήψεων λήψεων με προσωρινά σφάλματα",
|
||||
"description": "pref text"
|
||||
},
|
||||
"pref_retry_time": {
|
||||
"message": "Επανάληψη κάθε (σε λεπτά)",
|
||||
"description": "pref text"
|
||||
},
|
||||
"pref_show_urls": {
|
||||
"message": "Προβολή URLs αντί για Ονόματα",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_sounds": {
|
||||
"message": "Αναπαραγωγή ήχων",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_text_links": {
|
||||
"message": "Δοκιμή εύρεσης συνδέσμων στο κείμενο της ιστοσελίδας (αργό)",
|
||||
"description": "Preferences/General"
|
||||
@ -895,6 +951,20 @@
|
||||
"message": "Συνέχιση",
|
||||
"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": {
|
||||
"message": "Σε λειτουργία",
|
||||
"description": "Status text"
|
||||
@ -943,6 +1013,18 @@
|
||||
"message": "Ορισμός Μάσκας Μετονομασίας",
|
||||
"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": {
|
||||
"message": "Οι δέσμες υποστηρίζονται, π.χ.:",
|
||||
"description": "Header text; single window"
|
||||
@ -1123,6 +1205,14 @@
|
||||
"message": "Καμία νέα Λήψη δεν θα εκκινηθεί",
|
||||
"description": "Status bar tooltip; manager network icon"
|
||||
},
|
||||
"subfolder": {
|
||||
"message": "Υποφάκελος:",
|
||||
"description": "label text"
|
||||
},
|
||||
"subfolder_placeholder": {
|
||||
"message": "Τοποθέτηση αρχείων σε αυτό τον υποφάκελο μέσα στον φάκελο λήψεών σας",
|
||||
"description": "placeholder text within an input box"
|
||||
},
|
||||
"title": {
|
||||
"message": "Τίτλος",
|
||||
"description": "Column text; Title label (short)"
|
||||
@ -1166,5 +1256,9 @@
|
||||
"useonlyonce": {
|
||||
"message": "Χρήση μόνο μια φορά",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"add_paused_title": {
|
||||
@ -175,6 +175,22 @@
|
||||
"message": "Hapus",
|
||||
"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": {
|
||||
"message": "Keterangan",
|
||||
"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"
|
||||
},
|
||||
"dta_regular_all": {
|
||||
"message": "DownThemAll - Semua Tab",
|
||||
"message": "TraitorousDownloading - Semua Tab",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_image": {
|
||||
"message": "Simpan Gambar menggunakan DownThemAll!",
|
||||
"message": "Simpan Gambar menggunakan TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_link": {
|
||||
"message": "Simpan Tautan menggunakan DownThemAll!",
|
||||
"message": "Simpan Tautan menggunakan TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_media": {
|
||||
"message": "Simpan Media menggunakan DownThemAll!",
|
||||
"message": "Simpan Media menggunakan TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_selection": {
|
||||
"message": "Simpan Pilihan menggunakan DownThemAll!",
|
||||
"message": "Simpan Pilihan menggunakan TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_turbo_all": {
|
||||
@ -251,13 +267,37 @@
|
||||
"message": "URL Tidak Benar",
|
||||
"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": {
|
||||
"message": "Tidak ada item terpilih",
|
||||
"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": {
|
||||
"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": {
|
||||
"message": "Penyaringan Cepat",
|
||||
@ -280,15 +320,15 @@
|
||||
"description": "Message box title"
|
||||
},
|
||||
"filter_expression": {
|
||||
"message": "Ekspres-Filter",
|
||||
"message": "Ekspresi",
|
||||
"description": "Message box label"
|
||||
},
|
||||
"filter_label": {
|
||||
"message": "Label-Filter",
|
||||
"message": "Label",
|
||||
"description": "Message box label"
|
||||
},
|
||||
"filter_types": {
|
||||
"message": "Tipe-Filter",
|
||||
"message": "Tipe",
|
||||
"description": "Message box label"
|
||||
},
|
||||
"filter_type_link": {
|
||||
@ -307,6 +347,10 @@
|
||||
"message": "Paksa Mulai",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"import": {
|
||||
"message": "Impor Dari Berkas",
|
||||
"description": "menu text"
|
||||
},
|
||||
"information_title": {
|
||||
"message": "Informasi",
|
||||
"description": "Used in message boxes"
|
||||
@ -320,7 +364,7 @@
|
||||
"description": "Menu text"
|
||||
},
|
||||
"limited_to": {
|
||||
"message": "Terbatas ke",
|
||||
"message": "Batasi ke",
|
||||
"description": "Label text; used in prefs/network"
|
||||
},
|
||||
"links": {
|
||||
@ -354,7 +398,7 @@
|
||||
}
|
||||
},
|
||||
"manager_title": {
|
||||
"message": "Pengelola DownThemAll!",
|
||||
"message": "Pengelola TraitorousDownloading!",
|
||||
"description": "Window/tab title"
|
||||
},
|
||||
"mask_default": {
|
||||
@ -362,11 +406,11 @@
|
||||
"description": "Status text; Used in the mask column, select window"
|
||||
},
|
||||
"missing": {
|
||||
"message": "Tidak Ada",
|
||||
"message": "Hilang",
|
||||
"description": "Status text in manager"
|
||||
},
|
||||
"move_bottom": {
|
||||
"message": "Bawah",
|
||||
"message": "Ke Bawah",
|
||||
"description": "Action for moving a download to the bottom"
|
||||
},
|
||||
"move_down": {
|
||||
@ -374,7 +418,7 @@
|
||||
"description": "Action for moving a download down"
|
||||
},
|
||||
"move_top": {
|
||||
"message": "Atas",
|
||||
"message": "Ke Atas",
|
||||
"description": "Action for moving a download to the top"
|
||||
},
|
||||
"move_up": {
|
||||
@ -382,7 +426,7 @@
|
||||
"description": "Action for moving a download up"
|
||||
},
|
||||
"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",
|
||||
"placeholders": {
|
||||
"downloads": {
|
||||
@ -454,13 +498,29 @@
|
||||
"description": "Menu text; Preferences"
|
||||
},
|
||||
"prefs_title": {
|
||||
"message": "Preferensi DownThemAll!",
|
||||
"message": "Preferensi TraitorousDownloading!",
|
||||
"description": "Window/tab title; Preferences"
|
||||
},
|
||||
"pref_add_paused": {
|
||||
"message": "Tambahkan unduhan terpause, alih-alih langsung memulai mengunduh",
|
||||
"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": {
|
||||
"message": "Unduhan Bersamaan",
|
||||
"description": "Preferences/Network"
|
||||
@ -469,10 +529,6 @@
|
||||
"message": "Tampilkan pemberitahuan ketika antrian unduhan selesai",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_global_turbo": {
|
||||
"message": "Tombol peramban sebaiknya OneClick!",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_hide_context": {
|
||||
"message": "Jangan perlihatkan item menu general context",
|
||||
"description": "Preferences/General"
|
||||
@ -481,6 +537,10 @@
|
||||
"message": "Pengelola",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_manager_in_popup": {
|
||||
"message": "Buka pengelola di jendela popup baru",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_manager_tooltip": {
|
||||
"message": "Tampilkan tooltip di tab Pengelola",
|
||||
"description": "Preferences/General"
|
||||
@ -505,10 +565,22 @@
|
||||
"message": "Setelah restart, hapus unduhan yang tidak ada",
|
||||
"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": {
|
||||
"message": "Tampilan URL alih-alih Nama",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_sounds": {
|
||||
"message": "Bunyikan suara",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_text_links": {
|
||||
"message": "Coba cari tautan di teks di situs web (lambat)",
|
||||
"description": "Preferences/General"
|
||||
@ -560,7 +632,7 @@
|
||||
"description": "Menu text"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"remove_complete_downloads": {
|
||||
@ -600,7 +672,7 @@
|
||||
}
|
||||
},
|
||||
"remove_domain_downloads": {
|
||||
"message": "Hapus Domain Ini",
|
||||
"message": "Hapus Unduhan Dari Domain Ini",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"remove_domain_downloads_question": {
|
||||
@ -630,7 +702,7 @@
|
||||
"description": "Messagebox text"
|
||||
},
|
||||
"remove_failed_downloads": {
|
||||
"message": "Gagal Menghapus",
|
||||
"message": "Hapus Unduhan Gagal",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"remove_failed_downloads_question": {
|
||||
@ -648,11 +720,11 @@
|
||||
}
|
||||
},
|
||||
"remove_missing": {
|
||||
"message": "Hapus Unduhan Yang Tidak Ada",
|
||||
"message": "Hapus Unduhan Yang Hilang",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"remove_missing_downloads_question": {
|
||||
"message": "Hapus semua unduhan yang tidak ada?",
|
||||
"message": "Hapus semua unduhan yang hilang?",
|
||||
"description": "Messagebox text"
|
||||
},
|
||||
"remove_paused_downloads": {
|
||||
@ -664,7 +736,7 @@
|
||||
"description": "Messagebox text"
|
||||
},
|
||||
"remove_selected_complete_downloads": {
|
||||
"message": "Hapus Yang Selesai Di Pilihan",
|
||||
"message": "Hapus Yang Selesai Dari Unduhan Terpilih",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"remove_selected_complete_downloads_question": {
|
||||
@ -767,6 +839,10 @@
|
||||
"message": "Tanggal Ditambahkan - Detik",
|
||||
"description": "Mask text; see mask button"
|
||||
},
|
||||
"renamer_tags": {
|
||||
"message": "Penanda Mask Penamaan",
|
||||
"description": "Mask text; see mask button"
|
||||
},
|
||||
"renamer_text": {
|
||||
"message": "Teks Deskripsi",
|
||||
"description": "Mask text; see mask button"
|
||||
@ -783,6 +859,10 @@
|
||||
"message": "Tanggal Ditambahkan - Tahun",
|
||||
"description": "Mask text; see mask button"
|
||||
},
|
||||
"renmask": {
|
||||
"message": "Mask penamaan",
|
||||
"description": "Renaming mask (long)"
|
||||
},
|
||||
"reset": {
|
||||
"message": "Atur Ulang",
|
||||
"description": "Button text; pref window"
|
||||
@ -807,6 +887,20 @@
|
||||
"message": "Lanjutkan",
|
||||
"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": {
|
||||
"message": "Berjalan",
|
||||
"description": "Status text"
|
||||
@ -832,7 +926,7 @@
|
||||
"description": "Menu text; select context"
|
||||
},
|
||||
"select_title": {
|
||||
"message": "DownThemAll! - Pilih Unduhan Anda",
|
||||
"message": "TraitorousDownloading! - Pilih Unduhan Anda",
|
||||
"description": "Title of the select window"
|
||||
},
|
||||
"SERVER_BAD_CONTENT": {
|
||||
@ -851,6 +945,22 @@
|
||||
"message": "Tidak Diizinkan",
|
||||
"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": {
|
||||
"message": "Batch bisa digunakan, misalnya:",
|
||||
"description": "Header text; single window"
|
||||
@ -860,7 +970,7 @@
|
||||
"description": "Header text; single window"
|
||||
},
|
||||
"single_title": {
|
||||
"message": "DownThemAll! - Tambah tautan",
|
||||
"message": "TraitorousDownloading! - Tambah tautan",
|
||||
"description": "Title of single window"
|
||||
},
|
||||
"sizes_huge": {
|
||||
@ -971,6 +1081,10 @@
|
||||
"message": "Tidak ada Unduhan baru yang akan dimulai",
|
||||
"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": {
|
||||
"message": "Judul",
|
||||
"description": "Column text; Title label (short)"
|
||||
@ -1014,5 +1128,9 @@
|
||||
"useonlyonce": {
|
||||
"message": "Gunakan Sekali",
|
||||
"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": "삭제",
|
||||
"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": {
|
||||
"message": "설명",
|
||||
"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"
|
||||
},
|
||||
"dta_regular": {
|
||||
"message": "DownThemAll!",
|
||||
"message": "TraitorousDownloading!",
|
||||
"description": "Regular dta action; Menu text"
|
||||
},
|
||||
"dta_regular_all": {
|
||||
"message": "DownThemAll! - 모든 탭",
|
||||
"message": "TraitorousDownloading! - 모든 탭",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_image": {
|
||||
"message": "이미지를 DownThemAll!로 저장",
|
||||
"message": "이미지를 TraitorousDownloading!로 저장",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_link": {
|
||||
"message": "링크를 DownThemAll!로 저장",
|
||||
"message": "링크를 TraitorousDownloading!로 저장",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_media": {
|
||||
"message": "미디어를 DownThemAll!로 저장",
|
||||
"message": "미디어를 TraitorousDownloading!로 저장",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_selection": {
|
||||
"message": "선택영역을 DownThemAll!로 저장",
|
||||
"message": "선택영역을 TraitorousDownloading!로 저장",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_turbo": {
|
||||
@ -275,13 +291,37 @@
|
||||
"message": "잘못된 URL",
|
||||
"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": {
|
||||
"message": "선택된 항목 없음",
|
||||
"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": {
|
||||
"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": {
|
||||
"message": "빠른 필터링",
|
||||
@ -331,6 +371,10 @@
|
||||
"message": "강제 시작",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"import": {
|
||||
"message": "파일에서 가져오기",
|
||||
"description": "menu text"
|
||||
},
|
||||
"information_title": {
|
||||
"message": "정보",
|
||||
"description": "Used in message boxes"
|
||||
@ -406,7 +450,7 @@
|
||||
}
|
||||
},
|
||||
"manager_title": {
|
||||
"message": "DownThemAll! 관리자",
|
||||
"message": "TraitorousDownloading! 관리자",
|
||||
"description": "Window/tab title"
|
||||
},
|
||||
"mask": {
|
||||
@ -442,7 +486,7 @@
|
||||
"description": "Action for moving a download up"
|
||||
},
|
||||
"nagging_message": {
|
||||
"message": "지금까지 DownThemAll!로 $DOWNLOADS$ 다운로드를 추가하셨습니다. 일반 사용자로서 추가 개발을 지원하기 위해 기부를 고려할 수 있습니다. 감사합니다!",
|
||||
"message": "지금까지 TraitorousDownloading!로 $DOWNLOADS$ 다운로드를 추가하셨습니다. 일반 사용자로서 추가 개발을 지원하기 위해 기부를 고려할 수 있습니다. 감사합니다!",
|
||||
"description": "Donation nagging message; displayed as a notification bar in manager",
|
||||
"placeholders": {
|
||||
"downloads": {
|
||||
@ -522,13 +566,33 @@
|
||||
"description": "Menu text; Preferences"
|
||||
},
|
||||
"prefs_title": {
|
||||
"message": "DownThemAll! 설정",
|
||||
"message": "TraitorousDownloading! 설정",
|
||||
"description": "Window/tab title; Preferences"
|
||||
},
|
||||
"pref_add_paused": {
|
||||
"message": "새 다운로드를 즉시 시작하는 대신 일시중지로 추가",
|
||||
"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": {
|
||||
"message": "동시 다운로드",
|
||||
"description": "Preferences/Network"
|
||||
@ -537,10 +601,6 @@
|
||||
"message": "다운로드 대기열이 끝나면 알림 표시",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_global_turbo": {
|
||||
"message": "브라우저 버튼을 OneClick!으로 작동",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_hide_context": {
|
||||
"message": "일반 컨텍스트 메뉴 항목 표시 안 함",
|
||||
"description": "Preferences/General"
|
||||
@ -549,6 +609,10 @@
|
||||
"message": "관리자",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_manager_in_popup": {
|
||||
"message": "새 팝업 창에 관리자 열기",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_manager_tooltip": {
|
||||
"message": "관리자 탭에서 툴팁 표시",
|
||||
"description": "Preferences/General"
|
||||
@ -573,10 +637,22 @@
|
||||
"message": "다시 시작한 후 누락된 다운로드 제거",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_retries": {
|
||||
"message": "일시적인 오류에 대한 다운로드 재시도 횟수",
|
||||
"description": "pref text"
|
||||
},
|
||||
"pref_retry_time": {
|
||||
"message": "재시도 간격 (분)",
|
||||
"description": "pref text"
|
||||
},
|
||||
"pref_show_urls": {
|
||||
"message": "이름 대신 URL 표시",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_sounds": {
|
||||
"message": "소리 재생",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_text_links": {
|
||||
"message": "웹 사이트 텍스트에서 링크를 찾도록 시도 (느림)",
|
||||
"description": "Preferences/General"
|
||||
@ -895,6 +971,20 @@
|
||||
"message": "계속",
|
||||
"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": {
|
||||
"message": "실행중",
|
||||
"description": "Status text"
|
||||
@ -920,7 +1010,7 @@
|
||||
"description": "Menu text; select context"
|
||||
},
|
||||
"select_title": {
|
||||
"message": "DownThemAll! - 다운로드 선택",
|
||||
"message": "TraitorousDownloading! - 다운로드 선택",
|
||||
"description": "Title of the select window"
|
||||
},
|
||||
"SERVER_BAD_CONTENT": {
|
||||
@ -943,6 +1033,18 @@
|
||||
"message": "이름 바꾸기 마스크 설정",
|
||||
"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": {
|
||||
"message": "일괄 처리가 지원됩니다, 예:",
|
||||
"description": "Header text; single window"
|
||||
@ -952,7 +1054,7 @@
|
||||
"description": "Header text; single window"
|
||||
},
|
||||
"single_title": {
|
||||
"message": "DownThemAll! - 링크 추가",
|
||||
"message": "TraitorousDownloading! - 링크 추가",
|
||||
"description": "Title of single window"
|
||||
},
|
||||
"sizeB": {
|
||||
@ -1123,6 +1225,14 @@
|
||||
"message": "새 다운로드가 시작되지 않습니다",
|
||||
"description": "Status bar tooltip; manager network icon"
|
||||
},
|
||||
"subfolder": {
|
||||
"message": "하위 폴더",
|
||||
"description": "label text"
|
||||
},
|
||||
"subfolder_placeholder": {
|
||||
"message": "다운로드 디렉토리 내의 이 하위 폴더에 파일을 저장합니다",
|
||||
"description": "placeholder text within an input box"
|
||||
},
|
||||
"title": {
|
||||
"message": "제목",
|
||||
"description": "Column text; Title label (short)"
|
||||
@ -1166,5 +1276,9 @@
|
||||
"useonlyonce": {
|
||||
"message": "한번만",
|
||||
"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"
|
||||
},
|
||||
"add_download": {
|
||||
"message": "Добавить закачку",
|
||||
"message": "добавить закачку",
|
||||
"description": "Action for adding a download"
|
||||
},
|
||||
"add_new": {
|
||||
@ -191,6 +191,22 @@
|
||||
"message": "Удалить",
|
||||
"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": {
|
||||
"message": "Описание",
|
||||
"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"
|
||||
},
|
||||
"dta_regular": {
|
||||
"message": "DownThemAll!",
|
||||
"message": "TraitorousDownloading!",
|
||||
"description": "Regular dta action; Menu text"
|
||||
},
|
||||
"dta_regular_all": {
|
||||
"message": "DownThemAll! - Все вкладки",
|
||||
"message": "TraitorousDownloading! - Все вкладки",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_image": {
|
||||
"message": "Закачать изображение через DownThemAll!",
|
||||
"message": "Закачать изображение через TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_link": {
|
||||
"message": "Закачать ссылку через DownThemAll!",
|
||||
"message": "Закачать ссылку через TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_media": {
|
||||
"message": "Закачать медиа через DownThemAll!",
|
||||
"message": "Закачать медиа через TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_regular_selection": {
|
||||
"message": "Закачать выделенное через DownThemAll!",
|
||||
"message": "Закачать выделенное через TraitorousDownloading!",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"dta_turbo": {
|
||||
@ -275,13 +291,37 @@
|
||||
"message": "Неправильная ссылка",
|
||||
"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": {
|
||||
"message": "Ничего не выбрано",
|
||||
"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": {
|
||||
"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": {
|
||||
"message": "Быстрый фильтр",
|
||||
@ -331,6 +371,10 @@
|
||||
"message": "Принудительный старт",
|
||||
"description": "Menu text"
|
||||
},
|
||||
"import": {
|
||||
"message": "Импортировать из файла",
|
||||
"description": "menu text"
|
||||
},
|
||||
"information_title": {
|
||||
"message": "Информация",
|
||||
"description": "Used in message boxes"
|
||||
@ -406,7 +450,7 @@
|
||||
}
|
||||
},
|
||||
"manager_title": {
|
||||
"message": "DownThemAll! Менеджер",
|
||||
"message": "TraitorousDownloading! Менеджер",
|
||||
"description": "Window/tab title"
|
||||
},
|
||||
"mask": {
|
||||
@ -442,7 +486,7 @@
|
||||
"description": "Action for moving a download up"
|
||||
},
|
||||
"nagging_message": {
|
||||
"message": "Добавлено $DOWNLOADS$ закачек через DownThemAll! на текущий момент! Как постоянный пользователь подумайте над тем чтобы поддержать дальнейшую разработку. Спасибо!",
|
||||
"message": "Добавлено $DOWNLOADS$ закачек через TraitorousDownloading! на текущий момент! Как постоянный пользователь подумайте над тем чтобы поддержать дальнейшую разработку. Спасибо!",
|
||||
"description": "Donation nagging message; displayed as a notification bar in manager",
|
||||
"placeholders": {
|
||||
"downloads": {
|
||||
@ -522,13 +566,33 @@
|
||||
"description": "Menu text; Preferences"
|
||||
},
|
||||
"prefs_title": {
|
||||
"message": "Настройки DownThemAll!",
|
||||
"message": "Настройки TraitorousDownloading!",
|
||||
"description": "Window/tab title; Preferences"
|
||||
},
|
||||
"pref_add_paused": {
|
||||
"message": "Добавлять закачки приостановленными, вместо того чтобы качать сразу",
|
||||
"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": {
|
||||
"message": "Настройки/Сеть",
|
||||
"description": "Preferences/Network"
|
||||
@ -537,10 +601,6 @@
|
||||
"message": "Показывать уведомление когда список закачек завешен",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_global_turbo": {
|
||||
"message": "Использовать OneClick! по умолчанию на кнопке в браузере",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_hide_context": {
|
||||
"message": "Не добавлять пункты в общее контекстное меню",
|
||||
"description": "Preferences/General"
|
||||
@ -549,6 +609,10 @@
|
||||
"message": "Менеджер",
|
||||
"description": "Preferences/General; group text"
|
||||
},
|
||||
"pref_manager_in_popup": {
|
||||
"message": "Открывать менеджер в новом всплывающем окне",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_manager_tooltip": {
|
||||
"message": "Показывать подсказки на вкладках менеджера",
|
||||
"description": "Preferences/General"
|
||||
@ -573,14 +637,42 @@
|
||||
"message": "Удалять неудавшиеся закачки после перезапуска",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_retries": {
|
||||
"message": "Сколько раз пробовать перезапустить закачку при некритичных сбоях",
|
||||
"description": "pref text"
|
||||
},
|
||||
"pref_retry_time": {
|
||||
"message": "Как часто пробовать перезапустить закачку (в минутах)",
|
||||
"description": "pref text"
|
||||
},
|
||||
"pref_show_urls": {
|
||||
"message": "Показывать ссылки вместо имён",
|
||||
"description": "Preferences/General"
|
||||
},
|
||||
"pref_sounds": {
|
||||
"message": "Проигрывать звуки",
|
||||
"description": "checkbox text"
|
||||
},
|
||||
"pref_text_links": {
|
||||
"message": "Пытаться обнаружить ссылки в тексте на сайте",
|
||||
"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": {
|
||||
"message": "Интерфейс пользователя",
|
||||
"description": "Preferences/General; group text"
|
||||
@ -895,6 +987,20 @@
|
||||
"message": "Продолжить",
|
||||
"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": {
|
||||
"message": "В процессе",
|
||||
"description": "Status text"
|
||||
@ -920,7 +1026,7 @@
|
||||
"description": "Menu text; select context"
|
||||
},
|
||||
"select_title": {
|
||||
"message": "DownThemAll! - Выбрать ваши закачки",
|
||||
"message": "TraitorousDownloading! - Выбрать ваши закачки",
|
||||
"description": "Title of the select window"
|
||||
},
|
||||
"SERVER_BAD_CONTENT": {
|
||||
@ -943,6 +1049,18 @@
|
||||
"message": "Задать маску переименования",
|
||||
"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": {
|
||||
"message": "Можно задать группы, к примеру:",
|
||||
"description": "Header text; single window"
|
||||
@ -952,7 +1070,7 @@
|
||||
"description": "Header text; single window"
|
||||
},
|
||||
"single_title": {
|
||||
"message": "DownThemAll! - Добавить ссылку",
|
||||
"message": "TraitorousDownloading! - Добавить ссылку",
|
||||
"description": "Title of single window"
|
||||
},
|
||||
"sizeB": {
|
||||
@ -1123,6 +1241,14 @@
|
||||
"message": "Новые закачки запускаться не будут",
|
||||
"description": "Status bar tooltip; manager network icon"
|
||||
},
|
||||
"subfolder": {
|
||||
"message": "Подпапка",
|
||||
"description": "label text"
|
||||
},
|
||||
"subfolder_placeholder": {
|
||||
"message": "Размещать файлы в подпапке выбранного пути для закачек",
|
||||
"description": "placeholder text within an input box"
|
||||
},
|
||||
"title": {
|
||||
"message": "Заголовок",
|
||||
"description": "Column text; Title label (short)"
|
||||
@ -1166,5 +1292,9 @@
|
||||
"useonlyonce": {
|
||||
"message": "Только сейчас",
|
||||
"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": {
|
||||
"label": "Audio",
|
||||
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape|opus)$/i",
|
||||
"type": 1,
|
||||
"type": 3,
|
||||
"active": false,
|
||||
"icon": "mp3"
|
||||
},
|
||||
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
"global-turbo": false,
|
||||
"button-type": "popup",
|
||||
"manager-in-popup": false,
|
||||
"concurrent": 4,
|
||||
"queue-notification": true,
|
||||
"finish-notification": true,
|
||||
"sounds": true,
|
||||
"open-manager-on-queue": true,
|
||||
"text-links": true,
|
||||
"add-paused": false,
|
||||
@ -13,6 +15,9 @@
|
||||
"tooltip": true,
|
||||
"show-urls": false,
|
||||
"remove-missing-on-init": false,
|
||||
"retries": 5,
|
||||
"retry-time": 10,
|
||||
"theme": "default",
|
||||
"limits": [
|
||||
{
|
||||
"domain": "*",
|
||||
|
25
lib/api.ts
@ -11,7 +11,7 @@ import { getManager } from "./manager/man";
|
||||
import { select } from "./select";
|
||||
import { single } from "./single";
|
||||
import { Notification } from "./notifications";
|
||||
import { MASK, FASTFILTER } from "./recentlist";
|
||||
import { MASK, FASTFILTER, SUBFOLDER, SERVER } from "./recentlist";
|
||||
import { openManager } from "./windowutils";
|
||||
import { _ } from "./i18n";
|
||||
|
||||
@ -19,7 +19,10 @@ const MAX_BATCH = 10000;
|
||||
|
||||
export interface QueueOptions {
|
||||
mask?: string;
|
||||
subfolder?: string;
|
||||
server?: string;
|
||||
paused?: boolean;
|
||||
cookies?: boolean;
|
||||
}
|
||||
|
||||
export const API = new class APIImpl {
|
||||
@ -28,10 +31,13 @@ export const API = new class APIImpl {
|
||||
}
|
||||
|
||||
async queue(items: BaseItem[], options: QueueOptions) {
|
||||
await MASK.init();
|
||||
await Promise.all([MASK.init(), SUBFOLDER.init()]);
|
||||
const {mask = MASK.current} = options;
|
||||
const {subfolder = SUBFOLDER.current} = options;
|
||||
const {server = SERVER.current} = options;
|
||||
|
||||
const {paused = false} = options;
|
||||
const {cookies = false} = options;
|
||||
const defaults: any = {
|
||||
_idx: 0,
|
||||
get idx() {
|
||||
@ -46,8 +52,11 @@ export const API = new class APIImpl {
|
||||
private: false,
|
||||
postData: null,
|
||||
mask,
|
||||
subfolder,
|
||||
server,
|
||||
date: Date.now(),
|
||||
paused
|
||||
paused,
|
||||
cookies,
|
||||
};
|
||||
let currentBatch = await Prefs.get("currentBatch", 0);
|
||||
const initialBatch = currentBatch;
|
||||
@ -77,7 +86,7 @@ export const API = new class APIImpl {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -117,6 +126,14 @@ export const API = new class APIImpl {
|
||||
await FASTFILTER.init();
|
||||
await FASTFILTER.push(options.fast);
|
||||
}
|
||||
if (typeof options.subfolder === "string" && !options.subfolderOnce) {
|
||||
await SUBFOLDER.init();
|
||||
await SUBFOLDER.push(options.subfolder);
|
||||
}
|
||||
if (typeof options.server === "string" && !options.serverOnce) {
|
||||
await SERVER.init();
|
||||
await SERVER.push(options.server);
|
||||
}
|
||||
if (typeof options.type === "string") {
|
||||
await Prefs.set("last-type", options.type);
|
||||
}
|
||||
|
@ -19,9 +19,15 @@ import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MenuClickInfo,
|
||||
CHROME,
|
||||
runtime,
|
||||
history,
|
||||
sessions,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
OnInstalled,
|
||||
} from "./browser";
|
||||
import { Bus } from "./bus";
|
||||
import { filterInSitu } from "./util";
|
||||
import { DB } from "./db";
|
||||
|
||||
|
||||
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) {
|
||||
try {
|
||||
if (tab && tab.incognito && msg) {
|
||||
msg.private = tab.incognito;
|
||||
}
|
||||
const res = await tabs.executeScript(tab.id, {
|
||||
file,
|
||||
allFrames: true,
|
||||
@ -98,19 +107,19 @@ class Handler {
|
||||
|
||||
async performSelection(options: SelectionOptions) {
|
||||
try {
|
||||
const toptions: any = {
|
||||
const tabOptions: any = {
|
||||
currentWindow: true,
|
||||
discarded: false,
|
||||
};
|
||||
if (!CHROME) {
|
||||
toptions.hidden = false;
|
||||
tabOptions.hidden = false;
|
||||
}
|
||||
const selectedTabs = options.allTabs ?
|
||||
await tabs.query(toptions) as any[] :
|
||||
await tabs.query(tabOptions) as any[] :
|
||||
[options.tab];
|
||||
|
||||
const textLinks = await Prefs.get("text-links", true);
|
||||
const goptions = {
|
||||
const gatherOptions = {
|
||||
type: "DTA:gather",
|
||||
selectionOnly: options.selectionOnly,
|
||||
textLinks,
|
||||
@ -119,7 +128,7 @@ class Handler {
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
@ -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(() => {
|
||||
new class Action extends Handler {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
action.onClicked.addListener(this.onClicked);
|
||||
}
|
||||
|
||||
async onClicked(tab: {id: number}) {
|
||||
if (!tab.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.processResults(
|
||||
true,
|
||||
await runContentJob(
|
||||
tab, "/bundles/content-gather.js", {
|
||||
type: "DTA:gather",
|
||||
selectionOnly: false,
|
||||
textLinks: await Prefs.get("text-links", true),
|
||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
||||
transferable: TRANSFERABLE_PROPERTIES,
|
||||
}));
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
const menuHandler = new class Menus extends Handler {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
const alls = new Map<string, string[]>();
|
||||
const mcreate = (options: any) => {
|
||||
const menuCreate = (options: any) => {
|
||||
if (CHROME) {
|
||||
delete options.icons;
|
||||
options.contexts = options.contexts.
|
||||
@ -178,7 +185,7 @@ locale.then(() => {
|
||||
}
|
||||
menus.create(options);
|
||||
};
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTARegularLink",
|
||||
contexts: ["link"],
|
||||
icons: {
|
||||
@ -187,7 +194,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.regular.link"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTATurboLink",
|
||||
contexts: ["link"],
|
||||
icons: {
|
||||
@ -196,7 +203,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.turbo.link"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTARegularImage",
|
||||
contexts: ["image"],
|
||||
icons: {
|
||||
@ -205,7 +212,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.regular.image"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTATurboImage",
|
||||
contexts: ["image"],
|
||||
icons: {
|
||||
@ -214,7 +221,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.turbo.image"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTARegularMedia",
|
||||
contexts: ["video", "audio"],
|
||||
icons: {
|
||||
@ -223,7 +230,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.regular.media"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTATurboMedia",
|
||||
contexts: ["video", "audio"],
|
||||
icons: {
|
||||
@ -232,7 +239,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.turbo.media"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTARegularSelection",
|
||||
contexts: ["selection"],
|
||||
icons: {
|
||||
@ -241,7 +248,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.regular.selection"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTATurboSelection",
|
||||
contexts: ["selection"],
|
||||
icons: {
|
||||
@ -250,7 +257,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.turbo.selection"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTARegular",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -259,7 +266,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.regular"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTATurbo",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -268,12 +275,12 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta.turbo"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "sep-1",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
type: "separator"
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTARegularAll",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -282,7 +289,7 @@ locale.then(() => {
|
||||
},
|
||||
title: _("dta-regular-all"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTATurboAll",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -294,12 +301,12 @@ locale.then(() => {
|
||||
const sep2ctx = menus.ACTION_MENU_TOP_LEVEL_LIMIT === 6 ?
|
||||
["all", "tools_menu"] :
|
||||
["all", "browser_action", "tools_menu"];
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "sep-2",
|
||||
contexts: sep2ctx,
|
||||
type: "separator"
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "DTAAdd",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -310,12 +317,12 @@ locale.then(() => {
|
||||
},
|
||||
title: _("add-download"),
|
||||
});
|
||||
mcreate({
|
||||
menuCreate({
|
||||
id: "sep-3",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
type: "separator"
|
||||
});
|
||||
mcreate({
|
||||
/* menuCreate({
|
||||
id: "DTAManager",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -323,8 +330,8 @@ locale.then(() => {
|
||||
32: "/style/button-manager@2x.png",
|
||||
},
|
||||
title: _("manager.short"),
|
||||
});
|
||||
mcreate({
|
||||
});*/
|
||||
menuCreate({
|
||||
id: "DTAPrefs",
|
||||
contexts: ["all", "browser_action", "tools_menu"],
|
||||
icons: {
|
||||
@ -415,7 +422,7 @@ locale.then(() => {
|
||||
}
|
||||
}
|
||||
|
||||
async enumulate(action: string) {
|
||||
async emulate(action: string) {
|
||||
const tab = await tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
@ -537,40 +544,133 @@ locale.then(() => {
|
||||
}
|
||||
}();
|
||||
|
||||
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
|
||||
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
|
||||
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
|
||||
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
|
||||
new class Action extends Handler {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClicked = this.onClicked.bind(this);
|
||||
action.onClicked.addListener(this.onClicked);
|
||||
Prefs.get("button-type", false).then(v => this.adjust(v));
|
||||
Prefs.on("button-type", (prefs, key, value) => {
|
||||
this.adjust(value);
|
||||
});
|
||||
}
|
||||
|
||||
adjust(type: string) {
|
||||
action.setPopup({
|
||||
popup: type !== "popup" ? "" : "/windows/popup.html"
|
||||
});
|
||||
let icons;
|
||||
switch (type) {
|
||||
case "popup":
|
||||
icons = {
|
||||
16: "/style/icon16.png",
|
||||
32: "/style/icon32.png",
|
||||
48: "/style/icon48.png",
|
||||
64: "/style/icon64.png",
|
||||
128: "/style/icon128.png",
|
||||
256: "/style/icon256.png"
|
||||
};
|
||||
break;
|
||||
|
||||
case "dta":
|
||||
icons = {
|
||||
16: "/style/button-regular.png",
|
||||
32: "/style/button-regular@2x.png",
|
||||
};
|
||||
break;
|
||||
|
||||
case "turbo":
|
||||
icons = {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
};
|
||||
break;
|
||||
|
||||
case "manager":
|
||||
icons = {
|
||||
16: "/style/button-manager.png",
|
||||
32: "/style/button-manager@2x.png",
|
||||
};
|
||||
break;
|
||||
}
|
||||
action.setIcon({path: icons});
|
||||
}
|
||||
|
||||
async onClicked() {
|
||||
switch (await Prefs.get("button-type")) {
|
||||
case "popup":
|
||||
break;
|
||||
|
||||
case "dta":
|
||||
menuHandler.emulate("DTARegular");
|
||||
break;
|
||||
|
||||
case "turbo":
|
||||
menuHandler.emulate("DTATurbo");
|
||||
break;
|
||||
|
||||
case "manager":
|
||||
menuHandler.emulate("DTAManager");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
|
||||
Bus.on("do-regular", () => menuHandler.emulate("DTARegular"));
|
||||
Bus.on("do-regular-all", () => menuHandler.emulate("DTARegularAll"));
|
||||
Bus.on("do-turbo", () => menuHandler.emulate("DTATurbo"));
|
||||
Bus.on("do-turbo-all", () => menuHandler.emulate("DTATurboAll"));
|
||||
Bus.on("do-single", () => API.singleRegular(null));
|
||||
Bus.on("open-manager", () => openManager(true));
|
||||
Bus.on("open-prefs", () => openPrefs());
|
||||
|
||||
function adjustAction(globalTurbo: boolean) {
|
||||
action.setPopup({
|
||||
popup: globalTurbo ? "" : "/windows/popup.html"
|
||||
});
|
||||
action.setIcon({
|
||||
path: globalTurbo ? {
|
||||
16: "/style/button-turbo.png",
|
||||
32: "/style/button-turbo@2x.png",
|
||||
} : {
|
||||
16: "/style/icon16.png",
|
||||
32: "/style/icon32.png",
|
||||
48: "/style/icon48.png",
|
||||
64: "/style/icon64.png",
|
||||
96: "/style/icon96.png",
|
||||
128: "/style/icon128.png",
|
||||
256: "/style/icon256.png"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
await Prefs.set("last-run", new Date());
|
||||
Prefs.get("global-turbo", false).then(v => adjustAction(v));
|
||||
Prefs.on("global-turbo", (prefs, key, value) => {
|
||||
adjustAction(value);
|
||||
const urlBase = runtime.getURL("");
|
||||
history.onVisited.addListener(({url}: {url: string}) => {
|
||||
if (!url || !url.startsWith(urlBase)) {
|
||||
return;
|
||||
}
|
||||
history.deleteUrl({url});
|
||||
});
|
||||
const results: {url?: string}[] = await history.search({text: urlBase});
|
||||
for (const {url} of results) {
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
history.deleteUrl({url});
|
||||
}
|
||||
|
||||
if (!CHROME) {
|
||||
const sessionRemover = async () => {
|
||||
for (const s of await sessions.getRecentlyClosed()) {
|
||||
if (s.tab) {
|
||||
if (s.tab.url.startsWith(urlBase)) {
|
||||
await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!s.window || !s.window.tabs || s.window.tabs.length > 1) {
|
||||
continue;
|
||||
}
|
||||
const [tab] = s.window.tabs;
|
||||
if (tab.url.startsWith(urlBase)) {
|
||||
await sessions.forgetClosedWindow(s.window.sessionId);
|
||||
}
|
||||
}
|
||||
};
|
||||
sessions.onChanged.addListener(sessionRemover);
|
||||
await sessionRemover();
|
||||
}
|
||||
|
||||
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 getManager();
|
||||
})().catch(ex => {
|
||||
|
@ -73,7 +73,7 @@ class Numeral implements Generator {
|
||||
this.digits = dir ? rawpieces[0].length : rawpieces[1].length;
|
||||
this.length = Math.floor(
|
||||
(this.stop - this.start + (dir ? 1 : -1)) / this.step);
|
||||
this.preview = this[Symbol.iterator]().next().value;
|
||||
this.preview = this[Symbol.iterator]().next().value as string;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
@ -93,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 {
|
||||
private readonly gens: Generator[];
|
||||
|
||||
@ -120,9 +174,14 @@ export class BatchGenerator implements Generator {
|
||||
try {
|
||||
this.gens.push(new Numeral(tok));
|
||||
}
|
||||
catch (ex) {
|
||||
this.gens.push(new Literal(`[${tok}]`));
|
||||
this.hasInvalid = true;
|
||||
catch {
|
||||
try {
|
||||
this.gens.push(new Character(tok));
|
||||
}
|
||||
catch {
|
||||
this.gens.push(new Literal(`[${tok}]`));
|
||||
this.hasInvalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (str) {
|
||||
|
103
lib/browser.ts
@ -9,47 +9,112 @@ interface ExtensionListener {
|
||||
}
|
||||
|
||||
export interface MessageSender {
|
||||
tab?: Tab;
|
||||
frameId?: number;
|
||||
id?: number;
|
||||
url?: string;
|
||||
tlsChannelId?: string;
|
||||
readonly tab?: Tab;
|
||||
readonly frameId?: number;
|
||||
readonly id?: number;
|
||||
readonly url?: string;
|
||||
readonly tlsChannelId?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Tab {
|
||||
id?: number;
|
||||
readonly id?: number;
|
||||
readonly incognito?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuClickInfo {
|
||||
menuItemId: string | number;
|
||||
button?: number;
|
||||
linkUrl?: string;
|
||||
srcUrl?: string;
|
||||
readonly menuItemId: string | number;
|
||||
readonly button?: number;
|
||||
readonly linkUrl?: string;
|
||||
readonly srcUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface RawPort {
|
||||
error: any;
|
||||
name: string;
|
||||
onDisconnect: ExtensionListener;
|
||||
onMessage: ExtensionListener;
|
||||
sender?: MessageSender;
|
||||
readonly error: any;
|
||||
readonly name: string;
|
||||
readonly sender?: MessageSender;
|
||||
readonly onDisconnect: ExtensionListener;
|
||||
readonly onMessage: ExtensionListener;
|
||||
disconnect: () => void;
|
||||
postMessage: (message: any) => void;
|
||||
}
|
||||
|
||||
export const {extension} = polyfill;
|
||||
export const {notifications} = polyfill;
|
||||
interface WebRequestFilter {
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
interface WebRequestListener {
|
||||
addListener(
|
||||
callback: Function,
|
||||
filter: WebRequestFilter,
|
||||
extraInfoSpec: string[]
|
||||
): void;
|
||||
removeListener(callback: Function): void;
|
||||
}
|
||||
|
||||
type Header = {name: string; value: string};
|
||||
|
||||
export interface DownloadOptions {
|
||||
conflictAction: string;
|
||||
filename?: string;
|
||||
saveAs: boolean;
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
incognito?: boolean;
|
||||
headers: Header[];
|
||||
}
|
||||
|
||||
export interface DownloadsQuery {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface Downloads {
|
||||
download(download: DownloadOptions): Promise<number>;
|
||||
open(manId: number): Promise<void>;
|
||||
show(manId: number): Promise<void>;
|
||||
pause(manId: number): Promise<void>;
|
||||
resume(manId: number): Promise<void>;
|
||||
cancel(manId: number): Promise<void>;
|
||||
erase(query: DownloadsQuery): Promise<void>;
|
||||
search(query: DownloadsQuery): Promise<any[]>;
|
||||
getFileIcon(id: number, options?: any): Promise<string>;
|
||||
setShelfEnabled(state: boolean): void;
|
||||
removeFile(manId: number): Promise<void>;
|
||||
readonly onCreated: ExtensionListener;
|
||||
readonly onChanged: ExtensionListener;
|
||||
readonly onErased: ExtensionListener;
|
||||
readonly onDeterminingFilename?: ExtensionListener;
|
||||
}
|
||||
|
||||
interface WebRequest {
|
||||
readonly onBeforeSendHeaders: WebRequestListener;
|
||||
readonly onSendHeaders: WebRequestListener;
|
||||
readonly onHeadersReceived: WebRequestListener;
|
||||
}
|
||||
|
||||
export interface OnInstalled {
|
||||
readonly reason: string;
|
||||
readonly previousVersion?: string;
|
||||
readonly temporary: boolean;
|
||||
}
|
||||
|
||||
export const {browserAction} = polyfill;
|
||||
export const {contextMenus} = polyfill;
|
||||
export const {downloads} = polyfill;
|
||||
export const {downloads}: {downloads: Downloads} = polyfill;
|
||||
export const {extension} = polyfill;
|
||||
export const {history} = polyfill;
|
||||
export const {menus} = polyfill;
|
||||
export const {notifications} = polyfill;
|
||||
export const {runtime} = polyfill;
|
||||
export const {sessions} = polyfill;
|
||||
export const {storage} = polyfill;
|
||||
export const {tabs} = polyfill;
|
||||
export const {webNavigation} = polyfill;
|
||||
export const {webRequest} = polyfill;
|
||||
export const {webRequest}: {webRequest: WebRequest} = polyfill;
|
||||
export const {windows} = polyfill;
|
||||
export const {theme} = polyfill;
|
||||
|
||||
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 {
|
||||
private port: RawPort | null;
|
||||
|
||||
private disconnected = false;
|
||||
|
||||
constructor(port: RawPort) {
|
||||
super();
|
||||
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
|
||||
if (port.sender && port.sender.tab && port.sender.tab.id) {
|
||||
const otherTabId = port.sender.tab.id;
|
||||
const tabListener = function(tabId: number) {
|
||||
const tabListener = (tabId: number) => {
|
||||
if (tabId !== otherTabId) {
|
||||
return;
|
||||
}
|
||||
disconnect();
|
||||
this.disconnect();
|
||||
};
|
||||
tabs.onRemoved.addListener(tabListener);
|
||||
}
|
||||
port.onMessage.addListener(this.onMessage.bind(this));
|
||||
port.onDisconnect.addListener(disconnect);
|
||||
port.onDisconnect.addListener(this.disconnect.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.disconnected) {
|
||||
return;
|
||||
}
|
||||
this.disconnected = true;
|
||||
const {port} = this;
|
||||
this.port = null; // Break the cycle
|
||||
this.emit("disconnect", this, port);
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -120,6 +123,9 @@ export const Bus = new class extends EventEmitter {
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
this.ports.emit(port.name, new Port(port));
|
||||
const wrapped = new Port(port);
|
||||
if (!this.ports.emit(port.name, wrapped)) {
|
||||
wrapped.disconnect();
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
230
lib/cdheaderparser.ts
Normal file
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* (c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
/* eslint-disable max-len,no-magic-numbers */
|
||||
// License: MPL-2
|
||||
|
||||
/**
|
||||
* This typescript port was done by Nils Maier based on
|
||||
* https://github.com/Rob--W/open-in-browser/blob/83248155b633ed41bc9cdb1205042653e644abd2/extension/content-disposition.js
|
||||
* Special thanks goes to Rob doing all the heavy lifting and putting
|
||||
* it together in a reuseable, open source'd library.
|
||||
*/
|
||||
|
||||
const R_RFC6266 = /(?:^|;)\s*filename\*\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/i;
|
||||
const R_RFC5987 = /(?:^|;)\s*filename\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/i;
|
||||
|
||||
function unquoteRFC2616(value: string) {
|
||||
if (!value.startsWith("\"")) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const parts = value.slice(1).split("\\\"");
|
||||
// Find the first unescaped " and terminate there.
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
const quotindex = parts[i].indexOf("\"");
|
||||
if (quotindex !== -1) {
|
||||
parts[i] = parts[i].slice(0, quotindex);
|
||||
// Truncate and stop the iteration.
|
||||
parts.length = i + 1;
|
||||
}
|
||||
parts[i] = parts[i].replace(/\\(.)/g, "$1");
|
||||
}
|
||||
value = parts.join("\"");
|
||||
return value;
|
||||
}
|
||||
|
||||
export class CDHeaderParser {
|
||||
private needsFixup: boolean;
|
||||
|
||||
// We need to keep this per instance, because of the global flag.
|
||||
// Hence we need to reset it after a use.
|
||||
private R_MULTI = /(?:^|;)\s*filename\*((?!0\d)\d+)(\*?)\s*=\s*([^";\s][^;\s]*|"(?:[^"\\]|\\"?)+"?)/gi;
|
||||
|
||||
/**
|
||||
* Parse a content-disposition header, with relaxed spec tolerance
|
||||
*
|
||||
* @param {string} header Header to parse
|
||||
* @returns {string} Parsed header
|
||||
*/
|
||||
parse(header: string) {
|
||||
this.needsFixup = true;
|
||||
|
||||
// filename*=ext-value ("ext-value" from RFC 5987, referenced by RFC 6266).
|
||||
{
|
||||
const match = R_RFC6266.exec(header);
|
||||
if (match) {
|
||||
const [, tmp] = match;
|
||||
let filename = unquoteRFC2616(tmp);
|
||||
filename = unescape(filename);
|
||||
filename = this.decodeRFC5897(filename);
|
||||
filename = this.decodeRFC2047(filename);
|
||||
return this.maybeFixupEncoding(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// Continuations (RFC 2231 section 3, referenced by RFC 5987 section 3.1).
|
||||
// filename*n*=part
|
||||
// filename*n=part
|
||||
{
|
||||
const tmp = this.getParamRFC2231(header);
|
||||
if (tmp) {
|
||||
// RFC 2047, section
|
||||
const filename = this.decodeRFC2047(tmp);
|
||||
return this.maybeFixupEncoding(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// filename=value (RFC 5987, section 4.1).
|
||||
{
|
||||
const match = R_RFC5987.exec(header);
|
||||
if (match) {
|
||||
const [, tmp] = match;
|
||||
let filename = unquoteRFC2616(tmp);
|
||||
filename = this.decodeRFC2047(filename);
|
||||
return this.maybeFixupEncoding(filename);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private maybeDecode(encoding: string, value: string) {
|
||||
if (!encoding) {
|
||||
return value;
|
||||
}
|
||||
const bytes = Array.from(value, c => c.charCodeAt(0));
|
||||
if (!bytes.every(code => code <= 0xff)) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
value = new TextDecoder(encoding, {fatal: true}).
|
||||
decode(new Uint8Array(bytes));
|
||||
this.needsFixup = false;
|
||||
}
|
||||
catch {
|
||||
// TextDecoder constructor threw - unrecognized encoding.
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private maybeFixupEncoding(value: string) {
|
||||
if (!this.needsFixup && /[\x80-\xff]/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Maybe multi-byte UTF-8.
|
||||
value = this.maybeDecode("utf-8", value);
|
||||
if (!this.needsFixup) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Try iso-8859-1 encoding.
|
||||
return this.maybeDecode("iso-8859-1", value);
|
||||
}
|
||||
|
||||
private getParamRFC2231(value: string) {
|
||||
const matches: string[][] = [];
|
||||
|
||||
// Iterate over all filename*n= and filename*n*= with n being an integer
|
||||
// of at least zero. Any non-zero number must not start with '0'.
|
||||
let match;
|
||||
this.R_MULTI.lastIndex = 0;
|
||||
while ((match = this.R_MULTI.exec(value)) !== null) {
|
||||
const [, num, quot, part] = match;
|
||||
const n = parseInt(num, 10);
|
||||
if (n in matches) {
|
||||
// Ignore anything after the invalid second filename*0.
|
||||
if (n === 0) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
matches[n] = [quot, part];
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
for (let n = 0; n < matches.length; ++n) {
|
||||
if (!(n in matches)) {
|
||||
// Numbers must be consecutive. Truncate when there is a hole.
|
||||
break;
|
||||
}
|
||||
const [quot, rawPart] = matches[n];
|
||||
let part = unquoteRFC2616(rawPart);
|
||||
if (quot) {
|
||||
part = unescape(part);
|
||||
if (n === 0) {
|
||||
part = this.decodeRFC5897(part);
|
||||
}
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
private decodeRFC2047(value: string) {
|
||||
// RFC 2047-decode the result. Firefox tried to drop support for it, but
|
||||
// backed out because some servers use it - https://bugzil.la/875615
|
||||
// Firefox's condition for decoding is here:
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
// https://searchfox.org/mozilla-central/rev/4a590a5a15e35d88a3b23dd6ac3c471cf85b04a8/netwerk/mime/nsMIMEHeaderParamImpl.cpp#742-748
|
||||
|
||||
// We are more strict and only recognize RFC 2047-encoding if the value
|
||||
// starts with "=?", since then it is likely that the full value is
|
||||
// RFC 2047-encoded.
|
||||
|
||||
// Firefox also decodes words even where RFC 2047 section 5 states:
|
||||
// "An 'encoded-word' MUST NOT appear within a 'quoted-string'."
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (!value.startsWith("=?") || /[\x00-\x19\x80-\xff]/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
// RFC 2047, section 2.4
|
||||
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||
// charset = token (but let's restrict to characters that denote a
|
||||
// possibly valid encoding).
|
||||
// encoding = q or b
|
||||
// encoded-text = any printable ASCII character other than ? or space.
|
||||
// ... but Firefox permits ? and space.
|
||||
return value.replace(
|
||||
/=\?([\w-]*)\?([QqBb])\?((?:[^?]|\?(?!=))*)\?=/g,
|
||||
(_, charset, encoding, text) => {
|
||||
if (encoding === "q" || encoding === "Q") {
|
||||
// RFC 2047 section 4.2.
|
||||
text = text.replace(/_/g, " ");
|
||||
text = text.replace(/=([0-9a-fA-F]{2})/g,
|
||||
(_: string, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
||||
return this.maybeDecode(charset, text);
|
||||
}
|
||||
|
||||
// else encoding is b or B - base64 (RFC 2047 section 4.1)
|
||||
try {
|
||||
text = atob(text);
|
||||
}
|
||||
catch {
|
||||
// ignored
|
||||
}
|
||||
return this.maybeDecode(charset, text);
|
||||
});
|
||||
}
|
||||
|
||||
private decodeRFC5897(extValue: string) {
|
||||
// Decodes "ext-value" from RFC 5987.
|
||||
const extEnd = extValue.indexOf("'");
|
||||
if (extEnd < 0) {
|
||||
// Some servers send "filename*=" without encoding'language' prefix,
|
||||
// e.g. in https://github.com/Rob--W/open-in-browser/issues/26
|
||||
// Let's accept the value like Firefox (57) (Chrome 62 rejects it).
|
||||
return extValue;
|
||||
}
|
||||
const encoding = extValue.slice(0, extEnd);
|
||||
const langvalue = extValue.slice(extEnd + 1);
|
||||
// Ignore language (RFC 5987 section 3.2.1, and RFC 6266 section 4.1 ).
|
||||
return this.maybeDecode(encoding, langvalue.replace(/^[^']*'/, ""));
|
||||
}
|
||||
}
|
147
lib/db.ts
@ -1,14 +1,25 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
|
||||
// License: MIT
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Download } from "./manager/download";
|
||||
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
|
||||
import { storage } from "./browser";
|
||||
import { sort } from "./sorting";
|
||||
|
||||
const VERSION = 1;
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
@ -69,7 +80,7 @@ export const DB = new class DB {
|
||||
return await new Promise(this.getAllInternal);
|
||||
}
|
||||
|
||||
saveItemsInternal(items: any[], resolve: Function, reject: Function) {
|
||||
saveItemsInternal(items: Download[], resolve: Function, reject: Function) {
|
||||
if (!items || !items.length || !this.db) {
|
||||
resolve();
|
||||
return;
|
||||
@ -83,9 +94,13 @@ export const DB = new class DB {
|
||||
if (item.private) {
|
||||
continue;
|
||||
}
|
||||
const req = store.put(item.toJSON());
|
||||
const json = item.toJSON();
|
||||
if (item.state === RUNNING || item.state === RETRYING) {
|
||||
json.state = QUEUED;
|
||||
}
|
||||
const req = store.put(json);
|
||||
if (!("dbId" in item) || item.dbId < 0) {
|
||||
req.onsuccess = () => item.dbId = req.result;
|
||||
req.onsuccess = () => item.dbId = req.result as number;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,12 +109,12 @@ export const DB = new class DB {
|
||||
}
|
||||
}
|
||||
|
||||
async saveItems(items: any[]) {
|
||||
async saveItems(items: Download[]) {
|
||||
await this.init();
|
||||
return await new Promise(this.saveItemsInternal.bind(this, items));
|
||||
}
|
||||
|
||||
deleteItemsInternal(items: any[], resolve: Function, reject: Function) {
|
||||
deleteItemsInternal(items: any[], resolve: () => void, reject: Function) {
|
||||
if (!items || !items.length || !this.db) {
|
||||
resolve();
|
||||
return;
|
||||
@ -132,4 +147,120 @@ export const DB = new class DB {
|
||||
await this.init();
|
||||
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 langs from "../_locales/all.json";
|
||||
import { sorted, naturalCaseCompare } from "./sorting";
|
||||
|
||||
import lf from "localforage";
|
||||
|
||||
export const ALL_LANGS = Object.freeze(new Map<string, string>(
|
||||
sorted(Object.entries(langs), e => {
|
||||
@ -40,11 +40,11 @@ class Entry {
|
||||
this.message = entry.message.replace(/\$[A-Z0-9]+\$/g, (r: string) => {
|
||||
hit = true;
|
||||
const id = r.substr(1, r.length - 2).toLocaleLowerCase();
|
||||
const pholder = entry.placeholders[id];
|
||||
if (!pholder || !pholder.content) {
|
||||
const placeholder = entry.placeholders[id];
|
||||
if (!placeholder || !placeholder.content) {
|
||||
throw new Error(`Invalid placeholder: ${id}`);
|
||||
}
|
||||
return `${pholder.content}$`;
|
||||
return `${placeholder.content}$`;
|
||||
});
|
||||
if (!hit) {
|
||||
throw new Error("Not entry-able");
|
||||
@ -123,14 +123,17 @@ async function fetchLanguage(code: string) {
|
||||
}
|
||||
|
||||
|
||||
function loadCached() {
|
||||
if (document.location.pathname.includes("/windows/")) {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (cached) {
|
||||
return JSON.parse(cached) as any[];
|
||||
}
|
||||
async function loadCached(): Promise<any> {
|
||||
const cached = await lf.getItem<string>(CACHE_KEY);
|
||||
if (!cached) {
|
||||
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() {
|
||||
@ -187,16 +190,16 @@ async function load(): Promise<Localization> {
|
||||
}
|
||||
CURRENT = currentLang;
|
||||
// en is the base locale
|
||||
let valid = loadCached();
|
||||
let valid = await loadCached();
|
||||
if (!valid) {
|
||||
valid = await loadRawLocales();
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(valid));
|
||||
await lf.setItem(CACHE_KEY, JSON.stringify(valid));
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
valid.push(JSON.parse(custom));
|
||||
@ -239,7 +242,7 @@ locale.then(l => {
|
||||
/**
|
||||
* Localize a message
|
||||
* @param {string} id Identifier of the string to localize
|
||||
* @param {string[]} [subst] Message substituations
|
||||
* @param {string[]} [subst] Message substitutions
|
||||
* @returns {string} Localized message
|
||||
*/
|
||||
export function _(id: string, ...subst: any[]) {
|
||||
@ -302,11 +305,11 @@ export async function localize<T extends HTMLElement | DocumentFragment>(
|
||||
return localize_(elem);
|
||||
}
|
||||
|
||||
export function saveCustomLocale(data?: string) {
|
||||
export async function saveCustomLocale(data?: string) {
|
||||
if (!data) {
|
||||
localStorage.removeItem(CUSTOM_KEY);
|
||||
await lf.removeItem(CUSTOM_KEY);
|
||||
return;
|
||||
}
|
||||
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 { EventEmitter } from "../uikit/lib/events";
|
||||
import { PromiseSerializer } from "./pserializer";
|
||||
import lf from "localforage";
|
||||
|
||||
|
||||
const VERSION = 1;
|
||||
const STORE = "iconcache";
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
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 {
|
||||
private db: Promise<IDBDatabase>;
|
||||
private db = lf.createInstance({name: STORE});
|
||||
|
||||
private cache: Map<string, string>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.db = this.init();
|
||||
this.cache = new Map();
|
||||
this.get = PromiseSerializer.wrapNew(8, this, this.get);
|
||||
this.set = PromiseSerializer.wrapNew(1, this, this.set);
|
||||
}
|
||||
|
||||
private async init() {
|
||||
return await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const req = indexedDB.open(STORE, VERSION);
|
||||
req.onupgradeneeded = evt => {
|
||||
const db = req.result;
|
||||
switch (evt.oldVersion) {
|
||||
case 0: {
|
||||
db.createObjectStore(STORE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
req.onerror = ex => reject(ex);
|
||||
req.onsuccess = () => {
|
||||
resolve(req.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private normalize(ext: string) {
|
||||
ext = ext.toLocaleLowerCase();
|
||||
return SYNONYMS.get(ext) || ext;
|
||||
@ -95,36 +77,25 @@ export const IconCache = new class IconCache extends EventEmitter {
|
||||
if (rv) {
|
||||
return rv;
|
||||
}
|
||||
const db = await this.db;
|
||||
rv = this.cache.get(sext);
|
||||
if (rv) {
|
||||
return rv;
|
||||
}
|
||||
return await new Promise<string | undefined>(resolve => {
|
||||
const trans = db.transaction(STORE, "readonly");
|
||||
trans.onerror = () => resolve(undefined);
|
||||
const store = trans.objectStore(STORE);
|
||||
const req = store.get(sext);
|
||||
req.onerror = () => resolve(undefined);
|
||||
req.onsuccess = () => {
|
||||
const rv = this.cache.get(sext);
|
||||
if (rv) {
|
||||
resolve(rv);
|
||||
return;
|
||||
}
|
||||
let {result} = req;
|
||||
if (!result) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
if (typeof req.result !== "string") {
|
||||
result = URL.createObjectURL(result).toString();
|
||||
}
|
||||
this.cache.set(sext, result);
|
||||
this.cache.set(ext, "");
|
||||
resolve(result);
|
||||
};
|
||||
});
|
||||
let result = await this.db.getItem<any>(sext);
|
||||
if (!result) {
|
||||
return this.cache.get(sext);
|
||||
}
|
||||
rv = this.cache.get(sext);
|
||||
if (rv) {
|
||||
return rv;
|
||||
}
|
||||
if (typeof result !== "string") {
|
||||
result = URL.createObjectURL(result).toString();
|
||||
}
|
||||
|
||||
this.cache.set(sext, result);
|
||||
this.cache.set(ext, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
async set(ext: string, manId: number) {
|
||||
@ -145,18 +116,9 @@ export const IconCache = new class IconCache extends EventEmitter {
|
||||
}
|
||||
for (const {size, icon} of urls) {
|
||||
this.cache.set(`${ext}-${size}`, URL.createObjectURL(icon));
|
||||
await this.db.setItem(`${ext}-${size}`, icon);
|
||||
}
|
||||
this.cache.set(ext, "");
|
||||
const db = await this.db;
|
||||
await new Promise((resolve, reject) => {
|
||||
const trans = db.transaction(STORE, "readwrite");
|
||||
trans.onerror = reject;
|
||||
trans.oncomplete = resolve;
|
||||
const store = trans.objectStore(STORE);
|
||||
for (const {size, icon} of urls) {
|
||||
store.put(icon, `${ext}-${size}`);
|
||||
}
|
||||
});
|
||||
this.emit("cached", ext);
|
||||
}
|
||||
}();
|
||||
|
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;
|
||||
idx: number;
|
||||
mask?: string;
|
||||
subfolder?: string;
|
||||
startDate?: number;
|
||||
private?: boolean;
|
||||
postData?: string;
|
||||
@ -27,10 +28,12 @@ const OPTIONPROPS = Object.freeze([
|
||||
"fileName",
|
||||
"batch", "idx",
|
||||
"mask",
|
||||
"subfolder",
|
||||
"startDate",
|
||||
"private",
|
||||
"postData",
|
||||
"paused"
|
||||
"paused",
|
||||
"server", "cookies",
|
||||
]);
|
||||
|
||||
function maybeAssign(options: any, what: any) {
|
||||
|
@ -5,6 +5,8 @@
|
||||
import { parsePath, URLd } from "../util";
|
||||
import { QUEUED, RUNNING, PAUSED } from "./state";
|
||||
import Renamer from "./renamer";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "../item";
|
||||
|
||||
const SAVEDPROPS = [
|
||||
"state",
|
||||
@ -14,6 +16,7 @@ const SAVEDPROPS = [
|
||||
"usableReferrer",
|
||||
"fileName",
|
||||
"mask",
|
||||
"subfolder",
|
||||
"date",
|
||||
// batches
|
||||
"batch",
|
||||
@ -48,7 +51,9 @@ const DEFAULTS = {
|
||||
written: 0,
|
||||
manId: 0,
|
||||
mime: "",
|
||||
prerolled: false
|
||||
prerolled: false,
|
||||
retries: 0,
|
||||
deadline: 0
|
||||
};
|
||||
|
||||
let sessionId = 0;
|
||||
@ -103,9 +108,13 @@ export class BaseDownload {
|
||||
|
||||
public mask: string;
|
||||
|
||||
public subfolder: string;
|
||||
|
||||
public prerolled: boolean;
|
||||
|
||||
constructor(options: any) {
|
||||
public retries: number;
|
||||
|
||||
constructor(options: BaseItem) {
|
||||
Object.assign(this, DEFAULTS);
|
||||
this.assign(options);
|
||||
if (this.state === RUNNING) {
|
||||
@ -113,14 +122,16 @@ export class BaseDownload {
|
||||
}
|
||||
this.sessionId = ++sessionId;
|
||||
this.renamer = new Renamer(this);
|
||||
this.retries = 0;
|
||||
}
|
||||
|
||||
assign(options: any) {
|
||||
assign(options: BaseItem) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self: any = this;
|
||||
const other: any = options;
|
||||
for (const prop of SAVEDPROPS) {
|
||||
if (prop in options) {
|
||||
self[prop] = options[prop];
|
||||
self[prop] = other[prop];
|
||||
}
|
||||
}
|
||||
this.uURL = new URL(this.url) as URLd;
|
||||
@ -180,8 +191,10 @@ export class BaseDownload {
|
||||
rv.destPath = dest.path;
|
||||
rv.destFull = dest.full;
|
||||
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||
rv.currentFull = `${dest.path}/${rv.currentName}`;
|
||||
rv.error = this.error;
|
||||
rv.ext = this.renamer.p_ext;
|
||||
rv.retries = this.retries;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import MimeType from "whatwg-mimetype";
|
||||
import { CHROME, downloads, webRequest } from "../browser";
|
||||
import { Prefs } from "../prefs";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { CHROME, downloads, DownloadOptions } from "../browser";
|
||||
import { Prefs, PrefWatcher } from "../prefs";
|
||||
import { PromiseSerializer } from "../pserializer";
|
||||
import { filterInSitu, parsePath, sanitizePath } from "../util";
|
||||
import { filterInSitu, parsePath } from "../util";
|
||||
import { BaseDownload } from "./basedownload";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Manager } from "./man";
|
||||
@ -16,67 +16,27 @@ import {
|
||||
DONE,
|
||||
FORCABLE,
|
||||
MISSING,
|
||||
PAUSABLE,
|
||||
PAUSEABLE,
|
||||
PAUSED,
|
||||
QUEUED,
|
||||
RUNNING
|
||||
RUNNING,
|
||||
RETRYING
|
||||
} 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;
|
||||
const PREROLL_HOSTS = /4cdn|chan/;
|
||||
const PREROLL_TIMEOUT = 10000;
|
||||
const PREROLL_NOPE = new Set<string>();
|
||||
function isRecoverable(error: string) {
|
||||
switch (error) {
|
||||
case "SERVER_FAILED":
|
||||
return true;
|
||||
|
||||
function parseDisposition(disp: MimeType) {
|
||||
if (!disp) {
|
||||
return "";
|
||||
default:
|
||||
return error.startsWith("NETWORK_");
|
||||
}
|
||||
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};
|
||||
interface Options {
|
||||
conflictAction: string;
|
||||
filename: string;
|
||||
saveAs: boolean;
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
incognito?: boolean;
|
||||
headers: Header[];
|
||||
}
|
||||
const RETRIES = new PrefWatcher("retries", 5);
|
||||
const RETRY_TIME = new PrefWatcher("retry-time", 5);
|
||||
|
||||
export class Download extends BaseDownload {
|
||||
public manager: Manager;
|
||||
@ -89,6 +49,10 @@ export class Download extends BaseDownload {
|
||||
|
||||
public error: string;
|
||||
|
||||
public dbId: number;
|
||||
|
||||
public deadline: number;
|
||||
|
||||
constructor(manager: Manager, options: any) {
|
||||
super(options);
|
||||
this.manager = manager;
|
||||
@ -120,27 +84,28 @@ export class Download extends BaseDownload {
|
||||
if (this.manId) {
|
||||
const {manId: id} = this;
|
||||
try {
|
||||
const state = await downloads.search({id});
|
||||
if (state[0].state === "in_progress") {
|
||||
const state = (await downloads.search({id})).pop() || {};
|
||||
if (state.state === "in_progress" && !state.error && !state.paused) {
|
||||
this.changeState(RUNNING);
|
||||
this.updateStateFromBrowser();
|
||||
return;
|
||||
}
|
||||
if (state[0].state === "complete") {
|
||||
if (state.state === "complete") {
|
||||
this.changeState(DONE);
|
||||
this.updateStateFromBrowser();
|
||||
return;
|
||||
}
|
||||
if (!state[0].canResume) {
|
||||
if (!state.canResume) {
|
||||
throw new Error("Cannot resume");
|
||||
}
|
||||
// Cannot await here
|
||||
// Firefox bug: will not return until download is finished
|
||||
downloads.resume(id).catch(() => {});
|
||||
downloads.resume(id).catch(console.error);
|
||||
this.changeState(RUNNING);
|
||||
return;
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("cannot resume", ex);
|
||||
this.manager.removeManId(this.manId);
|
||||
this.removeFromBrowser();
|
||||
}
|
||||
@ -164,13 +129,15 @@ export class Download extends BaseDownload {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const options: Options = {
|
||||
const options: DownloadOptions = {
|
||||
conflictAction: await Prefs.get("conflict-action"),
|
||||
filename: this.dest.full,
|
||||
saveAs: false,
|
||||
url: this.url,
|
||||
headers: [],
|
||||
};
|
||||
if (!CHROME) {
|
||||
options.filename = this.dest.full;
|
||||
}
|
||||
if (!CHROME && this.private) {
|
||||
options.incognito = true;
|
||||
}
|
||||
@ -184,6 +151,12 @@ export class Download extends BaseDownload {
|
||||
value: this.referrer
|
||||
});
|
||||
}
|
||||
else if (CHROME) {
|
||||
options.headers.push({
|
||||
name: "X-DTA-ID",
|
||||
value: this.sessionId.toString(),
|
||||
});
|
||||
}
|
||||
if (this.manId) {
|
||||
this.manager.removeManId(this.manId);
|
||||
}
|
||||
@ -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() {
|
||||
try {
|
||||
if (this.prerolled) {
|
||||
// Check again, just in case, async and all
|
||||
return;
|
||||
}
|
||||
if (!this.shouldPreroll) {
|
||||
const roller = new Preroller(this);
|
||||
if (!roller.shouldPreroll) {
|
||||
return;
|
||||
}
|
||||
await (CHROME ? this.prerollChrome() : this.prerollFirefox());
|
||||
const res = await roller.roll();
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
this.adoptPrerollResults(res);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||
@ -255,99 +210,16 @@ export class Download extends BaseDownload {
|
||||
}
|
||||
}
|
||||
|
||||
private async prerollFirefox() {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
const res = await fetch(this.uURL.toString(), {
|
||||
method: "HEAD",
|
||||
mode: "same-origin",
|
||||
signal,
|
||||
});
|
||||
controller.abort();
|
||||
const {headers} = res;
|
||||
this.prerollFinialize(headers, res);
|
||||
}
|
||||
|
||||
async prerollChrome() {
|
||||
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 (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: "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, "-"));
|
||||
adoptPrerollResults(res: PrerollResults) {
|
||||
if (res.mime) {
|
||||
this.mime = res.mime;
|
||||
}
|
||||
if (type) {
|
||||
this.mime = type.essence;
|
||||
if (res.name) {
|
||||
this.serverName = res.name;
|
||||
}
|
||||
this.serverName = file;
|
||||
this.markDirty();
|
||||
const {status} = res;
|
||||
/* eslint-disable no-magic-numbers */
|
||||
if (status === 404) {
|
||||
this.cancel();
|
||||
this.error = "SERVER_BAD_CONTENT";
|
||||
if (res.error) {
|
||||
this.cancelAccordingToError(res.error);
|
||||
}
|
||||
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) {
|
||||
@ -362,20 +234,32 @@ export class Download extends BaseDownload {
|
||||
}
|
||||
}
|
||||
|
||||
async pause() {
|
||||
if (!(PAUSABLE & this.state)) {
|
||||
async pause(retry?: boolean) {
|
||||
if (!(PAUSEABLE & this.state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!retry) {
|
||||
this.retries = 0;
|
||||
this.deadline = 0;
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
this.deadline = Date.now() + RETRY_TIME.value * 60 * 1000;
|
||||
}
|
||||
|
||||
if (this.state === RUNNING && this.manId) {
|
||||
try {
|
||||
await downloads.pause(this.manId);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("pause", ex.toString(), ex);
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.changeState(PAUSED);
|
||||
|
||||
this.changeState(retry ? RETRYING : PAUSED);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@ -383,6 +267,8 @@ export class Download extends BaseDownload {
|
||||
this.manId = 0;
|
||||
this.written = this.totalSize = 0;
|
||||
this.mime = this.serverName = this.browserName = "";
|
||||
this.retries = 0;
|
||||
this.deadline = 0;
|
||||
}
|
||||
|
||||
async removeFromBrowser() {
|
||||
@ -391,7 +277,7 @@ export class Download extends BaseDownload {
|
||||
await downloads.cancel(id);
|
||||
}
|
||||
catch (ex) {
|
||||
// ingored
|
||||
// ignored
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
try {
|
||||
@ -399,7 +285,7 @@ export class Download extends BaseDownload {
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(id, ex.toString(), ex);
|
||||
// ingored
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,6 +301,17 @@ export class Download extends BaseDownload {
|
||||
this.changeState(CANCELED);
|
||||
}
|
||||
|
||||
async cancelAccordingToError(error: string) {
|
||||
if (!isRecoverable(error) || ++this.retries > RETRIES.value) {
|
||||
this.cancel();
|
||||
this.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.pause(true);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
setMissing() {
|
||||
if (this.manId) {
|
||||
this.manager.removeManId(this.manId);
|
||||
@ -467,9 +364,11 @@ export class Download extends BaseDownload {
|
||||
this.markDirty();
|
||||
switch (state.state) {
|
||||
case "in_progress":
|
||||
if (error) {
|
||||
this.cancel();
|
||||
this.error = error;
|
||||
if (state.paused) {
|
||||
this.changeState(PAUSED);
|
||||
}
|
||||
else if (error) {
|
||||
this.cancelAccordingToError(error);
|
||||
}
|
||||
else {
|
||||
this.changeState(RUNNING);
|
||||
@ -480,6 +379,9 @@ export class Download extends BaseDownload {
|
||||
if (state.paused) {
|
||||
this.changeState(PAUSED);
|
||||
}
|
||||
else if (error) {
|
||||
this.cancelAccordingToError(error);
|
||||
}
|
||||
else {
|
||||
this.cancel();
|
||||
this.error = error || "";
|
||||
@ -496,4 +398,27 @@ export class Download extends BaseDownload {
|
||||
this.setMissing();
|
||||
}
|
||||
}
|
||||
|
||||
updateFromSuggestion(state: any) {
|
||||
const res: PrerollResults = {};
|
||||
if (state.mime) {
|
||||
res.mime = state.mime;
|
||||
}
|
||||
if (state.filename) {
|
||||
res.name = state.filename;
|
||||
}
|
||||
if (state.finalUrl) {
|
||||
res.finalURL = state.finalUrl;
|
||||
const detected = Preroller.maybeFindNameFromSearchParams(this, res);
|
||||
if (detected) {
|
||||
res.name = detected;
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.adoptPrerollResults(res);
|
||||
}
|
||||
finally {
|
||||
this.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@
|
||||
import { EventEmitter } from "../events";
|
||||
import { Notification } from "../notifications";
|
||||
import { DB } from "../db";
|
||||
import { QUEUED, CANCELED, RUNNING } from "./state";
|
||||
import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Bus, Port } from "../bus";
|
||||
import { sort } from "../sorting";
|
||||
import { Prefs } from "../prefs";
|
||||
import { Prefs, PrefWatcher } from "../prefs";
|
||||
import { _ } from "../i18n";
|
||||
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
||||
import { PromiseSerializer } from "../pserializer";
|
||||
@ -16,8 +16,10 @@ import { Download } from "./download";
|
||||
import { ManagerPort } from "./port";
|
||||
import { Scheduler } from "./scheduler";
|
||||
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 DIRTY_TIMEOUT = 100;
|
||||
@ -29,6 +31,9 @@ const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||
// ignored
|
||||
};
|
||||
|
||||
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
|
||||
const SOUNDS = new PrefWatcher("sounds", false);
|
||||
|
||||
export class Manager extends EventEmitter {
|
||||
private items: Download[];
|
||||
|
||||
@ -48,11 +53,18 @@ export class Manager extends EventEmitter {
|
||||
|
||||
private readonly running: Set<Download>;
|
||||
|
||||
private readonly retrying: Set<Download>;
|
||||
|
||||
private scheduler: Scheduler | null;
|
||||
|
||||
private shouldReload: boolean;
|
||||
|
||||
private deadlineTimer: number;
|
||||
|
||||
constructor() {
|
||||
if (!document.location.href.includes("background")) {
|
||||
throw new Error("Not on background");
|
||||
}
|
||||
super();
|
||||
this.active = true;
|
||||
this.shouldReload = false;
|
||||
@ -62,27 +74,42 @@ export class Manager extends EventEmitter {
|
||||
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
||||
this.dirty = new CoalescedUpdate(
|
||||
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
||||
this.processDeadlines = this.processDeadlines.bind(this);
|
||||
this.sids = new Map();
|
||||
this.manIds = new Map();
|
||||
this.ports = new Set();
|
||||
this.scheduler = null;
|
||||
this.running = new Set();
|
||||
this.retrying = new Set();
|
||||
|
||||
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
||||
|
||||
downloads.onChanged.addListener(this.onChanged.bind(this));
|
||||
downloads.onErased.addListener(this.onErased.bind(this));
|
||||
if (CHROME && downloads.onDeterminingFilename) {
|
||||
downloads.onDeterminingFilename.addListener(
|
||||
this.onDeterminingFilename.bind(this));
|
||||
}
|
||||
|
||||
Bus.onPort("manager", (port: Port) => {
|
||||
const mport = new ManagerPort(this, port);
|
||||
const managerPort = new ManagerPort(this, port);
|
||||
port.on("disconnect", () => {
|
||||
this.ports.delete(mport);
|
||||
this.ports.delete(managerPort);
|
||||
});
|
||||
this.ports.add(mport);
|
||||
this.ports.add(managerPort);
|
||||
return true;
|
||||
});
|
||||
Limits.on("changed", () => {
|
||||
this.resetScheduler();
|
||||
});
|
||||
|
||||
if (CHROME) {
|
||||
webRequest.onBeforeSendHeaders.addListener(
|
||||
this.stuffReferrer.bind(this),
|
||||
{urls: ["<all_urls>"]},
|
||||
["blocking", "requestHeaders", "extraHeaders"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -100,7 +127,7 @@ export class Manager extends EventEmitter {
|
||||
// Do not wait for the scheduler
|
||||
this.resetScheduler();
|
||||
|
||||
this.emit("inited");
|
||||
this.emit("initialized");
|
||||
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
||||
runtime.onUpdateAvailable.addListener(() => {
|
||||
if (this.running.size) {
|
||||
@ -139,6 +166,20 @@ export class Manager extends EventEmitter {
|
||||
this.manIds.delete(downloadId);
|
||||
}
|
||||
|
||||
onDeterminingFilename(state: any, suggest: Function) {
|
||||
const download = this.manIds.get(state.id);
|
||||
if (!download) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
download.updateFromSuggestion(state);
|
||||
}
|
||||
finally {
|
||||
const suggestion = {filename: download.dest.full};
|
||||
suggest(suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
async resetScheduler() {
|
||||
this.scheduler = null;
|
||||
await this.startNext();
|
||||
@ -179,14 +220,11 @@ export class Manager extends EventEmitter {
|
||||
this.notifiedFinished = false;
|
||||
}
|
||||
|
||||
async maybeRunFinishActions() {
|
||||
if (this.running.size) {
|
||||
return;
|
||||
}
|
||||
await this.maybeNotifyFinished();
|
||||
maybeRunFinishActions() {
|
||||
if (this.running.size) {
|
||||
return;
|
||||
}
|
||||
this.maybeNotifyFinished();
|
||||
if (this.shouldReload) {
|
||||
this.saveQueue.trigger();
|
||||
setTimeout(() => {
|
||||
@ -199,15 +237,21 @@ export class Manager extends EventEmitter {
|
||||
setShelfEnabled(true);
|
||||
}
|
||||
|
||||
async maybeNotifyFinished() {
|
||||
if (!(await Prefs.get("finish-notification"))) {
|
||||
maybeNotifyFinished() {
|
||||
if (this.notifiedFinished || this.running.size || this.retrying.size) {
|
||||
return;
|
||||
}
|
||||
if (this.notifiedFinished || this.running.size) {
|
||||
return;
|
||||
if (SOUNDS.value && !OPERA) {
|
||||
const audio = new Audio(runtime.getURL("/style/done.opus"));
|
||||
audio.addEventListener("canplaythrough", () => audio.play());
|
||||
audio.addEventListener("ended", () => document.body.removeChild(audio));
|
||||
audio.addEventListener("error", () => document.body.removeChild(audio));
|
||||
document.body.appendChild(audio);
|
||||
}
|
||||
if (FINISH_NOTIFICATION.value) {
|
||||
new Notification(null, _("queue-finished"));
|
||||
}
|
||||
this.notifiedFinished = true;
|
||||
new Notification(null, _("queue-finished"));
|
||||
}
|
||||
|
||||
addManId(id: number, download: Download) {
|
||||
@ -218,6 +262,61 @@ export class Manager extends EventEmitter {
|
||||
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[]) {
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
@ -240,6 +339,7 @@ export class Manager extends EventEmitter {
|
||||
this.save(items);
|
||||
this.startNext();
|
||||
}
|
||||
*/
|
||||
|
||||
setDirty(item: Download) {
|
||||
this.dirty.add(item);
|
||||
@ -306,35 +406,86 @@ export class Manager extends EventEmitter {
|
||||
if (oldState === RUNNING) {
|
||||
this.running.delete(download);
|
||||
}
|
||||
else if (oldState === RETRYING) {
|
||||
this.retrying.delete(download);
|
||||
this.findDeadline();
|
||||
}
|
||||
if (newState === QUEUED) {
|
||||
this.resetScheduler();
|
||||
this.startNext().catch(console.error);
|
||||
}
|
||||
else if (newState === RUNNING) {
|
||||
// Usually we already added it. Buit if a user uses the built-in
|
||||
// download manager to resart
|
||||
// Usually we already added it. But if a user uses the built-in
|
||||
// download manager to restart
|
||||
// a download, we have not, so make sure it is added either way
|
||||
this.running.add(download);
|
||||
}
|
||||
else {
|
||||
if (newState === RETRYING) {
|
||||
this.addRetry(download);
|
||||
}
|
||||
this.startNext().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
addRetry(download: Download) {
|
||||
this.retrying.add(download);
|
||||
this.findDeadline();
|
||||
}
|
||||
|
||||
private findDeadline() {
|
||||
let deadline = Array.from(this.retrying).
|
||||
reduce<number>((deadline, item) => {
|
||||
if (deadline) {
|
||||
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
|
||||
}
|
||||
return item.deadline;
|
||||
}, 0);
|
||||
if (deadline <= 0) {
|
||||
return;
|
||||
}
|
||||
deadline -= Date.now();
|
||||
if (deadline <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.deadlineTimer) {
|
||||
window.clearTimeout(this.deadlineTimer);
|
||||
}
|
||||
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
|
||||
}
|
||||
|
||||
private processDeadlines() {
|
||||
this.deadlineTimer = 0;
|
||||
try {
|
||||
const now = Date.now();
|
||||
this.items.forEach(item => {
|
||||
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
|
||||
this.retrying.delete(item);
|
||||
item.resume(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
finally {
|
||||
this.findDeadline();
|
||||
}
|
||||
}
|
||||
|
||||
sorted(sids: number[]) {
|
||||
try {
|
||||
// Construct new items
|
||||
const csids = new Map(this.sids);
|
||||
const currentSids = new Map(this.sids);
|
||||
let items = mapFilterInSitu(sids, sid => {
|
||||
const item = csids.get(sid);
|
||||
const item = currentSids.get(sid);
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
csids.delete(sid);
|
||||
currentSids.delete(sid);
|
||||
return item;
|
||||
}, e => !!e);
|
||||
if (csids.size) {
|
||||
items = items.concat(sort(Array.from(csids.values()), i => i.position));
|
||||
if (currentSids.size) {
|
||||
items = items.concat(
|
||||
sort(Array.from(currentSids.values()), i => i.position));
|
||||
}
|
||||
this.items = items;
|
||||
this.setPositions();
|
||||
@ -384,6 +535,31 @@ export class Manager extends EventEmitter {
|
||||
getMsgItems() {
|
||||
return this.items.map(e => e.toMsg());
|
||||
}
|
||||
|
||||
stuffReferrer(details: any): any {
|
||||
if (details.tabId > 0 && !US.startsWith(details.initiator)) {
|
||||
return undefined;
|
||||
}
|
||||
const sidx = details.requestHeaders.findIndex(
|
||||
(e: any) => e.name.toLowerCase() === "x-dta-id");
|
||||
if (sidx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const sid = parseInt(details.requestHeaders[sidx].value, 10);
|
||||
details.requestHeaders.splice(sidx, 1);
|
||||
const item = this.sids.get(sid);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
details.requestHeaders.push({
|
||||
name: "Referer",
|
||||
value: (item.uReferrer || item.uURL).toString()
|
||||
});
|
||||
const rv: any = {
|
||||
requestHeaders: details.requestHeaders
|
||||
};
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
let inited: Promise<Manager>;
|
||||
|
@ -9,6 +9,8 @@ import { BaseDownload } from "./basedownload";
|
||||
import { Manager } from "./man";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Port } from "../bus";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "../item";
|
||||
|
||||
type SID = {sid: number};
|
||||
type SIDS = {
|
||||
@ -42,6 +44,9 @@ export class ManagerPort {
|
||||
port.on("prefs", () => {
|
||||
openPrefs();
|
||||
});
|
||||
port.on("import", ({items}: {items: BaseItem[]}) => {
|
||||
API.regular(items, []);
|
||||
});
|
||||
port.on("all", () => this.sendAll());
|
||||
port.on("removeSids", this.onMsgRemoveSids);
|
||||
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() {
|
||||
const {mask} = this.d;
|
||||
const {mask, subfolder} = this.d;
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self: any = this;
|
||||
// XXX flat
|
||||
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) {
|
||||
const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
|
||||
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
|
||||
let prop = type.substr(1, type.length - 2);
|
||||
const flat = prop.startsWith("flat");
|
||||
if (flat) {
|
||||
prop = prop.substr(4);
|
||||
}
|
||||
prop = `p_${prop}`;
|
||||
const rv = (prop in self) ?
|
||||
let rv = (prop in self) ?
|
||||
(self[prop] || "").trim() :
|
||||
type;
|
||||
if (flat) {
|
||||
return rv.replace(/[/\\]+/g, "-");
|
||||
rv = rv.replace(/[/\\]+/g, "-");
|
||||
}
|
||||
return rv;
|
||||
return rv.replace(/\/{2,}/g, "/");
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ export const PAUSED = 1 << 3;
|
||||
export const DONE = 1 << 4;
|
||||
export const CANCELED = 1 << 5;
|
||||
export const MISSING = 1 << 6;
|
||||
export const RETRYING = 1 << 7;
|
||||
|
||||
export const RESUMABLE = PAUSED | CANCELED;
|
||||
export const FORCABLE = PAUSED | QUEUED | CANCELED;
|
||||
export const PAUSABLE = QUEUED | CANCELED | RUNNING;
|
||||
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING;
|
||||
export const RESUMABLE = PAUSED | CANCELED | RETRYING;
|
||||
export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
|
||||
export const PAUSEABLE = QUEUED | CANCELED | RUNNING | RETRYING;
|
||||
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 registeredExtensions: Set<string>;
|
||||
|
||||
constructor() {
|
||||
const exts = new Map<string, string[]>();
|
||||
for (const [prim, more] of Object.entries(mime.e)) {
|
||||
@ -42,6 +44,10 @@ export const MimeDB = new class {
|
||||
Object.entries(mime.m),
|
||||
([mime, prim]) => [mime, new MimeInfo(mime, exts.get(prim) || [prim])]
|
||||
));
|
||||
const all = Array.from(
|
||||
this.mimeToExts.values(),
|
||||
m => Array.from(m.extensions, e => e.toLowerCase()));
|
||||
this.registeredExtensions = new Set(all.flat());
|
||||
}
|
||||
|
||||
getPrimary(mime: string) {
|
||||
@ -52,4 +58,8 @@ export const MimeDB = new class {
|
||||
getMime(mime: string) {
|
||||
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 = {
|
||||
type: "basic",
|
||||
iconUrl: extension.getURL("/style/icon64.png"),
|
||||
title: "DownThemAll!",
|
||||
title: "TraitorousDownloading!",
|
||||
message: "message",
|
||||
};
|
||||
|
||||
@ -25,7 +25,7 @@ export class Notification extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.generated = !id;
|
||||
id = id || `DownThemAll-notification${++gid}`;
|
||||
id = id || `TraitorousDownloading-notification${++gid}`;
|
||||
if (typeof options === "string") {
|
||||
options = {message: options};
|
||||
}
|
||||
|
@ -99,6 +99,5 @@ export class PrefWatcher {
|
||||
|
||||
changed(prefs: any, key: string, value: any) {
|
||||
this.value = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -116,3 +116,14 @@ export const FASTFILTER = new RecentList("fastfilter", [
|
||||
"*.z??, *.css, *.html"
|
||||
]);
|
||||
FASTFILTER.init().catch(console.error);
|
||||
|
||||
export const SUBFOLDER = new RecentList("subfolder", [
|
||||
"",
|
||||
"downthemall",
|
||||
]);
|
||||
SUBFOLDER.init().catch(console.error);
|
||||
|
||||
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
|
||||
import { filters, FAST, Filter } from "./filters";
|
||||
import { WindowStateTracker } from "./windowstatetracker";
|
||||
import { windows } from "./browser";
|
||||
import { windows, CHROME } from "./browser";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
|
||||
interface BaseMatchedItem extends BaseItem {
|
||||
sidx?: number;
|
||||
matched?: string | null;
|
||||
prevMatched?: string | null;
|
||||
}
|
||||
@ -28,7 +29,8 @@ function computeSelection(
|
||||
items: BaseMatchedItem[],
|
||||
onlyFast: boolean): ItemDelta[] {
|
||||
let ws = items.map((item, idx: number) => {
|
||||
item.idx = idx;
|
||||
item.idx = item.idx || idx;
|
||||
item.sidx = item.sidx || idx;
|
||||
const {matched = null} = item;
|
||||
item.prevMatched = matched;
|
||||
item.matched = null;
|
||||
@ -52,7 +54,7 @@ function computeSelection(
|
||||
}
|
||||
return items.filter(item => item.prevMatched !== item.matched).map(item => {
|
||||
return {
|
||||
idx: item.idx,
|
||||
idx: item.sidx as number,
|
||||
matched: item.matched
|
||||
};
|
||||
});
|
||||
@ -98,9 +100,16 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
||||
type: "popup",
|
||||
});
|
||||
const window = await windows.create(windowOptions);
|
||||
tracker.track(window.id);
|
||||
try {
|
||||
if (!CHROME) {
|
||||
windows.update(window.id, tracker.getOptions({}));
|
||||
}
|
||||
const port = await Promise.race<Port>([
|
||||
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
||||
new Promise<Port>(resolve => Bus.oncePort("select", port => {
|
||||
resolve(port);
|
||||
return true;
|
||||
})),
|
||||
timeout<Port>(5 * 1000)]);
|
||||
if (!port.isSelf) {
|
||||
throw Error("Invalid sender connected");
|
||||
@ -186,8 +195,8 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
||||
openPrefs();
|
||||
});
|
||||
|
||||
port.on("openUrls", ({urls}) => {
|
||||
openUrls(urls);
|
||||
port.on("openUrls", ({urls, incognito}) => {
|
||||
openUrls(urls, incognito);
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -6,7 +6,7 @@ import { Bus, Port } from "./bus";
|
||||
import { WindowStateTracker } from "./windowstatetracker";
|
||||
import { Promised, timeout } from "./util";
|
||||
import { donate } from "./windowutils";
|
||||
import { windows } from "./browser";
|
||||
import { windows, CHROME } from "./browser";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
|
||||
@ -21,9 +21,16 @@ export async function single(item: BaseItem | null) {
|
||||
type: "popup",
|
||||
});
|
||||
const window = await windows.create(windowOptions);
|
||||
tracker.track(window.id);
|
||||
try {
|
||||
if (!CHROME) {
|
||||
windows.update(window.id, tracker.getOptions({}));
|
||||
}
|
||||
const port: Port = await Promise.race<Port>([
|
||||
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
||||
new Promise<Port>(resolve => Bus.oncePort("single", port => {
|
||||
resolve(port);
|
||||
return true;
|
||||
})),
|
||||
timeout<Port>(5 * 1000)]);
|
||||
if (!port.isSelf) {
|
||||
throw Error("Invalid sender connected");
|
||||
|
23
lib/util.ts
@ -3,6 +3,7 @@
|
||||
|
||||
import * as psl from "psl";
|
||||
import { identity, memoize } from "./memoize";
|
||||
import { IPReg } from "./ipreg";
|
||||
export { debounce } from "../uikit/lib/util";
|
||||
|
||||
export class Promised {
|
||||
@ -237,7 +238,10 @@ export interface URLd extends URL {
|
||||
Object.defineProperty(URL.prototype, "domain", {
|
||||
get() {
|
||||
try {
|
||||
return hostToDomain(this.host) || this.host;
|
||||
const {hostname} = this;
|
||||
return IPReg.test(hostname) ?
|
||||
hostname :
|
||||
hostToDomain(hostname) || hostname;
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
@ -357,3 +361,20 @@ export function mapFilterInSitu<TRes, T>(
|
||||
export function randint(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
|
||||
export function validateSubFolder(folder: string) {
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
folder = folder.replace(/[/\\]+/g, "/");
|
||||
if (folder.startsWith("/")) {
|
||||
throw new Error("error.noabsolutepath");
|
||||
}
|
||||
if (/^[a-z]:\//i.test(folder)) {
|
||||
throw new Error("error.noabsolutepath");
|
||||
}
|
||||
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
|
||||
throw new Error("error.nodotsinpath");
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
import { Prefs } from "./prefs";
|
||||
import { windows } from "./browser";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Port } from "./bus";
|
||||
|
||||
|
||||
const VALID_WINDOW_STATES = Object.freeze(new Set(["normal", "maximized"]));
|
||||
@ -55,13 +57,15 @@ export class WindowStateTracker {
|
||||
|
||||
getOptions(options: any) {
|
||||
const result = Object.assign(options, {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
state: this.state,
|
||||
});
|
||||
if (this.top >= 0) {
|
||||
result.top = this.top;
|
||||
result.left = this.left;
|
||||
if (result.state !== "maximized") {
|
||||
result.width = this.width;
|
||||
result.height = this.height;
|
||||
if (this.top >= 0) {
|
||||
result.top = this.top;
|
||||
result.left = this.left;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -78,34 +82,48 @@ export class WindowStateTracker {
|
||||
if (!this.windowId) {
|
||||
return;
|
||||
}
|
||||
const window = await windows.get(this.windowId);
|
||||
if (!VALID_WINDOW_STATES.has(window.state)) {
|
||||
return;
|
||||
try {
|
||||
const window = await windows.get(this.windowId);
|
||||
if (!VALID_WINDOW_STATES.has(window.state)) {
|
||||
return;
|
||||
}
|
||||
const previous = JSON.stringify(this);
|
||||
this.width = window.width;
|
||||
this.height = window.height;
|
||||
this.left = window.left;
|
||||
this.top = window.top;
|
||||
this.state = window.state;
|
||||
this.validate();
|
||||
if (previous === JSON.stringify(this)) {
|
||||
// Nothing changed
|
||||
return;
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
const previous = JSON.stringify(this);
|
||||
this.width = window.width;
|
||||
this.height = window.height;
|
||||
this.left = window.left;
|
||||
this.top = window.top;
|
||||
this.state = window.state;
|
||||
this.validate();
|
||||
if (previous === JSON.stringify(this)) {
|
||||
// Nothing changed
|
||||
return;
|
||||
catch {
|
||||
// ignored
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
|
||||
track(windowId: number, port: any) {
|
||||
track(windowId: number, port?: Port) {
|
||||
if (port) {
|
||||
port.on("resized", this.update);
|
||||
port.on("unload", e => this.finalize(e));
|
||||
port.on("disconnect", this.finalize.bind(this));
|
||||
}
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
async finalize() {
|
||||
async finalize(state?: any) {
|
||||
if (state) {
|
||||
this.left = state.left;
|
||||
this.top = state.top;
|
||||
}
|
||||
await this.update();
|
||||
this.windowId = 0;
|
||||
if (state) {
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
@ -1,44 +1,61 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { windows, tabs, runtime } from "../lib/browser";
|
||||
import {getManager} from "./manager/man";
|
||||
import { windows, tabs, runtime, CHROME } from "../lib/browser";
|
||||
import { getManager } from "./manager/man";
|
||||
import DEFAULT_ICONS from "../data/icons.json";
|
||||
import { Prefs } from "./prefs";
|
||||
import { _ } from "./i18n";
|
||||
import { WindowStateTracker } from "./windowstatetracker";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Port, Bus } from "./bus";
|
||||
import { timeout } from "./util";
|
||||
|
||||
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
||||
const DONATE_LANG_URLS = Object.freeze(new Map([
|
||||
["de", "https://www.downthemall.org/howto/donate/spenden/"],
|
||||
]));
|
||||
const MANAGER_URL = "/windows/manager.html";
|
||||
|
||||
export async function mostRecentBrowser(): Promise<any> {
|
||||
export async function mostRecentBrowser(incognito: boolean): Promise<any> {
|
||||
let window;
|
||||
try {
|
||||
window = await windows.getCurrent({windowTypes: ["normal"]});
|
||||
window = await windows.getCurrent();
|
||||
if (window.type !== "normal") {
|
||||
throw new Error("not a normal window");
|
||||
}
|
||||
if (incognito && !window.incognito) {
|
||||
throw new Error("Not incognito");
|
||||
}
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
window = await windows.getlastFocused({windowTypes: ["normal"]});
|
||||
window = await windows.getLastFocused();
|
||||
if (window.type !== "normal") {
|
||||
throw new Error("not a normal window");
|
||||
}
|
||||
if (incognito && !window.incognito) {
|
||||
throw new Error("Not incognito");
|
||||
}
|
||||
}
|
||||
catch {
|
||||
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||
filter((w: any) => w.type === "normal").pop();
|
||||
filter(
|
||||
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
|
||||
pop();
|
||||
}
|
||||
}
|
||||
if (!window) {
|
||||
window = await windows.create({
|
||||
url: DONATE_URL,
|
||||
incognito: !!incognito,
|
||||
type: "normal",
|
||||
});
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
export async function openInTab(url: string) {
|
||||
const window = await mostRecentBrowser();
|
||||
export async function openInTab(url: string, incognito: boolean) {
|
||||
const window = await mostRecentBrowser(incognito);
|
||||
await tabs.create({
|
||||
active: true,
|
||||
url,
|
||||
@ -47,7 +64,7 @@ export async function openInTab(url: string) {
|
||||
await windows.update(window.id, {focused: true});
|
||||
}
|
||||
|
||||
export async function openInTabOrFocus(url: string) {
|
||||
export async function openInTabOrFocus(url: string, incognito: boolean) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
@ -57,21 +74,22 @@ export async function openInTabOrFocus(url: string) {
|
||||
await windows.update(tab.windowId, {focused: true});
|
||||
return;
|
||||
}
|
||||
await openInTab(url);
|
||||
await openInTab(url, incognito);
|
||||
}
|
||||
|
||||
export async function maybeOpenInTab(url: string) {
|
||||
export async function maybeOpenInTab(url: string, incognito: boolean) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
if (etabs.length) {
|
||||
return;
|
||||
}
|
||||
await openInTab(url);
|
||||
await openInTab(url, incognito);
|
||||
}
|
||||
|
||||
export async function donate() {
|
||||
await openInTab(DONATE_URL);
|
||||
const url = DONATE_LANG_URLS.get(_("language_code")) || DONATE_URL;
|
||||
await openInTab(url, false);
|
||||
}
|
||||
|
||||
export async function openPrefs() {
|
||||
@ -85,16 +103,64 @@ export async function openManager(focus = true) {
|
||||
catch (ex) {
|
||||
console.error(ex.toString(), ex);
|
||||
}
|
||||
const url = runtime.getURL(MANAGER_URL);
|
||||
const openInPopup = await Prefs.get("manager-in-popup");
|
||||
if (openInPopup) {
|
||||
const etabs = await tabs.query({
|
||||
url
|
||||
});
|
||||
if (etabs.length) {
|
||||
if (!focus) {
|
||||
return;
|
||||
}
|
||||
const tab = etabs.pop();
|
||||
await tabs.update(tab.id, {active: true});
|
||||
await windows.update(tab.windowId, {focused: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const tracker = new WindowStateTracker("manager", {
|
||||
minWidth: 700,
|
||||
minHeight: 500,
|
||||
});
|
||||
await tracker.init();
|
||||
const windowOptions = tracker.getOptions({
|
||||
url,
|
||||
type: "popup",
|
||||
});
|
||||
const window = await windows.create(windowOptions);
|
||||
tracker.track(window.id);
|
||||
try {
|
||||
if (!CHROME) {
|
||||
windows.update(window.id, tracker.getOptions({}));
|
||||
}
|
||||
const port = await Promise.race<Port>([
|
||||
new Promise<Port>(resolve => Bus.oncePort("manager", port => {
|
||||
resolve(port);
|
||||
return true;
|
||||
})),
|
||||
timeout<Port>(5 * 1000)]);
|
||||
if (!port.isSelf) {
|
||||
throw Error("Invalid sender connected");
|
||||
}
|
||||
tracker.track(window.id, port);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("couldn't track manager", ex);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (focus) {
|
||||
await openInTabOrFocus(await runtime.getURL(MANAGER_URL));
|
||||
await openInTabOrFocus(runtime.getURL(MANAGER_URL), false);
|
||||
}
|
||||
else {
|
||||
await maybeOpenInTab(await runtime.getURL(MANAGER_URL));
|
||||
await maybeOpenInTab(runtime.getURL(MANAGER_URL), false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openUrls(urls: string) {
|
||||
const window = await mostRecentBrowser();
|
||||
export async function openUrls(urls: string, incognito: boolean) {
|
||||
const window = await mostRecentBrowser(incognito);
|
||||
for (const url of urls) {
|
||||
try {
|
||||
await tabs.create({
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "DownThemAll!",
|
||||
"version": "4.0.9",
|
||||
"name": "TraitorousDownloading!",
|
||||
"version": "4.2.6",
|
||||
|
||||
"description": "__MSG_extensionDescription__",
|
||||
"homepage_url": "https://downthemall.org/",
|
||||
"author": "Nils Maier",
|
||||
"homepage_url": "https://github.com/lordwelch/downthemall",
|
||||
"author": "lordwelch",
|
||||
|
||||
"default_locale": "en",
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
"32": "style/icon32.png",
|
||||
"48": "style/icon48.png",
|
||||
"64": "style/icon64.png",
|
||||
"96": "style/icon96.png",
|
||||
"128": "style/icon128.png",
|
||||
"256": "style/icon256.png"
|
||||
},
|
||||
@ -24,15 +23,20 @@
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"contextMenus",
|
||||
"menus",
|
||||
"cookies",
|
||||
"downloads",
|
||||
"downloads.open",
|
||||
"downloads.shelf",
|
||||
"history",
|
||||
"menus",
|
||||
"notifications",
|
||||
"sessions",
|
||||
"storage",
|
||||
"tabs",
|
||||
"theme",
|
||||
"webNavigation",
|
||||
"webRequest"
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
],
|
||||
|
||||
"background": {
|
||||
@ -50,11 +54,10 @@
|
||||
"32": "style/icon32.png",
|
||||
"48": "style/icon48.png",
|
||||
"64": "style/icon64.png",
|
||||
"96": "style/icon96.png",
|
||||
"128": "style/icon128.png",
|
||||
"256": "style/icon256.png"
|
||||
},
|
||||
"default_title": "DownThemAll!"
|
||||
"default_title": "TraitorousDownloading!"
|
||||
},
|
||||
|
||||
"options_ui": {
|
||||
@ -64,7 +67,7 @@
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "dtalite@downthemall.org",
|
||||
"id": "downloading@traitorousenterprises.net",
|
||||
"strict_min_version": "67.0"
|
||||
}
|
||||
}
|
||||
|
31
package.json
@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "dtalite",
|
||||
"name": "tdl",
|
||||
"version": "4.0.0",
|
||||
"description": "DownThemAll! lite",
|
||||
"description": "TraitorousDownloading!",
|
||||
"main": "main.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "util/build.py",
|
||||
"build:cleanup": "rm -rf bundles",
|
||||
"build:bundles": "webpack",
|
||||
"build:regexps": "node util/makexregexps.js > data/xregexps.json",
|
||||
"stats": "cloc --vcs=git --exclude-lang=Markdown,SVG",
|
||||
@ -18,24 +19,26 @@
|
||||
"author": "Nils Maier",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||
"@typescript-eslint/parser": "^2.0.0",
|
||||
"@types/node": "^12.7.8",
|
||||
"@typescript-eslint/eslint-plugin": "^2.3.2",
|
||||
"@typescript-eslint/parser": "^2.3.2",
|
||||
"chai": "^4.1.2",
|
||||
"eslint": "^6.2.2",
|
||||
"mocha": "^6.2.0",
|
||||
"ts-loader": "^6.0.4",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "^3.5.3",
|
||||
"webpack": "^4.39.3",
|
||||
"webpack-cli": "^3.3.7",
|
||||
"eslint": "^6.5.1",
|
||||
"mocha": "^6.2.1",
|
||||
"ts-loader": "^6.2.0",
|
||||
"ts-node": "^8.4.1",
|
||||
"typescript": "^3.6.3",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-cli": "^3.3.9",
|
||||
"xregexp": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/psl": "^1.1.0",
|
||||
"@types/whatwg-mimetype": "^2.1.0",
|
||||
"psl": "^1.3.0",
|
||||
"webextension-polyfill": "^0.4.0",
|
||||
"localforage": "^1.9.0",
|
||||
"psl": "^1.4.0",
|
||||
"webextension-polyfill": "^0.5.0",
|
||||
"webextension-polyfill-ts": "^0.22.0",
|
||||
"whatwg-mimetype": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,8 @@ function urlToUsable(e: any, u: string) {
|
||||
}
|
||||
|
||||
class Gatherer {
|
||||
private: boolean;
|
||||
|
||||
textLinks: boolean;
|
||||
|
||||
selectionOnly: boolean;
|
||||
@ -88,6 +90,7 @@ class Gatherer {
|
||||
transferable: string[];
|
||||
|
||||
constructor(options: any) {
|
||||
this.private = !!options.private;
|
||||
this.textLinks = options.textLinks;
|
||||
this.selectionOnly = options.selectionOnly;
|
||||
this.selection = options.selectionOnly ? getSelection() : null;
|
||||
@ -118,30 +121,43 @@ class Gatherer {
|
||||
|
||||
*collectImageInternal(img: HTMLImageElement) {
|
||||
try {
|
||||
const src = img.currentSrc || img.src;
|
||||
const item = this.makeItem(src, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
|
||||
const {srcset} = img;
|
||||
if (!srcset) {
|
||||
return;
|
||||
}
|
||||
const imgs = srcset.split(",").flatMap(e => {
|
||||
const idx = e.lastIndexOf(" ");
|
||||
return (idx > 0 ? e.slice(0, idx) : e).trim();
|
||||
});
|
||||
for (const i of imgs) {
|
||||
const item = this.makeItem(i, img);
|
||||
{
|
||||
const {src} = img;
|
||||
const item = this.makeItem(src, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
{
|
||||
const {currentSrc} = img;
|
||||
const item = this.makeItem(currentSrc, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const {srcset} = img;
|
||||
if (!srcset) {
|
||||
return;
|
||||
}
|
||||
const imgs = srcset.split(",").flatMap(e => {
|
||||
const idx = e.lastIndexOf(" ");
|
||||
return (idx > 0 ? e.slice(0, idx) : e).trim();
|
||||
});
|
||||
for (const i of imgs) {
|
||||
const item = this.makeItem(i, img);
|
||||
if (item) {
|
||||
item.fileName = "";
|
||||
item.description = item.title;
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("oops image", ex.toString(), ex.stack, ex);
|
||||
@ -255,6 +271,7 @@ class Gatherer {
|
||||
return {
|
||||
url: url.href,
|
||||
title,
|
||||
private: this.private
|
||||
};
|
||||
}
|
||||
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 |
419
style/common.css
@ -2,14 +2,29 @@
|
||||
/* License: gpl-v2 */
|
||||
|
||||
: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-active-border-color: #478de7;
|
||||
--toolbar-hover-border-color: red;
|
||||
--toolbar-hover-background: rgb(247, 149, 37);
|
||||
--toolbar-border-width: 2px;
|
||||
--toolbar-border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
--add-color: navy;
|
||||
--queue-color: gray;
|
||||
--pause-color: #ffa318;
|
||||
--retry-color: rgb(0, 112, 204);
|
||||
--error-color: rgb(160, 13, 42);
|
||||
--running-color: #aae061;
|
||||
--finishing-color: #57cc12;
|
||||
@ -19,100 +34,278 @@
|
||||
--maskbutton-color: rgb(236, 185, 16);
|
||||
--missing-color: rgb(0, 82, 204);
|
||||
--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"] {
|
||||
--folder-color: rgb(4, 102, 214);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'downthemall';
|
||||
src: url('downthemall.woff2?75791791') format('woff2');
|
||||
font-family: "downthemall";
|
||||
src: url("downthemall.woff2?75791791") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||
[class^="icon-"]:before,
|
||||
[class*=" icon-"]:before {
|
||||
font-family: "downthemall";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
|
||||
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
|
||||
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-add:before { content: '\e800'; } /* '' */
|
||||
.icon-addsegment:before { content: '\e801'; } /* '' */
|
||||
.icon-bottom:before { content: '\e802'; } /* '' */
|
||||
.icon-picture:before { content: '\e803'; } /* '' */
|
||||
.icon-circle:before { content: '\e804'; } /* '' */
|
||||
.icon-delete:before { content: '\e805'; } /* '' */
|
||||
.icon-done:before { content: '\e806'; } /* '' */
|
||||
.icon-down:before { content: '\e807'; } /* '' */
|
||||
.icon-download:before { content: '\e808'; } /* '' */
|
||||
.icon-dupe:before { content: '\e809'; } /* '' */
|
||||
.icon-error:before { content: '\e80a'; } /* '' */
|
||||
.icon-failed:before { content: '\e80b'; } /* '' */
|
||||
.icon-file:before { content: '\e80c'; } /* '' */
|
||||
.icon-find:before { content: '\e80d'; } /* '' */
|
||||
.icon-folder:before { content: '\e80e'; } /* '' */
|
||||
.icon-force:before { content: '\e80f'; } /* '' */
|
||||
.icon-go:before { content: '\e810'; } /* '' */
|
||||
.icon-import:before { content: '\e811'; } /* '' */
|
||||
.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-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:before { content: '\f205'; } /* '' */
|
||||
.icon-server:before { content: '\f233'; } /* '' */
|
||||
.icon-question-light:before { content: '\f29c'; } /* '' */
|
||||
|
||||
.icon-add:before {
|
||||
content: "\e800";
|
||||
} /* '' */
|
||||
.icon-addsegment:before {
|
||||
content: "\e801";
|
||||
} /* '' */
|
||||
.icon-bottom:before {
|
||||
content: "\e802";
|
||||
} /* '' */
|
||||
.icon-picture:before {
|
||||
content: "\e803";
|
||||
} /* '' */
|
||||
.icon-circle:before {
|
||||
content: "\e804";
|
||||
} /* '' */
|
||||
.icon-delete:before {
|
||||
content: "\e805";
|
||||
} /* '' */
|
||||
.icon-done:before {
|
||||
content: "\e806";
|
||||
} /* '' */
|
||||
.icon-down:before {
|
||||
content: "\e807";
|
||||
} /* '' */
|
||||
.icon-download:before {
|
||||
content: "\e808";
|
||||
} /* '' */
|
||||
.icon-dupe:before {
|
||||
content: "\e809";
|
||||
} /* '' */
|
||||
.icon-error:before {
|
||||
content: "\e80a";
|
||||
} /* '' */
|
||||
.icon-failed:before {
|
||||
content: "\e80b";
|
||||
} /* '' */
|
||||
.icon-file:before {
|
||||
content: "\e80c";
|
||||
} /* '' */
|
||||
.icon-find:before {
|
||||
content: "\e80d";
|
||||
} /* '' */
|
||||
.icon-folder:before {
|
||||
content: "\e80e";
|
||||
} /* '' */
|
||||
.icon-force:before {
|
||||
content: "\e80f";
|
||||
} /* '' */
|
||||
.icon-go:before {
|
||||
content: "\e810";
|
||||
} /* '' */
|
||||
.icon-import:before {
|
||||
content: "\e811";
|
||||
} /* '' */
|
||||
.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) {
|
||||
[class^="icon-file-"]:before, [class*=" icon-file-"]:before {
|
||||
[class^="icon-file-"]:before,
|
||||
[class*=" icon-file-"]:before {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-file-image {
|
||||
color: rgb(17, 107, 163);
|
||||
color: var(--file-icon-image-color);
|
||||
}
|
||||
|
||||
.icon-file-pdf,
|
||||
@ -133,18 +326,29 @@ html[data-platform="mac"] {
|
||||
color: rgb(202, 81, 198);
|
||||
}
|
||||
|
||||
body, html {
|
||||
background: #F6F6F8;
|
||||
color: #0C0C0D;
|
||||
body,
|
||||
html {
|
||||
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%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
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-weight: bold;
|
||||
}
|
||||
@ -184,7 +388,11 @@ section {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -211,8 +419,12 @@ td.virtualtable {
|
||||
font-size: 12px;
|
||||
align-items: stretch;
|
||||
justify-items: center;
|
||||
background: linear-gradient(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);
|
||||
background: linear-gradient(
|
||||
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;
|
||||
margin-bottom: 1ex;
|
||||
overflow: auto;
|
||||
@ -239,15 +451,15 @@ td.virtualtable {
|
||||
flex-grow: 3;
|
||||
margin-right: 2ex;
|
||||
padding-right: 1ex;
|
||||
border-right: 1px dotted rgba(128,128,128,0.6);
|
||||
border-right: 1px dotted rgba(128, 128, 128, 0.6);
|
||||
}
|
||||
|
||||
#statusPrefs {
|
||||
cursor: pointer;
|
||||
color: #363636;
|
||||
color: var(--status-icon-color);
|
||||
}
|
||||
#statusPrefs:hover {
|
||||
color: #6e6d6d;
|
||||
color: var(--status-icon-color-hover);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@ -265,13 +477,14 @@ td.virtualtable {
|
||||
outline: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width:100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown input {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
color: black;
|
||||
background: white;
|
||||
border: none;
|
||||
bottom: 2px;
|
||||
@ -294,7 +507,6 @@ td.virtualtable {
|
||||
padding-bottom: 1ex;
|
||||
}
|
||||
|
||||
|
||||
@supports (not (-moz-appearance: none)) {
|
||||
.dropdown select {
|
||||
background: white;
|
||||
@ -363,4 +575,55 @@ td.virtualtable {
|
||||
|
||||
#maskButton {
|
||||
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 {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
background: var(--toolbar-bg-color) url(tile.png) repeat-x;
|
||||
background: var(--toolbar-bg-color) var(--tile-url) repeat-x;
|
||||
}
|
||||
|
||||
#toolbar .spacer {
|
||||
@ -42,9 +42,9 @@ body > * {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 5px 1px rgba(128,128,128,0.5);
|
||||
background: rgb(246,246,246);
|
||||
color: black;
|
||||
box-shadow: var(--general-button-shadow);
|
||||
background: var(--general-button-bgcolor);
|
||||
color: var(--general-button-color);
|
||||
transition: box-shadow 0.5s, background 1s;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
@ -60,7 +60,7 @@ body > * {
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
@ -88,14 +88,14 @@ body > * {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
background: white;
|
||||
background: var(--general-bgcolor);
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255,255,255,0.9);
|
||||
background: var(--general-button-bgcolor);
|
||||
font-weight: bolder;
|
||||
font-size: 200%;
|
||||
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,
|
||||
.canceled .virtualtable-column-2 .virtualtable-icon {
|
||||
color: var(--error-color);
|
||||
@ -294,7 +311,7 @@ body > * {
|
||||
color: crimson;
|
||||
}
|
||||
#statusNetwork.icon-network-on {
|
||||
color: navy;
|
||||
color: var(--add-color);
|
||||
}
|
||||
|
||||
#statusFilter {
|
||||
@ -327,6 +344,7 @@ body > * {
|
||||
height: 16px;
|
||||
-moz-appearance: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
width: calc(100% - 28px);
|
||||
}
|
||||
@ -386,7 +404,7 @@ body > * {
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
#nagging {
|
||||
border-top: 1px solid lightgray;
|
||||
border-top: 1px solid var(--general-border-color);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
align-content: center;
|
||||
@ -511,4 +529,24 @@ body > * {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--done-color);
|
||||
}
|
||||
|
||||
#tooltip-eta.single {
|
||||
font-weight: bold;
|
||||
grid-column-end: span 2;
|
||||
}
|
||||
|
||||
.deletefiles-list {
|
||||
padding-left: 1ex;
|
||||
padding-right: 1.5ex;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128,128,128,0.1);
|
||||
max-height: 8em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.deletefiles-list > li {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
/* License: gpl-v2 */
|
||||
@import 'common.css';
|
||||
@import "common.css";
|
||||
|
||||
html, body {
|
||||
background: transparent !important;
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@ -19,9 +19,10 @@ article {
|
||||
|
||||
#tabs {
|
||||
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);
|
||||
color: white;
|
||||
color: var(--general-bgcolor);
|
||||
}
|
||||
|
||||
input.tab {
|
||||
@ -54,9 +55,10 @@ input.tab {
|
||||
#tabsel-general:checked ~ #tabs #tabel-general,
|
||||
#tabsel-filters:checked ~ #tabs #tabel-filters,
|
||||
#tabsel-network:checked ~ #tabs #tabel-network {
|
||||
color: black !important;
|
||||
background: white;
|
||||
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color);
|
||||
color: var(--general-color) !important;
|
||||
background: var(--general-bgcolor);
|
||||
border-top: var(--toolbar-border-width) solid
|
||||
var(--toolbar-active-border-color);
|
||||
}
|
||||
|
||||
#tabs > label {
|
||||
@ -64,13 +66,14 @@ input.tab {
|
||||
border-top: var(--toolbar-border-width) solid transparent;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-left: var(--toolbar-border);
|
||||
border-right: var(--toolbar-border);
|
||||
background: var(--toolbar-bg-color);
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
@ -102,7 +105,7 @@ input.tab {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.buttons > button{
|
||||
.buttons > button {
|
||||
margin: 0 2em;
|
||||
}
|
||||
|
||||
@ -113,15 +116,27 @@ input.tab {
|
||||
fieldset {
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid lightgray;
|
||||
border: 1px solid var(--general-border-color);
|
||||
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);
|
||||
flex-direction: column;
|
||||
max-width: 60em;
|
||||
padding: 1.2em;
|
||||
}
|
||||
|
||||
.optiongroups,
|
||||
fieldset > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
fieldset > label > input,
|
||||
fieldset > label > select {
|
||||
margin-left: 1ex;
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
@ -134,8 +149,36 @@ legend {
|
||||
}
|
||||
|
||||
.virtualtable-container {
|
||||
border: 1px solid lightgray;
|
||||
border: 1px solid var(--general-border-color);
|
||||
border-radius: 6px;
|
||||
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-left: calc(2em + 32px);
|
||||
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-size: 150%;
|
||||
font-weight: bold;
|
||||
@ -117,7 +117,7 @@ body > * {
|
||||
}
|
||||
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
||||
#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;
|
||||
user-select: none;
|
||||
background: var(--toolbar-bg-color);
|
||||
color: white;
|
||||
color: var(--general-color);
|
||||
min-width: 10em;
|
||||
padding: 1ex;
|
||||
padding-left: 1em;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-top: var(--toolbar-border-width) solid transparent;
|
||||
border-left: 1px solid rgba(255,255,255,0.3);
|
||||
border-right: 1px solid rgba(255,255,255,0.3);
|
||||
border-left: var(--toolbar-border);
|
||||
border-right: var(--toolbar-border);
|
||||
transition: border 1s;
|
||||
}
|
||||
|
||||
.tab:not(.active):not(.disabled):hover {
|
||||
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);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: black;
|
||||
background: white;
|
||||
color: var(--general-color);
|
||||
background: var(--table-head-bgcolor);
|
||||
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color);
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
|
@ -63,7 +63,8 @@ p.example {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#options > #maskOptions {
|
||||
#options > #subfolderOptions,
|
||||
#options > #maskOptions, #options > #serverOptions {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr auto auto;
|
||||
}
|
||||
@ -84,4 +85,4 @@ h3 {
|
||||
|
||||
#btnDownload {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
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(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);
|
||||
this.disabled = options.disabled === "true";
|
||||
this.elem.setAttribute("aria-role", "menuitem");
|
||||
this.elem.addEventListener(
|
||||
"click", () => this.owner.emit("clicked", this.id, this.autoHide));
|
||||
this.clicked = this.clicked.bind(this);
|
||||
this.elem.addEventListener("click", this.clicked);
|
||||
this.elem.addEventListener("contextmenu", this.clicked);
|
||||
}
|
||||
|
||||
clicked() {
|
||||
this.owner.emit("clicked", this.id, this.autoHide);
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
|
@ -104,7 +104,7 @@ export class EventEmitter {
|
||||
for (const e of Array.from(handlers)) {
|
||||
try {
|
||||
// eslint-disable-next-line prefer-spread
|
||||
handled = handled || !!e.apply(null, args);
|
||||
handled = !!e.apply(null, args) || handled;
|
||||
}
|
||||
catch (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), {
|
||||
passive: true
|
||||
});
|
||||
body.addEventListener("contextmenu", this.contextmenu.bind(this), true);
|
||||
|
||||
table.addEventListener("keypress", 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));
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ def main():
|
||||
if modified:
|
||||
try:
|
||||
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
|
||||
json.dump(data, outp, sort_keys=True, indent=2)
|
||||
json.dump(data, outp, sort_keys=True, indent=2, ensure_ascii=False)
|
||||
os.rename("messages.json.tmp", "_locales/en/messages.json")
|
||||
finally:
|
||||
try:
|
||||
|
@ -20,16 +20,19 @@ FILES = [
|
||||
"LICENSE.*",
|
||||
]
|
||||
|
||||
RELEASE_ID = "{DDC359D1-844A-42a7-9AA1-88A850A938A8}"
|
||||
RELEASE_ID = "downloading@traitorousenterprises.net"
|
||||
|
||||
UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
||||
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
||||
IGNORED = set((".DS_Store", "Thumbs.db"))
|
||||
# XXX: #125
|
||||
IGNORED_OPERA = set(("done.opus", "error.opus"))
|
||||
|
||||
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest"))
|
||||
PERM_IGNORED_CHROME = set(("menus",))
|
||||
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest", "webRequestBlocking"))
|
||||
PERM_IGNORED_CHROME = set(("menus", "sessions", "theme"))
|
||||
|
||||
SCRIPTS = [
|
||||
"yarn build:cleanup",
|
||||
"yarn build:regexps",
|
||||
"yarn build:bundles",
|
||||
]
|
||||
@ -45,17 +48,17 @@ def check_licenses():
|
||||
raise Exception(f"No license in {file}")
|
||||
|
||||
|
||||
def files():
|
||||
def files(additional_ignored):
|
||||
p = Path("")
|
||||
for pattern in FILES:
|
||||
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
|
||||
yield file
|
||||
|
||||
def build(out, manifest):
|
||||
with ZipFile(out, "w", compression=ZIP_DEFLATED, allowZip64=False, compresslevel=2) as zp:
|
||||
for file in files():
|
||||
def build(out, manifest, additional_ignored=set()):
|
||||
with ZipFile(out, "w", compression=ZIP_DEFLATED, allowZip64=False) as zp:
|
||||
for file in files(additional_ignored):
|
||||
if str(file) == "manifest.json":
|
||||
buf = manifest
|
||||
else:
|
||||
@ -68,7 +71,7 @@ def build(out, manifest):
|
||||
if file.suffix in UNCOMPRESSABLE:
|
||||
zp.writestr(zinfo, buf, compress_type=ZIP_STORED)
|
||||
else:
|
||||
zp.writestr(zinfo, buf, compress_type=ZIP_DEFLATED, compresslevel=2)
|
||||
zp.writestr(zinfo, buf, compress_type=ZIP_DEFLATED)
|
||||
print(file)
|
||||
|
||||
|
||||
@ -85,14 +88,14 @@ def build_firefox(args):
|
||||
|
||||
if args.mode != "release":
|
||||
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["name"] = f"{infos.get('name')} {args.mode}"
|
||||
else:
|
||||
infos["browser_specific_settings"]["gecko"]["id"] = RELEASE_ID
|
||||
|
||||
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_FX]
|
||||
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-fx.zip"
|
||||
out = Path("web-ext-artifacts") / f"tdl-{version}-{args.mode}-fx.zip"
|
||||
if not out.parent.exists():
|
||||
out.parent.mkdir()
|
||||
if out.exists():
|
||||
@ -100,8 +103,8 @@ def build_firefox(args):
|
||||
print("Output", out)
|
||||
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")
|
||||
with open("manifest.json") as manip:
|
||||
infos = json.load(manip, object_pairs_hook=OrderedDict)
|
||||
@ -117,15 +120,15 @@ def build_chrome(args):
|
||||
infos["version_name"] = f"{version}-{args.mode}"
|
||||
infos["short_name"] = infos.get("name")
|
||||
infos["name"] = f"{infos.get('name')} {args.mode}"
|
||||
|
||||
|
||||
infos["permissions"] = [p for p in infos.get("permissions") if not p in PERM_IGNORED_CHROME]
|
||||
out = Path("web-ext-artifacts") / f"dta-{version}-{args.mode}-crx.zip"
|
||||
out = Path("web-ext-artifacts") / f"tdl-{version}-{args.mode}-{pkg}.zip"
|
||||
if not out.parent.exists():
|
||||
out.parent.mkdir()
|
||||
if out.exists():
|
||||
out.unlink()
|
||||
print("Output", out)
|
||||
build(out, json.dumps(infos, indent=2).encode("utf-8"))
|
||||
build(out, json.dumps(infos, indent=2).encode("utf-8"), additional_ignored=additional_ignored)
|
||||
|
||||
def main():
|
||||
from argparse import ArgumentParser
|
||||
@ -140,8 +143,9 @@ def main():
|
||||
else:
|
||||
run([script], shell=True)
|
||||
build_firefox(args)
|
||||
build_chrome(args)
|
||||
build_chromium(args, "crx")
|
||||
build_chromium(args, "opr", IGNORED_OPERA)
|
||||
print("DONE.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
@ -6,7 +6,7 @@ langs = sorted(Path("_locales").glob("**/messages.json"), key=lambda p: p.parent
|
||||
all = {}
|
||||
for m in langs:
|
||||
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")
|
||||
if not lang:
|
||||
raise Exception(f"{m}: no language")
|
||||
|