Compare commits
100 Commits
Author | SHA1 | Date | |
---|---|---|---|
31999bba9f | |||
73d90662e3 | |||
5f5deb09f3 | |||
b68245f4f0 | |||
34c8537cb5 | |||
6b7c9d461d | |||
d2f09ca592 | |||
919c6a8f10 | |||
dcd7e7cd0e | |||
c545bbab1d | |||
dc6e64e690 | |||
578762db27 | |||
e996a2b41a | |||
38496d9161 | |||
4a0756aa26 | |||
63d0ff22fa | |||
5390642978 | |||
2e59dedda3 | |||
ee649717a2 | |||
078ce277ce | |||
7a3cad83b0 | |||
4d953c373f | |||
da9832552f | |||
5909633a04 | |||
750fd987bd | |||
b87e0d6138 | |||
31cb23923a | |||
71d98bc603 | |||
4b09a0db67 | |||
da6c6bcf68 | |||
84fea3ba35 | |||
33de1cbce9 | |||
04b8a981ef | |||
58c7955c64 | |||
dcf9603da8 | |||
c2b9664b4b | |||
e760d2b022 | |||
1a836d914b | |||
1b0e6eb6c4 | |||
39827ad485 | |||
79c4d4e98f | |||
427bd2f348 | |||
4fefd0e128 | |||
d79060237d | |||
2df7a1c592 | |||
8c4ceb3e4b | |||
bf725ece72 | |||
76992bd4f4 | |||
dccd530475 | |||
f1fa01a0eb | |||
7949142ef6 | |||
af1da8fc0a | |||
39f4237cde | |||
b676ed74cd | |||
bf474877ca | |||
7ee13af238 | |||
d488e5874a | |||
b1a7c22452 | |||
e928d202ee | |||
c39961d253 | |||
c6d11fcd7f | |||
eb96103478 | |||
583ccfc7b1 | |||
e0437718a0 | |||
2126ae022b | |||
2ef39dcb19 | |||
047c865e76 | |||
c586cd00cc | |||
ee7f470269 | |||
f04dda308b | |||
071458e262 | |||
9ffc96de4d | |||
26e9a5404a | |||
f44fe59054 | |||
e4b0629dee | |||
5c2700ca36 | |||
639a582804 | |||
2d1f185fcd | |||
38735ed0ae | |||
216bc590da | |||
1c10d8005a | |||
1fcfbe5360 | |||
8d3dda1cec | |||
be18f667d9 | |||
027b2c4fb1 | |||
4ed92878be | |||
a6930f309e | |||
fdcdae0412 | |||
2c18ddaaa8 | |||
994e7ad0a6 | |||
95536b36be | |||
9c159d5d24 | |||
42ccfd5dc5 | |||
dabf7f8a28 | |||
9cac48f439 | |||
ef6bc840d8 | |||
1c38ec1357 | |||
a4436bd6c8 | |||
5a4b8143b2 | |||
00a5712427 |
10
LICENSE.md
10
LICENSE.md
@ -77,3 +77,13 @@ Licensed under the Mozilla Public License 2.0.
|
|||||||
|
|
||||||
The list itself is licensed under the Mozilla Public License 2.0.
|
The list itself is licensed under the Mozilla Public License 2.0.
|
||||||
The javascript library accessing it is licensed under the MIT license.
|
The javascript library accessing it is licensed under the MIT license.
|
||||||
|
|
||||||
|
## whatwg-mimetype
|
||||||
|
|
||||||
|
Licensed under MIT
|
||||||
|
|
||||||
|
|
||||||
|
## CDHeaderParser
|
||||||
|
|
||||||
|
Licensed under MPL-2
|
||||||
|
(c) 2017 Rob Wu <rob@robwu.nl> (https://robwu.nl)
|
||||||
|
6
TODO.md
6
TODO.md
@ -8,12 +8,8 @@ P2
|
|||||||
|
|
||||||
Planned for later.
|
Planned for later.
|
||||||
|
|
||||||
* Soft errors and retry logic
|
|
||||||
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
|
|
||||||
* Delete files (well, as far as the browser allows)
|
|
||||||
* Inter-addon API (basic)
|
* Inter-addon API (basic)
|
||||||
* Add downloads
|
* Add downloads
|
||||||
* Chrome support
|
|
||||||
* vtable perf: cache column widths
|
* vtable perf: cache column widths
|
||||||
* Download options
|
* Download options
|
||||||
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
|
||||||
@ -46,8 +42,6 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
|
|||||||
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
|
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
|
||||||
* Segmented downloads
|
* Segmented downloads
|
||||||
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
|
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
|
||||||
* Handle errors, 404 and such
|
|
||||||
* The Firefox download manager is too stupid and webRequest does not see Downloads, so cannot be done right now.
|
|
||||||
* Conflicts: ask when a file exists
|
* Conflicts: ask when a file exists
|
||||||
* Not supported by Firefox
|
* Not supported by Firefox
|
||||||
* Speed limiter
|
* Speed limiter
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
{
|
{
|
||||||
|
"ar": "العربية [ar]",
|
||||||
|
"bg": "Български [bg]",
|
||||||
"cs": "Čeština (CZ) [cs]",
|
"cs": "Čeština (CZ) [cs]",
|
||||||
"de": "Deutsch [de]",
|
"de": "Deutsch [de]",
|
||||||
"el": "Ελληνικά [el]",
|
"el": "Ελληνικά [el]",
|
||||||
"en": "English (US) [en]",
|
"en": "English (US) [en]",
|
||||||
"es": "Español (España) [es]",
|
"es": "Español (España) [es]",
|
||||||
"et": "Eesti Keel [et]",
|
"et": "Eesti keel [et]",
|
||||||
"fr": "Français (FR) [fr]",
|
"fr": "Français [fr]",
|
||||||
|
"hu": "Magyar (HU) [hu]",
|
||||||
"id": "Bahasa Indonesia [id]",
|
"id": "Bahasa Indonesia [id]",
|
||||||
|
"it": "Italiano [it]",
|
||||||
|
"ja": "日本語 (JP) [ja]",
|
||||||
"ko": "한국어 [ko]",
|
"ko": "한국어 [ko]",
|
||||||
"lt": "Lietuvių [lt]",
|
"lt": "Lietuvių [lt]",
|
||||||
"nl": "Nederlands [nl]",
|
"nl": "Nederlands [nl]",
|
||||||
"pl": "Polski (PL) [pl]",
|
"pl": "Polski [pl]",
|
||||||
"pt": "Português (Brasil) [pt]",
|
"pt": "Português (Brasil) [pt]",
|
||||||
"ru": "Русский [ru]",
|
"ru": "Русский [ru]",
|
||||||
"zh_CN": "简体中文 [zh_CN]"
|
"zh_CN": "简体中文 [zh_CN]",
|
||||||
|
"zh_TW": "正體中文 [zh_TW]"
|
||||||
}
|
}
|
1170
_locales/ar/messages.json
Normal file
1170
_locales/ar/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1284
_locales/bg/messages.json
Normal file
1284
_locales/bg/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -191,6 +191,22 @@
|
|||||||
"message": "Διαγραφή",
|
"message": "Διαγραφή",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Διαγραφή Αρχείων",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Διαγραφή",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Θέλετε σίγουρα να διαγράψετε τα ακόλουθα αρχεία;",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Διαγραφή Αρχείων",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Περιγραφή",
|
"message": "Περιγραφή",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -275,6 +291,14 @@
|
|||||||
"message": "Μη έγκυρο URL",
|
"message": "Μη έγκυρο URL",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Οι πλήρεις διαδρομές υποφακέλων δεν υποστηρίζονται από τους φυλλομετρητές",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Οι τελείες (.) στους υποφακέλους δεν υποστηρίζονται από τους φυλλομετρητές",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Δεν επιλέχθηκαν αντικείμενα",
|
"message": "Δεν επιλέχθηκαν αντικείμενα",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
@ -529,6 +553,26 @@
|
|||||||
"message": "Προσθήκη νέων λήψεων σε παύση, αντί για άμεση εκκίνησή τους",
|
"message": "Προσθήκη νέων λήψεων σε παύση, αντί για άμεση εκκίνησή τους",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Κουμπί Λήψης Όλων",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "Επιλογή Λήψης Όλων",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Άνοιγμα Διαχειριστή",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Αναδυόμενο μενού",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"message": "ΜονόΚλικ!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Ταυτόχρονες λήψεις",
|
"message": "Ταυτόχρονες λήψεις",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -537,10 +581,6 @@
|
|||||||
"message": "Προβολή ειδοποίησης με την ολοκλήρωση της ουράς λήψεων",
|
"message": "Προβολή ειδοποίησης με την ολοκλήρωση της ουράς λήψεων",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Το κουμπί φυλλομετρητή πρέπει να είναι το ΜονόΚλικ!",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Απόκρυψη αντικειμένων μενού γενικού περιεχομένου",
|
"message": "Απόκρυψη αντικειμένων μενού γενικού περιεχομένου",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -549,6 +589,10 @@
|
|||||||
"message": "Διαχειριστής",
|
"message": "Διαχειριστής",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Άνοιγμα διαχειριστή σε νέο αναδυόμενο παράθυρο",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Προβολή επεξηγήσεων στις καρτέλες Διαχειριστή",
|
"message": "Προβολή επεξηγήσεων στις καρτέλες Διαχειριστή",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -573,10 +617,22 @@
|
|||||||
"message": "Απομάκρυνση αγνοημένων λήψεων μετά την επανεκκίνηση",
|
"message": "Απομάκρυνση αγνοημένων λήψεων μετά την επανεκκίνηση",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Αριθμός επαναλήψεων λήψεων με προσωρινά σφάλματα",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Επανάληψη κάθε (σε λεπτά)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Προβολή URLs αντί για Ονόματα",
|
"message": "Προβολή URLs αντί για Ονόματα",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Αναπαραγωγή ήχων",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Δοκιμή εύρεσης συνδέσμων στο κείμενο της ιστοσελίδας (αργό)",
|
"message": "Δοκιμή εύρεσης συνδέσμων στο κείμενο της ιστοσελίδας (αργό)",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -895,6 +951,20 @@
|
|||||||
"message": "Συνέχιση",
|
"message": "Συνέχιση",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Επανάληψη",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Επανάληψη - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "Σε λειτουργία",
|
"message": "Σε λειτουργία",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -943,6 +1013,18 @@
|
|||||||
"message": "Ορισμός Μάσκας Μετονομασίας",
|
"message": "Ορισμός Μάσκας Μετονομασίας",
|
||||||
"description": "Menu text; select window"
|
"description": "Menu text; select window"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Ορισμός μιας νέας μάσκας μετονομασίας",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Ορισμός Αναφορέα",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Ορισμός νέου αναφορέα",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Οι δέσμες υποστηρίζονται, π.χ.:",
|
"message": "Οι δέσμες υποστηρίζονται, π.χ.:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -1123,6 +1205,14 @@
|
|||||||
"message": "Καμία νέα Λήψη δεν θα εκκινηθεί",
|
"message": "Καμία νέα Λήψη δεν θα εκκινηθεί",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"message": "Υποφάκελος:",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Τοποθέτηση αρχείων σε αυτό τον υποφάκελο μέσα στον φάκελο λήψεών σας",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Τίτλος",
|
"message": "Τίτλος",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1166,5 +1256,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Χρήση μόνο μια φορά",
|
"message": "Χρήση μόνο μια φορά",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Ακύρωση του χρήστη",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,18 +27,90 @@
|
|||||||
"description": "Error message",
|
"description": "Error message",
|
||||||
"message": "Unauthorized"
|
"message": "Unauthorized"
|
||||||
},
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"description": "Error message",
|
||||||
|
"message": "User Canceled"
|
||||||
|
},
|
||||||
|
"add_download": {
|
||||||
|
"description": "Action for adding a download",
|
||||||
|
"message": "Add Download"
|
||||||
|
},
|
||||||
|
"add_new": {
|
||||||
|
"description": "Button text (adding filters, limits and such)",
|
||||||
|
"message": "Add New"
|
||||||
|
},
|
||||||
|
"add_paused_once": {
|
||||||
|
"description": "Checkbox label",
|
||||||
|
"message": "Only add paused this time"
|
||||||
|
},
|
||||||
|
"add_paused_question": {
|
||||||
|
"description": "Messagebox text",
|
||||||
|
"message": "Do you want to remember this decision and always add new downloads paused from now on?"
|
||||||
|
},
|
||||||
|
"add_paused_title": {
|
||||||
|
"description": "Title for the add-paused dialog",
|
||||||
|
"message": "Always add paused?"
|
||||||
|
},
|
||||||
"addpaused": {
|
"addpaused": {
|
||||||
"description": "Action: Add paused",
|
"description": "Action: Add paused",
|
||||||
"message": "Add paused"
|
"message": "Add paused"
|
||||||
},
|
},
|
||||||
|
"ask_again_later": {
|
||||||
|
"description": "Button text",
|
||||||
|
"message": "Ask me again later"
|
||||||
|
},
|
||||||
|
"batch_batch": {
|
||||||
|
"description": "Button text",
|
||||||
|
"message": "Batch Download"
|
||||||
|
},
|
||||||
|
"batch_desc": {
|
||||||
|
"description": "",
|
||||||
|
"message": "The current URL seems to contain instructions for a batch download."
|
||||||
|
},
|
||||||
|
"batch_items": {
|
||||||
|
"description": "Messagebox info text for batch confirmations",
|
||||||
|
"message": "Number of items:"
|
||||||
|
},
|
||||||
|
"batch_preview": {
|
||||||
|
"description": "Messagebox info text for batch confirmations",
|
||||||
|
"message": "Preview:"
|
||||||
|
},
|
||||||
|
"batch_question": {
|
||||||
|
"description": "Messagebox info text for batch confirmations",
|
||||||
|
"message": "Do you want to add this as a batch or as a single download?"
|
||||||
|
},
|
||||||
|
"batch_single": {
|
||||||
|
"description": "Button text for batch confirmation",
|
||||||
|
"message": "Single Download"
|
||||||
|
},
|
||||||
|
"batch_title": {
|
||||||
|
"description": "Messagebox title for batch confirmations",
|
||||||
|
"message": "Batch Download"
|
||||||
|
},
|
||||||
"cancel": {
|
"cancel": {
|
||||||
"description": "Button text: Cancel",
|
"description": "Button text: Cancel",
|
||||||
"message": "Cancel"
|
"message": "Cancel"
|
||||||
},
|
},
|
||||||
|
"cancel_download": {
|
||||||
|
"description": "Action to cancel downloads, e.g. from the context menu",
|
||||||
|
"message": "Cancel"
|
||||||
|
},
|
||||||
"canceled": {
|
"canceled": {
|
||||||
"description": "Download status text",
|
"description": "Download status text",
|
||||||
"message": "Canceled"
|
"message": "Canceled"
|
||||||
},
|
},
|
||||||
|
"cannot_be_empty": {
|
||||||
|
"description": "Error message when an input field is empty but has to have a value",
|
||||||
|
"message": "This field cannot be empty"
|
||||||
|
},
|
||||||
|
"change_later_reminder": {
|
||||||
|
"description": "Checkbox label text for decision confirmations",
|
||||||
|
"message": "You can later change this decision in the Preferences"
|
||||||
|
},
|
||||||
|
"check_selected_items": {
|
||||||
|
"description": "Menu text",
|
||||||
|
"message": "Check Selected Items"
|
||||||
|
},
|
||||||
"colConnections": {
|
"colConnections": {
|
||||||
"description": "Table column in prefs/network",
|
"description": "Table column in prefs/network",
|
||||||
"message": "Concurrent Connections"
|
"message": "Concurrent Connections"
|
||||||
@ -75,276 +147,6 @@
|
|||||||
"description": "Table column in manager",
|
"description": "Table column in manager",
|
||||||
"message": "Speed"
|
"message": "Speed"
|
||||||
},
|
},
|
||||||
"delete": {
|
|
||||||
"description": "button text",
|
|
||||||
"message": "Delete"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"description": "Description (keep it short); e.g. the description column in select",
|
|
||||||
"message": "Description"
|
|
||||||
},
|
|
||||||
"donate": {
|
|
||||||
"description": "Donate button",
|
|
||||||
"message": "Donate!"
|
|
||||||
},
|
|
||||||
"done": {
|
|
||||||
"description": "Status text",
|
|
||||||
"message": "Done"
|
|
||||||
},
|
|
||||||
"download": {
|
|
||||||
"description": "Download (noun); e.g. Download column in select",
|
|
||||||
"message": "Download"
|
|
||||||
},
|
|
||||||
"extensionDescription": {
|
|
||||||
"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",
|
|
||||||
"message": "The Mass Downloader for your browser"
|
|
||||||
},
|
|
||||||
"fastfiltering": {
|
|
||||||
"description": "Label for Fast Filtering input",
|
|
||||||
"message": "Fast Filtering"
|
|
||||||
},
|
|
||||||
"finishing": {
|
|
||||||
"description": "Status text",
|
|
||||||
"message": "Finishing"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"description": "Lanuage Name in your language",
|
|
||||||
"message": "English (US)"
|
|
||||||
},
|
|
||||||
"language_code": {
|
|
||||||
"description": "Language code the locale will use, e.g. de or en-GB or pt-BR",
|
|
||||||
"message": "en"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"description": "Links tab label (short); select window",
|
|
||||||
"message": "Links"
|
|
||||||
},
|
|
||||||
"mask": {
|
|
||||||
"description": "Renaming mask (short); used in e.g. select",
|
|
||||||
"message": "Mask"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"description": "Media label (short)",
|
|
||||||
"message": "Media"
|
|
||||||
},
|
|
||||||
"missing": {
|
|
||||||
"description": "Status text in manager",
|
|
||||||
"message": "Missing"
|
|
||||||
},
|
|
||||||
"ok": {
|
|
||||||
"description": "Button text; Used in message boxes",
|
|
||||||
"message": "OK"
|
|
||||||
},
|
|
||||||
"paused": {
|
|
||||||
"description": "Status text; manager",
|
|
||||||
"message": "Paused"
|
|
||||||
},
|
|
||||||
"queued": {
|
|
||||||
"description": "Status text",
|
|
||||||
"message": "Queued"
|
|
||||||
},
|
|
||||||
"referrer": {
|
|
||||||
"description": "Label for \"Referrer\"",
|
|
||||||
"message": "Referrer"
|
|
||||||
},
|
|
||||||
"remember": {
|
|
||||||
"description": "Checkbox text for confirmation, e.g. when removing a download in manager",
|
|
||||||
"message": "Remember this decision"
|
|
||||||
},
|
|
||||||
"rename": {
|
|
||||||
"description": "UI for renaming; currently unused",
|
|
||||||
"message": "Rename"
|
|
||||||
},
|
|
||||||
"renmask": {
|
|
||||||
"description": "Renaming mask (long)",
|
|
||||||
"message": "Renaming mask"
|
|
||||||
},
|
|
||||||
"reset": {
|
|
||||||
"description": "Button text; pref window",
|
|
||||||
"message": "Reset"
|
|
||||||
},
|
|
||||||
"running": {
|
|
||||||
"description": "Status text",
|
|
||||||
"message": "Running"
|
|
||||||
},
|
|
||||||
"save": {
|
|
||||||
"description": "Button text; e.g. prefs/Network",
|
|
||||||
"message": "Save"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"description": "Placeholder text; manager status search field",
|
|
||||||
"message": "Search…"
|
|
||||||
},
|
|
||||||
"sizeB": {
|
|
||||||
"description": "Size formatting; bytes",
|
|
||||||
"message": "$S$B",
|
|
||||||
"placeholders": {
|
|
||||||
"s": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100b"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sizeGB": {
|
|
||||||
"description": "Size formatting; giga bytes",
|
|
||||||
"message": "$S$GB",
|
|
||||||
"placeholders": {
|
|
||||||
"s": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.200GB"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sizeKB": {
|
|
||||||
"description": "Size formatting; kilo bytes",
|
|
||||||
"message": "$S$KB",
|
|
||||||
"placeholders": {
|
|
||||||
"s": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.2KB"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sizeMB": {
|
|
||||||
"description": "Size formatting; mega bytes",
|
|
||||||
"message": "$S$MB",
|
|
||||||
"placeholders": {
|
|
||||||
"s": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.22MB"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sizePB": {
|
|
||||||
"description": "Size formatting; peta bytes (you never know)",
|
|
||||||
"message": "$S$PB",
|
|
||||||
"placeholders": {
|
|
||||||
"s": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.212PB"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sizeTB": {
|
|
||||||
"description": "Size formatting; tera bytes (you never know)",
|
|
||||||
"message": "$S$TB",
|
|
||||||
"placeholders": {
|
|
||||||
"s": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.002TB"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speedB": {
|
|
||||||
"description": "Speed formatting; bytes",
|
|
||||||
"message": "$SPEED$b/s",
|
|
||||||
"placeholders": {
|
|
||||||
"speed": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100b/s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speedKB": {
|
|
||||||
"description": "Speed formatting; kilo bytes",
|
|
||||||
"message": "$SPEED$KB/s",
|
|
||||||
"placeholders": {
|
|
||||||
"speed": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.1KB/s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speedMB": {
|
|
||||||
"description": "Speed formatting; mega bytes",
|
|
||||||
"message": "$SPEED$MB/s",
|
|
||||||
"placeholders": {
|
|
||||||
"speed": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "100.20MB/s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"description": "Column text; Title label (short)",
|
|
||||||
"message": "Title"
|
|
||||||
},
|
|
||||||
"unlimited": {
|
|
||||||
"description": "Option text; Prefs/Network",
|
|
||||||
"message": "Unlimited"
|
|
||||||
},
|
|
||||||
"useonlyonce": {
|
|
||||||
"description": "Label for Use-Once checkboxes",
|
|
||||||
"message": "Use Once"
|
|
||||||
},
|
|
||||||
"add_download": {
|
|
||||||
"description": "Action for adding a download",
|
|
||||||
"message": "Add Download"
|
|
||||||
},
|
|
||||||
"add_new": {
|
|
||||||
"description": "Button text (adding filters, limits and such)",
|
|
||||||
"message": "Add New"
|
|
||||||
},
|
|
||||||
"add_paused_once": {
|
|
||||||
"description": "Checkbox label",
|
|
||||||
"message": "Only add paused this time"
|
|
||||||
},
|
|
||||||
"add_paused_question": {
|
|
||||||
"description": "Messagebox text",
|
|
||||||
"message": "Do you want to remember this decision and always add new downloads paused from now on?"
|
|
||||||
},
|
|
||||||
"add_paused_title": {
|
|
||||||
"description": "Title for the add-paused dialog",
|
|
||||||
"message": "Always add paused?"
|
|
||||||
},
|
|
||||||
"ask_again_later": {
|
|
||||||
"description": "Button text",
|
|
||||||
"message": "Ask me again later"
|
|
||||||
},
|
|
||||||
"batch_batch": {
|
|
||||||
"description": "Button text",
|
|
||||||
"message": "Batch Download"
|
|
||||||
},
|
|
||||||
"batch_desc": {
|
|
||||||
"description": "",
|
|
||||||
"message": "The current URL seems to contain instructions for a batch download."
|
|
||||||
},
|
|
||||||
"batch_items": {
|
|
||||||
"description": "Messagebox info text for batch confirmations",
|
|
||||||
"message": "Number of items:"
|
|
||||||
},
|
|
||||||
"batch_preview": {
|
|
||||||
"description": "Messagebox info text for batch confirmations",
|
|
||||||
"message": "Preview:"
|
|
||||||
},
|
|
||||||
"batch_question": {
|
|
||||||
"description": "Messagebox info text for batch confirmations",
|
|
||||||
"message": "Do you want to add this as a batch or as a single download?"
|
|
||||||
},
|
|
||||||
"batch_single": {
|
|
||||||
"description": "Button text for batch confirmation",
|
|
||||||
"message": "Single Download"
|
|
||||||
},
|
|
||||||
"batch_title": {
|
|
||||||
"description": "Messagebox title for batch confirmations",
|
|
||||||
"message": "Batch Download"
|
|
||||||
},
|
|
||||||
"cancel_download": {
|
|
||||||
"description": "Action to cancel downloads, e.g. from the context menu",
|
|
||||||
"message": "Cancel"
|
|
||||||
},
|
|
||||||
"cannot_be_empty": {
|
|
||||||
"description": "Error message when an input field is empty but has to have a value",
|
|
||||||
"message": "This field cannot be empty"
|
|
||||||
},
|
|
||||||
"change_later_reminder": {
|
|
||||||
"description": "Checkbox label text for decision confirmations",
|
|
||||||
"message": "You can later change this decision in the Preferences"
|
|
||||||
},
|
|
||||||
"check_selected_items": {
|
|
||||||
"description": "Menu text",
|
|
||||||
"message": "Check Selected Items"
|
|
||||||
},
|
|
||||||
"conflict_overwrite": {
|
"conflict_overwrite": {
|
||||||
"description": "Option text; prefs/general",
|
"description": "Option text; prefs/general",
|
||||||
"message": "Overwrite"
|
"message": "Overwrite"
|
||||||
@ -405,26 +207,58 @@
|
|||||||
"description": "Filter label for the Videos filter",
|
"description": "Filter label for the Videos filter",
|
||||||
"message": "Videos (mp4, webm, mkv, …)"
|
"message": "Videos (mp4, webm, mkv, …)"
|
||||||
},
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "button text",
|
||||||
|
"message": "Delete"
|
||||||
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"description": "menu action",
|
||||||
|
"message": "Delete Files"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"description": "button text",
|
||||||
|
"message": "Delete"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"description": "messagebox text",
|
||||||
|
"message": "Are you sure you want to delete the following files?"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"description": "messagebox title",
|
||||||
|
"message": "Delete Files"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Description (keep it short); e.g. the description column in select",
|
||||||
|
"message": "Description"
|
||||||
|
},
|
||||||
"disable_other_filters": {
|
"disable_other_filters": {
|
||||||
"description": "Checkbox label. Keep it short",
|
"description": "Checkbox label. Keep it short",
|
||||||
"message": "Disable others"
|
"message": "Disable others"
|
||||||
},
|
},
|
||||||
|
"donate": {
|
||||||
|
"description": "Donate button",
|
||||||
|
"message": "Donate!"
|
||||||
|
},
|
||||||
|
"done": {
|
||||||
|
"description": "Status text",
|
||||||
|
"message": "Done"
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"description": "Download (noun); e.g. Download column in select",
|
||||||
|
"message": "Download"
|
||||||
|
},
|
||||||
"download_verb": {
|
"download_verb": {
|
||||||
"description": "Download (verb/action); e.g. in single and select buttons",
|
"description": "Download (verb/action); e.g. in single and select buttons",
|
||||||
"message": "Download"
|
"message": "Download"
|
||||||
},
|
},
|
||||||
"dta_regular_all": {
|
|
||||||
"description": "Menu text",
|
|
||||||
"message": "DownThemAll! - All Tabs"
|
|
||||||
},
|
|
||||||
"dta_turbo_all": {
|
|
||||||
"description": "Menu text",
|
|
||||||
"message": "OneClick! - All Tabs"
|
|
||||||
},
|
|
||||||
"dta_regular": {
|
"dta_regular": {
|
||||||
"description": "Regular dta action; Menu text",
|
"description": "Regular dta action; Menu text",
|
||||||
"message": "DownThemAll!"
|
"message": "DownThemAll!"
|
||||||
},
|
},
|
||||||
|
"dta_regular_all": {
|
||||||
|
"description": "Menu text",
|
||||||
|
"message": "DownThemAll! - All Tabs"
|
||||||
|
},
|
||||||
"dta_regular_image": {
|
"dta_regular_image": {
|
||||||
"description": "Menu text",
|
"description": "Menu text",
|
||||||
"message": "Save Image with DownThemAll!"
|
"message": "Save Image with DownThemAll!"
|
||||||
@ -445,6 +279,10 @@
|
|||||||
"description": "OneClick! action; Menu text",
|
"description": "OneClick! action; Menu text",
|
||||||
"message": "OneClick!"
|
"message": "OneClick!"
|
||||||
},
|
},
|
||||||
|
"dta_turbo_all": {
|
||||||
|
"description": "Menu text",
|
||||||
|
"message": "OneClick! - All Tabs"
|
||||||
|
},
|
||||||
"dta_turbo_image": {
|
"dta_turbo_image": {
|
||||||
"description": "Menu text",
|
"description": "Menu text",
|
||||||
"message": "Save Image with OneClick!"
|
"message": "Save Image with OneClick!"
|
||||||
@ -477,10 +315,42 @@
|
|||||||
"description": "Error Message; select window",
|
"description": "Error Message; select window",
|
||||||
"message": "No items selected"
|
"message": "No items selected"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"description": "Error Message; select/single window",
|
||||||
|
"message": "Absolute paths for subfolders are not supported by browsers"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"description": "Error Message; select/single window",
|
||||||
|
"message": "Dots (.) in subfolders are not supported by browsers"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export To File"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export As aria2 List"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export As Metalink"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export As Text"
|
||||||
|
},
|
||||||
|
"extensionDescription": {
|
||||||
|
"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",
|
||||||
|
"message": "The Mass Downloader for your browser"
|
||||||
|
},
|
||||||
"fastfilter_placeholder": {
|
"fastfilter_placeholder": {
|
||||||
"description": "Placeholder for fastfilter inputs",
|
"description": "Placeholder for fastfilter inputs",
|
||||||
"message": "Wildcard expression or regular expression"
|
"message": "Wildcard expression or regular expression"
|
||||||
},
|
},
|
||||||
|
"fastfiltering": {
|
||||||
|
"description": "Label for Fast Filtering input",
|
||||||
|
"message": "Fast Filtering"
|
||||||
|
},
|
||||||
"filter_at_least_one": {
|
"filter_at_least_one": {
|
||||||
"description": "Error message when no filter types are selected for a filter in the preferences UI",
|
"description": "Error message when no filter types are selected for a filter in the preferences UI",
|
||||||
"message": "You must select at least one filter type!"
|
"message": "You must select at least one filter type!"
|
||||||
@ -509,10 +379,18 @@
|
|||||||
"description": "Message box label",
|
"description": "Message box label",
|
||||||
"message": "Filter-Types"
|
"message": "Filter-Types"
|
||||||
},
|
},
|
||||||
|
"finishing": {
|
||||||
|
"description": "Status text",
|
||||||
|
"message": "Finishing"
|
||||||
|
},
|
||||||
"force_start": {
|
"force_start": {
|
||||||
"description": "Menu text",
|
"description": "Menu text",
|
||||||
"message": "Force Start"
|
"message": "Force Start"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Import From File"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"description": "Used in message boxes",
|
"description": "Used in message boxes",
|
||||||
"message": "Information"
|
"message": "Information"
|
||||||
@ -553,10 +431,26 @@
|
|||||||
"description": "Short for PageUp-key",
|
"description": "Short for PageUp-key",
|
||||||
"message": "PageUp"
|
"message": "PageUp"
|
||||||
},
|
},
|
||||||
|
"language": {
|
||||||
|
"description": "Lanuage Name in your language",
|
||||||
|
"message": "English (US)"
|
||||||
|
},
|
||||||
|
"language_code": {
|
||||||
|
"description": "Language code the locale will use, e.g. de or en-GB or pt-BR",
|
||||||
|
"message": "en"
|
||||||
|
},
|
||||||
"limited_to": {
|
"limited_to": {
|
||||||
"description": "Label text; used in prefs/network",
|
"description": "Label text; used in prefs/network",
|
||||||
"message": "Limited to"
|
"message": "Limited to"
|
||||||
},
|
},
|
||||||
|
"links": {
|
||||||
|
"description": "Links tab label (short); select window",
|
||||||
|
"message": "Links"
|
||||||
|
},
|
||||||
|
"manager_short": {
|
||||||
|
"description": "Menu text",
|
||||||
|
"message": "Manager"
|
||||||
|
},
|
||||||
"manager_status_items": {
|
"manager_status_items": {
|
||||||
"description": "Status bar text; manager",
|
"description": "Status bar text; manager",
|
||||||
"message": "Completed $COMPLETE$ of $TOTAL$ downloads ($SHOWING$ displayed), $RUNNING$ running",
|
"message": "Completed $COMPLETE$ of $TOTAL$ downloads ($SHOWING$ displayed), $RUNNING$ running",
|
||||||
@ -579,18 +473,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manager_short": {
|
|
||||||
"description": "Menu text",
|
|
||||||
"message": "Manager"
|
|
||||||
},
|
|
||||||
"manager_title": {
|
"manager_title": {
|
||||||
"description": "Window/tab title",
|
"description": "Window/tab title",
|
||||||
"message": "DownThemAll! Manager"
|
"message": "DownThemAll! Manager"
|
||||||
},
|
},
|
||||||
|
"mask": {
|
||||||
|
"description": "Renaming mask (short); used in e.g. select",
|
||||||
|
"message": "Mask"
|
||||||
|
},
|
||||||
"mask_default": {
|
"mask_default": {
|
||||||
"description": "Status text; Used in the mask column, select window",
|
"description": "Status text; Used in the mask column, select window",
|
||||||
"message": "Default Mask"
|
"message": "Default Mask"
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"description": "Media label (short)",
|
||||||
|
"message": "Media"
|
||||||
|
},
|
||||||
|
"missing": {
|
||||||
|
"description": "Status text in manager",
|
||||||
|
"message": "Missing"
|
||||||
|
},
|
||||||
"move_bottom": {
|
"move_bottom": {
|
||||||
"description": "Action for moving a download to the bottom",
|
"description": "Action for moving a download to the bottom",
|
||||||
"message": "Bottom"
|
"message": "Bottom"
|
||||||
@ -639,6 +541,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ok": {
|
||||||
|
"description": "Button text; Used in message boxes",
|
||||||
|
"message": "OK"
|
||||||
|
},
|
||||||
"open_directory": {
|
"open_directory": {
|
||||||
"description": "Menu text; manager context",
|
"description": "Menu text; manager context",
|
||||||
"message": "Open Directory"
|
"message": "Open Directory"
|
||||||
@ -667,10 +573,34 @@
|
|||||||
"description": "Action for pausing a download",
|
"description": "Action for pausing a download",
|
||||||
"message": "Pause"
|
"message": "Pause"
|
||||||
},
|
},
|
||||||
|
"paused": {
|
||||||
|
"description": "Status text; manager",
|
||||||
|
"message": "Paused"
|
||||||
|
},
|
||||||
"pref_add_paused": {
|
"pref_add_paused": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Add new downloads paused, instead of starting them immediately"
|
"message": "Add new downloads paused, instead of starting them immediately"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"description": "label",
|
||||||
|
"message": "DownThemAll! button:"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"description": "label",
|
||||||
|
"message": "DownThemAll! selection"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"description": "label",
|
||||||
|
"message": "Open Manager"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"description": "label",
|
||||||
|
"message": "Popup menu"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"description": "label",
|
||||||
|
"message": "OneClick!"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"description": "Preferences/Network",
|
"description": "Preferences/Network",
|
||||||
"message": "Concurrent downloads"
|
"message": "Concurrent downloads"
|
||||||
@ -679,18 +609,26 @@
|
|||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Show a notification when the queue finishes downloading"
|
"message": "Show a notification when the queue finishes downloading"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"description": "Preferences/General",
|
|
||||||
"message": "Browser button should be OneClick!"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Do not show general context menu items"
|
"message": "Do not show general context menu items"
|
||||||
},
|
},
|
||||||
|
"pref_manager": {
|
||||||
|
"description": "Preferences/General; group text",
|
||||||
|
"message": "Manager"
|
||||||
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"description": "checkbox text",
|
||||||
|
"message": "Open manager in a new popup window"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Show tooltips in Manager tabs"
|
"message": "Show tooltips in Manager tabs"
|
||||||
},
|
},
|
||||||
|
"pref_netglobal": {
|
||||||
|
"description": "Preferences/General; group text",
|
||||||
|
"message": "Global Network Limits"
|
||||||
|
},
|
||||||
"pref_open_manager_on_queue": {
|
"pref_open_manager_on_queue": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Open the Manager tab after queuing some downloads"
|
"message": "Open the Manager tab after queuing some downloads"
|
||||||
@ -699,30 +637,34 @@
|
|||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Show a notification when queuing new downloads"
|
"message": "Show a notification when queuing new downloads"
|
||||||
},
|
},
|
||||||
|
"pref_queueing": {
|
||||||
|
"description": "Preferences/General; group text",
|
||||||
|
"message": "Queuing Downloads"
|
||||||
|
},
|
||||||
"pref_remove_missing_on_init": {
|
"pref_remove_missing_on_init": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Remove missing downloads after a restart"
|
"message": "Remove missing downloads after a restart"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"description": "pref text",
|
||||||
|
"message": "Number of retries of downloads on temporary errors"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"description": "pref text",
|
||||||
|
"message": "Retry every (in minutes)"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Show URLs instead of Names"
|
"message": "Show URLs instead of Names"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"description": "checkbox text",
|
||||||
|
"message": "Play sounds"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Try to find links in the website text (slower)"
|
"message": "Try to find links in the website text (slower)"
|
||||||
},
|
},
|
||||||
"pref_manager": {
|
|
||||||
"description": "Preferences/General; group text",
|
|
||||||
"message": "Manager"
|
|
||||||
},
|
|
||||||
"pref_netglobal": {
|
|
||||||
"description": "Preferences/General; group text",
|
|
||||||
"message": "Global Network Limits"
|
|
||||||
},
|
|
||||||
"pref_queueing": {
|
|
||||||
"description": "Preferences/General; group text",
|
|
||||||
"message": "Queuing Downloads"
|
|
||||||
},
|
|
||||||
"pref_ui": {
|
"pref_ui": {
|
||||||
"description": "Preferences/General; group text",
|
"description": "Preferences/General; group text",
|
||||||
"message": "User Interface"
|
"message": "User Interface"
|
||||||
@ -743,6 +685,10 @@
|
|||||||
"description": "Notification text",
|
"description": "Notification text",
|
||||||
"message": "The download queue has finished"
|
"message": "The download queue has finished"
|
||||||
},
|
},
|
||||||
|
"queued": {
|
||||||
|
"description": "Status text",
|
||||||
|
"message": "Queued"
|
||||||
|
},
|
||||||
"queued_download": {
|
"queued_download": {
|
||||||
"description": "Notification text; single download",
|
"description": "Notification text; single download",
|
||||||
"message": "Queued 1 download!"
|
"message": "Queued 1 download!"
|
||||||
@ -757,6 +703,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"referrer": {
|
||||||
|
"description": "Label for \"Referrer\"",
|
||||||
|
"message": "Referrer"
|
||||||
|
},
|
||||||
|
"remember": {
|
||||||
|
"description": "Checkbox text for confirmation, e.g. when removing a download in manager",
|
||||||
|
"message": "Remember this decision"
|
||||||
|
},
|
||||||
"remove_all_complete_downloads": {
|
"remove_all_complete_downloads": {
|
||||||
"description": "Menu text",
|
"description": "Menu text",
|
||||||
"message": "Remove All Complete"
|
"message": "Remove All Complete"
|
||||||
@ -889,6 +843,10 @@
|
|||||||
"description": "Menu text",
|
"description": "Menu text",
|
||||||
"message": "Remove Selected"
|
"message": "Remove Selected"
|
||||||
},
|
},
|
||||||
|
"rename": {
|
||||||
|
"description": "UI for renaming; currently unused",
|
||||||
|
"message": "Rename"
|
||||||
|
},
|
||||||
"renamer_batch": {
|
"renamer_batch": {
|
||||||
"description": "Mask text; see mask button",
|
"description": "Mask text; see mask button",
|
||||||
"message": "Batch Number"
|
"message": "Batch Number"
|
||||||
@ -1005,6 +963,14 @@
|
|||||||
"description": "Mask text; see mask button",
|
"description": "Mask text; see mask button",
|
||||||
"message": "Date Added - Year"
|
"message": "Date Added - Year"
|
||||||
},
|
},
|
||||||
|
"renmask": {
|
||||||
|
"description": "Renaming mask (long)",
|
||||||
|
"message": "Renaming mask"
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"description": "Button text; pref window",
|
||||||
|
"message": "Reset"
|
||||||
|
},
|
||||||
"reset_confirmations": {
|
"reset_confirmations": {
|
||||||
"description": "Button text; pref/General",
|
"description": "Button text; pref/General",
|
||||||
"message": "Reset remembered confirmations"
|
"message": "Reset remembered confirmations"
|
||||||
@ -1025,6 +991,32 @@
|
|||||||
"description": "Action for resuming a download",
|
"description": "Action for resuming a download",
|
||||||
"message": "Resume"
|
"message": "Resume"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"description": "Status text",
|
||||||
|
"message": "Retrying"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"description": "status text",
|
||||||
|
"message": "Retrying - $ERROR$",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"running": {
|
||||||
|
"description": "Status text",
|
||||||
|
"message": "Running"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"description": "Button text; e.g. prefs/Network",
|
||||||
|
"message": "Save"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"description": "Placeholder text; manager status search field",
|
||||||
|
"message": "Search…"
|
||||||
|
},
|
||||||
"select_all": {
|
"select_all": {
|
||||||
"description": "Menu text; e.g. select context",
|
"description": "Menu text; e.g. select context",
|
||||||
"message": "Select All"
|
"message": "Select All"
|
||||||
@ -1045,6 +1037,18 @@
|
|||||||
"description": "Menu text; select window",
|
"description": "Menu text; select window",
|
||||||
"message": "Set Renaming Mask"
|
"message": "Set Renaming Mask"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"description": "dialog text",
|
||||||
|
"message": "Set a new renaming mask"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Set Referrer"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"description": "dialog text",
|
||||||
|
"message": "Set a new referrer"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"description": "Header text; single window",
|
"description": "Header text; single window",
|
||||||
"message": "Batches are supported, e.g.:"
|
"message": "Batches are supported, e.g.:"
|
||||||
@ -1057,6 +1061,66 @@
|
|||||||
"description": "Title of single window",
|
"description": "Title of single window",
|
||||||
"message": "DownThemAll! - Add a link"
|
"message": "DownThemAll! - Add a link"
|
||||||
},
|
},
|
||||||
|
"sizeB": {
|
||||||
|
"description": "Size formatting; bytes",
|
||||||
|
"message": "$S$B",
|
||||||
|
"placeholders": {
|
||||||
|
"s": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sizeGB": {
|
||||||
|
"description": "Size formatting; giga bytes",
|
||||||
|
"message": "$S$GB",
|
||||||
|
"placeholders": {
|
||||||
|
"s": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.200GB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sizeKB": {
|
||||||
|
"description": "Size formatting; kilo bytes",
|
||||||
|
"message": "$S$KB",
|
||||||
|
"placeholders": {
|
||||||
|
"s": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.2KB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sizeMB": {
|
||||||
|
"description": "Size formatting; mega bytes",
|
||||||
|
"message": "$S$MB",
|
||||||
|
"placeholders": {
|
||||||
|
"s": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.22MB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sizePB": {
|
||||||
|
"description": "Size formatting; peta bytes (you never know)",
|
||||||
|
"message": "$S$PB",
|
||||||
|
"placeholders": {
|
||||||
|
"s": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.212PB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sizeTB": {
|
||||||
|
"description": "Size formatting; tera bytes (you never know)",
|
||||||
|
"message": "$S$TB",
|
||||||
|
"placeholders": {
|
||||||
|
"s": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.002TB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"size_progress": {
|
"size_progress": {
|
||||||
"description": "Status text; manager size column",
|
"description": "Status text; manager size column",
|
||||||
"message": "$WRITTEN$ of $TOTAL$",
|
"message": "$WRITTEN$ of $TOTAL$",
|
||||||
@ -1127,6 +1191,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"speedB": {
|
||||||
|
"description": "Speed formatting; bytes",
|
||||||
|
"message": "$SPEED$b/s",
|
||||||
|
"placeholders": {
|
||||||
|
"speed": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100b/s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speedKB": {
|
||||||
|
"description": "Speed formatting; kilo bytes",
|
||||||
|
"message": "$SPEED$KB/s",
|
||||||
|
"placeholders": {
|
||||||
|
"speed": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.1KB/s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speedMB": {
|
||||||
|
"description": "Speed formatting; mega bytes",
|
||||||
|
"message": "$SPEED$MB/s",
|
||||||
|
"placeholders": {
|
||||||
|
"speed": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "100.20MB/s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"statusNetwork_active_title": {
|
"statusNetwork_active_title": {
|
||||||
"description": "Status bar tooltip; manager network icon",
|
"description": "Status bar tooltip; manager network icon",
|
||||||
"message": "New Downloads will be started"
|
"message": "New Downloads will be started"
|
||||||
@ -1135,6 +1229,18 @@
|
|||||||
"description": "Status bar tooltip; manager network icon",
|
"description": "Status bar tooltip; manager network icon",
|
||||||
"message": "No new Downloads will be started"
|
"message": "No new Downloads will be started"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"description": "label text",
|
||||||
|
"message": "Subfolder:"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"description": "placeholder text within an input box",
|
||||||
|
"message": "Place files in this subfolder within your downloads directory"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"description": "Column text; Title label (short)",
|
||||||
|
"message": "Title"
|
||||||
|
},
|
||||||
"toggle_selected_items": {
|
"toggle_selected_items": {
|
||||||
"description": "Menu text; select",
|
"description": "Menu text; select",
|
||||||
"message": "Toggle Check for Selected Items"
|
"message": "Toggle Check for Selected Items"
|
||||||
@ -1166,5 +1272,13 @@
|
|||||||
"uncheck_selected_items": {
|
"uncheck_selected_items": {
|
||||||
"description": "Menu text; select",
|
"description": "Menu text; select",
|
||||||
"message": "Uncheck Selected Items"
|
"message": "Uncheck Selected Items"
|
||||||
|
},
|
||||||
|
"unlimited": {
|
||||||
|
"description": "Option text; Prefs/Network",
|
||||||
|
"message": "Unlimited"
|
||||||
|
},
|
||||||
|
"useonlyonce": {
|
||||||
|
"description": "Label for Use-Once checkboxes",
|
||||||
|
"message": "Use Once"
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1284
_locales/hu/messages.json
Normal file
1284
_locales/hu/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,7 @@
|
|||||||
"description": "Checkbox label"
|
"description": "Checkbox label"
|
||||||
},
|
},
|
||||||
"add_paused_question": {
|
"add_paused_question": {
|
||||||
"message": "Apa Anda ingin mengingat keputusan ini dan mulai sekarang tambahkan unduhan dalam kondisi terpause?",
|
"message": "Ingat keputusan ini dan tambahkan unduhan dalam kondisi terpause mulai sekarang?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"add_paused_title": {
|
"add_paused_title": {
|
||||||
@ -175,6 +175,22 @@
|
|||||||
"message": "Hapus",
|
"message": "Hapus",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Hapus Berkas",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Hapus",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Hapus berkas-berkas berikut ini?",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Hapus Berkas",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Keterangan",
|
"message": "Keterangan",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -251,10 +267,34 @@
|
|||||||
"message": "URL Tidak Benar",
|
"message": "URL Tidak Benar",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Path absolut untuk subfolder tidak didukung oleh peramban",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Titik (.) di subfolder tidak didukung oleh peramban",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Tidak ada item terpilih",
|
"message": "Tidak ada item terpilih",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"message": "Ekspor Ke Berkas",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"message": "Ekspor Sebagai Daftar aria2",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"message": "Ekspor Sebagai Metalink",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"message": "Ekspor Sebagai Teks",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Pengunduh Masal untuk browser anda",
|
"message": "Pengunduh Masal untuk browser anda",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "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"
|
||||||
@ -280,15 +320,15 @@
|
|||||||
"description": "Message box title"
|
"description": "Message box title"
|
||||||
},
|
},
|
||||||
"filter_expression": {
|
"filter_expression": {
|
||||||
"message": "Ekspres-Filter",
|
"message": "Ekspresi",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_label": {
|
"filter_label": {
|
||||||
"message": "Label-Filter",
|
"message": "Label",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_types": {
|
"filter_types": {
|
||||||
"message": "Tipe-Filter",
|
"message": "Tipe",
|
||||||
"description": "Message box label"
|
"description": "Message box label"
|
||||||
},
|
},
|
||||||
"filter_type_link": {
|
"filter_type_link": {
|
||||||
@ -307,6 +347,10 @@
|
|||||||
"message": "Paksa Mulai",
|
"message": "Paksa Mulai",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"message": "Impor Dari Berkas",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"message": "Informasi",
|
"message": "Informasi",
|
||||||
"description": "Used in message boxes"
|
"description": "Used in message boxes"
|
||||||
@ -320,7 +364,7 @@
|
|||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"limited_to": {
|
"limited_to": {
|
||||||
"message": "Terbatas ke",
|
"message": "Batasi ke",
|
||||||
"description": "Label text; used in prefs/network"
|
"description": "Label text; used in prefs/network"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
@ -362,11 +406,11 @@
|
|||||||
"description": "Status text; Used in the mask column, select window"
|
"description": "Status text; Used in the mask column, select window"
|
||||||
},
|
},
|
||||||
"missing": {
|
"missing": {
|
||||||
"message": "Tidak Ada",
|
"message": "Hilang",
|
||||||
"description": "Status text in manager"
|
"description": "Status text in manager"
|
||||||
},
|
},
|
||||||
"move_bottom": {
|
"move_bottom": {
|
||||||
"message": "Bawah",
|
"message": "Ke Bawah",
|
||||||
"description": "Action for moving a download to the bottom"
|
"description": "Action for moving a download to the bottom"
|
||||||
},
|
},
|
||||||
"move_down": {
|
"move_down": {
|
||||||
@ -374,7 +418,7 @@
|
|||||||
"description": "Action for moving a download down"
|
"description": "Action for moving a download down"
|
||||||
},
|
},
|
||||||
"move_top": {
|
"move_top": {
|
||||||
"message": "Atas",
|
"message": "Ke Atas",
|
||||||
"description": "Action for moving a download to the top"
|
"description": "Action for moving a download to the top"
|
||||||
},
|
},
|
||||||
"move_up": {
|
"move_up": {
|
||||||
@ -461,6 +505,22 @@
|
|||||||
"message": "Tambahkan unduhan terpause, alih-alih langsung memulai mengunduh",
|
"message": "Tambahkan unduhan terpause, alih-alih langsung memulai mengunduh",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Tombol DownThemAll!:",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "Pilihan DownThemAll!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Buka Pengelola",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Menu popup",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Unduhan Bersamaan",
|
"message": "Unduhan Bersamaan",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -469,10 +529,6 @@
|
|||||||
"message": "Tampilkan pemberitahuan ketika antrian unduhan selesai",
|
"message": "Tampilkan pemberitahuan ketika antrian unduhan selesai",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Tombol peramban sebaiknya OneClick!",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Jangan perlihatkan item menu general context",
|
"message": "Jangan perlihatkan item menu general context",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -481,6 +537,10 @@
|
|||||||
"message": "Pengelola",
|
"message": "Pengelola",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Buka pengelola di jendela popup baru",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Tampilkan tooltip di tab Pengelola",
|
"message": "Tampilkan tooltip di tab Pengelola",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -505,10 +565,22 @@
|
|||||||
"message": "Setelah restart, hapus unduhan yang tidak ada",
|
"message": "Setelah restart, hapus unduhan yang tidak ada",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Jumlah percobaan ulang ketika gagal mengunduh",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Coba setiap (dalam menit)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Tampilan URL alih-alih Nama",
|
"message": "Tampilan URL alih-alih Nama",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Bunyikan suara",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Coba cari tautan di teks di situs web (lambat)",
|
"message": "Coba cari tautan di teks di situs web (lambat)",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -560,7 +632,7 @@
|
|||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_batch_downloads_question": {
|
"remove_batch_downloads_question": {
|
||||||
"message": "Hapus semua unduhan dari kumpulan yang sama dengan unduhan terpilih?",
|
"message": "Hapus semua unduhan dari batch yang sama dengan unduhan terpilih?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_complete_downloads": {
|
"remove_complete_downloads": {
|
||||||
@ -600,7 +672,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_domain_downloads": {
|
"remove_domain_downloads": {
|
||||||
"message": "Hapus Domain Ini",
|
"message": "Hapus Unduhan Dari Domain Ini",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_domain_downloads_question": {
|
"remove_domain_downloads_question": {
|
||||||
@ -630,7 +702,7 @@
|
|||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_failed_downloads": {
|
"remove_failed_downloads": {
|
||||||
"message": "Gagal Menghapus",
|
"message": "Hapus Unduhan Gagal",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_failed_downloads_question": {
|
"remove_failed_downloads_question": {
|
||||||
@ -648,11 +720,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remove_missing": {
|
"remove_missing": {
|
||||||
"message": "Hapus Unduhan Yang Tidak Ada",
|
"message": "Hapus Unduhan Yang Hilang",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_missing_downloads_question": {
|
"remove_missing_downloads_question": {
|
||||||
"message": "Hapus semua unduhan yang tidak ada?",
|
"message": "Hapus semua unduhan yang hilang?",
|
||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_paused_downloads": {
|
"remove_paused_downloads": {
|
||||||
@ -664,7 +736,7 @@
|
|||||||
"description": "Messagebox text"
|
"description": "Messagebox text"
|
||||||
},
|
},
|
||||||
"remove_selected_complete_downloads": {
|
"remove_selected_complete_downloads": {
|
||||||
"message": "Hapus Yang Selesai Di Pilihan",
|
"message": "Hapus Yang Selesai Dari Unduhan Terpilih",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"remove_selected_complete_downloads_question": {
|
"remove_selected_complete_downloads_question": {
|
||||||
@ -767,6 +839,10 @@
|
|||||||
"message": "Tanggal Ditambahkan - Detik",
|
"message": "Tanggal Ditambahkan - Detik",
|
||||||
"description": "Mask text; see mask button"
|
"description": "Mask text; see mask button"
|
||||||
},
|
},
|
||||||
|
"renamer_tags": {
|
||||||
|
"message": "Penanda Mask Penamaan",
|
||||||
|
"description": "Mask text; see mask button"
|
||||||
|
},
|
||||||
"renamer_text": {
|
"renamer_text": {
|
||||||
"message": "Teks Deskripsi",
|
"message": "Teks Deskripsi",
|
||||||
"description": "Mask text; see mask button"
|
"description": "Mask text; see mask button"
|
||||||
@ -783,6 +859,10 @@
|
|||||||
"message": "Tanggal Ditambahkan - Tahun",
|
"message": "Tanggal Ditambahkan - Tahun",
|
||||||
"description": "Mask text; see mask button"
|
"description": "Mask text; see mask button"
|
||||||
},
|
},
|
||||||
|
"renmask": {
|
||||||
|
"message": "Mask penamaan",
|
||||||
|
"description": "Renaming mask (long)"
|
||||||
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"message": "Atur Ulang",
|
"message": "Atur Ulang",
|
||||||
"description": "Button text; pref window"
|
"description": "Button text; pref window"
|
||||||
@ -807,6 +887,20 @@
|
|||||||
"message": "Lanjutkan",
|
"message": "Lanjutkan",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Mengulang",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Mengulang - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "Berjalan",
|
"message": "Berjalan",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -851,6 +945,22 @@
|
|||||||
"message": "Tidak Diizinkan",
|
"message": "Tidak Diizinkan",
|
||||||
"description": "Error message"
|
"description": "Error message"
|
||||||
},
|
},
|
||||||
|
"set_mask": {
|
||||||
|
"message": "Set Mask Penamaan",
|
||||||
|
"description": "Menu text; select window"
|
||||||
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Tentukan mask penamaan baru",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Tentukan Referrer",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Tentukan referrer baru",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Batch bisa digunakan, misalnya:",
|
"message": "Batch bisa digunakan, misalnya:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -971,6 +1081,10 @@
|
|||||||
"message": "Tidak ada Unduhan baru yang akan dimulai",
|
"message": "Tidak ada Unduhan baru yang akan dimulai",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Simpan berkas di subfolder berikut di dalam direktori unduhan Anda",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Judul",
|
"message": "Judul",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1014,5 +1128,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Gunakan Sekali",
|
"message": "Gunakan Sekali",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Dibatalkan Pengguna",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
1284
_locales/it/messages.json
Normal file
1284
_locales/it/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
1284
_locales/ja/messages.json
Normal file
1284
_locales/ja/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
124
_locales/ru/messages.json
Normal file → Executable file
124
_locales/ru/messages.json
Normal file → Executable file
@ -191,6 +191,22 @@
|
|||||||
"message": "Удалить",
|
"message": "Удалить",
|
||||||
"description": "button text"
|
"description": "button text"
|
||||||
},
|
},
|
||||||
|
"deletefiles": {
|
||||||
|
"message": "Удалить файлы",
|
||||||
|
"description": "menu action"
|
||||||
|
},
|
||||||
|
"deletefiles_button": {
|
||||||
|
"message": "Удалить",
|
||||||
|
"description": "button text"
|
||||||
|
},
|
||||||
|
"deletefiles_text": {
|
||||||
|
"message": "Точно удалить эти файлы?",
|
||||||
|
"description": "messagebox text"
|
||||||
|
},
|
||||||
|
"deletefiles_title": {
|
||||||
|
"message": "Удаление файлов",
|
||||||
|
"description": "messagebox title"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Описание",
|
"message": "Описание",
|
||||||
"description": "Description (keep it short); e.g. the description column in select"
|
"description": "Description (keep it short); e.g. the description column in select"
|
||||||
@ -244,7 +260,7 @@
|
|||||||
"description": "OneClick! action; Menu text"
|
"description": "OneClick! action; Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo_all": {
|
"dta_turbo_all": {
|
||||||
"message": "ОднимКликом! - Все вкладки",
|
"message": "OneClick! - Все вкладки",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
"dta_turbo_image": {
|
"dta_turbo_image": {
|
||||||
@ -275,10 +291,34 @@
|
|||||||
"message": "Неправильная ссылка",
|
"message": "Неправильная ссылка",
|
||||||
"description": "Error message; single window"
|
"description": "Error message; single window"
|
||||||
},
|
},
|
||||||
|
"error_noabsolutepath": {
|
||||||
|
"message": "Полные пути к папкам не поддерживаются браузерами",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
|
"error_nodotsinpath": {
|
||||||
|
"message": "Точки (.) в названии папок не поддерживаются браузерами",
|
||||||
|
"description": "Error Message; select/single window"
|
||||||
|
},
|
||||||
"error_noItemsSelected": {
|
"error_noItemsSelected": {
|
||||||
"message": "Ничего не выбрано",
|
"message": "Ничего не выбрано",
|
||||||
"description": "Error Message; select window"
|
"description": "Error Message; select window"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"message": "Экспортировать в файл",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"message": "Экспортировать как список для aria2",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"message": "Экспортировать как Metalink",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"message": "Экспортировать как текст",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Универсальная качалка для вашего браузера",
|
"message": "Универсальная качалка для вашего браузера",
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one"
|
"description": "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"
|
||||||
@ -331,6 +371,10 @@
|
|||||||
"message": "Принудительный старт",
|
"message": "Принудительный старт",
|
||||||
"description": "Menu text"
|
"description": "Menu text"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"message": "Импортировать из файла",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"message": "Информация",
|
"message": "Информация",
|
||||||
"description": "Used in message boxes"
|
"description": "Used in message boxes"
|
||||||
@ -529,6 +573,26 @@
|
|||||||
"message": "Добавлять закачки приостановленными, вместо того чтобы качать сразу",
|
"message": "Добавлять закачки приостановленными, вместо того чтобы качать сразу",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_button_type": {
|
||||||
|
"message": "Кнопка DownThemAll!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_dta": {
|
||||||
|
"message": "DownThemAll! выделенного",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_manager": {
|
||||||
|
"message": "Открыть менеджер",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_popup": {
|
||||||
|
"message": "Всплывающее меню",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
|
"pref_button_type_turbo": {
|
||||||
|
"message": "OneClick!",
|
||||||
|
"description": "label"
|
||||||
|
},
|
||||||
"pref_concurrent_downloads": {
|
"pref_concurrent_downloads": {
|
||||||
"message": "Настройки/Сеть",
|
"message": "Настройки/Сеть",
|
||||||
"description": "Preferences/Network"
|
"description": "Preferences/Network"
|
||||||
@ -537,10 +601,6 @@
|
|||||||
"message": "Показывать уведомление когда список закачек завешен",
|
"message": "Показывать уведомление когда список закачек завешен",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
"pref_global_turbo": {
|
|
||||||
"message": "Использовать OneClick! по умолчанию на кнопке в браузере",
|
|
||||||
"description": "Preferences/General"
|
|
||||||
},
|
|
||||||
"pref_hide_context": {
|
"pref_hide_context": {
|
||||||
"message": "Не добавлять пункты в общее контекстное меню",
|
"message": "Не добавлять пункты в общее контекстное меню",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -549,6 +609,10 @@
|
|||||||
"message": "Менеджер",
|
"message": "Менеджер",
|
||||||
"description": "Preferences/General; group text"
|
"description": "Preferences/General; group text"
|
||||||
},
|
},
|
||||||
|
"pref_manager_in_popup": {
|
||||||
|
"message": "Открывать менеджер в новом всплывающем окне",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_manager_tooltip": {
|
"pref_manager_tooltip": {
|
||||||
"message": "Показывать подсказки на вкладках менеджера",
|
"message": "Показывать подсказки на вкладках менеджера",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -573,10 +637,22 @@
|
|||||||
"message": "Удалять неудавшиеся закачки после перезапуска",
|
"message": "Удалять неудавшиеся закачки после перезапуска",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"message": "Сколько раз пробовать перезапустить закачку при некритичных сбоях",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"message": "Как часто пробовать перезапустить закачку (в минутах)",
|
||||||
|
"description": "pref text"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"message": "Показывать ссылки вместо имён",
|
"message": "Показывать ссылки вместо имён",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
},
|
},
|
||||||
|
"pref_sounds": {
|
||||||
|
"message": "Проигрывать звуки",
|
||||||
|
"description": "checkbox text"
|
||||||
|
},
|
||||||
"pref_text_links": {
|
"pref_text_links": {
|
||||||
"message": "Пытаться обнаружить ссылки в тексте на сайте",
|
"message": "Пытаться обнаружить ссылки в тексте на сайте",
|
||||||
"description": "Preferences/General"
|
"description": "Preferences/General"
|
||||||
@ -895,6 +971,20 @@
|
|||||||
"message": "Продолжить",
|
"message": "Продолжить",
|
||||||
"description": "Action for resuming a download"
|
"description": "Action for resuming a download"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"message": "Перезапуск",
|
||||||
|
"description": "Status text"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"message": "Перезапуск - $ERROR$",
|
||||||
|
"description": "status text",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"message": "В процессе",
|
"message": "В процессе",
|
||||||
"description": "Status text"
|
"description": "Status text"
|
||||||
@ -943,6 +1033,18 @@
|
|||||||
"message": "Задать маску переименования",
|
"message": "Задать маску переименования",
|
||||||
"description": "Menu text; select window"
|
"description": "Menu text; select window"
|
||||||
},
|
},
|
||||||
|
"set_mask_text": {
|
||||||
|
"message": "Задать новую маску переименования",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
|
"set_referrer": {
|
||||||
|
"message": "Задать реферера",
|
||||||
|
"description": "menu text"
|
||||||
|
},
|
||||||
|
"set_referrer_text": {
|
||||||
|
"message": "Задать нового реферера",
|
||||||
|
"description": "dialog text"
|
||||||
|
},
|
||||||
"single_batchexamples": {
|
"single_batchexamples": {
|
||||||
"message": "Можно задать группы, к примеру:",
|
"message": "Можно задать группы, к примеру:",
|
||||||
"description": "Header text; single window"
|
"description": "Header text; single window"
|
||||||
@ -1123,6 +1225,14 @@
|
|||||||
"message": "Новые закачки запускаться не будут",
|
"message": "Новые закачки запускаться не будут",
|
||||||
"description": "Status bar tooltip; manager network icon"
|
"description": "Status bar tooltip; manager network icon"
|
||||||
},
|
},
|
||||||
|
"subfolder": {
|
||||||
|
"message": "Подпапка",
|
||||||
|
"description": "label text"
|
||||||
|
},
|
||||||
|
"subfolder_placeholder": {
|
||||||
|
"message": "Размещать файлы в подпапке выбранного пути для закачек",
|
||||||
|
"description": "placeholder text within an input box"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"message": "Заголовок",
|
"message": "Заголовок",
|
||||||
"description": "Column text; Title label (short)"
|
"description": "Column text; Title label (short)"
|
||||||
@ -1166,5 +1276,9 @@
|
|||||||
"useonlyonce": {
|
"useonlyonce": {
|
||||||
"message": "Только сейчас",
|
"message": "Только сейчас",
|
||||||
"description": "Label for Use-Once checkboxes"
|
"description": "Label for Use-Once checkboxes"
|
||||||
|
},
|
||||||
|
"USER_CANCELED": {
|
||||||
|
"message": "Отменено пользователем",
|
||||||
|
"description": "Error message"
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
1170
_locales/zh_TW/messages.json
Normal file
1170
_locales/zh_TW/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
396
data/mime.json
Normal file
396
data/mime.json
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
{
|
||||||
|
"e": {
|
||||||
|
"3gpp": "3gp",
|
||||||
|
"asx": "asf",
|
||||||
|
"gz": "gzip",
|
||||||
|
"heic": "heif",
|
||||||
|
"html": [
|
||||||
|
"htm",
|
||||||
|
"shtml",
|
||||||
|
"php"
|
||||||
|
],
|
||||||
|
"jar": [
|
||||||
|
"war",
|
||||||
|
"ear"
|
||||||
|
],
|
||||||
|
"jpg": [
|
||||||
|
"jpeg",
|
||||||
|
"jpe",
|
||||||
|
"jfif"
|
||||||
|
],
|
||||||
|
"js": "jsx",
|
||||||
|
"mid": [
|
||||||
|
"midi",
|
||||||
|
"kar"
|
||||||
|
],
|
||||||
|
"mkv": [
|
||||||
|
"mk3d",
|
||||||
|
"mks"
|
||||||
|
],
|
||||||
|
"mov": [
|
||||||
|
"qt",
|
||||||
|
"moov"
|
||||||
|
],
|
||||||
|
"mpg": [
|
||||||
|
"mpe",
|
||||||
|
"mpeg"
|
||||||
|
],
|
||||||
|
"pem": [
|
||||||
|
"crt",
|
||||||
|
"der"
|
||||||
|
],
|
||||||
|
"pl": "pm",
|
||||||
|
"prc": "pdb",
|
||||||
|
"ps": [
|
||||||
|
"eps",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"svg": "svgz",
|
||||||
|
"tcl": "tk",
|
||||||
|
"tif": "tiff"
|
||||||
|
},
|
||||||
|
"m": {
|
||||||
|
"application/7z": "7z",
|
||||||
|
"application/7z-compressed": "7z",
|
||||||
|
"application/ai": "ps",
|
||||||
|
"application/atom": "atom",
|
||||||
|
"application/atom+xml": "atom",
|
||||||
|
"application/bz2": "bz2",
|
||||||
|
"application/bzip2": "bz2",
|
||||||
|
"application/cco": "cco",
|
||||||
|
"application/cocoa": "cco",
|
||||||
|
"application/compressed": "gz",
|
||||||
|
"application/crt": "pem",
|
||||||
|
"application/der": "pem",
|
||||||
|
"application/doc": "doc",
|
||||||
|
"application/ear": "jar",
|
||||||
|
"application/eot": "eot",
|
||||||
|
"application/eps": "ps",
|
||||||
|
"application/gz": "gz",
|
||||||
|
"application/gzip": "gz",
|
||||||
|
"application/hqx": "hqx",
|
||||||
|
"application/jar": "jar",
|
||||||
|
"application/jardiff": "jardiff",
|
||||||
|
"application/java-archive": "jar",
|
||||||
|
"application/java-archive-diff": "jardiff",
|
||||||
|
"application/java-jnlp-file": "jnlp",
|
||||||
|
"application/javascript": "js",
|
||||||
|
"application/jnlp": "jnlp",
|
||||||
|
"application/js": "js",
|
||||||
|
"application/json": "json",
|
||||||
|
"application/jsx": "js",
|
||||||
|
"application/kml": "kml",
|
||||||
|
"application/kmz": "kmz",
|
||||||
|
"application/m3u8": "m3u8",
|
||||||
|
"application/mac-binhex40": "hqx",
|
||||||
|
"application/makeself": "run",
|
||||||
|
"application/msword": "doc",
|
||||||
|
"application/odg": "odg",
|
||||||
|
"application/odp": "odp",
|
||||||
|
"application/ods": "ods",
|
||||||
|
"application/odt": "odt",
|
||||||
|
"application/pdb": "prc",
|
||||||
|
"application/pdf": "pdf",
|
||||||
|
"application/pem": "pem",
|
||||||
|
"application/perl": "pl",
|
||||||
|
"application/pilot": "prc",
|
||||||
|
"application/pl": "pl",
|
||||||
|
"application/pm": "pl",
|
||||||
|
"application/postscript": "ps",
|
||||||
|
"application/ppt": "ppt",
|
||||||
|
"application/prc": "prc",
|
||||||
|
"application/ps": "ps",
|
||||||
|
"application/rar": "rar",
|
||||||
|
"application/rar-compressed": "rar",
|
||||||
|
"application/redhat-package-manager": "rpm",
|
||||||
|
"application/rpm": "rpm",
|
||||||
|
"application/rss": "rss",
|
||||||
|
"application/rss+xml": "rss",
|
||||||
|
"application/rtf": "rtf",
|
||||||
|
"application/run": "run",
|
||||||
|
"application/sea": "sea",
|
||||||
|
"application/shockwave-flash": "swf",
|
||||||
|
"application/sit": "sit",
|
||||||
|
"application/stuffit": "sit",
|
||||||
|
"application/swf": "swf",
|
||||||
|
"application/tar": "tar",
|
||||||
|
"application/tcl": "tcl",
|
||||||
|
"application/tk": "tcl",
|
||||||
|
"application/vnd.apple.mpegurl": "m3u8",
|
||||||
|
"application/vnd.google-earth.kml+xml": "kml",
|
||||||
|
"application/vnd.google-earth.kmz": "kmz",
|
||||||
|
"application/vnd.ms-excel": "xls",
|
||||||
|
"application/vnd.ms-fontobject": "eot",
|
||||||
|
"application/vnd.ms-powerpoint": "ppt",
|
||||||
|
"application/vnd.oasis.opendocument.graphics": "odg",
|
||||||
|
"application/vnd.oasis.opendocument.presentation": "odp",
|
||||||
|
"application/vnd.oasis.opendocument.spreadsheet": "ods",
|
||||||
|
"application/vnd.oasis.opendocument.text": "odt",
|
||||||
|
"application/vnd.wap.wmlc": "wmlc",
|
||||||
|
"application/war": "jar",
|
||||||
|
"application/wmlc": "wmlc",
|
||||||
|
"application/x-7z": "7z",
|
||||||
|
"application/x-7z-compressed": "7z",
|
||||||
|
"application/x-ai": "ps",
|
||||||
|
"application/x-atom": "atom",
|
||||||
|
"application/x-atom+xml": "atom",
|
||||||
|
"application/x-bz2": "bz2",
|
||||||
|
"application/x-bzip2": "bz2",
|
||||||
|
"application/x-cco": "cco",
|
||||||
|
"application/x-cocoa": "cco",
|
||||||
|
"application/x-compressed": "gz",
|
||||||
|
"application/x-crt": "pem",
|
||||||
|
"application/x-der": "pem",
|
||||||
|
"application/x-doc": "doc",
|
||||||
|
"application/x-ear": "jar",
|
||||||
|
"application/x-eot": "eot",
|
||||||
|
"application/x-eps": "ps",
|
||||||
|
"application/x-gz": "gz",
|
||||||
|
"application/x-gzip": "gz",
|
||||||
|
"application/x-hqx": "hqx",
|
||||||
|
"application/x-jar": "jar",
|
||||||
|
"application/x-jardiff": "jardiff",
|
||||||
|
"application/x-java-archive": "jar",
|
||||||
|
"application/x-java-archive-diff": "jardiff",
|
||||||
|
"application/x-java-jnlp-file": "jnlp",
|
||||||
|
"application/x-javascript": "js",
|
||||||
|
"application/x-jnlp": "jnlp",
|
||||||
|
"application/x-js": "js",
|
||||||
|
"application/x-json": "json",
|
||||||
|
"application/x-jsx": "js",
|
||||||
|
"application/x-kml": "kml",
|
||||||
|
"application/x-kmz": "kmz",
|
||||||
|
"application/x-m3u8": "m3u8",
|
||||||
|
"application/x-mac-binhex40": "hqx",
|
||||||
|
"application/x-makeself": "run",
|
||||||
|
"application/x-msword": "doc",
|
||||||
|
"application/x-odg": "odg",
|
||||||
|
"application/x-odp": "odp",
|
||||||
|
"application/x-ods": "ods",
|
||||||
|
"application/x-odt": "odt",
|
||||||
|
"application/x-pdb": "prc",
|
||||||
|
"application/x-pdf": "pdf",
|
||||||
|
"application/x-pem": "pem",
|
||||||
|
"application/x-perl": "pl",
|
||||||
|
"application/x-pilot": "prc",
|
||||||
|
"application/x-pl": "pl",
|
||||||
|
"application/x-pm": "pl",
|
||||||
|
"application/x-postscript": "ps",
|
||||||
|
"application/x-ppt": "ppt",
|
||||||
|
"application/x-prc": "prc",
|
||||||
|
"application/x-ps": "ps",
|
||||||
|
"application/x-rar": "rar",
|
||||||
|
"application/x-rar-compressed": "rar",
|
||||||
|
"application/x-redhat-package-manager": "rpm",
|
||||||
|
"application/x-rpm": "rpm",
|
||||||
|
"application/x-rss": "rss",
|
||||||
|
"application/x-rss+xml": "rss",
|
||||||
|
"application/x-rtf": "rtf",
|
||||||
|
"application/x-run": "run",
|
||||||
|
"application/x-sea": "sea",
|
||||||
|
"application/x-shockwave-flash": "swf",
|
||||||
|
"application/x-sit": "sit",
|
||||||
|
"application/x-stuffit": "sit",
|
||||||
|
"application/x-swf": "swf",
|
||||||
|
"application/x-tar": "tar",
|
||||||
|
"application/x-tcl": "tcl",
|
||||||
|
"application/x-tk": "tcl",
|
||||||
|
"application/x-vnd.apple.mpegurl": "m3u8",
|
||||||
|
"application/x-vnd.google-earth.kml+xml": "kml",
|
||||||
|
"application/x-vnd.google-earth.kmz": "kmz",
|
||||||
|
"application/x-vnd.ms-excel": "xls",
|
||||||
|
"application/x-vnd.ms-fontobject": "eot",
|
||||||
|
"application/x-vnd.ms-powerpoint": "ppt",
|
||||||
|
"application/x-vnd.oasis.opendocument.graphics": "odg",
|
||||||
|
"application/x-vnd.oasis.opendocument.presentation": "odp",
|
||||||
|
"application/x-vnd.oasis.opendocument.spreadsheet": "ods",
|
||||||
|
"application/x-vnd.oasis.opendocument.text": "odt",
|
||||||
|
"application/x-vnd.wap.wmlc": "wmlc",
|
||||||
|
"application/x-war": "jar",
|
||||||
|
"application/x-wmlc": "wmlc",
|
||||||
|
"application/x-x509-ca-cert": "pem",
|
||||||
|
"application/x-xhtml": "xhtml",
|
||||||
|
"application/x-xhtml+xml": "xhtml",
|
||||||
|
"application/x-xls": "xls",
|
||||||
|
"application/x-xpi": "xpi",
|
||||||
|
"application/x-xpinstall": "xpi",
|
||||||
|
"application/x-xspf": "xspf",
|
||||||
|
"application/x-xspf+xml": "xspf",
|
||||||
|
"application/x-xz": "xz",
|
||||||
|
"application/x-zip": "zip",
|
||||||
|
"application/x509-ca-cert": "pem",
|
||||||
|
"application/xhtml": "xhtml",
|
||||||
|
"application/xhtml+xml": "xhtml",
|
||||||
|
"application/xls": "xls",
|
||||||
|
"application/xpi": "xpi",
|
||||||
|
"application/xpinstall": "xpi",
|
||||||
|
"application/xspf": "xspf",
|
||||||
|
"application/xspf+xml": "xspf",
|
||||||
|
"application/xz": "xz",
|
||||||
|
"application/zip": "zip",
|
||||||
|
"audio/kar": "mid",
|
||||||
|
"audio/m4a": "m4a",
|
||||||
|
"audio/matroska": "mka",
|
||||||
|
"audio/mid": "mid",
|
||||||
|
"audio/midi": "mid",
|
||||||
|
"audio/mka": "mka",
|
||||||
|
"audio/mp3": "mp3",
|
||||||
|
"audio/mpeg": "mp3",
|
||||||
|
"audio/ogg": "ogg",
|
||||||
|
"audio/ra": "ra",
|
||||||
|
"audio/realaudio": "ra",
|
||||||
|
"audio/x-kar": "mid",
|
||||||
|
"audio/x-m4a": "m4a",
|
||||||
|
"audio/x-matroska": "mka",
|
||||||
|
"audio/x-mid": "mid",
|
||||||
|
"audio/x-midi": "mid",
|
||||||
|
"audio/x-mka": "mka",
|
||||||
|
"audio/x-mp3": "mp3",
|
||||||
|
"audio/x-mpeg": "mp3",
|
||||||
|
"audio/x-ogg": "ogg",
|
||||||
|
"audio/x-ra": "ra",
|
||||||
|
"audio/x-realaudio": "ra",
|
||||||
|
"font/woff": "woff",
|
||||||
|
"font/woff2": "woff2",
|
||||||
|
"font/x-woff": "woff",
|
||||||
|
"font/x-woff2": "woff2",
|
||||||
|
"image/bmp": "bmp",
|
||||||
|
"image/gif": "gif",
|
||||||
|
"image/heic": "heic",
|
||||||
|
"image/heif": "heic",
|
||||||
|
"image/heif-sequence": "heic",
|
||||||
|
"image/ico": "ico",
|
||||||
|
"image/icon": "ico",
|
||||||
|
"image/jfif": "jpg",
|
||||||
|
"image/jng": "jng",
|
||||||
|
"image/jpe": "jpg",
|
||||||
|
"image/jpeg": "jpg",
|
||||||
|
"image/jpg": "jpg",
|
||||||
|
"image/ms-bmp": "bmp",
|
||||||
|
"image/png": "png",
|
||||||
|
"image/svg": "svg",
|
||||||
|
"image/svg+xml": "svg",
|
||||||
|
"image/svgz": "svg",
|
||||||
|
"image/tif": "tif",
|
||||||
|
"image/tiff": "tif",
|
||||||
|
"image/vnd.wap.wbmp": "wbmp",
|
||||||
|
"image/wbmp": "wbmp",
|
||||||
|
"image/webp": "webp",
|
||||||
|
"image/x-bmp": "bmp",
|
||||||
|
"image/x-gif": "gif",
|
||||||
|
"image/x-heic": "heic",
|
||||||
|
"image/x-heif": "heic",
|
||||||
|
"image/x-heif-sequence": "heic",
|
||||||
|
"image/x-ico": "ico",
|
||||||
|
"image/x-icon": "ico",
|
||||||
|
"image/x-jfif": "jpg",
|
||||||
|
"image/x-jng": "jng",
|
||||||
|
"image/x-jpe": "jpg",
|
||||||
|
"image/x-jpeg": "jpg",
|
||||||
|
"image/x-jpg": "jpg",
|
||||||
|
"image/x-ms-bmp": "bmp",
|
||||||
|
"image/x-png": "png",
|
||||||
|
"image/x-svg": "svg",
|
||||||
|
"image/x-svg+xml": "svg",
|
||||||
|
"image/x-svgz": "svg",
|
||||||
|
"image/x-tif": "tif",
|
||||||
|
"image/x-tiff": "tif",
|
||||||
|
"image/x-vnd.wap.wbmp": "wbmp",
|
||||||
|
"image/x-wbmp": "wbmp",
|
||||||
|
"image/x-webp": "webp",
|
||||||
|
"text/component": "htc",
|
||||||
|
"text/css": "css",
|
||||||
|
"text/htc": "htc",
|
||||||
|
"text/htm": "html",
|
||||||
|
"text/html": "html",
|
||||||
|
"text/jad": "jad",
|
||||||
|
"text/javascript": "js",
|
||||||
|
"text/js": "js",
|
||||||
|
"text/jsx": "js",
|
||||||
|
"text/mathml": "mml",
|
||||||
|
"text/mml": "mml",
|
||||||
|
"text/php": "html",
|
||||||
|
"text/plain": "txt",
|
||||||
|
"text/shtml": "html",
|
||||||
|
"text/txt": "txt",
|
||||||
|
"text/vnd.sun.j2me.app-descriptor": "jad",
|
||||||
|
"text/vnd.wap.wml": "wml",
|
||||||
|
"text/wml": "wml",
|
||||||
|
"text/x-component": "htc",
|
||||||
|
"text/x-css": "css",
|
||||||
|
"text/x-htc": "htc",
|
||||||
|
"text/x-htm": "html",
|
||||||
|
"text/x-html": "html",
|
||||||
|
"text/x-jad": "jad",
|
||||||
|
"text/x-javascript": "js",
|
||||||
|
"text/x-js": "js",
|
||||||
|
"text/x-jsx": "js",
|
||||||
|
"text/x-mathml": "mml",
|
||||||
|
"text/x-mml": "mml",
|
||||||
|
"text/x-php": "html",
|
||||||
|
"text/x-plain": "txt",
|
||||||
|
"text/x-shtml": "html",
|
||||||
|
"text/x-txt": "txt",
|
||||||
|
"text/x-vnd.sun.j2me.app-descriptor": "jad",
|
||||||
|
"text/x-vnd.wap.wml": "wml",
|
||||||
|
"text/x-wml": "wml",
|
||||||
|
"text/x-xml": "xml",
|
||||||
|
"text/xml": "xml",
|
||||||
|
"video/3gp": "3gpp",
|
||||||
|
"video/3gpp": "3gpp",
|
||||||
|
"video/asf": "asx",
|
||||||
|
"video/asx": "asx",
|
||||||
|
"video/avi": "avi",
|
||||||
|
"video/flv": "flv",
|
||||||
|
"video/m4v": "m4v",
|
||||||
|
"video/matroska": "mkv",
|
||||||
|
"video/mk3d": "mkv",
|
||||||
|
"video/mks": "mkv",
|
||||||
|
"video/mkv": "mkv",
|
||||||
|
"video/mng": "mng",
|
||||||
|
"video/moov": "mov",
|
||||||
|
"video/mov": "mov",
|
||||||
|
"video/mp2t": "ts",
|
||||||
|
"video/mp4": "mp4",
|
||||||
|
"video/mpe": "mpg",
|
||||||
|
"video/mpeg": "mpg",
|
||||||
|
"video/mpg": "mpg",
|
||||||
|
"video/ms-asf": "asx",
|
||||||
|
"video/ms-wmv": "wmv",
|
||||||
|
"video/msvideo": "avi",
|
||||||
|
"video/opus": "opus",
|
||||||
|
"video/qt": "mov",
|
||||||
|
"video/quicktime": "mov",
|
||||||
|
"video/ts": "ts",
|
||||||
|
"video/webm": "webm",
|
||||||
|
"video/wmv": "wmv",
|
||||||
|
"video/x-3gp": "3gpp",
|
||||||
|
"video/x-3gpp": "3gpp",
|
||||||
|
"video/x-asf": "asx",
|
||||||
|
"video/x-asx": "asx",
|
||||||
|
"video/x-avi": "avi",
|
||||||
|
"video/x-flv": "flv",
|
||||||
|
"video/x-m4v": "m4v",
|
||||||
|
"video/x-matroska": "mkv",
|
||||||
|
"video/x-mk3d": "mkv",
|
||||||
|
"video/x-mks": "mkv",
|
||||||
|
"video/x-mkv": "mkv",
|
||||||
|
"video/x-mng": "mng",
|
||||||
|
"video/x-moov": "mov",
|
||||||
|
"video/x-mov": "mov",
|
||||||
|
"video/x-mp2t": "ts",
|
||||||
|
"video/x-mp4": "mp4",
|
||||||
|
"video/x-mpe": "mpg",
|
||||||
|
"video/x-mpeg": "mpg",
|
||||||
|
"video/x-mpg": "mpg",
|
||||||
|
"video/x-ms-asf": "asx",
|
||||||
|
"video/x-ms-wmv": "wmv",
|
||||||
|
"video/x-msvideo": "avi",
|
||||||
|
"video/x-opus": "opus",
|
||||||
|
"video/x-qt": "mov",
|
||||||
|
"video/x-quicktime": "mov",
|
||||||
|
"video/x-ts": "ts",
|
||||||
|
"video/x-webm": "webm",
|
||||||
|
"video/x-wmv": "wmv"
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"global-turbo": false,
|
"button-type": "popup",
|
||||||
|
"manager-in-popup": false,
|
||||||
"concurrent": 4,
|
"concurrent": 4,
|
||||||
"queue-notification": true,
|
"queue-notification": true,
|
||||||
"finish-notification": true,
|
"finish-notification": true,
|
||||||
|
"sounds": true,
|
||||||
"open-manager-on-queue": true,
|
"open-manager-on-queue": true,
|
||||||
"text-links": true,
|
"text-links": true,
|
||||||
"add-paused": false,
|
"add-paused": false,
|
||||||
@ -13,6 +15,8 @@
|
|||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"show-urls": false,
|
"show-urls": false,
|
||||||
"remove-missing-on-init": false,
|
"remove-missing-on-init": false,
|
||||||
|
"retries": 5,
|
||||||
|
"retry-time": 10,
|
||||||
"limits": [
|
"limits": [
|
||||||
{
|
{
|
||||||
"domain": "*",
|
"domain": "*",
|
||||||
|
11
lib/api.ts
11
lib/api.ts
@ -11,7 +11,7 @@ import { getManager } from "./manager/man";
|
|||||||
import { select } from "./select";
|
import { select } from "./select";
|
||||||
import { single } from "./single";
|
import { single } from "./single";
|
||||||
import { Notification } from "./notifications";
|
import { Notification } from "./notifications";
|
||||||
import { MASK, FASTFILTER } from "./recentlist";
|
import { MASK, FASTFILTER, SUBFOLDER } from "./recentlist";
|
||||||
import { openManager } from "./windowutils";
|
import { openManager } from "./windowutils";
|
||||||
import { _ } from "./i18n";
|
import { _ } from "./i18n";
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ const MAX_BATCH = 10000;
|
|||||||
|
|
||||||
export interface QueueOptions {
|
export interface QueueOptions {
|
||||||
mask?: string;
|
mask?: string;
|
||||||
|
subfolder?: string;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,8 +29,9 @@ export const API = new class APIImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async queue(items: BaseItem[], options: QueueOptions) {
|
async queue(items: BaseItem[], options: QueueOptions) {
|
||||||
await MASK.init();
|
await Promise.all([MASK.init(), SUBFOLDER.init()]);
|
||||||
const {mask = MASK.current} = options;
|
const {mask = MASK.current} = options;
|
||||||
|
const {subfolder = SUBFOLDER.current} = options;
|
||||||
|
|
||||||
const {paused = false} = options;
|
const {paused = false} = options;
|
||||||
const defaults: any = {
|
const defaults: any = {
|
||||||
@ -46,6 +48,7 @@ export const API = new class APIImpl {
|
|||||||
private: false,
|
private: false,
|
||||||
postData: null,
|
postData: null,
|
||||||
mask,
|
mask,
|
||||||
|
subfolder,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
paused
|
paused
|
||||||
};
|
};
|
||||||
@ -117,6 +120,10 @@ export const API = new class APIImpl {
|
|||||||
await FASTFILTER.init();
|
await FASTFILTER.init();
|
||||||
await FASTFILTER.push(options.fast);
|
await FASTFILTER.push(options.fast);
|
||||||
}
|
}
|
||||||
|
if (typeof options.subfolder === "string" && !options.subfolderOnce) {
|
||||||
|
await SUBFOLDER.init();
|
||||||
|
await SUBFOLDER.push(options.subfolder);
|
||||||
|
}
|
||||||
if (typeof options.type === "string") {
|
if (typeof options.type === "string") {
|
||||||
await Prefs.set("last-type", options.type);
|
await Prefs.set("last-type", options.type);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,9 @@ import {
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
MenuClickInfo,
|
MenuClickInfo,
|
||||||
CHROME,
|
CHROME,
|
||||||
|
runtime,
|
||||||
|
history,
|
||||||
|
sessions,
|
||||||
} from "./browser";
|
} from "./browser";
|
||||||
import { Bus } from "./bus";
|
import { Bus } from "./bus";
|
||||||
import { filterInSitu } from "./util";
|
import { filterInSitu } from "./util";
|
||||||
@ -45,6 +48,9 @@ const CHROME_CONTEXTS = Object.freeze(new Set([
|
|||||||
|
|
||||||
async function runContentJob(tab: Tab, file: string, msg: any) {
|
async function runContentJob(tab: Tab, file: string, msg: any) {
|
||||||
try {
|
try {
|
||||||
|
if (tab && tab.incognito && msg) {
|
||||||
|
msg.private = tab.incognito;
|
||||||
|
}
|
||||||
const res = await tabs.executeScript(tab.id, {
|
const res = await tabs.executeScript(tab.id, {
|
||||||
file,
|
file,
|
||||||
allFrames: true,
|
allFrames: true,
|
||||||
@ -103,7 +109,7 @@ class Handler {
|
|||||||
discarded: false,
|
discarded: false,
|
||||||
};
|
};
|
||||||
if (!CHROME) {
|
if (!CHROME) {
|
||||||
toptions.hidden = true;
|
toptions.hidden = false;
|
||||||
}
|
}
|
||||||
const selectedTabs = options.allTabs ?
|
const selectedTabs = options.allTabs ?
|
||||||
await tabs.query(toptions) as any[] :
|
await tabs.query(toptions) as any[] :
|
||||||
@ -130,35 +136,6 @@ class Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
locale.then(() => {
|
locale.then(() => {
|
||||||
new class Action extends Handler {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.onClicked = this.onClicked.bind(this);
|
|
||||||
action.onClicked.addListener(this.onClicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClicked(tab: {id: number}) {
|
|
||||||
if (!tab.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.processResults(
|
|
||||||
true,
|
|
||||||
await runContentJob(
|
|
||||||
tab, "/bundles/content-gather.js", {
|
|
||||||
type: "DTA:gather",
|
|
||||||
selectionOnly: false,
|
|
||||||
textLinks: await Prefs.get("text-links", true),
|
|
||||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
|
||||||
transferable: TRANSFERABLE_PROPERTIES,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
const menuHandler = new class Menus extends Handler {
|
const menuHandler = new class Menus extends Handler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -415,7 +392,7 @@ locale.then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async enumulate(action: string) {
|
async emulate(action: string) {
|
||||||
const tab = await tabs.query({
|
const tab = await tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
@ -537,23 +514,25 @@ locale.then(() => {
|
|||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
|
new class Action extends Handler {
|
||||||
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
|
constructor() {
|
||||||
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
|
super();
|
||||||
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
|
this.onClicked = this.onClicked.bind(this);
|
||||||
Bus.on("do-single", () => API.singleRegular(null));
|
action.onClicked.addListener(this.onClicked);
|
||||||
Bus.on("open-manager", () => openManager(true));
|
Prefs.get("button-type", false).then(v => this.adjust(v));
|
||||||
Bus.on("open-prefs", () => openPrefs());
|
Prefs.on("button-type", (prefs, key, value) => {
|
||||||
|
this.adjust(value);
|
||||||
function adjustAction(globalTurbo: boolean) {
|
|
||||||
action.setPopup({
|
|
||||||
popup: globalTurbo ? "" : "/windows/popup.html"
|
|
||||||
});
|
});
|
||||||
action.setIcon({
|
}
|
||||||
path: globalTurbo ? {
|
|
||||||
16: "/style/button-turbo.png",
|
adjust(type: string) {
|
||||||
32: "/style/button-turbo@2x.png",
|
action.setPopup({
|
||||||
} : {
|
popup: type !== "popup" ? "" : "/windows/popup.html"
|
||||||
|
});
|
||||||
|
let icons;
|
||||||
|
switch (type) {
|
||||||
|
case "popup":
|
||||||
|
icons = {
|
||||||
16: "/style/icon16.png",
|
16: "/style/icon16.png",
|
||||||
32: "/style/icon32.png",
|
32: "/style/icon32.png",
|
||||||
48: "/style/icon48.png",
|
48: "/style/icon48.png",
|
||||||
@ -561,16 +540,101 @@ locale.then(() => {
|
|||||||
96: "/style/icon96.png",
|
96: "/style/icon96.png",
|
||||||
128: "/style/icon128.png",
|
128: "/style/icon128.png",
|
||||||
256: "/style/icon256.png"
|
256: "/style/icon256.png"
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "dta":
|
||||||
|
icons = {
|
||||||
|
16: "/style/button-regular.png",
|
||||||
|
32: "/style/button-regular@2x.png",
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "turbo":
|
||||||
|
icons = {
|
||||||
|
16: "/style/button-turbo.png",
|
||||||
|
32: "/style/button-turbo@2x.png",
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "manager":
|
||||||
|
icons = {
|
||||||
|
16: "/style/button-manager.png",
|
||||||
|
32: "/style/button-manager@2x.png",
|
||||||
|
};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
action.setIcon({path: icons});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onClicked() {
|
||||||
|
switch (await Prefs.get("button-type")) {
|
||||||
|
case "popup":
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "dta":
|
||||||
|
menuHandler.emulate("DTARegular");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "turbo":
|
||||||
|
menuHandler.emulate("DTATurbo");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "manager":
|
||||||
|
menuHandler.emulate("DTAManager");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
|
||||||
|
Bus.on("do-regular", () => menuHandler.emulate("DTARegular"));
|
||||||
|
Bus.on("do-regular-all", () => menuHandler.emulate("DTARegularAll"));
|
||||||
|
Bus.on("do-turbo", () => menuHandler.emulate("DTATurbo"));
|
||||||
|
Bus.on("do-turbo-all", () => menuHandler.emulate("DTATurboAll"));
|
||||||
|
Bus.on("do-single", () => API.singleRegular(null));
|
||||||
|
Bus.on("open-manager", () => openManager(true));
|
||||||
|
Bus.on("open-prefs", () => openPrefs());
|
||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
await Prefs.set("last-run", new Date());
|
const urlBase = runtime.getURL("");
|
||||||
Prefs.get("global-turbo", false).then(v => adjustAction(v));
|
history.onVisited.addListener(({url}: {url: string}) => {
|
||||||
Prefs.on("global-turbo", (prefs, key, value) => {
|
if (!url || !url.startsWith(urlBase)) {
|
||||||
adjustAction(value);
|
return;
|
||||||
|
}
|
||||||
|
history.deleteUrl({url});
|
||||||
});
|
});
|
||||||
|
const results: {url?: string}[] = await history.search({text: urlBase});
|
||||||
|
for (const {url} of results) {
|
||||||
|
if (!url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
history.deleteUrl({url});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CHROME) {
|
||||||
|
const sessionRemover = async () => {
|
||||||
|
for (const s of await sessions.getRecentlyClosed()) {
|
||||||
|
if (s.tab) {
|
||||||
|
if (s.tab.url.startsWith(urlBase)) {
|
||||||
|
await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!s.window || !s.window.tabs || s.window.tabs.length > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [tab] = s.window.tabs;
|
||||||
|
if (tab.url.startsWith(urlBase)) {
|
||||||
|
await sessions.forgetClosedWindow(s.window.sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sessions.onChanged.addListener(sessionRemover);
|
||||||
|
await sessionRemover();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Prefs.set("last-run", new Date());
|
||||||
await filters();
|
await filters();
|
||||||
await getManager();
|
await getManager();
|
||||||
})().catch(ex => {
|
})().catch(ex => {
|
||||||
|
@ -19,6 +19,7 @@ export interface MessageSender {
|
|||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
incognito?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuClickInfo {
|
export interface MenuClickInfo {
|
||||||
@ -39,16 +40,72 @@ export interface RawPort {
|
|||||||
postMessage: (message: any) => void;
|
postMessage: (message: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {extension} = polyfill;
|
interface WebRequestFilter {
|
||||||
export const {notifications} = polyfill;
|
urls?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebRequestListener {
|
||||||
|
addListener(
|
||||||
|
callback: Function,
|
||||||
|
filter: WebRequestFilter,
|
||||||
|
extraInfoSpec: string[]
|
||||||
|
): void;
|
||||||
|
removeListener(callback: Function): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Header = {name: string; value: string};
|
||||||
|
|
||||||
|
export interface DownloadOptions {
|
||||||
|
conflictAction: string;
|
||||||
|
filename: string;
|
||||||
|
saveAs: boolean;
|
||||||
|
url: string;
|
||||||
|
method?: string;
|
||||||
|
body?: string;
|
||||||
|
incognito?: boolean;
|
||||||
|
headers: Header[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadsQuery {
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Downloads {
|
||||||
|
download(download: DownloadOptions): Promise<number>;
|
||||||
|
open(manId: number): Promise<void>;
|
||||||
|
show(manId: number): Promise<void>;
|
||||||
|
pause(manId: number): Promise<void>;
|
||||||
|
resume(manId: number): Promise<void>;
|
||||||
|
cancel(manId: number): Promise<void>;
|
||||||
|
erase(query: DownloadsQuery): Promise<void>;
|
||||||
|
search(query: DownloadsQuery): Promise<any[]>;
|
||||||
|
getFileIcon(id: number, options?: any): Promise<string>;
|
||||||
|
setShelfEnabled(state: boolean): void;
|
||||||
|
removeFile(manId: number): Promise<void>;
|
||||||
|
onCreated: ExtensionListener;
|
||||||
|
onChanged: ExtensionListener;
|
||||||
|
onErased: ExtensionListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebRequest {
|
||||||
|
onBeforeSendHeaders: WebRequestListener;
|
||||||
|
onSendHeaders: WebRequestListener;
|
||||||
|
onHeadersReceived: WebRequestListener;
|
||||||
|
}
|
||||||
|
|
||||||
export const {browserAction} = polyfill;
|
export const {browserAction} = polyfill;
|
||||||
export const {contextMenus} = polyfill;
|
export const {contextMenus} = polyfill;
|
||||||
export const {downloads} = polyfill;
|
export const {downloads}: {downloads: Downloads} = polyfill;
|
||||||
|
export const {extension} = polyfill;
|
||||||
|
export const {history} = polyfill;
|
||||||
export const {menus} = polyfill;
|
export const {menus} = polyfill;
|
||||||
|
export const {notifications} = polyfill;
|
||||||
export const {runtime} = polyfill;
|
export const {runtime} = polyfill;
|
||||||
|
export const {sessions} = polyfill;
|
||||||
export const {storage} = polyfill;
|
export const {storage} = polyfill;
|
||||||
export const {tabs} = polyfill;
|
export const {tabs} = polyfill;
|
||||||
export const {webNavigation} = polyfill;
|
export const {webNavigation} = polyfill;
|
||||||
|
export const {webRequest}: {webRequest: WebRequest} = polyfill;
|
||||||
export const {windows} = polyfill;
|
export const {windows} = polyfill;
|
||||||
|
|
||||||
export const CHROME = navigator.appVersion.includes("Chrome/");
|
export const CHROME = navigator.appVersion.includes("Chrome/");
|
||||||
|
230
lib/cdheaderparser.ts
Normal file
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(/^[^']*'/, ""));
|
||||||
|
}
|
||||||
|
}
|
15
lib/db.ts
15
lib/db.ts
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseItem } from "./item";
|
import { BaseItem } from "./item";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Download } from "./manager/download";
|
||||||
|
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
|
||||||
|
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
@ -69,7 +72,7 @@ export const DB = new class DB {
|
|||||||
return await new Promise(this.getAllInternal);
|
return await new Promise(this.getAllInternal);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveItemsInternal(items: any[], resolve: Function, reject: Function) {
|
saveItemsInternal(items: Download[], resolve: Function, reject: Function) {
|
||||||
if (!items || !items.length || !this.db) {
|
if (!items || !items.length || !this.db) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@ -83,9 +86,13 @@ export const DB = new class DB {
|
|||||||
if (item.private) {
|
if (item.private) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const req = store.put(item.toJSON());
|
const json = item.toJSON();
|
||||||
|
if (item.state === RUNNING || item.state === RETRYING) {
|
||||||
|
json.state = QUEUED;
|
||||||
|
}
|
||||||
|
const req = store.put(json);
|
||||||
if (!("dbId" in item) || item.dbId < 0) {
|
if (!("dbId" in item) || item.dbId < 0) {
|
||||||
req.onsuccess = () => item.dbId = req.result;
|
req.onsuccess = () => item.dbId = req.result as number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,7 +101,7 @@ export const DB = new class DB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveItems(items: any[]) {
|
async saveItems(items: Download[]) {
|
||||||
await this.init();
|
await this.init();
|
||||||
return await new Promise(this.saveItemsInternal.bind(this, items));
|
return await new Promise(this.saveItemsInternal.bind(this, items));
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import { EventEmitter } from "./events";
|
|||||||
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Overlayable } from "./objectoverlay";
|
import { Overlayable } from "./objectoverlay";
|
||||||
import * as DEFAULT_FILTERS from "../data/filters.json";
|
import DEFAULT_FILTERS from "../data/filters.json";
|
||||||
import { FASTFILTER } from "./recentlist";
|
import { FASTFILTER } from "./recentlist";
|
||||||
import { _, locale } from "./i18n";
|
import { _, locale } from "./i18n";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import {memoize} from "./memoize";
|
import {memoize} from "./memoize";
|
||||||
import * as langs from "../_locales/all.json";
|
import langs from "../_locales/all.json";
|
||||||
import { sorted, naturalCaseCompare } from "./sorting";
|
import { sorted, naturalCaseCompare } from "./sorting";
|
||||||
|
|
||||||
|
|
||||||
|
247
lib/imex.ts
Normal file
247
lib/imex.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
import { getTextLinks } from "./textlinks";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
import { ALLOWED_SCHEMES } from "./constants";
|
||||||
|
|
||||||
|
export const NS_METALINK_RFC5854 = "urn:ietf:params:xml:ns:metalink";
|
||||||
|
export const NS_DTA = "http://www.downthemall.net/properties#";
|
||||||
|
|
||||||
|
function parseNum(
|
||||||
|
file: Element,
|
||||||
|
attr: string,
|
||||||
|
defaultValue: number,
|
||||||
|
ns = NS_METALINK_RFC5854) {
|
||||||
|
const val = file.getAttributeNS(ns, attr);
|
||||||
|
if (!val) {
|
||||||
|
return defaultValue + 1;
|
||||||
|
}
|
||||||
|
const num = parseInt(val, 10);
|
||||||
|
if (isFinite(num)) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
return defaultValue + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importMeta4(data: string) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const document = parser.parseFromString(data, "text/xml");
|
||||||
|
const {documentElement} = document;
|
||||||
|
const items: BaseItem[] = [];
|
||||||
|
let batch = 0;
|
||||||
|
for (const file of documentElement.querySelectorAll("file")) {
|
||||||
|
try {
|
||||||
|
const url = Array.from(file.querySelectorAll("url")).map(u => {
|
||||||
|
try {
|
||||||
|
const {textContent} = u;
|
||||||
|
if (!textContent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const url = new URL(textContent);
|
||||||
|
if (!ALLOWED_SCHEMES.has(url.protocol)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const prio = parseNum(u, "priority", 0);
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
prio
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(u => !!u).reduce((p, c) => {
|
||||||
|
if (!c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!p || p.prio < c.prio) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
if (!url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
batch = parseNum(file, "num", batch, NS_DTA);
|
||||||
|
const idx = parseNum(file, "idx", 0, NS_DTA);
|
||||||
|
const item: BaseItem = {
|
||||||
|
url: url.url.toString(),
|
||||||
|
usable: decodeURIComponent(url.url.toString()),
|
||||||
|
batch,
|
||||||
|
idx
|
||||||
|
};
|
||||||
|
const ref = file.getAttributeNS(NS_DTA, "referrer");
|
||||||
|
if (ref) {
|
||||||
|
item.referrer = ref;
|
||||||
|
item.usableReferrer = decodeURIComponent(ref);
|
||||||
|
}
|
||||||
|
const mask = file.getAttributeNS(NS_DTA, "mask");
|
||||||
|
if (mask) {
|
||||||
|
item.mask = mask;
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to import file", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKV(current: BaseItem, line: string) {
|
||||||
|
const [k, v] = line.split("=", 2);
|
||||||
|
switch (k.toLocaleLowerCase().trim()) {
|
||||||
|
case "referer": {
|
||||||
|
const rurls = getTextLinks(v);
|
||||||
|
if (rurls && rurls.length) {
|
||||||
|
current.referrer = rurls.pop();
|
||||||
|
current.usableReferrer = decodeURIComponent(current.referrer || "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importText(data: string) {
|
||||||
|
if (data.includes(NS_METALINK_RFC5854)) {
|
||||||
|
return importMeta4(data);
|
||||||
|
}
|
||||||
|
const splitter = /(.+)\n|(.+)$/g;
|
||||||
|
const spacer = /^\s+/;
|
||||||
|
let match;
|
||||||
|
let current: BaseItem | undefined = undefined;
|
||||||
|
let idx = 0;
|
||||||
|
const items = [];
|
||||||
|
while ((match = splitter.exec(data)) !== null) {
|
||||||
|
try {
|
||||||
|
const line = match[0].trimRight();
|
||||||
|
if (!line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spacer.test(line)) {
|
||||||
|
if (!current) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parseKV(current, line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const urls = getTextLinks(line);
|
||||||
|
if (!urls || !urls.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current = {
|
||||||
|
url: urls[0],
|
||||||
|
usable: decodeURIComponent(urls[0]),
|
||||||
|
idx: ++idx
|
||||||
|
};
|
||||||
|
items.push(current);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
current = undefined;
|
||||||
|
console.error("Failed to import", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exporter {
|
||||||
|
fileName: string;
|
||||||
|
getText(items: BaseItem[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextExporter {
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fileName = "links.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(items: BaseItem[]) {
|
||||||
|
const lines = [];
|
||||||
|
for (const item of items) {
|
||||||
|
lines.push(item.url);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Aria2Exporter {
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fileName = "links.aria2.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(items: BaseItem[]) {
|
||||||
|
const lines = [];
|
||||||
|
for (const item of items) {
|
||||||
|
lines.push(item.url);
|
||||||
|
if (item.referrer) {
|
||||||
|
lines.push(` referer=${item.referrer}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetalinkExporter {
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fileName = "links.meta4";
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(items: BaseItem[]) {
|
||||||
|
const document = window.document.implementation.
|
||||||
|
createDocument(NS_METALINK_RFC5854, "metalink", null);
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttributeNS(NS_DTA, "generator", "DownThemAll!");
|
||||||
|
root.appendChild(document.createComment(
|
||||||
|
"metalink as exported by DownThemAll!",
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const aitem = item as any;
|
||||||
|
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
|
||||||
|
f.setAttribute("name", aitem.currentName);
|
||||||
|
if (item.batch) {
|
||||||
|
f.setAttributeNS(NS_DTA, "num", item.batch.toString());
|
||||||
|
}
|
||||||
|
if (item.idx) {
|
||||||
|
f.setAttributeNS(NS_DTA, "idx", item.idx.toString());
|
||||||
|
}
|
||||||
|
if (item.referrer) {
|
||||||
|
f.setAttributeNS(NS_DTA, "referrer", item.referrer);
|
||||||
|
}
|
||||||
|
if (item.mask) {
|
||||||
|
f.setAttributeNS(NS_DTA, "mask", item.mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.description) {
|
||||||
|
const n = document.createElementNS(NS_METALINK_RFC5854, "description");
|
||||||
|
n.textContent = item.description;
|
||||||
|
f.appendChild(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
|
||||||
|
u.textContent = item.url;
|
||||||
|
f.appendChild(u);
|
||||||
|
|
||||||
|
if (aitem.totalSize > 0) {
|
||||||
|
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
|
||||||
|
s.textContent = aitem.totalSize.toString();
|
||||||
|
f.appendChild(s);
|
||||||
|
}
|
||||||
|
root.appendChild(f);
|
||||||
|
}
|
||||||
|
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||||
|
xml += root.outerHTML;
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const textExporter = new TextExporter();
|
||||||
|
export const aria2Exporter = new Aria2Exporter();
|
||||||
|
export const metalinkExporter = new MetalinkExporter();
|
@ -15,6 +15,7 @@ export interface BaseItem {
|
|||||||
batch?: number;
|
batch?: number;
|
||||||
idx: number;
|
idx: number;
|
||||||
mask?: string;
|
mask?: string;
|
||||||
|
subfolder?: string;
|
||||||
startDate?: number;
|
startDate?: number;
|
||||||
private?: boolean;
|
private?: boolean;
|
||||||
postData?: string;
|
postData?: string;
|
||||||
@ -27,6 +28,7 @@ const OPTIONPROPS = Object.freeze([
|
|||||||
"fileName",
|
"fileName",
|
||||||
"batch", "idx",
|
"batch", "idx",
|
||||||
"mask",
|
"mask",
|
||||||
|
"subfolder",
|
||||||
"startDate",
|
"startDate",
|
||||||
"private",
|
"private",
|
||||||
"postData",
|
"postData",
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import { parsePath, URLd } from "../util";
|
import { parsePath, URLd } from "../util";
|
||||||
import { QUEUED, RUNNING, PAUSED } from "./state";
|
import { QUEUED, RUNNING, PAUSED } from "./state";
|
||||||
import Renamer from "./renamer";
|
import Renamer from "./renamer";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "../item";
|
||||||
|
|
||||||
const SAVEDPROPS = [
|
const SAVEDPROPS = [
|
||||||
"state",
|
"state",
|
||||||
@ -14,6 +16,7 @@ const SAVEDPROPS = [
|
|||||||
"usableReferrer",
|
"usableReferrer",
|
||||||
"fileName",
|
"fileName",
|
||||||
"mask",
|
"mask",
|
||||||
|
"subfolder",
|
||||||
"date",
|
"date",
|
||||||
// batches
|
// batches
|
||||||
"batch",
|
"batch",
|
||||||
@ -27,6 +30,9 @@ const SAVEDPROPS = [
|
|||||||
"written",
|
"written",
|
||||||
// server stuff
|
// server stuff
|
||||||
"serverName",
|
"serverName",
|
||||||
|
"browserName",
|
||||||
|
"mime",
|
||||||
|
"prerolled",
|
||||||
// other options
|
// other options
|
||||||
"private",
|
"private",
|
||||||
// db
|
// db
|
||||||
@ -39,10 +45,15 @@ const DEFAULTS = {
|
|||||||
state: QUEUED,
|
state: QUEUED,
|
||||||
error: "",
|
error: "",
|
||||||
serverName: "",
|
serverName: "",
|
||||||
|
browserName: "",
|
||||||
fileName: "",
|
fileName: "",
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
written: 0,
|
written: 0,
|
||||||
manId: 0,
|
manId: 0,
|
||||||
|
mime: "",
|
||||||
|
prerolled: false,
|
||||||
|
retries: 0,
|
||||||
|
deadline: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let sessionId = 0;
|
let sessionId = 0;
|
||||||
@ -59,14 +70,26 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public url: string;
|
public url: string;
|
||||||
|
|
||||||
|
public usable: string;
|
||||||
|
|
||||||
public uReferrer: URLd;
|
public uReferrer: URLd;
|
||||||
|
|
||||||
public referrer: string;
|
public referrer: string;
|
||||||
|
|
||||||
|
public usableReferrer: string;
|
||||||
|
|
||||||
public startDate: Date;
|
public startDate: Date;
|
||||||
|
|
||||||
public fileName: string;
|
public fileName: string;
|
||||||
|
|
||||||
|
public description?: string;
|
||||||
|
|
||||||
|
public title?: string;
|
||||||
|
|
||||||
|
public batch: number;
|
||||||
|
|
||||||
|
public idx: number;
|
||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public postData: any;
|
public postData: any;
|
||||||
@ -79,10 +102,19 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public serverName: string;
|
public serverName: string;
|
||||||
|
|
||||||
|
public browserName: string;
|
||||||
|
|
||||||
|
public mime: string;
|
||||||
|
|
||||||
public mask: string;
|
public mask: string;
|
||||||
|
|
||||||
|
public subfolder: string;
|
||||||
|
|
||||||
constructor(options: any) {
|
public prerolled: boolean;
|
||||||
|
|
||||||
|
public retries: number;
|
||||||
|
|
||||||
|
constructor(options: BaseItem) {
|
||||||
Object.assign(this, DEFAULTS);
|
Object.assign(this, DEFAULTS);
|
||||||
this.assign(options);
|
this.assign(options);
|
||||||
if (this.state === RUNNING) {
|
if (this.state === RUNNING) {
|
||||||
@ -90,14 +122,16 @@ export class BaseDownload {
|
|||||||
}
|
}
|
||||||
this.sessionId = ++sessionId;
|
this.sessionId = ++sessionId;
|
||||||
this.renamer = new Renamer(this);
|
this.renamer = new Renamer(this);
|
||||||
|
this.retries = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
assign(options: any) {
|
assign(options: BaseItem) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self: any = this;
|
const self: any = this;
|
||||||
|
const other: any = options;
|
||||||
for (const prop of SAVEDPROPS) {
|
for (const prop of SAVEDPROPS) {
|
||||||
if (prop in options) {
|
if (prop in options) {
|
||||||
self[prop] = options[prop];
|
self[prop] = other[prop];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.uURL = new URL(this.url) as URLd;
|
this.uURL = new URL(this.url) as URLd;
|
||||||
@ -115,6 +149,10 @@ export class BaseDownload {
|
|||||||
return this.serverName || this.fileName || this.urlName || "index.html";
|
return this.serverName || this.fileName || this.urlName || "index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentName() {
|
||||||
|
return this.browserName || this.dest.name || this.finalName;
|
||||||
|
}
|
||||||
|
|
||||||
get urlName() {
|
get urlName() {
|
||||||
const path = parsePath(this.uURL);
|
const path = parsePath(this.uURL);
|
||||||
if (path.name) {
|
if (path.name) {
|
||||||
@ -152,8 +190,10 @@ export class BaseDownload {
|
|||||||
rv.destName = dest.name;
|
rv.destName = dest.name;
|
||||||
rv.destPath = dest.path;
|
rv.destPath = dest.path;
|
||||||
rv.destFull = dest.full;
|
rv.destFull = dest.full;
|
||||||
|
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||||
rv.error = this.error;
|
rv.error = this.error;
|
||||||
rv.ext = this.renamer.p_ext;
|
rv.ext = this.renamer.p_ext;
|
||||||
|
rv.retries = this.retries;
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,41 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { Prefs } from "../prefs";
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { parsePath, filterInSitu } from "../util";
|
import { CHROME, downloads, DownloadOptions } from "../browser";
|
||||||
import {
|
import { Prefs, PrefWatcher } from "../prefs";
|
||||||
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
|
|
||||||
FORCABLE, PAUSABLE, CANCELABLE,
|
|
||||||
} from "./state";
|
|
||||||
import { BaseDownload } from "./basedownload";
|
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
|
import { filterInSitu, parsePath } from "../util";
|
||||||
|
import { BaseDownload } from "./basedownload";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Manager } from "./man";
|
import { Manager } from "./man";
|
||||||
import { downloads, CHROME } from "../browser";
|
import Renamer from "./renamer";
|
||||||
import { debounce } from "../../uikit/lib/util";
|
import {
|
||||||
|
CANCELABLE,
|
||||||
|
CANCELED,
|
||||||
|
DONE,
|
||||||
|
FORCABLE,
|
||||||
|
MISSING,
|
||||||
|
PAUSABLE,
|
||||||
|
PAUSED,
|
||||||
|
QUEUED,
|
||||||
|
RUNNING,
|
||||||
|
RETRYING
|
||||||
|
} from "./state";
|
||||||
|
import { Preroller } from "./preroller";
|
||||||
|
|
||||||
|
function isRecoverable(error: string) {
|
||||||
|
switch (error) {
|
||||||
|
case "SERVER_FAILED":
|
||||||
|
return true;
|
||||||
|
|
||||||
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
default:
|
||||||
// ignored
|
return error.startsWith("NETWORK_");
|
||||||
};
|
|
||||||
|
|
||||||
const reenableShelf = debounce(() => setShelfEnabled(true), 1000, true);
|
|
||||||
|
|
||||||
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 {
|
export class Download extends BaseDownload {
|
||||||
public manager: Manager;
|
public manager: Manager;
|
||||||
@ -44,6 +48,10 @@ export class Download extends BaseDownload {
|
|||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
|
public dbId: number;
|
||||||
|
|
||||||
|
public deadline: number;
|
||||||
|
|
||||||
constructor(manager: Manager, options: any) {
|
constructor(manager: Manager, options: any) {
|
||||||
super(options);
|
super(options);
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -53,6 +61,7 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markDirty() {
|
markDirty() {
|
||||||
|
this.renamer = new Renamer(this);
|
||||||
this.manager.setDirty(this);
|
this.manager.setDirty(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,22 +83,28 @@ export class Download extends BaseDownload {
|
|||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
const {manId: id} = this;
|
const {manId: id} = this;
|
||||||
try {
|
try {
|
||||||
const state = await downloads.search({id});
|
const state = (await downloads.search({id})).pop() || {};
|
||||||
if (state[0].state === "in_progress") {
|
if (state.state === "in_progress" && !state.error && !state.paused) {
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
this.updateStateFromBrowser();
|
this.updateStateFromBrowser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!state[0].canResume) {
|
if (state.state === "complete") {
|
||||||
|
this.changeState(DONE);
|
||||||
|
this.updateStateFromBrowser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.canResume) {
|
||||||
throw new Error("Cannot resume");
|
throw new Error("Cannot resume");
|
||||||
}
|
}
|
||||||
// Cannot await here
|
// Cannot await here
|
||||||
// Firefox bug: will not return until download is finished
|
// Firefox bug: will not return until download is finished
|
||||||
downloads.resume(id).catch(() => {});
|
downloads.resume(id).catch(console.error);
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
console.error("cannot resume", ex);
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
this.removeFromBrowser();
|
this.removeFromBrowser();
|
||||||
}
|
}
|
||||||
@ -97,10 +112,23 @@ export class Download extends BaseDownload {
|
|||||||
if (this.state !== QUEUED) {
|
if (this.state !== QUEUED) {
|
||||||
throw new Error("invalid state");
|
throw new Error("invalid state");
|
||||||
}
|
}
|
||||||
console.trace("starting", this.toString(), this.toMsg());
|
console.log("starting", this.toString(), this.toMsg());
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
|
|
||||||
|
// Do NOT await
|
||||||
|
this.reallyStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reallyStart() {
|
||||||
try {
|
try {
|
||||||
const options: Options = {
|
if (!this.prerolled) {
|
||||||
|
await this.maybePreroll();
|
||||||
|
if (this.state !== RUNNING) {
|
||||||
|
// Aborted by preroll
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const options: DownloadOptions = {
|
||||||
conflictAction: await Prefs.get("conflict-action"),
|
conflictAction: await Prefs.get("conflict-action"),
|
||||||
filename: this.dest.full,
|
filename: this.dest.full,
|
||||||
saveAs: false,
|
saveAs: false,
|
||||||
@ -120,12 +148,16 @@ export class Download extends BaseDownload {
|
|||||||
value: this.referrer
|
value: this.referrer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (CHROME) {
|
||||||
|
options.headers.push({
|
||||||
|
name: "X-DTA-ID",
|
||||||
|
value: this.sessionId.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShelfEnabled(false);
|
|
||||||
try {
|
|
||||||
try {
|
try {
|
||||||
this.manager.addManId(
|
this.manager.addManId(
|
||||||
this.manId = await downloads.download(options), this);
|
this.manId = await downloads.download(options), this);
|
||||||
@ -139,10 +171,6 @@ export class Download extends BaseDownload {
|
|||||||
this.manager.addManId(
|
this.manager.addManId(
|
||||||
this.manId = await downloads.download(options), this);
|
this.manId = await downloads.download(options), this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally {
|
|
||||||
reenableShelf();
|
|
||||||
}
|
|
||||||
this.markDirty();
|
this.markDirty();
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
@ -152,6 +180,41 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async maybePreroll() {
|
||||||
|
try {
|
||||||
|
if (this.prerolled) {
|
||||||
|
// Check again, just in case, async and all
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roller = new Preroller(this);
|
||||||
|
if (!roller.shouldPreroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await roller.roll();
|
||||||
|
if (!res) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.mime) {
|
||||||
|
this.mime = res.mime;
|
||||||
|
}
|
||||||
|
if (res.name) {
|
||||||
|
this.serverName = res.name;
|
||||||
|
}
|
||||||
|
if (res.error) {
|
||||||
|
this.cancelAccordingToError(res.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (this.state === RUNNING) {
|
||||||
|
this.prerolled = true;
|
||||||
|
this.markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resume(forced = false) {
|
resume(forced = false) {
|
||||||
if (!(FORCABLE & this.state)) {
|
if (!(FORCABLE & this.state)) {
|
||||||
return;
|
return;
|
||||||
@ -164,26 +227,41 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause(retry?: boolean) {
|
||||||
if (!(PAUSABLE & this.state)) {
|
if (!(PAUSABLE & this.state)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!retry) {
|
||||||
|
this.retries = 0;
|
||||||
|
this.deadline = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
this.deadline = Date.now() + RETRY_TIME.value * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state === RUNNING && this.manId) {
|
if (this.state === RUNNING && this.manId) {
|
||||||
try {
|
try {
|
||||||
await downloads.pause(this.manId);
|
await downloads.pause(this.manId);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error("pause", ex.toString(), ex);
|
console.error("pause", ex.toString(), ex);
|
||||||
|
this.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.changeState(PAUSED);
|
|
||||||
|
this.changeState(retry ? RETRYING : PAUSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
this.prerolled = false;
|
||||||
this.manId = 0;
|
this.manId = 0;
|
||||||
this.written = this.totalSize = 0;
|
this.written = this.totalSize = 0;
|
||||||
this.serverName = "";
|
this.mime = this.serverName = this.browserName = "";
|
||||||
|
this.retries = 0;
|
||||||
|
this.deadline = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromBrowser() {
|
async removeFromBrowser() {
|
||||||
@ -216,6 +294,17 @@ export class Download extends BaseDownload {
|
|||||||
this.changeState(CANCELED);
|
this.changeState(CANCELED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelAccordingToError(error: string) {
|
||||||
|
if (!isRecoverable(error) || ++this.retries > RETRIES.value) {
|
||||||
|
this.cancel();
|
||||||
|
this.error = error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.pause(true);
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
setMissing() {
|
setMissing() {
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
@ -260,14 +349,19 @@ export class Download extends BaseDownload {
|
|||||||
const state = (await downloads.search({id: this.manId})).pop();
|
const state = (await downloads.search({id: this.manId})).pop();
|
||||||
const {filename, error} = state;
|
const {filename, error} = state;
|
||||||
const path = parsePath(filename);
|
const path = parsePath(filename);
|
||||||
this.serverName = path.name;
|
this.browserName = path.name;
|
||||||
this.adoptSize(state);
|
this.adoptSize(state);
|
||||||
|
if (!this.mime && state.mime) {
|
||||||
|
this.mime = state.mime;
|
||||||
|
}
|
||||||
this.markDirty();
|
this.markDirty();
|
||||||
switch (state.state) {
|
switch (state.state) {
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
if (error) {
|
if (state.paused) {
|
||||||
this.cancel();
|
this.changeState(PAUSED);
|
||||||
this.error = error;
|
}
|
||||||
|
else if (error) {
|
||||||
|
this.cancelAccordingToError(error);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
@ -278,6 +372,9 @@ export class Download extends BaseDownload {
|
|||||||
if (state.paused) {
|
if (state.paused) {
|
||||||
this.changeState(PAUSED);
|
this.changeState(PAUSED);
|
||||||
}
|
}
|
||||||
|
else if (error) {
|
||||||
|
this.cancelAccordingToError(error);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
this.error = error || "";
|
this.error = error || "";
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
import { EventEmitter } from "../events";
|
import { EventEmitter } from "../events";
|
||||||
import { Notification } from "../notifications";
|
import { Notification } from "../notifications";
|
||||||
import { DB } from "../db";
|
import { DB } from "../db";
|
||||||
import { QUEUED, CANCELED, RUNNING } from "./state";
|
import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Bus, Port } from "../bus";
|
import { Bus, Port } from "../bus";
|
||||||
import { sort } from "../sorting";
|
import { sort } from "../sorting";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs, PrefWatcher } from "../prefs";
|
||||||
import { _ } from "../i18n";
|
import { _ } from "../i18n";
|
||||||
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
@ -16,8 +16,9 @@ import { Download } from "./download";
|
|||||||
import { ManagerPort } from "./port";
|
import { ManagerPort } from "./port";
|
||||||
import { Scheduler } from "./scheduler";
|
import { Scheduler } from "./scheduler";
|
||||||
import { Limits } from "./limits";
|
import { Limits } from "./limits";
|
||||||
import { downloads, runtime } from "../browser";
|
import { downloads, runtime, webRequest, CHROME } from "../browser";
|
||||||
|
|
||||||
|
const US = runtime.getURL("");
|
||||||
|
|
||||||
const AUTOSAVE_TIMEOUT = 2000;
|
const AUTOSAVE_TIMEOUT = 2000;
|
||||||
const DIRTY_TIMEOUT = 100;
|
const DIRTY_TIMEOUT = 100;
|
||||||
@ -25,11 +26,17 @@ const DIRTY_TIMEOUT = 100;
|
|||||||
const MISSING_TIMEOUT = 12 * 1000;
|
const MISSING_TIMEOUT = 12 * 1000;
|
||||||
const RELOAD_TIMEOUT = 10 * 1000;
|
const RELOAD_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||||
|
// ignored
|
||||||
|
};
|
||||||
|
|
||||||
|
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
|
||||||
|
const SOUNDS = new PrefWatcher("sounds", false);
|
||||||
|
|
||||||
export class Manager extends EventEmitter {
|
export class Manager extends EventEmitter {
|
||||||
private items: Download[];
|
private items: Download[];
|
||||||
|
|
||||||
private active: boolean;
|
public active: boolean;
|
||||||
|
|
||||||
private notifiedFinished: boolean;
|
private notifiedFinished: boolean;
|
||||||
|
|
||||||
@ -45,10 +52,14 @@ export class Manager extends EventEmitter {
|
|||||||
|
|
||||||
private readonly running: Set<Download>;
|
private readonly running: Set<Download>;
|
||||||
|
|
||||||
|
private readonly retrying: Set<Download>;
|
||||||
|
|
||||||
private scheduler: Scheduler | null;
|
private scheduler: Scheduler | null;
|
||||||
|
|
||||||
private shouldReload: boolean;
|
private shouldReload: boolean;
|
||||||
|
|
||||||
|
private deadlineTimer: number;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.active = true;
|
this.active = true;
|
||||||
@ -59,11 +70,13 @@ export class Manager extends EventEmitter {
|
|||||||
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
||||||
this.dirty = new CoalescedUpdate(
|
this.dirty = new CoalescedUpdate(
|
||||||
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
||||||
|
this.processDeadlines = this.processDeadlines.bind(this);
|
||||||
this.sids = new Map();
|
this.sids = new Map();
|
||||||
this.manIds = new Map();
|
this.manIds = new Map();
|
||||||
this.ports = new Set();
|
this.ports = new Set();
|
||||||
this.scheduler = null;
|
this.scheduler = null;
|
||||||
this.running = new Set();
|
this.running = new Set();
|
||||||
|
this.retrying = new Set();
|
||||||
|
|
||||||
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
||||||
|
|
||||||
@ -80,6 +93,14 @@ export class Manager extends EventEmitter {
|
|||||||
Limits.on("changed", () => {
|
Limits.on("changed", () => {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (CHROME) {
|
||||||
|
webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
this.stuffReferrer.bind(this),
|
||||||
|
{urls: ["<all_urls>"]},
|
||||||
|
["blocking", "requestHeaders", "extraHeaders"]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -93,7 +114,10 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.items.push(rv);
|
this.items.push(rv);
|
||||||
});
|
});
|
||||||
await this.resetScheduler();
|
|
||||||
|
// Do not wait for the scheduler
|
||||||
|
this.resetScheduler();
|
||||||
|
|
||||||
this.emit("inited");
|
this.emit("inited");
|
||||||
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
|
||||||
runtime.onUpdateAvailable.addListener(() => {
|
runtime.onUpdateAvailable.addListener(() => {
|
||||||
@ -148,7 +172,7 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const next = await this.scheduler.next(this.running);
|
const next = await this.scheduler.next(this.running);
|
||||||
if (!next) {
|
if (!next) {
|
||||||
this.maybeNotifyFinished();
|
this.maybeRunFinishActions();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.running.has(next) || next.state !== QUEUED) {
|
if (this.running.has(next) || next.state !== QUEUED) {
|
||||||
@ -168,20 +192,18 @@ export class Manager extends EventEmitter {
|
|||||||
async startDownload(download: Download) {
|
async startDownload(download: Download) {
|
||||||
// Add to running first, so we don't confuse the scheduler and other parts
|
// Add to running first, so we don't confuse the scheduler and other parts
|
||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
|
setShelfEnabled(false);
|
||||||
await download.start();
|
await download.start();
|
||||||
this.notifiedFinished = false;
|
this.notifiedFinished = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async maybeNotifyFinished() {
|
maybeRunFinishActions() {
|
||||||
if (!(await Prefs.get("finish-notification"))) {
|
if (this.running.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.notifiedFinished || this.running.size) {
|
this.maybeNotifyFinished();
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.notifiedFinished = true;
|
|
||||||
new Notification(null, _("queue-finished"));
|
|
||||||
if (this.shouldReload) {
|
if (this.shouldReload) {
|
||||||
|
this.saveQueue.trigger();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.running.size) {
|
if (this.running.size) {
|
||||||
return;
|
return;
|
||||||
@ -189,6 +211,24 @@ export class Manager extends EventEmitter {
|
|||||||
runtime.reload();
|
runtime.reload();
|
||||||
}, RELOAD_TIMEOUT);
|
}, RELOAD_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
setShelfEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeNotifyFinished() {
|
||||||
|
if (this.notifiedFinished || this.running.size || this.retrying.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SOUNDS.value) {
|
||||||
|
const audio = new Audio(runtime.getURL("/style/done.opus"));
|
||||||
|
audio.addEventListener("canplaythrough", () => audio.play());
|
||||||
|
audio.addEventListener("ended", () => document.body.removeChild(audio));
|
||||||
|
audio.addEventListener("error", () => document.body.removeChild(audio));
|
||||||
|
document.body.appendChild(audio);
|
||||||
|
}
|
||||||
|
if (FINISH_NOTIFICATION.value) {
|
||||||
|
new Notification(null, _("queue-finished"));
|
||||||
|
}
|
||||||
|
this.notifiedFinished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
addManId(id: number, download: Download) {
|
addManId(id: number, download: Download) {
|
||||||
@ -236,7 +276,7 @@ export class Manager extends EventEmitter {
|
|||||||
this.emit("dirty", items);
|
this.emit("dirty", items);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(items: Download[]) {
|
private save(items: Download[]) {
|
||||||
DB.saveItems(items.filter(i => !i.removed)).
|
DB.saveItems(items.filter(i => !i.removed)).
|
||||||
catch(console.error);
|
catch(console.error);
|
||||||
}
|
}
|
||||||
@ -287,6 +327,10 @@ export class Manager extends EventEmitter {
|
|||||||
if (oldState === RUNNING) {
|
if (oldState === RUNNING) {
|
||||||
this.running.delete(download);
|
this.running.delete(download);
|
||||||
}
|
}
|
||||||
|
else if (oldState === RETRYING) {
|
||||||
|
this.retrying.delete(download);
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
if (newState === QUEUED) {
|
if (newState === QUEUED) {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
this.startNext().catch(console.error);
|
this.startNext().catch(console.error);
|
||||||
@ -298,10 +342,56 @@ export class Manager extends EventEmitter {
|
|||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (newState === RETRYING) {
|
||||||
|
this.addRetry(download);
|
||||||
|
}
|
||||||
this.startNext().catch(console.error);
|
this.startNext().catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRetry(download: Download) {
|
||||||
|
this.retrying.add(download);
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private findDeadline() {
|
||||||
|
let deadline = Array.from(this.retrying).
|
||||||
|
reduce<number>((deadline, item) => {
|
||||||
|
if (deadline) {
|
||||||
|
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
|
||||||
|
}
|
||||||
|
return item.deadline;
|
||||||
|
}, 0);
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deadline -= Date.now();
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.deadlineTimer) {
|
||||||
|
window.clearTimeout(this.deadlineTimer);
|
||||||
|
}
|
||||||
|
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processDeadlines() {
|
||||||
|
this.deadlineTimer = 0;
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.items.forEach(item => {
|
||||||
|
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
|
||||||
|
this.retrying.delete(item);
|
||||||
|
item.resume(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sorted(sids: number[]) {
|
sorted(sids: number[]) {
|
||||||
try {
|
try {
|
||||||
// Construct new items
|
// Construct new items
|
||||||
@ -361,6 +451,35 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit("active", this.active);
|
this.emit("active", this.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMsgItems() {
|
||||||
|
return this.items.map(e => e.toMsg());
|
||||||
|
}
|
||||||
|
|
||||||
|
stuffReferrer(details: any): any {
|
||||||
|
if (details.tabId > 0 && !US.startsWith(details.initiator)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const sidx = details.requestHeaders.findIndex(
|
||||||
|
(e: any) => e.name.toLowerCase() === "x-dta-id");
|
||||||
|
if (sidx < 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const sid = parseInt(details.requestHeaders[sidx].value, 10);
|
||||||
|
details.requestHeaders.splice(sidx, 1);
|
||||||
|
const item = this.sids.get(sid);
|
||||||
|
if (!item) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
details.requestHeaders.push({
|
||||||
|
name: "Referer",
|
||||||
|
value: (item.uReferrer || item.uURL).toString()
|
||||||
|
});
|
||||||
|
const rv: any = {
|
||||||
|
requestHeaders: details.requestHeaders
|
||||||
|
};
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inited: Promise<Manager>;
|
let inited: Promise<Manager>;
|
||||||
|
@ -5,6 +5,10 @@ import { donate, openPrefs } from "../windowutils";
|
|||||||
import { API } from "../api";
|
import { API } from "../api";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseDownload } from "./basedownload";
|
import { BaseDownload } from "./basedownload";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Manager } from "./man";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Port } from "../bus";
|
||||||
|
|
||||||
type SID = {sid: number};
|
type SID = {sid: number};
|
||||||
type SIDS = {
|
type SIDS = {
|
||||||
@ -13,9 +17,9 @@ type SIDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ManagerPort {
|
export class ManagerPort {
|
||||||
private manager: any;
|
private manager: Manager;
|
||||||
|
|
||||||
private port: any;
|
private port: Port;
|
||||||
|
|
||||||
constructor(manager: any, port: any) {
|
constructor(manager: any, port: any) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -79,7 +83,6 @@ export class ManagerPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendAll() {
|
sendAll() {
|
||||||
this.port.post(
|
this.port.post("all", this.manager.getMsgItems());
|
||||||
"all", this.manager.items.map((e: BaseDownload) => e.toMsg()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
234
lib/manager/preroller.ts
Normal file
234
lib/manager/preroller.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
import MimeType from "whatwg-mimetype";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Download } from "./download";
|
||||||
|
import { CHROME, webRequest } from "../browser";
|
||||||
|
import { CDHeaderParser } from "../cdheaderparser";
|
||||||
|
import { sanitizePath, parsePath } from "../util";
|
||||||
|
import { MimeDB } from "../mime";
|
||||||
|
|
||||||
|
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
|
||||||
|
const PREROLL_HOSTS = /4cdn|chan/;
|
||||||
|
const PREROLL_TIMEOUT = 10000;
|
||||||
|
const PREROLL_NOPE = new Set<string>();
|
||||||
|
|
||||||
|
/* eslint-disable no-magic-numbers */
|
||||||
|
const NOPE_STATUSES = Object.freeze(new Set([
|
||||||
|
400,
|
||||||
|
401,
|
||||||
|
402,
|
||||||
|
405,
|
||||||
|
416,
|
||||||
|
]));
|
||||||
|
/* eslint-enable no-magic-numbers */
|
||||||
|
|
||||||
|
const PREROLL_SEARCHEXTS = Object.freeze(new Set<string>([
|
||||||
|
"php",
|
||||||
|
"asp",
|
||||||
|
"aspx",
|
||||||
|
"inc",
|
||||||
|
"py",
|
||||||
|
"pl",
|
||||||
|
"action",
|
||||||
|
"htm",
|
||||||
|
"html",
|
||||||
|
"shtml"
|
||||||
|
]));
|
||||||
|
const NAME_TESTER = /\.[a-z0-9]{1,5}$/i;
|
||||||
|
const CDPARSER = new CDHeaderParser();
|
||||||
|
|
||||||
|
export interface PrerollResults {
|
||||||
|
error?: string;
|
||||||
|
name?: string;
|
||||||
|
mime?: string;
|
||||||
|
finalURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Preroller {
|
||||||
|
private readonly download: Download
|
||||||
|
|
||||||
|
constructor(download: Download) {
|
||||||
|
this.download = download;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldPreroll() {
|
||||||
|
const {uURL, renamer} = this.download;
|
||||||
|
const {pathname, search, host} = uURL;
|
||||||
|
if (PREROLL_NOPE.has(host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!renamer.p_ext) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (search.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (uURL.pathname.endsWith("/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (PREROLL_HEURISTICS.test(pathname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (PREROLL_HOSTS.test(host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async roll() {
|
||||||
|
try {
|
||||||
|
return await (CHROME ? this.prerollChrome() : this.prerollFirefox());
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prerollFirefox() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const {signal} = controller;
|
||||||
|
const {uURL, uReferrer} = this.download;
|
||||||
|
const res = await fetch(uURL.toString(), {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Range: "bytes=0-1",
|
||||||
|
}),
|
||||||
|
mode: "same-origin",
|
||||||
|
signal,
|
||||||
|
referrer: (uReferrer || uURL).toString(),
|
||||||
|
});
|
||||||
|
if (res.body) {
|
||||||
|
res.body.cancel();
|
||||||
|
}
|
||||||
|
controller.abort();
|
||||||
|
const {headers} = res;
|
||||||
|
return this.finalize(headers, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prerollChrome() {
|
||||||
|
let rid = "";
|
||||||
|
const {uURL, uReferrer} = this.download;
|
||||||
|
const rurl = uURL.toString();
|
||||||
|
let listener: any;
|
||||||
|
const wr = new Promise<any[]>(resolve => {
|
||||||
|
listener = (details: any) => {
|
||||||
|
const {url, requestId, statusCode} = details;
|
||||||
|
if (rid !== requestId && url !== rurl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
if (statusCode >= 300 && statusCode < 400) {
|
||||||
|
// Redirect, continue tracking;
|
||||||
|
rid = requestId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(details.responseHeaders);
|
||||||
|
};
|
||||||
|
webRequest.onHeadersReceived.addListener(
|
||||||
|
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
|
||||||
|
});
|
||||||
|
const p = Promise.race([
|
||||||
|
wr,
|
||||||
|
new Promise<any[]>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
|
||||||
|
]);
|
||||||
|
|
||||||
|
p.finally(() => {
|
||||||
|
webRequest.onHeadersReceived.removeListener(listener);
|
||||||
|
});
|
||||||
|
const controller = new AbortController();
|
||||||
|
const {signal} = controller;
|
||||||
|
const res = await fetch(rurl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
"Range": "bytes=0-1",
|
||||||
|
"X-DTA-ID": this.download.sessionId.toString(),
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
referrer: (uReferrer || uURL).toString(),
|
||||||
|
});
|
||||||
|
if (res.body) {
|
||||||
|
res.body.cancel();
|
||||||
|
}
|
||||||
|
controller.abort();
|
||||||
|
const headers = await p;
|
||||||
|
return this.finalize(
|
||||||
|
new Headers(headers.map(i => [i.name, i.value])), res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private finalize(headers: Headers, res: Response): PrerollResults {
|
||||||
|
const rv: PrerollResults = {};
|
||||||
|
|
||||||
|
const type = MimeType.parse(headers.get("content-type") || "");
|
||||||
|
if (type) {
|
||||||
|
rv.mime = type.essence;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {p_ext: ext} = this.download.renamer;
|
||||||
|
const dispHeader = headers.get("content-disposition");
|
||||||
|
if (dispHeader) {
|
||||||
|
const file = CDPARSER.parse(dispHeader);
|
||||||
|
// Sanitize
|
||||||
|
rv.name = sanitizePath(file.replace(/[/\\]+/g, "-"));
|
||||||
|
}
|
||||||
|
else if (!ext || PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) {
|
||||||
|
const {searchParams} = this.download.uURL;
|
||||||
|
let detected = "";
|
||||||
|
for (const [, value] of searchParams) {
|
||||||
|
if (!NAME_TESTER.test(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const p = parsePath(value);
|
||||||
|
if (!p.base || !p.ext) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!MimeDB.hasExtension(p.ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rv.mime) {
|
||||||
|
const mime = MimeDB.getMime(rv.mime);
|
||||||
|
if (mime && !mime.extensions.has(p.ext.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sanitized = sanitizePath(p.name);
|
||||||
|
if (sanitized.length <= detected.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
detected = sanitized;
|
||||||
|
}
|
||||||
|
if (detected) {
|
||||||
|
rv.name = detected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.finalURL = res.url;
|
||||||
|
|
||||||
|
/* eslint-disable no-magic-numbers */
|
||||||
|
const {status} = res;
|
||||||
|
if (status === 404) {
|
||||||
|
rv.error = "SERVER_BAD_CONTENT";
|
||||||
|
}
|
||||||
|
else if (status === 403) {
|
||||||
|
rv.error = "SERVER_FORBIDDEN";
|
||||||
|
}
|
||||||
|
else if (status === 402 || status === 407) {
|
||||||
|
rv.error = "SERVER_UNAUTHORIZED";
|
||||||
|
}
|
||||||
|
else if (NOPE_STATUSES.has(status)) {
|
||||||
|
PREROLL_NOPE.add(this.download.uURL.host);
|
||||||
|
if (PREROLL_NOPE.size > 1000) {
|
||||||
|
PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (status > 400 && status < 500) {
|
||||||
|
rv.error = "SERVER_FAILED";
|
||||||
|
}
|
||||||
|
/* eslint-enable no-magic-numbers */
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { parsePath, sanitizePath } from "../util";
|
|
||||||
import { _ } from "../i18n";
|
import { _ } from "../i18n";
|
||||||
|
import { MimeDB } from "../mime";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { parsePath, PathInfo, sanitizePath } from "../util";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseDownload } from "./basedownload";
|
||||||
|
|
||||||
const REPLACE_EXPR = /\*\w+\*/gi;
|
const REPLACE_EXPR = /\*\w+\*/gi;
|
||||||
|
|
||||||
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class Renamer {
|
export default class Renamer {
|
||||||
private readonly d: any;
|
private readonly d: BaseDownload;
|
||||||
|
|
||||||
constructor(download: any) {
|
private readonly nameinfo: PathInfo;
|
||||||
|
|
||||||
|
constructor(download: BaseDownload) {
|
||||||
this.d = download;
|
this.d = download;
|
||||||
|
const info = parsePath(this.d.finalName);
|
||||||
|
this.nameinfo = this.fixupExtension(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
get nameinfo() {
|
private fixupExtension(info: PathInfo): PathInfo {
|
||||||
return parsePath(this.d.finalName);
|
if (!this.d.mime) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const mime = MimeDB.getMime(this.d.mime);
|
||||||
|
if (!mime) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const {ext} = info;
|
||||||
|
if (mime.major === "image" || mime.major === "video") {
|
||||||
|
if (ext && mime.extensions.has(ext.toLowerCase())) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return new PathInfo(info.base, mime.primary, info.path);
|
||||||
|
}
|
||||||
|
if (ext) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return new PathInfo(info.base, mime.primary, info.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
get ref() {
|
get ref() {
|
||||||
return this.d.uReferrer;
|
return this.d.uReferrer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get p_name() {
|
get p_name() {
|
||||||
return this.nameinfo.base;
|
return this.nameinfo.base;
|
||||||
}
|
}
|
||||||
@ -169,24 +193,24 @@ export default class Renamer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
const {mask} = this.d;
|
const {mask, subfolder} = this.d;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self: any = this;
|
const self: any = this;
|
||||||
// XXX flat
|
const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
|
||||||
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) {
|
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
|
||||||
let prop = type.substr(1, type.length - 2);
|
let prop = type.substr(1, type.length - 2);
|
||||||
const flat = prop.startsWith("flat");
|
const flat = prop.startsWith("flat");
|
||||||
if (flat) {
|
if (flat) {
|
||||||
prop = prop.substr(4);
|
prop = prop.substr(4);
|
||||||
}
|
}
|
||||||
prop = `p_${prop}`;
|
prop = `p_${prop}`;
|
||||||
const rv = (prop in self) ?
|
let rv = (prop in self) ?
|
||||||
(self[prop] || "").trim() :
|
(self[prop] || "").trim() :
|
||||||
type;
|
type;
|
||||||
if (flat) {
|
if (flat) {
|
||||||
return rv.replace(/\/+/g, "-");
|
rv = rv.replace(/[/\\]+/g, "-");
|
||||||
}
|
}
|
||||||
return rv;
|
return rv.replace(/\/{2,}/g, "/");
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,9 @@ export const PAUSED = 1 << 3;
|
|||||||
export const DONE = 1 << 4;
|
export const DONE = 1 << 4;
|
||||||
export const CANCELED = 1 << 5;
|
export const CANCELED = 1 << 5;
|
||||||
export const MISSING = 1 << 6;
|
export const MISSING = 1 << 6;
|
||||||
|
export const RETRYING = 1 << 7;
|
||||||
|
|
||||||
export const RESUMABLE = PAUSED | CANCELED;
|
export const RESUMABLE = PAUSED | CANCELED | RETRYING;
|
||||||
export const FORCABLE = PAUSED | QUEUED | CANCELED;
|
export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
|
||||||
export const PAUSABLE = QUEUED | CANCELED | RUNNING;
|
export const PAUSABLE = QUEUED | CANCELED | RUNNING | RETRYING;
|
||||||
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING;
|
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING | RETRYING;
|
||||||
|
65
lib/mime.ts
Normal file
65
lib/mime.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
import mime from "../data/mime.json";
|
||||||
|
|
||||||
|
export class MimeInfo {
|
||||||
|
public readonly type: string;
|
||||||
|
|
||||||
|
public readonly extensions: Set<string>;
|
||||||
|
|
||||||
|
public readonly major: string;
|
||||||
|
|
||||||
|
public readonly minor: string;
|
||||||
|
|
||||||
|
public readonly primary: string;
|
||||||
|
|
||||||
|
constructor(type: string, extensions: string[]) {
|
||||||
|
this.type = type;
|
||||||
|
const [major, minor] = type.split("/", 2);
|
||||||
|
this.major = major;
|
||||||
|
this.minor = minor;
|
||||||
|
[this.primary] = extensions;
|
||||||
|
this.extensions = new Set(extensions);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MimeDB = new class MimeDB {
|
||||||
|
private readonly mimeToExts: Map<string, MimeInfo>;
|
||||||
|
|
||||||
|
private readonly registeredExtensions: Set<string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const exts = new Map<string, string[]>();
|
||||||
|
for (const [prim, more] of Object.entries(mime.e)) {
|
||||||
|
let toadd = more;
|
||||||
|
if (!Array.isArray(toadd)) {
|
||||||
|
toadd = [toadd];
|
||||||
|
}
|
||||||
|
toadd.unshift(prim);
|
||||||
|
exts.set(prim, toadd);
|
||||||
|
}
|
||||||
|
this.mimeToExts = new Map(Array.from(
|
||||||
|
Object.entries(mime.m),
|
||||||
|
([mime, prim]) => [mime, new MimeInfo(mime, exts.get(prim) || [prim])]
|
||||||
|
));
|
||||||
|
const all = Array.from(
|
||||||
|
this.mimeToExts.values(),
|
||||||
|
m => Array.from(m.extensions, e => e.toLowerCase()));
|
||||||
|
this.registeredExtensions = new Set(all.flat());
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrimary(mime: string) {
|
||||||
|
const info = this.mimeToExts.get(mime.trim().toLocaleLowerCase());
|
||||||
|
return info ? info.primary : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getMime(mime: string) {
|
||||||
|
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
hasExtension(ext: string) {
|
||||||
|
return this.registeredExtensions.has(ext.toLowerCase());
|
||||||
|
}
|
||||||
|
}();
|
@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as DEFAULT_PREFS from "../data/prefs.json";
|
import DEFAULT_PREFS from "../data/prefs.json";
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import { loadOverlay } from "./objectoverlay";
|
import { loadOverlay } from "./objectoverlay";
|
||||||
import { storage } from "./browser";
|
import { storage } from "./browser";
|
||||||
|
@ -116,3 +116,9 @@ export const FASTFILTER = new RecentList("fastfilter", [
|
|||||||
"*.z??, *.css, *.html"
|
"*.z??, *.css, *.html"
|
||||||
]);
|
]);
|
||||||
FASTFILTER.init().catch(console.error);
|
FASTFILTER.init().catch(console.error);
|
||||||
|
|
||||||
|
export const SUBFOLDER = new RecentList("subfolder", [
|
||||||
|
"",
|
||||||
|
"downthemall",
|
||||||
|
]);
|
||||||
|
SUBFOLDER.init().catch(console.error);
|
||||||
|
@ -28,7 +28,7 @@ function computeSelection(
|
|||||||
items: BaseMatchedItem[],
|
items: BaseMatchedItem[],
|
||||||
onlyFast: boolean): ItemDelta[] {
|
onlyFast: boolean): ItemDelta[] {
|
||||||
let ws = items.map((item, idx: number) => {
|
let ws = items.map((item, idx: number) => {
|
||||||
item.idx = idx;
|
item.idx = item.idx || idx;
|
||||||
const {matched = null} = item;
|
const {matched = null} = item;
|
||||||
item.prevMatched = matched;
|
item.prevMatched = matched;
|
||||||
item.matched = null;
|
item.matched = null;
|
||||||
@ -98,6 +98,7 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id, null);
|
||||||
try {
|
try {
|
||||||
const port = await Promise.race<Port>([
|
const port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
|
||||||
@ -186,8 +187,8 @@ export async function select(links: BaseItem[], media: BaseItem[]) {
|
|||||||
openPrefs();
|
openPrefs();
|
||||||
});
|
});
|
||||||
|
|
||||||
port.on("openUrls", ({urls}) => {
|
port.on("openUrls", ({urls, incognito}) => {
|
||||||
openUrls(urls);
|
openUrls(urls, incognito);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -21,6 +21,7 @@ export async function single(item: BaseItem | null) {
|
|||||||
type: "popup",
|
type: "popup",
|
||||||
});
|
});
|
||||||
const window = await windows.create(windowOptions);
|
const window = await windows.create(windowOptions);
|
||||||
|
tracker.track(window.id, null);
|
||||||
try {
|
try {
|
||||||
const port: Port = await Promise.race<Port>([
|
const port: Port = await Promise.race<Port>([
|
||||||
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
new Promise<Port>(resolve => Bus.oncePort("single", resolve)),
|
||||||
|
93
lib/util.ts
93
lib/util.ts
@ -2,7 +2,7 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as psl from "psl";
|
import * as psl from "psl";
|
||||||
import {memoize, identity} from "./memoize";
|
import { identity, memoize } from "./memoize";
|
||||||
export { debounce } from "../uikit/lib/util";
|
export { debounce } from "../uikit/lib/util";
|
||||||
|
|
||||||
export class Promised {
|
export class Promised {
|
||||||
@ -96,8 +96,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
|
|||||||
export const sanitizePath = identity(
|
export const sanitizePath = identity(
|
||||||
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
|
IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
|
||||||
|
|
||||||
|
export class PathInfo {
|
||||||
|
private baseField: string;
|
||||||
|
|
||||||
|
private extField: string;
|
||||||
|
|
||||||
|
private pathField: string;
|
||||||
|
|
||||||
|
private nameField: string;
|
||||||
|
|
||||||
|
private fullField: string;
|
||||||
|
|
||||||
|
constructor(base: string, ext: string, path: string) {
|
||||||
|
this.baseField = base;
|
||||||
|
this.extField = ext;
|
||||||
|
this.pathField = path;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get base() {
|
||||||
|
return this.baseField;
|
||||||
|
}
|
||||||
|
|
||||||
|
set base(nv) {
|
||||||
|
this.baseField = sanitizePath(nv);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get ext() {
|
||||||
|
return this.extField;
|
||||||
|
}
|
||||||
|
|
||||||
|
set ext(nv) {
|
||||||
|
this.extField = sanitizePath(nv);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.nameField;
|
||||||
|
}
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
return this.pathField;
|
||||||
|
}
|
||||||
|
|
||||||
|
set path(nv) {
|
||||||
|
this.pathField = sanitizePath(nv);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get full() {
|
||||||
|
return this.fullField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
|
||||||
|
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new PathInfo(this.baseField, this.extField, this.pathField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// XXX cleanup + test
|
// XXX cleanup + test
|
||||||
export const parsePath = memoize(function parsePath(path: string | URL) {
|
export const parsePath = memoize(function parsePath(
|
||||||
|
path: string | URL): PathInfo {
|
||||||
if (path instanceof URL) {
|
if (path instanceof URL) {
|
||||||
path = decodeURIComponent(path.pathname);
|
path = decodeURIComponent(path.pathname);
|
||||||
}
|
}
|
||||||
@ -127,13 +191,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path = pieces.join("/");
|
path = pieces.join("/");
|
||||||
return {
|
return new PathInfo(base, ext, path);
|
||||||
path,
|
|
||||||
name,
|
|
||||||
base,
|
|
||||||
ext,
|
|
||||||
full: path ? `${path}/${name}` : name
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export class CoalescedUpdate<T> extends Set<T> {
|
export class CoalescedUpdate<T> extends Set<T> {
|
||||||
@ -299,3 +357,20 @@ export function mapFilterInSitu<TRes, T>(
|
|||||||
export function randint(min: number, max: number) {
|
export function randint(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function validateSubFolder(folder: string) {
|
||||||
|
if (!folder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
folder = folder.replace(/[/\\]+/g, "/");
|
||||||
|
if (folder.startsWith("/")) {
|
||||||
|
throw new Error("error.noabsolutepath");
|
||||||
|
}
|
||||||
|
if (/^[a-z]:\//i.test(folder)) {
|
||||||
|
throw new Error("error.noabsolutepath");
|
||||||
|
}
|
||||||
|
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
|
||||||
|
throw new Error("error.nodotsinpath");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -55,14 +55,16 @@ export class WindowStateTracker {
|
|||||||
|
|
||||||
getOptions(options: any) {
|
getOptions(options: any) {
|
||||||
const result = Object.assign(options, {
|
const result = Object.assign(options, {
|
||||||
width: this.width,
|
|
||||||
height: this.height,
|
|
||||||
state: this.state,
|
state: this.state,
|
||||||
});
|
});
|
||||||
|
if (result.state !== "maximized") {
|
||||||
|
result.width = this.width;
|
||||||
|
result.height = this.height;
|
||||||
if (this.top >= 0) {
|
if (this.top >= 0) {
|
||||||
result.top = this.top;
|
result.top = this.top;
|
||||||
result.left = this.left;
|
result.left = this.left;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,42 +3,55 @@
|
|||||||
|
|
||||||
import { windows, tabs, runtime } from "../lib/browser";
|
import { windows, tabs, runtime } from "../lib/browser";
|
||||||
import { getManager } from "./manager/man";
|
import { getManager } from "./manager/man";
|
||||||
import * as DEFAULT_ICONS from "../data/icons.json";
|
import DEFAULT_ICONS from "../data/icons.json";
|
||||||
|
import { Prefs } from "./prefs";
|
||||||
|
import { _ } from "./i18n";
|
||||||
|
|
||||||
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
const DONATE_URL = "https://www.downthemall.org/howto/donate/";
|
||||||
|
const DONATE_LANG_URLS = Object.freeze(new Map([
|
||||||
|
["de", "https://www.downthemall.org/howto/donate/spenden/"],
|
||||||
|
]));
|
||||||
const MANAGER_URL = "/windows/manager.html";
|
const MANAGER_URL = "/windows/manager.html";
|
||||||
|
|
||||||
export async function mostRecentBrowser(): Promise<any> {
|
export async function mostRecentBrowser(incognito: boolean): Promise<any> {
|
||||||
let window;
|
let window;
|
||||||
try {
|
try {
|
||||||
window = await windows.getCurrent({windowTypes: ["normal"]});
|
window = await windows.getCurrent();
|
||||||
if (window.type !== "normal") {
|
if (window.type !== "normal") {
|
||||||
throw new Error("not a normal window");
|
throw new Error("not a normal window");
|
||||||
}
|
}
|
||||||
|
if (incognito && !window.incognito) {
|
||||||
|
throw new Error("Not incognito");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
try {
|
try {
|
||||||
window = await windows.getlastFocused({windowTypes: ["normal"]});
|
window = await windows.getlastFocused();
|
||||||
if (window.type !== "normal") {
|
if (window.type !== "normal") {
|
||||||
throw new Error("not a normal window");
|
throw new Error("not a normal window");
|
||||||
}
|
}
|
||||||
|
if (incognito && !window.incognito) {
|
||||||
|
throw new Error("Not incognito");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
|
||||||
filter((w: any) => w.type === "normal").pop();
|
filter(
|
||||||
|
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
|
||||||
|
pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!window) {
|
if (!window) {
|
||||||
window = await windows.create({
|
window = await windows.create({
|
||||||
url: DONATE_URL,
|
incognito: !!incognito,
|
||||||
type: "normal",
|
type: "normal",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openInTab(url: string) {
|
export async function openInTab(url: string, incognito: boolean) {
|
||||||
const window = await mostRecentBrowser();
|
const window = await mostRecentBrowser(incognito);
|
||||||
await tabs.create({
|
await tabs.create({
|
||||||
active: true,
|
active: true,
|
||||||
url,
|
url,
|
||||||
@ -47,7 +60,7 @@ export async function openInTab(url: string) {
|
|||||||
await windows.update(window.id, {focused: true});
|
await windows.update(window.id, {focused: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openInTabOrFocus(url: string) {
|
export async function openInTabOrFocus(url: string, incognito: boolean) {
|
||||||
const etabs = await tabs.query({
|
const etabs = await tabs.query({
|
||||||
url
|
url
|
||||||
});
|
});
|
||||||
@ -57,21 +70,22 @@ export async function openInTabOrFocus(url: string) {
|
|||||||
await windows.update(tab.windowId, {focused: true});
|
await windows.update(tab.windowId, {focused: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await openInTab(url);
|
await openInTab(url, incognito);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function maybeOpenInTab(url: string) {
|
export async function maybeOpenInTab(url: string, incognito: boolean) {
|
||||||
const etabs = await tabs.query({
|
const etabs = await tabs.query({
|
||||||
url
|
url
|
||||||
});
|
});
|
||||||
if (etabs.length) {
|
if (etabs.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await openInTab(url);
|
await openInTab(url, incognito);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function donate() {
|
export async function donate() {
|
||||||
await openInTab(DONATE_URL);
|
const url = DONATE_LANG_URLS.get(_("language_code")) || DONATE_URL;
|
||||||
|
await openInTab(url, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPrefs() {
|
export async function openPrefs() {
|
||||||
@ -85,16 +99,38 @@ export async function openManager(focus = true) {
|
|||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex.toString(), ex);
|
console.error(ex.toString(), ex);
|
||||||
}
|
}
|
||||||
|
const url = runtime.getURL(MANAGER_URL);
|
||||||
|
const openInPopup = await Prefs.get("manager-in-popup");
|
||||||
|
if (openInPopup) {
|
||||||
|
const etabs = await tabs.query({
|
||||||
|
url
|
||||||
|
});
|
||||||
|
if (etabs.length) {
|
||||||
|
if (!focus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tab = etabs.pop();
|
||||||
|
await tabs.update(tab.id, {active: true});
|
||||||
|
await windows.update(tab.windowId, {focused: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const windowOptions = {
|
||||||
|
url,
|
||||||
|
type: "popup",
|
||||||
|
};
|
||||||
|
await windows.create(windowOptions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (focus) {
|
if (focus) {
|
||||||
await openInTabOrFocus(await runtime.getURL(MANAGER_URL));
|
await openInTabOrFocus(runtime.getURL(MANAGER_URL), false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await maybeOpenInTab(await runtime.getURL(MANAGER_URL));
|
await maybeOpenInTab(runtime.getURL(MANAGER_URL), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openUrls(urls: string) {
|
export async function openUrls(urls: string, incognito: boolean) {
|
||||||
const window = await mostRecentBrowser();
|
const window = await mostRecentBrowser(incognito);
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
try {
|
try {
|
||||||
await tabs.create({
|
await tabs.create({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "DownThemAll!",
|
"name": "DownThemAll!",
|
||||||
"version": "4.0.8",
|
"version": "4.1.1",
|
||||||
|
|
||||||
"description": "__MSG_extensionDescription__",
|
"description": "__MSG_extensionDescription__",
|
||||||
"homepage_url": "https://downthemall.org/",
|
"homepage_url": "https://downthemall.org/",
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
|
|
||||||
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: 'self'; default-src 'self'",
|
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: http: https: 'self'; default-src 'self'",
|
||||||
|
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "style/icon16.png",
|
"16": "style/icon16.png",
|
||||||
@ -24,14 +24,18 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"menus",
|
|
||||||
"downloads",
|
"downloads",
|
||||||
"downloads.open",
|
"downloads.open",
|
||||||
"downloads.shelf",
|
"downloads.shelf",
|
||||||
|
"history",
|
||||||
|
"menus",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"sessions",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webNavigation"
|
"webNavigation",
|
||||||
|
"webRequest",
|
||||||
|
"webRequestBlocking"
|
||||||
],
|
],
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
|
@ -33,7 +33,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/psl": "^1.1.0",
|
"@types/psl": "^1.1.0",
|
||||||
|
"@types/whatwg-mimetype": "^2.1.0",
|
||||||
"psl": "^1.3.0",
|
"psl": "^1.3.0",
|
||||||
"webextension-polyfill": "^0.4.0"
|
"webextension-polyfill": "^0.4.0",
|
||||||
|
"whatwg-mimetype": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@ function urlToUsable(e: any, u: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Gatherer {
|
class Gatherer {
|
||||||
|
private: boolean;
|
||||||
|
|
||||||
textLinks: boolean;
|
textLinks: boolean;
|
||||||
|
|
||||||
selectionOnly: boolean;
|
selectionOnly: boolean;
|
||||||
@ -88,6 +90,7 @@ class Gatherer {
|
|||||||
transferable: string[];
|
transferable: string[];
|
||||||
|
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
|
this.private = !!options.private;
|
||||||
this.textLinks = options.textLinks;
|
this.textLinks = options.textLinks;
|
||||||
this.selectionOnly = options.selectionOnly;
|
this.selectionOnly = options.selectionOnly;
|
||||||
this.selection = options.selectionOnly ? getSelection() : null;
|
this.selection = options.selectionOnly ? getSelection() : null;
|
||||||
@ -255,6 +258,7 @@ class Gatherer {
|
|||||||
return {
|
return {
|
||||||
url: url.href,
|
url: url.href,
|
||||||
title,
|
title,
|
||||||
|
private: this.private
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
@ -295,7 +299,7 @@ class Gatherer {
|
|||||||
function gather(msg: any, sender: any, callback: Function) {
|
function gather(msg: any, sender: any, callback: Function) {
|
||||||
try {
|
try {
|
||||||
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
if (!msg || msg.type !== "DTA:gather" || !callback) {
|
||||||
return;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
const gatherer = new Gatherer(msg);
|
const gatherer = new Gatherer(msg);
|
||||||
const result = {
|
const result = {
|
||||||
@ -313,10 +317,11 @@ function gather(msg: any, sender: any, callback: Function) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
urlToUsable(result, result.baseURL);
|
urlToUsable(result, result.baseURL);
|
||||||
callback(result);
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex.toString(), ex.stack, ex);
|
console.error(ex.toString(), ex.stack, ex);
|
||||||
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
sounds/done.wav
Normal file
BIN
sounds/done.wav
Normal file
Binary file not shown.
BIN
sounds/error.wav
Normal file
BIN
sounds/error.wav
Normal file
Binary file not shown.
@ -10,6 +10,7 @@
|
|||||||
--add-color: navy;
|
--add-color: navy;
|
||||||
--queue-color: gray;
|
--queue-color: gray;
|
||||||
--pause-color: #ffa318;
|
--pause-color: #ffa318;
|
||||||
|
--retry-color: rgb(0, 112, 204);
|
||||||
--error-color: rgb(160, 13, 42);
|
--error-color: rgb(160, 13, 42);
|
||||||
--running-color: #aae061;
|
--running-color: #aae061;
|
||||||
--finishing-color: #57cc12;
|
--finishing-color: #57cc12;
|
||||||
@ -18,12 +19,17 @@
|
|||||||
--folder-color: rgb(214, 165, 4);
|
--folder-color: rgb(214, 165, 4);
|
||||||
--maskbutton-color: rgb(236, 185, 16);
|
--maskbutton-color: rgb(236, 185, 16);
|
||||||
--missing-color: rgb(0, 82, 204);
|
--missing-color: rgb(0, 82, 204);
|
||||||
|
--open-color: rgba(236, 185, 16, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-platform="mac"] {
|
html[data-platform="mac"] {
|
||||||
--folder-color: rgb(4, 102, 214);
|
--folder-color: rgb(4, 102, 214);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-size: 10pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'downthemall';
|
font-family: 'downthemall';
|
||||||
src: url('downthemall.woff2?75791791') format('woff2');
|
src: url('downthemall.woff2?75791791') format('woff2');
|
||||||
@ -88,6 +94,7 @@ html[data-platform="mac"] {
|
|||||||
.icon-file-video:before { content: '\e825'; } /* '' */
|
.icon-file-video:before { content: '\e825'; } /* '' */
|
||||||
.icon-file-generic:before { content: '\e826'; } /* '' */
|
.icon-file-generic:before { content: '\e826'; } /* '' */
|
||||||
.icon-question-dark:before { content: '\e827'; } /* '' */
|
.icon-question-dark:before { content: '\e827'; } /* '' */
|
||||||
|
.icon-forward:before { content: '\e828'; } /* '' */
|
||||||
.icon-filter:before { content: '\f0b0'; } /* '' */
|
.icon-filter:before { content: '\f0b0'; } /* '' */
|
||||||
.icon-donate:before { content: '\f0d6'; } /* '' */
|
.icon-donate:before { content: '\f0d6'; } /* '' */
|
||||||
.icon-file-doc:before { content: '\f0f6'; } /* '' */
|
.icon-file-doc:before { content: '\f0f6'; } /* '' */
|
||||||
@ -100,7 +107,8 @@ html[data-platform="mac"] {
|
|||||||
.icon-file-image:before { content: '\f1c5'; } /* '' */
|
.icon-file-image:before { content: '\f1c5'; } /* '' */
|
||||||
.icon-file-archive:before { content: '\f1c6'; } /* '' */
|
.icon-file-archive:before { content: '\f1c6'; } /* '' */
|
||||||
.icon-file-audio:before { content: '\f1c7'; } /* '' */
|
.icon-file-audio:before { content: '\f1c7'; } /* '' */
|
||||||
.icon-toggle:before { content: '\f205'; } /* '' */
|
.icon-toggle-off:before { content: '\f204'; } /* '' */
|
||||||
|
.icon-toggle-on:before { content: '\f205'; } /* '' */
|
||||||
.icon-server:before { content: '\f233'; } /* '' */
|
.icon-server:before { content: '\f233'; } /* '' */
|
||||||
.icon-question-light:before { content: '\f29c'; } /* '' */
|
.icon-question-light:before { content: '\f29c'; } /* '' */
|
||||||
|
|
||||||
|
BIN
style/done.opus
Normal file
BIN
style/done.opus
Normal file
Binary file not shown.
BIN
style/downthemall.woff2
Normal file → Executable file
BIN
style/downthemall.woff2
Normal file → Executable file
Binary file not shown.
BIN
style/error.opus
Normal file
BIN
style/error.opus
Normal file
Binary file not shown.
@ -108,11 +108,11 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#colURL {
|
#colURL {
|
||||||
width: 38%;
|
width: 42%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colPercent {
|
#colPercent {
|
||||||
width: 3em;
|
width: 4em;
|
||||||
min-width: 3em;
|
min-width: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,11 +121,11 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#colSize {
|
#colSize {
|
||||||
width: 15em;
|
width: 14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colSpeed {
|
#colSpeed {
|
||||||
width: 6em;
|
width: 7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#colDomain,
|
#colDomain,
|
||||||
@ -154,6 +154,14 @@ body > * {
|
|||||||
height: 26px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtualtable-row.opening {
|
||||||
|
background: var(--open-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtualtable-progress-container {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.virtualtable-progress-bar {
|
.virtualtable-progress-bar {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
@ -194,6 +202,23 @@ body > * {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-icon {
|
||||||
|
color: var(--retry-color);
|
||||||
|
}
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-progress-bar {
|
||||||
|
background: var(--retry-color);
|
||||||
|
}
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-progress-undetermined {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--retry-color),
|
||||||
|
var(--retry-color) 6px,
|
||||||
|
transparent 6px,
|
||||||
|
transparent 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.missing .virtualtable-column-2 .virtualtable-icon,
|
.missing .virtualtable-column-2 .virtualtable-icon,
|
||||||
.canceled .virtualtable-column-2 .virtualtable-icon {
|
.canceled .virtualtable-column-2 .virtualtable-icon {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
@ -262,6 +287,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virtualtable-column-6,
|
.virtualtable-column-6,
|
||||||
|
.virtualtable-column-4,
|
||||||
.virtualtable-column-3 {
|
.virtualtable-column-3 {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -430,6 +456,8 @@ body > * {
|
|||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 2px 2px 6px black;
|
box-shadow: 2px 2px 6px black;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tooltip-infos {
|
#tooltip-infos {
|
||||||
@ -501,3 +529,23 @@ body > * {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--done-color);
|
background: var(--done-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tooltip-eta.single {
|
||||||
|
font-weight: bold;
|
||||||
|
grid-column-end: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deletefiles-list {
|
||||||
|
padding-left: 1ex;
|
||||||
|
padding-right: 1.5ex;
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(128,128,128,0.1);
|
||||||
|
max-height: 8em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.deletefiles-list > li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
@ -139,3 +139,10 @@ legend {
|
|||||||
background: rgba(128, 128, 128, 0.05);
|
background: rgba(128, 128, 128, 0.05);
|
||||||
box-shadow: 1px 1px 6px lightgray;
|
box-shadow: 1px 1px 6px lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#network-general {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 1ex;
|
||||||
|
}
|
@ -232,3 +232,7 @@ body > * {
|
|||||||
#maskButton {
|
#maskButton {
|
||||||
justify-self: flex-start;
|
justify-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btnDownload {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@ -63,6 +63,7 @@ p.example {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#options > #subfolderOptions,
|
||||||
#options > #maskOptions {
|
#options > #maskOptions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr auto auto;
|
grid-template-columns: 2fr auto auto;
|
||||||
@ -81,3 +82,7 @@ h3 {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btnDownload {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
289
tests/test_cdheaderparser.js
Normal file
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
30
tests/test_mime.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: CC0 1.0
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const {MimeDB} = require("../lib/mime");
|
||||||
|
|
||||||
|
describe("MIME", function() {
|
||||||
|
it("general", function() {
|
||||||
|
expect(MimeDB.getMime("image/jpeg").major).to.equal("image");
|
||||||
|
expect(MimeDB.getMime("image/jpeg").minor).to.equal("jpeg");
|
||||||
|
expect(MimeDB.getMime("iMage/jPeg").major).to.equal("image");
|
||||||
|
expect(MimeDB.getMime("imAge/jpEg").minor).to.equal("jpeg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exts", function() {
|
||||||
|
expect(MimeDB.getMime("image/jpeg").primary).to.equal("jpg");
|
||||||
|
expect(MimeDB.getMime("image/jpeg").primary).to.equal(
|
||||||
|
MimeDB.getPrimary("image/jpeg"));
|
||||||
|
expect(MimeDB.getMime("iMage/jPeg").primary).to.equal("jpg");
|
||||||
|
expect(MimeDB.getMime("imAge/jpEg").primary).to.equal(
|
||||||
|
MimeDB.getPrimary("image/jpeg"));
|
||||||
|
expect(Array.from(MimeDB.getMime("imAge/jpEg").extensions)).to.deep.equal(
|
||||||
|
["jpg", "jpeg", "jpe", "jfif"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("application/octet-stream should not yield results", function() {
|
||||||
|
expect(MimeDB.getPrimary("application/octet-stream")).to.equal("");
|
||||||
|
expect(MimeDB.getMime("application/octet-Stream")).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
@ -19,7 +19,7 @@ const OPTS = {
|
|||||||
state: DownloadState.QUEUED,
|
state: DownloadState.QUEUED,
|
||||||
batch: 42,
|
batch: 42,
|
||||||
idx: 23,
|
idx: 23,
|
||||||
mask: "*name*.*ext",
|
mask: "*name*.*ext*",
|
||||||
description: "desc / ript.ion .",
|
description: "desc / ript.ion .",
|
||||||
title: " *** TITLE *** ",
|
title: " *** TITLE *** ",
|
||||||
};
|
};
|
||||||
@ -57,6 +57,49 @@ describe("Renamer", function() {
|
|||||||
expect(dest.path).to.equal("");
|
expect(dest.path).to.equal("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("*name*.*ext* (mime override)", function() {
|
||||||
|
const {dest} = new BaseDownload(
|
||||||
|
Object.assign({}, OPTS, {
|
||||||
|
mask: "*name* *batch*.*ext*",
|
||||||
|
mime: "image/jpeg"
|
||||||
|
}));
|
||||||
|
expect(dest.full).to.equal("filenäme 042.jpg");
|
||||||
|
expect(dest.name).to.equal("filenäme 042.jpg");
|
||||||
|
expect(dest.base).to.equal("filenäme 042");
|
||||||
|
expect(dest.ext).to.equal("jpg");
|
||||||
|
expect(dest.path).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("*name*.*ext* (mime no override)", function() {
|
||||||
|
const {dest} = new BaseDownload(
|
||||||
|
Object.assign({}, OPTS, {
|
||||||
|
mask: "*name* *batch*.*ext*",
|
||||||
|
mime: "image/jpeg",
|
||||||
|
url: "https://www.example.co.uk/filen%C3%A4me.JPe",
|
||||||
|
usable: "https://www.example.co.uk/filenäme.JPe",
|
||||||
|
}));
|
||||||
|
expect(dest.full).to.equal("filenäme 042.JPe");
|
||||||
|
expect(dest.name).to.equal("filenäme 042.JPe");
|
||||||
|
expect(dest.base).to.equal("filenäme 042");
|
||||||
|
expect(dest.ext).to.equal("JPe");
|
||||||
|
expect(dest.path).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("*name*.*ext* (mime override; missing ext)", function() {
|
||||||
|
const {dest} = new BaseDownload(
|
||||||
|
Object.assign({}, OPTS, {
|
||||||
|
mask: "*name* *batch*.*ext*",
|
||||||
|
mime: "application/json",
|
||||||
|
url: "https://www.example.co.uk/filen%C3%A4me",
|
||||||
|
usable: "https://www.example.co.uk/filenäme",
|
||||||
|
}));
|
||||||
|
expect(dest.full).to.equal("filenäme 042.json");
|
||||||
|
expect(dest.name).to.equal("filenäme 042.json");
|
||||||
|
expect(dest.base).to.equal("filenäme 042");
|
||||||
|
expect(dest.ext).to.equal("json");
|
||||||
|
expect(dest.path).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
it("*text*", function() {
|
it("*text*", function() {
|
||||||
const dest = makeOne("*text*");
|
const dest = makeOne("*text*");
|
||||||
expect(dest.full).to.equal("desc/ript.ion");
|
expect(dest.full).to.equal("desc/ript.ion");
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"importHelpers": true,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
util/additional.types
Normal file
23
util/additional.types
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
types {
|
||||||
|
application/x-x509-ca-cert pem crt der;
|
||||||
|
application/javascript js jsx;
|
||||||
|
audio/x-matroska mka;
|
||||||
|
image/bmp bmp;
|
||||||
|
image/heic heic heif;
|
||||||
|
image/heic heic heif;
|
||||||
|
image/heif-sequence heic heif;
|
||||||
|
image/heif-sequence heic heif;
|
||||||
|
image/jpeg jpg jpeg jpe jfif;
|
||||||
|
image/webp webp;
|
||||||
|
text/html html htm shtml php;
|
||||||
|
text/javascript js jsx;
|
||||||
|
video/mpeg mpg mpe mpeg mpg;
|
||||||
|
video/opus opus;
|
||||||
|
video/x-matroska mkv mk3d mks;
|
||||||
|
video/quicktime mov qt moov;
|
||||||
|
application/x-compressed gz;
|
||||||
|
application/x-gzip gz gzip;
|
||||||
|
application/x-bzip2 bz2;
|
||||||
|
application/x-tar tar;
|
||||||
|
application/x-xz xz;
|
||||||
|
}
|
@ -70,7 +70,7 @@ def main():
|
|||||||
if modified:
|
if modified:
|
||||||
try:
|
try:
|
||||||
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
|
with open("messages.json.tmp", "w", encoding="utf-8") as outp:
|
||||||
json.dump(data, outp, sort_keys=True, indent=2)
|
json.dump(data, outp, sort_keys=True, indent=2, ensure_ascii=False)
|
||||||
os.rename("messages.json.tmp", "_locales/en/messages.json")
|
os.rename("messages.json.tmp", "_locales/en/messages.json")
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
@ -26,8 +26,8 @@ UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
|
|||||||
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
LICENSED = set((".css", ".html", ".js", "*.ts"))
|
||||||
IGNORED = set((".DS_Store", "Thumbs.db"))
|
IGNORED = set((".DS_Store", "Thumbs.db"))
|
||||||
|
|
||||||
PERM_IGNORED_FX = set(("downloads.shelf",))
|
PERM_IGNORED_FX = set(("downloads.shelf", "webRequest", "webRequestBlocking"))
|
||||||
PERM_IGNORED_CHROME = set(("menus",))
|
PERM_IGNORED_CHROME = set(("menus", "sessions"))
|
||||||
|
|
||||||
SCRIPTS = [
|
SCRIPTS = [
|
||||||
"yarn build:regexps",
|
"yarn build:regexps",
|
||||||
|
98
util/mime.types
Normal file
98
util/mime.types
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
https://github.com/nginx/nginx/raw/master/conf/mime.types
|
||||||
|
|
||||||
|
types {
|
||||||
|
text/html html htm shtml;
|
||||||
|
text/css css;
|
||||||
|
text/xml xml;
|
||||||
|
image/gif gif;
|
||||||
|
image/jpeg jpeg jpg;
|
||||||
|
application/javascript js;
|
||||||
|
application/atom+xml atom;
|
||||||
|
application/rss+xml rss;
|
||||||
|
|
||||||
|
text/mathml mml;
|
||||||
|
text/plain txt;
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad;
|
||||||
|
text/vnd.wap.wml wml;
|
||||||
|
text/x-component htc;
|
||||||
|
|
||||||
|
image/png png;
|
||||||
|
image/svg+xml svg svgz;
|
||||||
|
image/tiff tif tiff;
|
||||||
|
image/vnd.wap.wbmp wbmp;
|
||||||
|
image/webp webp;
|
||||||
|
image/x-icon ico;
|
||||||
|
image/x-jng jng;
|
||||||
|
image/x-ms-bmp bmp;
|
||||||
|
|
||||||
|
font/woff woff;
|
||||||
|
font/woff2 woff2;
|
||||||
|
|
||||||
|
application/java-archive jar war ear;
|
||||||
|
application/json json;
|
||||||
|
application/mac-binhex40 hqx;
|
||||||
|
application/msword doc;
|
||||||
|
application/pdf pdf;
|
||||||
|
application/postscript ps eps ai;
|
||||||
|
application/rtf rtf;
|
||||||
|
application/vnd.apple.mpegurl m3u8;
|
||||||
|
application/vnd.google-earth.kml+xml kml;
|
||||||
|
application/vnd.google-earth.kmz kmz;
|
||||||
|
application/vnd.ms-excel xls;
|
||||||
|
application/vnd.ms-fontobject eot;
|
||||||
|
application/vnd.ms-powerpoint ppt;
|
||||||
|
application/vnd.oasis.opendocument.graphics odg;
|
||||||
|
application/vnd.oasis.opendocument.presentation odp;
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||||
|
application/vnd.oasis.opendocument.text odt;
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
pptx;
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
xlsx;
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
docx;
|
||||||
|
application/vnd.wap.wmlc wmlc;
|
||||||
|
application/x-7z-compressed 7z;
|
||||||
|
application/x-cocoa cco;
|
||||||
|
application/x-java-archive-diff jardiff;
|
||||||
|
application/x-java-jnlp-file jnlp;
|
||||||
|
application/x-makeself run;
|
||||||
|
application/x-perl pl pm;
|
||||||
|
application/x-pilot prc pdb;
|
||||||
|
application/x-rar-compressed rar;
|
||||||
|
application/x-redhat-package-manager rpm;
|
||||||
|
application/x-sea sea;
|
||||||
|
application/x-shockwave-flash swf;
|
||||||
|
application/x-stuffit sit;
|
||||||
|
application/x-tcl tcl tk;
|
||||||
|
application/x-x509-ca-cert der pem crt;
|
||||||
|
application/x-xpinstall xpi;
|
||||||
|
application/xhtml+xml xhtml;
|
||||||
|
application/xspf+xml xspf;
|
||||||
|
application/zip zip;
|
||||||
|
|
||||||
|
application/octet-stream bin exe dll;
|
||||||
|
application/octet-stream deb;
|
||||||
|
application/octet-stream dmg;
|
||||||
|
application/octet-stream iso img;
|
||||||
|
application/octet-stream msi msp msm;
|
||||||
|
|
||||||
|
audio/midi mid midi kar;
|
||||||
|
audio/mpeg mp3;
|
||||||
|
audio/ogg ogg;
|
||||||
|
audio/x-m4a m4a;
|
||||||
|
audio/x-realaudio ra;
|
||||||
|
|
||||||
|
video/3gpp 3gpp 3gp;
|
||||||
|
video/mp2t ts;
|
||||||
|
video/mp4 mp4;
|
||||||
|
video/mpeg mpeg mpg;
|
||||||
|
video/quicktime mov;
|
||||||
|
video/webm webm;
|
||||||
|
video/x-flv flv;
|
||||||
|
video/x-m4v m4v;
|
||||||
|
video/x-mng mng;
|
||||||
|
video/x-ms-asf asx asf;
|
||||||
|
video/x-ms-wmv wmv;
|
||||||
|
video/x-msvideo avi;
|
||||||
|
}
|
76
util/seed_mime.py
Executable file
76
util/seed_mime.py
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
def unique(seq):
|
||||||
|
return list(OrderedDict([i, None] for i in seq if i))
|
||||||
|
|
||||||
|
def generate(major, minor, exts):
|
||||||
|
exts = exts[:]
|
||||||
|
yield f"{major}/{minor}", exts
|
||||||
|
if (minor.startswith("x-")):
|
||||||
|
yield f"{major}/{minor[2:]}", exts
|
||||||
|
else:
|
||||||
|
yield f"{major}/x-{minor}", exts
|
||||||
|
for ext in exts:
|
||||||
|
yield f"{major}/{ext}", exts
|
||||||
|
yield f"{major}/x-{ext}", exts
|
||||||
|
|
||||||
|
|
||||||
|
def make(text, final):
|
||||||
|
lines = "".join([
|
||||||
|
line.strip()
|
||||||
|
for line in re.search(r"\{(.*)\}", text, re.S).group(1).split("\n")
|
||||||
|
if line.strip() and not line.strip().startswith("#")
|
||||||
|
]).split(";")
|
||||||
|
|
||||||
|
additional = []
|
||||||
|
for line in lines:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
m = re.match(r"([a-z1-9]+)/([^\s]+)\s+(.+?)$", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
[major, minor, exts] = m.groups()
|
||||||
|
exts = unique(e.lower().strip() for e in exts.split(" ") if e.strip())
|
||||||
|
mime = f"{major}/{minor}"
|
||||||
|
if mime == "application/octet-stream":
|
||||||
|
continue
|
||||||
|
if mime in final:
|
||||||
|
final[mime] += exts
|
||||||
|
continue
|
||||||
|
final[mime] = exts
|
||||||
|
additional += (major, minor, exts),
|
||||||
|
|
||||||
|
for [major, minor, exts] in additional:
|
||||||
|
for [mime, exts] in generate(major, minor, exts):
|
||||||
|
if mime in final:
|
||||||
|
continue
|
||||||
|
final[mime] = exts
|
||||||
|
|
||||||
|
final = OrderedDict()
|
||||||
|
for file in sys.argv[1:]:
|
||||||
|
with open(file, "r") as fp:
|
||||||
|
make(fp.read(), final)
|
||||||
|
|
||||||
|
multi = dict()
|
||||||
|
for [mime, exts] in list(final.items()):
|
||||||
|
exts = unique(exts)
|
||||||
|
prim = exts[0]
|
||||||
|
final[mime] = prim
|
||||||
|
if len(exts) == 1:
|
||||||
|
continue
|
||||||
|
exts = exts[1:]
|
||||||
|
if len(exts) == 1:
|
||||||
|
multi[prim] = exts[0]
|
||||||
|
else:
|
||||||
|
multi[prim] = exts
|
||||||
|
|
||||||
|
final = OrderedDict(sorted(final.items()))
|
||||||
|
multi = OrderedDict(sorted(multi.items()))
|
||||||
|
|
||||||
|
print(json.dumps(dict(e=multi, m=final), indent=2))
|
||||||
|
print("generated", len(final), "mimes", "with", len(multi), "multis", file=sys.stderr)
|
@ -22,6 +22,9 @@ export class Dropdown extends EventEmitter {
|
|||||||
|
|
||||||
this.container = document.createElement("div");
|
this.container = document.createElement("div");
|
||||||
this.container.classList.add("dropdown");
|
this.container.classList.add("dropdown");
|
||||||
|
if (input.id) {
|
||||||
|
this.container.id = `${input.id}-dropdown`;
|
||||||
|
}
|
||||||
|
|
||||||
input = input.parentElement.replaceChild(this.container, input);
|
input = input.parentElement.replaceChild(this.container, input);
|
||||||
this.input = input as HTMLInputElement;
|
this.input = input as HTMLInputElement;
|
||||||
|
@ -75,6 +75,7 @@
|
|||||||
<ul id="table-context" class="table-context">
|
<ul id="table-context" class="table-context">
|
||||||
<li id="ctx-open-file" data-key="Enter" data-i18n="open-file" data-icon="icon-file">Open File</li>
|
<li id="ctx-open-file" data-key="Enter" data-i18n="open-file" data-icon="icon-file">Open File</li>
|
||||||
<li id="ctx-open-directory" data-key="ACCEL-Enter" data-i18n="open-directory" data-icon="icon-folder">Open Directory</li>
|
<li id="ctx-open-directory" data-key="ACCEL-Enter" data-i18n="open-directory" data-icon="icon-folder">Open Directory</li>
|
||||||
|
<li id="ctx-delete-files" data-key="ACCEL-Delete" data-i18n="deletefiles" data-icon="icon-delete"></li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
<li id="ctx-resume" data-key="ACCEL-KeyR" data-i18n="resume-download" data-icon="icon-go">Resume</li>
|
<li id="ctx-resume" data-key="ACCEL-KeyR" data-i18n="resume-download" data-icon="icon-go">Resume</li>
|
||||||
<li id="ctx-pause" data-key="ACCEL-KeyP" data-i18n="pause-download" data-icon="icon-pause">Pause</li>
|
<li id="ctx-pause" data-key="ACCEL-KeyP" data-i18n="pause-download" data-icon="icon-pause">Pause</li>
|
||||||
@ -109,6 +110,15 @@
|
|||||||
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
||||||
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
|
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
|
<li id="ctx-import" data-icon="icon-import" data-i18n="import"></li>
|
||||||
|
<li id="ctx-export" data-icon="icon-download" data-i18n="export">
|
||||||
|
<ul class="table-context">
|
||||||
|
<li id="ctx-export-text" data-i18n="export-text"></li>
|
||||||
|
<li id="ctx-export-aria2" data-i18n="export-aria2"></li>
|
||||||
|
<li id="ctx-export-metalink" data-i18n="export-metalink"></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>-</li>
|
||||||
<li id="ctx-move-top" data-key="ALT-Home" data-i18n="move-top" data-icon="icon-top">Top</li>
|
<li id="ctx-move-top" data-key="ALT-Home" data-i18n="move-top" data-icon="icon-top">Top</li>
|
||||||
<li id="ctx-move-up" data-key="ALT-PageUp" data-i18n="move-up" data-icon="icon-up">Up</li>
|
<li id="ctx-move-up" data-key="ALT-PageUp" data-i18n="move-up" data-icon="icon-up">Up</li>
|
||||||
<li id="ctx-move-down" data-key="ALT-PageDown" data-i18n="move-down" data-icon="icon-down">down</li>
|
<li id="ctx-move-down" data-key="ALT-PageDown" data-i18n="move-down" data-icon="icon-down">down</li>
|
||||||
@ -125,6 +135,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="deletefiles-template">
|
||||||
|
<h1 class="deletefiles-title" data-i18n="deletefiles_title"></h1>
|
||||||
|
<p class="deletefiles-text" data-i18n="deletefiles_text"></p>
|
||||||
|
<ul class="deletefiles-list"></ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template id="menufilter-template">
|
<template id="menufilter-template">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="ctx-menufilter-seperator">-</li>
|
<li id="ctx-menufilter-seperator">-</li>
|
||||||
|
@ -94,8 +94,10 @@ export class TextFilter extends ItemFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allow(item: DownloadItem) {
|
allow(item: DownloadItem) {
|
||||||
return this.expr.test(
|
const {expr} = this;
|
||||||
[item.usable, item.description, item.finalName].join(" "));
|
return expr.test(item.currentName) ||
|
||||||
|
expr.test(item.usable) ||
|
||||||
|
expr.test(item.description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { Prefs } from "../../lib/prefs";
|
|||||||
import { Keys } from "../keys";
|
import { Keys } from "../keys";
|
||||||
import { $ } from "../winutil";
|
import { $ } from "../winutil";
|
||||||
|
|
||||||
export default class RemovalModalDialog extends ModalDialog {
|
export class RemovalModalDialog extends ModalDialog {
|
||||||
private readonly text: string;
|
private readonly text: string;
|
||||||
|
|
||||||
private readonly pref: string;
|
private readonly pref: string;
|
||||||
@ -68,3 +68,57 @@ export default class RemovalModalDialog extends ModalDialog {
|
|||||||
this.focusDefault();
|
this.focusDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DeleteFilesDialog extends ModalDialog {
|
||||||
|
private readonly paths: string[];
|
||||||
|
|
||||||
|
constructor(paths: string[]) {
|
||||||
|
super();
|
||||||
|
this.paths = paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContent() {
|
||||||
|
const content = $<HTMLTemplateElement>("#deletefiles-template").
|
||||||
|
content.cloneNode(true) as DocumentFragment;
|
||||||
|
await localize(content);
|
||||||
|
const list = $(".deletefiles-list", content);
|
||||||
|
for (const path of this.paths) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = path;
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
get buttons() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: _("deletefiles_button"),
|
||||||
|
value: "ok",
|
||||||
|
default: true,
|
||||||
|
dismiss: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: _("cancel"),
|
||||||
|
value: "cancel",
|
||||||
|
default: false,
|
||||||
|
dismiss: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async show() {
|
||||||
|
Keys.suppressed = true;
|
||||||
|
try {
|
||||||
|
return await super.show();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Keys.suppressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shown() {
|
||||||
|
this.focusDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ export const StateTexts = locale.then(() => Object.freeze(new Map([
|
|||||||
[DownloadState.QUEUED, _("queued")],
|
[DownloadState.QUEUED, _("queued")],
|
||||||
[DownloadState.RUNNING, _("running")],
|
[DownloadState.RUNNING, _("running")],
|
||||||
[DownloadState.FINISHING, _("finishing")],
|
[DownloadState.FINISHING, _("finishing")],
|
||||||
|
[DownloadState.RETRYING, _("paused")],
|
||||||
[DownloadState.PAUSED, _("paused")],
|
[DownloadState.PAUSED, _("paused")],
|
||||||
[DownloadState.DONE, _("done")],
|
[DownloadState.DONE, _("done")],
|
||||||
[DownloadState.CANCELED, _("canceled")],
|
[DownloadState.CANCELED, _("canceled")],
|
||||||
@ -21,6 +22,7 @@ export const StateClasses = Object.freeze(new Map([
|
|||||||
[DownloadState.RUNNING, "running"],
|
[DownloadState.RUNNING, "running"],
|
||||||
[DownloadState.FINISHING, "finishing"],
|
[DownloadState.FINISHING, "finishing"],
|
||||||
[DownloadState.PAUSED, "paused"],
|
[DownloadState.PAUSED, "paused"],
|
||||||
|
[DownloadState.RETRYING, "retrying"],
|
||||||
[DownloadState.DONE, "done"],
|
[DownloadState.DONE, "done"],
|
||||||
[DownloadState.CANCELED, "canceled"],
|
[DownloadState.CANCELED, "canceled"],
|
||||||
[DownloadState.MISSING, "missing"],
|
[DownloadState.MISSING, "missing"],
|
||||||
@ -31,6 +33,7 @@ export const StateIcons = Object.freeze(new Map([
|
|||||||
[DownloadState.RUNNING, "icon-go"],
|
[DownloadState.RUNNING, "icon-go"],
|
||||||
[DownloadState.FINISHING, "icon-go"],
|
[DownloadState.FINISHING, "icon-go"],
|
||||||
[DownloadState.PAUSED, "icon-pause"],
|
[DownloadState.PAUSED, "icon-pause"],
|
||||||
|
[DownloadState.RETRYING, "icon-pause"],
|
||||||
[DownloadState.DONE, "icon-done"],
|
[DownloadState.DONE, "icon-done"],
|
||||||
[DownloadState.CANCELED, "icon-error"],
|
[DownloadState.CANCELED, "icon-error"],
|
||||||
[DownloadState.MISSING, "icon-failed"],
|
[DownloadState.MISSING, "icon-failed"],
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
MenuFilter
|
MenuFilter
|
||||||
} from "./itemfilters";
|
} from "./itemfilters";
|
||||||
import { FilteredCollection } from "./itemfilters";
|
import { FilteredCollection } from "./itemfilters";
|
||||||
import RemovalModalDialog from "./removaldlg";
|
import { RemovalModalDialog, DeleteFilesDialog } from "./removaldlg";
|
||||||
import { Stats } from "./stats";
|
import { Stats } from "./stats";
|
||||||
import PORT from "./port";
|
import PORT from "./port";
|
||||||
import { DownloadState, StateTexts, StateClasses, StateIcons } from "./state";
|
import { DownloadState, StateTexts, StateClasses, StateIcons } from "./state";
|
||||||
@ -38,6 +38,10 @@ import { $ } from "../winutil";
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { TableConfig } from "../../uikit/lib/config";
|
import { TableConfig } from "../../uikit/lib/config";
|
||||||
import { IconCache } from "../../lib/iconcache";
|
import { IconCache } from "../../lib/iconcache";
|
||||||
|
import * as imex from "../../lib/imex";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "../../lib/item";
|
||||||
|
import { API } from "../../lib/api";
|
||||||
|
|
||||||
const TREE_CONFIG_VERSION = 2;
|
const TREE_CONFIG_VERSION = 2;
|
||||||
const RUNNING_TIMEOUT = 1000;
|
const RUNNING_TIMEOUT = 1000;
|
||||||
@ -116,7 +120,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public finalName: string;
|
public currentName: string;
|
||||||
|
|
||||||
public ext?: string;
|
public ext?: string;
|
||||||
|
|
||||||
@ -144,6 +148,10 @@ export class DownloadItem extends EventEmitter {
|
|||||||
|
|
||||||
private largeIconField?: string;
|
private largeIconField?: string;
|
||||||
|
|
||||||
|
public opening: boolean;
|
||||||
|
|
||||||
|
public retries: number;
|
||||||
|
|
||||||
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, raw);
|
Object.assign(this, raw);
|
||||||
@ -159,7 +167,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
return this.iconField;
|
return this.iconField;
|
||||||
}
|
}
|
||||||
this.iconField = this.owner.icons.get(
|
this.iconField = this.owner.icons.get(
|
||||||
iconForPath(this.finalName, ICON_BASE_SIZE));
|
iconForPath(this.currentName, ICON_BASE_SIZE));
|
||||||
if (this.ext) {
|
if (this.ext) {
|
||||||
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
|
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
|
||||||
if (icon) {
|
if (icon) {
|
||||||
@ -178,7 +186,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
return this.largeIconField;
|
return this.largeIconField;
|
||||||
}
|
}
|
||||||
this.largeIconField = this.owner.icons.get(
|
this.largeIconField = this.owner.icons.get(
|
||||||
iconForPath(this.finalName, LARGE_ICON_BASE_SIZE));
|
iconForPath(this.currentName, LARGE_ICON_BASE_SIZE));
|
||||||
if (this.ext) {
|
if (this.ext) {
|
||||||
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
|
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
|
||||||
if (icon) {
|
if (icon) {
|
||||||
@ -217,7 +225,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
if (this.owner.showUrls.value) {
|
if (this.owner.showUrls.value) {
|
||||||
return this.usable;
|
return this.usable;
|
||||||
}
|
}
|
||||||
return this.finalName;
|
return this.currentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fmtSize() {
|
get fmtSize() {
|
||||||
@ -245,6 +253,12 @@ export class DownloadItem extends EventEmitter {
|
|||||||
if (this.state === DownloadState.RUNNING) {
|
if (this.state === DownloadState.RUNNING) {
|
||||||
return this.eta;
|
return this.eta;
|
||||||
}
|
}
|
||||||
|
if (this.state === DownloadState.RETRYING) {
|
||||||
|
if (this.error) {
|
||||||
|
return _("retrying_error", _(this.error) || this.error);
|
||||||
|
}
|
||||||
|
return _("retrying");
|
||||||
|
}
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
return _(this.error) || this.error;
|
return _(this.error) || this.error;
|
||||||
}
|
}
|
||||||
@ -402,6 +416,8 @@ export class DownloadTable extends VirtualTable {
|
|||||||
|
|
||||||
private readonly openDirectoryAction: Broadcaster;
|
private readonly openDirectoryAction: Broadcaster;
|
||||||
|
|
||||||
|
private readonly deleteFilesAction: Broadcaster;
|
||||||
|
|
||||||
private readonly moveTopAction: Broadcaster;
|
private readonly moveTopAction: Broadcaster;
|
||||||
|
|
||||||
private readonly moveUpAction: Broadcaster;
|
private readonly moveUpAction: Broadcaster;
|
||||||
@ -522,8 +538,16 @@ export class DownloadTable extends VirtualTable {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Keys.on("SHIFT-Delete", (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (target.localName === "input") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.removeCompleteDownloads(false);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
ctx.on("ctx-remove-all", () => this.removeAllDownloads());
|
ctx.on("ctx-remove-all", () => this.removeAllDownloads());
|
||||||
ctx.on("ctx-remove-complete", () => this.removeCompleteDownloads(false));
|
|
||||||
ctx.on("ctx-remove-complete-all",
|
ctx.on("ctx-remove-complete-all",
|
||||||
() => this.removeCompleteDownloads(false));
|
() => this.removeCompleteDownloads(false));
|
||||||
ctx.on("ctx-remove-complete-selected",
|
ctx.on("ctx-remove-complete-selected",
|
||||||
@ -536,6 +560,12 @@ export class DownloadTable extends VirtualTable {
|
|||||||
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
|
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
|
||||||
ctx.on("ctx-remove-batch", () => this.removeBatchDownloads());
|
ctx.on("ctx-remove-batch", () => this.removeBatchDownloads());
|
||||||
|
|
||||||
|
ctx.on("ctx-import", () => this.importDownloads());
|
||||||
|
ctx.on("ctx-export-text", () => this.exportDownloads(imex.textExporter));
|
||||||
|
ctx.on("ctx-export-aria2", () => this.exportDownloads(imex.aria2Exporter));
|
||||||
|
ctx.on("ctx-export-metalink",
|
||||||
|
() => this.exportDownloads(imex.metalinkExporter));
|
||||||
|
|
||||||
ctx.on("dismissed", () => this.table.focus());
|
ctx.on("dismissed", () => this.table.focus());
|
||||||
|
|
||||||
this.on("contextmenu", (tree, event) => {
|
this.on("contextmenu", (tree, event) => {
|
||||||
@ -569,6 +599,9 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.openDirectoryAction = new Broadcaster("ctx-open-directory");
|
this.openDirectoryAction = new Broadcaster("ctx-open-directory");
|
||||||
this.openDirectoryAction.onaction = this.openDirectory.bind(this);
|
this.openDirectoryAction.onaction = this.openDirectory.bind(this);
|
||||||
|
|
||||||
|
this.deleteFilesAction = new Broadcaster("ctx-delete-files");
|
||||||
|
this.deleteFilesAction.onaction = this.deleteFiles.bind(this);
|
||||||
|
|
||||||
const moveAction = (method: string) => {
|
const moveAction = (method: string) => {
|
||||||
if (this.selection.empty) {
|
if (this.selection.empty) {
|
||||||
return;
|
return;
|
||||||
@ -600,6 +633,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.moveBottomAction,
|
this.moveBottomAction,
|
||||||
this.openFileAction,
|
this.openFileAction,
|
||||||
this.openDirectoryAction,
|
this.openDirectoryAction,
|
||||||
|
this.deleteFilesAction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.on(
|
this.on(
|
||||||
@ -607,8 +641,12 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
|
|
||||||
this.tooltip = null;
|
this.tooltip = null;
|
||||||
this.on("hover", async info => {
|
const tooltipWatcher = new PrefWatcher("tooltip", true);
|
||||||
if (!(await Prefs.get("tooltip"))) {
|
this.on("hover", info => {
|
||||||
|
if (!document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tooltipWatcher.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.downloads.filtered[info.rowid];
|
const item = this.downloads.filtered[info.rowid];
|
||||||
@ -737,6 +775,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectionChanged() {
|
selectionChanged() {
|
||||||
|
this.dismissTooltip();
|
||||||
const {empty} = this.selection;
|
const {empty} = this.selection;
|
||||||
if (empty) {
|
if (empty) {
|
||||||
for (const d of this.disableSet) {
|
for (const d of this.disableSet) {
|
||||||
@ -767,6 +806,10 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.cancelAction.disabled = true;
|
this.cancelAction.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(states & DownloadState.DONE)) {
|
||||||
|
this.deleteFilesAction.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
const item = this.focusRow >= 0 ?
|
const item = this.focusRow >= 0 ?
|
||||||
this.downloads.filtered[this.focusRow] :
|
this.downloads.filtered[this.focusRow] :
|
||||||
null;
|
null;
|
||||||
@ -777,7 +820,8 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resumeDownloads(forced = false) {
|
resumeDownloads(forced = false) {
|
||||||
const sids = this.getSelectedSids(DownloadState.RESUMABLE);
|
const sids = this.getSelectedSids(
|
||||||
|
forced ? DownloadState.FORCABLE : DownloadState.RESUMABLE);
|
||||||
if (!sids.length) {
|
if (!sids.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -801,20 +845,30 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async openFile() {
|
async openFile() {
|
||||||
if (this.focusRow < 0) {
|
this.dismissTooltip();
|
||||||
|
const {focusRow} = this;
|
||||||
|
if (focusRow < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.downloads.filtered[this.focusRow];
|
const item = this.downloads.filtered[focusRow];
|
||||||
if (!item || !item.manId || item.state !== DownloadState.DONE) {
|
if (!item || !item.manId || item.state !== DownloadState.DONE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
item.opening = true;
|
||||||
try {
|
try {
|
||||||
|
this.invalidateRow(focusRow);
|
||||||
await downloads.open(item.manId);
|
await downloads.open(item.manId);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error(ex, ex.toString(), ex);
|
console.error(ex, ex.toString(), ex);
|
||||||
PORT.post("missing", {sid: item.sessionId});
|
PORT.post("missing", {sid: item.sessionId});
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
item.opening = false;
|
||||||
|
this.invalidateRow(focusRow);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openDirectory() {
|
async openDirectory() {
|
||||||
@ -834,6 +888,33 @@ export class DownloadTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteFiles() {
|
||||||
|
const items = [];
|
||||||
|
for (const rowid of this.selection) {
|
||||||
|
const item = this.downloads.filtered[rowid];
|
||||||
|
if (item.state === DownloadState.DONE && item.manId) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sids = items.map(i => i.sessionId);
|
||||||
|
const paths = items.map(i => i.destFull);
|
||||||
|
await new DeleteFilesDialog(paths).show();
|
||||||
|
await Promise.all(items.map(async item => {
|
||||||
|
try {
|
||||||
|
if (item.manId && item.state === DownloadState.DONE) {
|
||||||
|
await downloads.removeFile(item.manId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.removeDownloadsInternal(sids);
|
||||||
|
}
|
||||||
|
|
||||||
removeDownloadsInternal(sids?: number[]) {
|
removeDownloadsInternal(sids?: number[]) {
|
||||||
if (!sids) {
|
if (!sids) {
|
||||||
sids = [];
|
sids = [];
|
||||||
@ -1106,12 +1187,64 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.selection.toggle(0, this.rowCount - 1);
|
this.selection.toggle(0, this.rowCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importDownloads() {
|
||||||
|
const picker = document.createElement("input");
|
||||||
|
picker.setAttribute("type", "file");
|
||||||
|
picker.setAttribute("accept", "text/*,.txt,.lst,.metalink,.meta4");
|
||||||
|
picker.onchange = () => {
|
||||||
|
if (!picker.files || !picker.files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
if (!reader.result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = imex.importText(reader.result as string);
|
||||||
|
if (!items || !items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
API.regular(items, []);
|
||||||
|
};
|
||||||
|
reader.readAsText(picker.files[0], "utf-8");
|
||||||
|
};
|
||||||
|
picker.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
exportDownloads(exporter: imex.Exporter) {
|
||||||
|
const items = this.getSelectedItems();
|
||||||
|
if (!items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = exporter.getText(items as unknown as BaseItem[]);
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const data = enc.encode(text);
|
||||||
|
const url = URL.createObjectURL(new Blob([data], {type: "text/plain"}));
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", exporter.fileName);
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
getRowClasses(rowid: number) {
|
getRowClasses(rowid: number) {
|
||||||
const item = this.downloads.filtered[rowid];
|
const item = this.downloads.filtered[rowid];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (item.opening) {
|
||||||
|
return ["opening"];
|
||||||
|
}
|
||||||
const cls = StateClasses.get(item.state);
|
const cls = StateClasses.get(item.state);
|
||||||
|
if (cls && item.opening) {
|
||||||
|
return [cls, "opening"];
|
||||||
|
}
|
||||||
|
if (item.opening) {
|
||||||
|
return ["opening"];
|
||||||
|
}
|
||||||
return cls && [cls] || null;
|
return cls && [cls] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +192,7 @@ export class Tooltip {
|
|||||||
const hidden = this.speedbox.classList.contains("hidden");
|
const hidden = this.speedbox.classList.contains("hidden");
|
||||||
|
|
||||||
if (!running && !hidden) {
|
if (!running && !hidden) {
|
||||||
this.eta.style.fontWeight = "bold";
|
this.eta.classList.add("single");
|
||||||
this.etalabel.classList.add("hidden");
|
this.etalabel.classList.add("hidden");
|
||||||
this.speedbox.classList.add("hidden");
|
this.speedbox.classList.add("hidden");
|
||||||
this.progressbar.classList.add("hidden");
|
this.progressbar.classList.add("hidden");
|
||||||
@ -202,7 +202,7 @@ export class Tooltip {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
this.eta.style.fontWeight = "auto";
|
this.eta.classList.remove("single");
|
||||||
this.etalabel.classList.remove("hidden");
|
this.etalabel.classList.remove("hidden");
|
||||||
this.speedbox.classList.remove("hidden");
|
this.speedbox.classList.remove("hidden");
|
||||||
this.progressbar.classList.remove("hidden");
|
this.progressbar.classList.remove("hidden");
|
||||||
|
@ -43,8 +43,17 @@
|
|||||||
<article id="tab-general" class="tab">
|
<article id="tab-general" class="tab">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend data-i18n="pref.ui">UI</legend>
|
<legend data-i18n="pref.ui">UI</legend>
|
||||||
<label><input type="checkbox" id="pref-global-turbo"> <span data-i18n="pref-global-turbo">Global turbo</span></label>
|
<div id="pref-button-type">
|
||||||
|
<label data-i18n="pref-button-type"></label>
|
||||||
|
<label><input type="radio" name="pref-button-type" value="popup"> <span data-i18n="pref-button-type-popup"></span></label>
|
||||||
|
<label><input type="radio" name="pref-button-type" value="dta"> <span data-i18n="pref-button-type-dta"></span></label>
|
||||||
|
<label><input type="radio" name="pref-button-type" value="turbo"> <span data-i18n="pref-button-type-turbo"></span></label>
|
||||||
|
<label><input type="radio" name="pref-button-type" value="manager"> <span data-i18n="pref-button-type-manager"></span></label>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<label><input type="checkbox" id="pref-manager-in-popup"> <span data-i18n="pref-manager-in-popup"></span></label>
|
||||||
<label><input type="checkbox" id="pref-finish-notification"> <span data-i18n="pref-finish-notification"></span></label>
|
<label><input type="checkbox" id="pref-finish-notification"> <span data-i18n="pref-finish-notification"></span></label>
|
||||||
|
<label><input type="checkbox" id="pref-sounds"> <span data-i18n="pref-sounds"></span></label>
|
||||||
<label><input type="checkbox" id="pref-hide-context"> <span data-i18n="pref-hide-context"></span></label>
|
<label><input type="checkbox" id="pref-hide-context"> <span data-i18n="pref-hide-context"></span></label>
|
||||||
<div style="margin-top: 1em;">
|
<div style="margin-top: 1em;">
|
||||||
<button id="reset-confirmations" data-i18n="reset-confirmations"></button>
|
<button id="reset-confirmations" data-i18n="reset-confirmations"></button>
|
||||||
@ -114,9 +123,14 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article id="tab-network" class="tab">
|
<article id="tab-network" class="tab">
|
||||||
<fieldset>
|
<fieldset id="network-general">
|
||||||
<legend data-i18n="pref.netglobal"></legend>
|
<legend data-i18n="pref.netglobal"></legend>
|
||||||
<label><span data-i18n="pref-concurrent-downloads">Concurrent downloads</span> <input id="pref-concurrent-downloads" type="number" min="1" max="10"></label>
|
<label data-i18n="pref-concurrent-downloads">Concurrent downloads</label>
|
||||||
|
<input id="pref-concurrent-downloads" type="number" min="1" max="10">
|
||||||
|
<label data-i18n="pref-retries"></label>
|
||||||
|
<input id="pref-retries" type="number" min="0" max="100">
|
||||||
|
<label data-i18n="pref-retry-time"></label>
|
||||||
|
<input id="pref-retry-time" type="number" min="1" max="600">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<table id="limits" data-singleselect="true">
|
<table id="limits" data-singleselect="true">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -554,9 +554,10 @@ addEventListener("DOMContentLoaded", async () => {
|
|||||||
await localize(document.documentElement);
|
await localize(document.documentElement);
|
||||||
|
|
||||||
// General
|
// General
|
||||||
new BoolPref("pref-global-turbo", "global-turbo");
|
new BoolPref("pref-manager-in-popup", "manager-in-popup");
|
||||||
new BoolPref("pref-queue-notification", "queue-notification");
|
new BoolPref("pref-queue-notification", "queue-notification");
|
||||||
new BoolPref("pref-finish-notification", "finish-notification");
|
new BoolPref("pref-finish-notification", "finish-notification");
|
||||||
|
new BoolPref("pref-sounds", "sounds");
|
||||||
new BoolPref("pref-hide-context", "hide-context");
|
new BoolPref("pref-hide-context", "hide-context");
|
||||||
new BoolPref("pref-tooltip", "tooltip");
|
new BoolPref("pref-tooltip", "tooltip");
|
||||||
new BoolPref("pref-open-manager-on-queue", "open-manager-on-queue");
|
new BoolPref("pref-open-manager-on-queue", "open-manager-on-queue");
|
||||||
@ -564,6 +565,7 @@ addEventListener("DOMContentLoaded", async () => {
|
|||||||
new BoolPref("pref-add-paused", "add-paused");
|
new BoolPref("pref-add-paused", "add-paused");
|
||||||
new BoolPref("pref-show-urls", "show-urls");
|
new BoolPref("pref-show-urls", "show-urls");
|
||||||
new BoolPref("pref-remove-missing-on-init", "remove-missing-on-init");
|
new BoolPref("pref-remove-missing-on-init", "remove-missing-on-init");
|
||||||
|
new OptionPref("pref-button-type", "button-type");
|
||||||
new OptionPref("pref-conflict-action", "conflict-action");
|
new OptionPref("pref-conflict-action", "conflict-action");
|
||||||
|
|
||||||
$("#reset-confirmations").addEventListener("click", async () => {
|
$("#reset-confirmations").addEventListener("click", async () => {
|
||||||
@ -621,6 +623,8 @@ addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
// Network
|
// Network
|
||||||
new IntPref("pref-concurrent-downloads", "concurrent");
|
new IntPref("pref-concurrent-downloads", "concurrent");
|
||||||
|
new IntPref("pref-retries", "retries");
|
||||||
|
new IntPref("pref-retry-time", "retry-time");
|
||||||
|
|
||||||
visible("#limits").then(() => new LimitsUI());
|
visible("#limits").then(() => new LimitsUI());
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<th id="colTitle" data-i18n="title">Title</th>
|
<th id="colTitle" data-i18n="title">Title</th>
|
||||||
<th id="colDescription" data-i18n="description">Description</th>
|
<th id="colDescription" data-i18n="description">Description</th>
|
||||||
<th id="colMask" data-i18n="mask">Mask</th>
|
<th id="colMask" data-i18n="mask">Mask</th>
|
||||||
<!--<th id="colReferrer" data-i18n="referrer">Referrer</th>-->
|
<th id="colReferrer" data-i18n="referrer">Referrer</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -59,6 +59,15 @@
|
|||||||
<input type="checkbox" id="fastOnceCheck">
|
<input type="checkbox" id="fastOnceCheck">
|
||||||
<span data-i18n="useonlyonce">Use only once</span>
|
<span data-i18n="useonlyonce">Use only once</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<h2 data-i18n="subfolder"></h2>
|
||||||
|
<input id="subfolder" data-i18n="placeholder=subfolder.placeholder">
|
||||||
|
<span></span>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="subfolderOnceCheck">
|
||||||
|
<span data-i18n="useonlyonce">Use only once</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<h2 data-i18n="mask">Mask</h2>
|
<h2 data-i18n="mask">Mask</h2>
|
||||||
<input id="mask">
|
<input id="mask">
|
||||||
<button id="maskButton" class="icon-tags"></button>
|
<button id="maskButton" class="icon-tags"></button>
|
||||||
@ -84,6 +93,7 @@
|
|||||||
<li id="ctx-toggle-selected" data-icon="icon-unchecked" data-i18n="toggle-selected-items">Toggle Check for Selected Items</li>
|
<li id="ctx-toggle-selected" data-icon="icon-unchecked" data-i18n="toggle-selected-items">Toggle Check for Selected Items</li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
<li id="ctx-mask" data-icon="icon-tags" data-i18n="set-mask">Mask</li>
|
<li id="ctx-mask" data-icon="icon-tags" data-i18n="set-mask">Mask</li>
|
||||||
|
<li id="ctx-referrer" data-icon="icon-forward" data-i18n="set-referrer">Referrer</li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
||||||
<li id="ctx-select-filtered" data-key="ACCEL-KeyF" data-i18n="select-checked">Select Checked</li>
|
<li id="ctx-select-filtered" data-key="ACCEL-KeyF" data-i18n="select-checked">Select Checked</li>
|
||||||
|
@ -7,7 +7,7 @@ import { ContextMenu } from "./contextmenu";
|
|||||||
import { iconForPath } from "../lib/windowutils";
|
import { iconForPath } from "../lib/windowutils";
|
||||||
import { _, localize } from "../lib/i18n";
|
import { _, localize } from "../lib/i18n";
|
||||||
import { Prefs } from "../lib/prefs";
|
import { Prefs } from "../lib/prefs";
|
||||||
import { MASK, FASTFILTER } from "../lib/recentlist";
|
import { MASK, FASTFILTER, SUBFOLDER } from "../lib/recentlist";
|
||||||
import { WindowState } from "./windowstate";
|
import { WindowState } from "./windowstate";
|
||||||
import { Dropdown } from "./dropdown";
|
import { Dropdown } from "./dropdown";
|
||||||
import { Keys } from "./keys";
|
import { Keys } from "./keys";
|
||||||
@ -24,6 +24,7 @@ import { BaseItem } from "../lib/item";
|
|||||||
import { ItemDelta } from "../lib/select";
|
import { ItemDelta } from "../lib/select";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { TableConfig } from "../uikit/lib/config";
|
import { TableConfig } from "../uikit/lib/config";
|
||||||
|
import { validateSubFolder as validateSubfolder } from "../lib/util";
|
||||||
|
|
||||||
const PORT: RawPort = runtime.connect(null, { name: "select" });
|
const PORT: RawPort = runtime.connect(null, { name: "select" });
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ const NUM_FILTER_CLASSES = 8;
|
|||||||
let Table: SelectionTable;
|
let Table: SelectionTable;
|
||||||
let Mask: Dropdown;
|
let Mask: Dropdown;
|
||||||
let FastFilter: Dropdown;
|
let FastFilter: Dropdown;
|
||||||
|
let Subfolder: Dropdown;
|
||||||
|
|
||||||
|
|
||||||
type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]};
|
type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]};
|
||||||
@ -51,7 +53,7 @@ interface BaseMatchedItem extends BaseItem {
|
|||||||
rowid: number;
|
rowid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleaErrors() {
|
function clearErrors() {
|
||||||
const not = $("#notification");
|
const not = $("#notification");
|
||||||
not.textContent = "";
|
not.textContent = "";
|
||||||
not.style.display = "none";
|
not.style.display = "none";
|
||||||
@ -117,8 +119,10 @@ class CheckClasser extends Map<string, string> {
|
|||||||
let result = super.get(key);
|
let result = super.get(key);
|
||||||
if (typeof result !== "string") {
|
if (typeof result !== "string") {
|
||||||
result = this.gen.next().value;
|
result = this.gen.next().value;
|
||||||
|
if (result) {
|
||||||
super.set(key, result);
|
super.set(key, result);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,7 +356,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
try {
|
try {
|
||||||
Keys.suppressed = true;
|
Keys.suppressed = true;
|
||||||
const newmask = await ModalDialog.prompt(
|
const newmask = await ModalDialog.prompt(
|
||||||
"Renaming mask", "Set new renaming mask", oldmask);
|
_("set_mask"), _("set_mask_text"), oldmask);
|
||||||
for (const r of this.selection) {
|
for (const r of this.selection) {
|
||||||
this.items.at(r).mask = newmask;
|
this.items.at(r).mask = newmask;
|
||||||
this.invalidateRow(r);
|
this.invalidateRow(r);
|
||||||
@ -366,6 +370,57 @@ class SelectionTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.contextMenu.on("ctx-referrer", async() => {
|
||||||
|
if (this.selection.empty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let oldref = "";
|
||||||
|
for (const r of this.selection) {
|
||||||
|
const m = this.items.at(r).usableReferrer;
|
||||||
|
if (oldref && m !== oldref) {
|
||||||
|
oldref = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
oldref = m || oldref;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Keys.suppressed = true;
|
||||||
|
const newref = await ModalDialog.prompt(
|
||||||
|
_("set_referrer"), _("set_referrer_text"), oldref);
|
||||||
|
try {
|
||||||
|
let ref;
|
||||||
|
if (!newref) {
|
||||||
|
ref = {
|
||||||
|
referrer: undefined,
|
||||||
|
usableReferrer: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const u = new URL(newref);
|
||||||
|
u.hash = "";
|
||||||
|
ref = {
|
||||||
|
referrer: u.toString(),
|
||||||
|
usableReferrer: decodeURIComponent(u.toString()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (const r of this.selection) {
|
||||||
|
Object.assign(this.items.at(r), ref);
|
||||||
|
this.invalidateRow(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.warn("mask dismissed", ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Keys.suppressed = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
this.contextMenu.on("dismissed", () => this.table.focus());
|
this.contextMenu.on("dismissed", () => this.table.focus());
|
||||||
|
|
||||||
this.on("contextmenu", (tree, event) => {
|
this.on("contextmenu", (tree, event) => {
|
||||||
@ -428,18 +483,43 @@ class SelectionTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSelection() {
|
openSelection() {
|
||||||
const items = this.items.filter((i, idx) => this.selection.contains(idx));
|
const privates: BaseMatchedItem[] = [];
|
||||||
if (!items.length) {
|
const items = this.items.filter((i, idx) => this.selection.contains(idx)).
|
||||||
|
filter(i => {
|
||||||
|
if (i.private) {
|
||||||
|
privates.push(i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!items.length && !privates.length) {
|
||||||
if (this.focusRow < 0) {
|
if (this.focusRow < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
items.push(this.items.at(this.focusRow));
|
const item = this.items.at(this.focusRow);
|
||||||
|
if (item.private) {
|
||||||
|
privates.push(item);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
PORT.postMessage({
|
PORT.postMessage({
|
||||||
msg: "openUrls",
|
msg: "openUrls",
|
||||||
urls: items.map(e => e.url)
|
urls: items.map(e => e.url),
|
||||||
|
incognito: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (privates.length) {
|
||||||
|
PORT.postMessage({
|
||||||
|
msg: "openUrls",
|
||||||
|
urls: privates.map(e => e.url),
|
||||||
|
incognito: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applyDeltaTo(delta: ItemDelta[], items: ItemCollection) {
|
applyDeltaTo(delta: ItemDelta[], items: ItemCollection) {
|
||||||
const active = items === this.items;
|
const active = items === this.items;
|
||||||
@ -494,7 +574,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
else {
|
else {
|
||||||
this.status.textContent = _("numitems.label", [selected]);
|
this.status.textContent = _("numitems.label", [selected]);
|
||||||
}
|
}
|
||||||
cleaErrors();
|
clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowClasses(rowid: number) {
|
getRowClasses(rowid: number) {
|
||||||
@ -502,7 +582,11 @@ class SelectionTable extends VirtualTable {
|
|||||||
if (!item || !matched(item) || !item.matched) {
|
if (!item || !matched(item) || !item.matched) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ["filtered", this.checkClasser.get(item.matched)];
|
const m = this.checkClasser.get(item.matched);
|
||||||
|
if (!m) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ["filtered", m];
|
||||||
}
|
}
|
||||||
|
|
||||||
getCellIcon(rowid: number, colid: number) {
|
getCellIcon(rowid: number, colid: number) {
|
||||||
@ -591,6 +675,9 @@ async function download(paused = false) {
|
|||||||
if (!mask) {
|
if (!mask) {
|
||||||
throw new Error("error.invalidMask");
|
throw new Error("error.invalidMask");
|
||||||
}
|
}
|
||||||
|
const subfolder = Subfolder.value;
|
||||||
|
validateSubfolder(subfolder);
|
||||||
|
|
||||||
const items = Table.items.checkedIndexes;
|
const items = Table.items.checkedIndexes;
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
throw new Error("error.noItemsSelected");
|
throw new Error("error.noItemsSelected");
|
||||||
@ -624,6 +711,8 @@ async function download(paused = false) {
|
|||||||
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
||||||
fast: FastFilter.value,
|
fast: FastFilter.value,
|
||||||
fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
|
fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
|
||||||
|
subfolder,
|
||||||
|
subfolderOnce: $<HTMLInputElement>("#subfolderOnceCheck").checked,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -699,9 +788,9 @@ function cancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await Promise.all([MASK.init(), FASTFILTER.init()]);
|
await Promise.all([MASK.init(), FASTFILTER.init(), SUBFOLDER.init()]);
|
||||||
Mask = new Dropdown("#mask", MASK.values);
|
Mask = new Dropdown("#mask", MASK.values);
|
||||||
Mask.on("changed", cleaErrors);
|
Mask.on("changed", clearErrors);
|
||||||
FastFilter = new Dropdown("#fast", FASTFILTER.values);
|
FastFilter = new Dropdown("#fast", FASTFILTER.values);
|
||||||
FastFilter.on("changed", () => {
|
FastFilter.on("changed", () => {
|
||||||
PORT.postMessage({
|
PORT.postMessage({
|
||||||
@ -709,6 +798,8 @@ async function init() {
|
|||||||
fastFilter: FastFilter.value
|
fastFilter: FastFilter.value
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Subfolder = new Dropdown("#subfolder", SUBFOLDER.values);
|
||||||
|
Subfolder.on("changed", clearErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOADED = new Promise(resolve => {
|
const LOADED = new Promise(resolve => {
|
||||||
|
@ -47,6 +47,15 @@
|
|||||||
<span data-i18n="useonlyonce">Use only once</span>
|
<span data-i18n="useonlyonce">Use only once</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 data-i18n="subfolder"></h3>
|
||||||
|
<div id="subfolderOptions">
|
||||||
|
<input type="text" id="subfolder" data-i18n="placeholder=subfolder.placeholder">
|
||||||
|
<span></span>
|
||||||
|
<label id="subfolderOnce">
|
||||||
|
<input type="checkbox" id="subfolderOnceCheck">
|
||||||
|
<span data-i18n="useonlyonce">Use only once</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="notification"></section>
|
<section id="notification"></section>
|
||||||
<section id="buttons">
|
<section id="buttons">
|
||||||
|
@ -6,7 +6,7 @@ import ModalDialog from "../uikit/lib/modal";
|
|||||||
import { _, localize } from "../lib/i18n";
|
import { _, localize } from "../lib/i18n";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Item, BaseItem } from "../lib/item";
|
import { Item, BaseItem } from "../lib/item";
|
||||||
import { MASK } from "../lib/recentlist";
|
import { MASK, SUBFOLDER } from "../lib/recentlist";
|
||||||
import { BatchGenerator } from "../lib/batches";
|
import { BatchGenerator } from "../lib/batches";
|
||||||
import { WindowState } from "./windowstate";
|
import { WindowState } from "./windowstate";
|
||||||
import { Dropdown } from "./dropdown";
|
import { Dropdown } from "./dropdown";
|
||||||
@ -14,11 +14,13 @@ import { Keys } from "./keys";
|
|||||||
import { hookButton } from "../lib/manager/renamer";
|
import { hookButton } from "../lib/manager/renamer";
|
||||||
import { runtime } from "../lib/browser";
|
import { runtime } from "../lib/browser";
|
||||||
import { $ } from "./winutil";
|
import { $ } from "./winutil";
|
||||||
|
import { validateSubFolder } from "../lib/util";
|
||||||
|
|
||||||
const PORT = runtime.connect(null, { name: "single" });
|
const PORT = runtime.connect(null, { name: "single" });
|
||||||
|
|
||||||
let ITEM: BaseItem;
|
let ITEM: BaseItem;
|
||||||
let Mask: Dropdown;
|
let Mask: Dropdown;
|
||||||
|
let Subfolder: Dropdown;
|
||||||
|
|
||||||
class BatchModalDialog extends ModalDialog {
|
class BatchModalDialog extends ModalDialog {
|
||||||
private readonly gen: BatchGenerator;
|
private readonly gen: BatchGenerator;
|
||||||
@ -71,7 +73,8 @@ function setItem(item: BaseItem) {
|
|||||||
title = "",
|
title = "",
|
||||||
description = "",
|
description = "",
|
||||||
usableReferrer = "",
|
usableReferrer = "",
|
||||||
mask = ""
|
mask = "",
|
||||||
|
subfolder = "",
|
||||||
} = item;
|
} = item;
|
||||||
$<HTMLInputElement>("#URL").value = usable;
|
$<HTMLInputElement>("#URL").value = usable;
|
||||||
$<HTMLInputElement>("#filename").value = fileName;
|
$<HTMLInputElement>("#filename").value = fileName;
|
||||||
@ -81,6 +84,9 @@ function setItem(item: BaseItem) {
|
|||||||
if (mask) {
|
if (mask) {
|
||||||
Mask.value = mask;
|
Mask.value = mask;
|
||||||
}
|
}
|
||||||
|
if (subfolder) {
|
||||||
|
Subfolder.value = subfolder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayError(err: string) {
|
function displayError(err: string) {
|
||||||
@ -124,6 +130,9 @@ async function downloadInternal(paused: boolean) {
|
|||||||
return displayError("error.invalidMask");
|
return displayError("error.invalidMask");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subfolder = Subfolder.value.trim();
|
||||||
|
validateSubFolder(subfolder);
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
if (!ITEM) {
|
if (!ITEM) {
|
||||||
@ -136,6 +145,7 @@ async function downloadInternal(paused: boolean) {
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
mask,
|
mask,
|
||||||
|
subfolder
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -143,6 +153,7 @@ async function downloadInternal(paused: boolean) {
|
|||||||
ITEM.title = title;
|
ITEM.title = title;
|
||||||
ITEM.description = description;
|
ITEM.description = description;
|
||||||
ITEM.mask = mask;
|
ITEM.mask = mask;
|
||||||
|
ITEM.subfolder = subfolder;
|
||||||
if (usableReferrer !== ITEM.usableReferrer) {
|
if (usableReferrer !== ITEM.usableReferrer) {
|
||||||
ITEM.referrer = referrer;
|
ITEM.referrer = referrer;
|
||||||
ITEM.usableReferrer = usableReferrer;
|
ITEM.usableReferrer = usableReferrer;
|
||||||
@ -185,6 +196,8 @@ async function downloadInternal(paused: boolean) {
|
|||||||
paused,
|
paused,
|
||||||
mask,
|
mask,
|
||||||
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
||||||
|
subfolder,
|
||||||
|
subfolderOnce: $<HTMLInputElement>("#subfolderOnceCheck").checked,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@ -201,8 +214,9 @@ function cancel() {
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await localize(document.documentElement);
|
await localize(document.documentElement);
|
||||||
await Promise.all([MASK.init()]);
|
await Promise.all([MASK.init(), SUBFOLDER.init()]);
|
||||||
Mask = new Dropdown("#mask", MASK.values);
|
Mask = new Dropdown("#mask", MASK.values);
|
||||||
|
Subfolder = new Dropdown("#subfolder", SUBFOLDER.values);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener("DOMContentLoaded", async function dom() {
|
addEventListener("DOMContentLoaded", async function dom() {
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -46,6 +46,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.0.tgz#390c5df1613b166ce3c3eb9fda4d93dc3eeec7b5"
|
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.0.tgz#390c5df1613b166ce3c3eb9fda4d93dc3eeec7b5"
|
||||||
integrity sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==
|
integrity sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==
|
||||||
|
|
||||||
|
"@types/whatwg-mimetype@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f981bbdf1813a75820a6ec3a7fdfa0d452552cc7"
|
||||||
|
integrity sha512-bJ/bZ+pA69lm+Ll8JJRoAD9saH7unIMfxPQQpl7bxa00qNqvUXSyk3xvoRMea1uCpAOxweI7CzjWx48ysX6yug==
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^2.0.0":
|
"@typescript-eslint/eslint-plugin@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz#609a5d7b00ce21a6f94d7ef282eba9da57ca1e42"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz#609a5d7b00ce21a6f94d7ef282eba9da57ca1e42"
|
||||||
@ -3718,6 +3723,11 @@ webpack@^4.39.3:
|
|||||||
watchpack "^1.6.0"
|
watchpack "^1.6.0"
|
||||||
webpack-sources "^1.4.1"
|
webpack-sources "^1.4.1"
|
||||||
|
|
||||||
|
whatwg-mimetype@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||||
|
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||||
|
|
||||||
which-module@^2.0.0:
|
which-module@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
|
Reference in New Issue
Block a user