53 Commits

Author SHA1 Message Date
76992bd4f4 Version 4.0.11 2019-09-10 09:37:00 +02:00
dccd530475 Typo 2019-09-10 09:33:32 +02:00
f1fa01a0eb Add ja to locale list 2019-09-10 09:30:59 +02:00
7949142ef6 Japanese translation (#83) 2019-09-10 09:29:12 +02:00
af1da8fc0a Fix maximized state being refused with dims
Closes #84
2019-09-10 09:23:43 +02:00
39f4237cde Update Indonesian translation (#80) 2019-09-09 18:07:07 +02:00
b676ed74cd Update locale list 2019-09-08 21:30:21 +02:00
bf474877ca Add Italian translation (#49) 2019-09-08 21:27:42 +02:00
7ee13af238 Version 4.0.10 2019-09-07 22:51:06 +02:00
d488e5874a Correct license header 2019-09-07 22:51:06 +02:00
b1a7c22452 Dismiss tooltip when the selection changes 2019-09-07 22:46:24 +02:00
e928d202ee Use the same base font size on all platforms 2019-09-07 20:13:37 +02:00
c39961d253 Make sure application/octet-stream is not mime-able 2019-09-07 19:39:43 +02:00
c6d11fcd7f Give MimeDB a class name 2019-09-07 19:34:12 +02:00
eb96103478 Sanitiy check detected names if we got a mime 2019-09-07 19:32:51 +02:00
583ccfc7b1 Detect name from query string 2019-09-07 19:27:28 +02:00
e0437718a0 Do not register remove-complete twice 2019-09-07 18:58:27 +02:00
2126ae022b Move preroller to it's own class 2019-09-07 18:27:31 +02:00
2ef39dcb19 Add some minor tests for mime 2019-09-07 17:54:12 +02:00
047c865e76 The things you see only after yu push... 2019-09-07 10:27:01 +02:00
c586cd00cc Switch to @Rob--W's CD parser
I "cleaned" up the library according to my personal preferences.
I tend to do this because in the process I need to develop a deeper
understanding of the code, and not because there was nothing wrong
with it.
I deeply appreciate the work that went into creating this library
and releasing it open source 👍

Closes #72
2019-09-07 10:22:09 +02:00
ee7f470269 Use ranges in preroll to avoid large downloads
This will effectively request the first 2 bytes from a server?
Why 2 bytes? Because a lot of servers are broken when bytes=0-0

Related to #70
2019-09-06 21:56:00 +02:00
f04dda308b Adding Hungarian locale, translation (#69) 2019-09-06 20:55:46 +02:00
071458e262 Switch preroll to GET
Fixes #70
2019-09-06 20:42:00 +02:00
9ffc96de4d Purge ourselves from history and sessions
Closes #66
2019-09-06 09:57:24 +02:00
AC
26e9a5404a Add Shift-Delete keyboard shortcut 2019-09-06 04:38:04 +02:00
f44fe59054 Polishing of some lt locale translations (#67) 2019-09-05 10:35:30 +02:00
e4b0629dee Version 4.0.9 2019-09-05 09:18:08 +02:00
5c2700ca36 Tooltip improvements 2019-09-05 09:03:50 +02:00
639a582804 Add small animation when opening files 2019-09-05 08:55:11 +02:00
2d1f185fcd Disable shelf in chrome
We cannot just disable it for our downloads (reliably)
so disable it completely while we're running.
2019-09-05 07:40:17 +02:00
38735ed0ae Make progress bar a little round 2019-09-05 07:39:19 +02:00
216bc590da Do not forget about 405 - Method not allowed 2019-09-04 21:34:52 +02:00
1c10d8005a Improve PREROLL based on user feedback 2019-09-04 21:27:03 +02:00
1fcfbe5360 Adjust default widths a bit 2019-09-04 21:15:32 +02:00
8d3dda1cec Bold buttons 2019-09-04 14:56:37 +02:00
be18f667d9 Trigger the saveQueue before reload 2019-09-04 14:48:35 +02:00
027b2c4fb1 Saving after preroll may have cause dupes 2019-09-04 14:48:35 +02:00
4ed92878be Update of zh_CN locale (#56) 2019-09-04 14:20:13 +02:00
a6930f309e Update ru locale 2019-09-04 14:15:06 +02:00
fdcdae0412 Use promises in content scripts 2019-09-04 14:15:06 +02:00
2c18ddaaa8 Increase shelf timeout 2019-09-04 14:15:06 +02:00
994e7ad0a6 Do not wait on downloads to finish prerollling 2019-09-04 14:15:06 +02:00
95536b36be Do not attempt to restart bg complete 2019-09-04 14:15:06 +02:00
9c159d5d24 Do not wait for the scheduler on startup 2019-09-04 14:15:06 +02:00
42ccfd5dc5 Do not abuse serverName to store browserName 2019-09-04 14:15:05 +02:00
dabf7f8a28 Prerolling and mime detection for some downloads
Only attempt this for a limited subset of downloads for now.
Related #45
2019-09-04 14:15:05 +02:00
9cac48f439 Make all tabs work again 2019-09-04 14:15:05 +02:00
ef6bc840d8 Do not trace 2019-09-04 14:15:00 +02:00
1c38ec1357 Add zh_TW locale (#62) 2019-09-04 12:03:50 +02:00
a4436bd6c8 Force Start should apply to Queued downloads
Closes #57
2019-09-03 18:18:07 +02:00
5a4b8143b2 Remove some any-types 2019-09-03 18:18:07 +02:00
00a5712427 Update of lt locale (#55) 2019-09-03 15:08:24 +02:00
47 changed files with 7180 additions and 689 deletions

View File

@ -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)

View File

@ -5,13 +5,16 @@
"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]",
"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/hu/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,18 @@
"message": "id", "message": "id",
"description": "Language code the locale will use, e.g. de or en-GB or pt-BR" "description": "Language code the locale will use, e.g. de or en-GB or pt-BR"
}, },
"renamer_tags": {
"message": "Penanda Mask Penamaan",
"description": "Mask text; see mask button"
},
"renmask": {
"message": "Mask penamaan",
"description": "Renaming mask (long)"
},
"set_mask": {
"message": "Set Mask Penamaan",
"description": "Menu text; select window"
},
"addpaused": { "addpaused": {
"message": "Tambahkan dalam kondisi terpause", "message": "Tambahkan dalam kondisi terpause",
"description": "Action: Add paused" "description": "Action: Add paused"
@ -280,15 +292,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": {
@ -320,7 +332,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 +374,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 +386,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": {
@ -560,7 +572,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 +612,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 +642,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 +660,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 +676,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": {

1170
_locales/it/messages.json Normal file

File diff suppressed because it is too large Load Diff

1170
_locales/ja/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -108,7 +108,7 @@
"description": "Media label (short)" "description": "Media label (short)"
}, },
"missing": { "missing": {
"message": "Trūksta", "message": "Nėra",
"description": "Status text in manager" "description": "Status text in manager"
}, },
"NETWORK_FAILED": { "NETWORK_FAILED": {
@ -684,7 +684,7 @@
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_hide_context": { "pref_hide_context": {
"message": "Nerodyti bendrųjų kontekstinio meniu elementų", "message": "Kontekstiniame meniu nerodyti bendrųjų elementų",
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_manager_tooltip": { "pref_manager_tooltip": {
@ -692,15 +692,15 @@
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_open_manager_on_queue": { "pref_open_manager_on_queue": {
"message": "Atidaryti Menedžerio kortelę, po kai kurių parsisiuntimų atsiradimo eilėje", "message": "Atidaryti Menedžerio kortelę po parsisiuntimo pridėjimo",
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_queue_notification": { "pref_queue_notification": {
"message": "Parodyti pranešimą, kai eilėje atsiranda nauji parsisiuntimai", "message": "Rodyti pranešimą po naujų parsisiuntimų pridėjimo",
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_remove_missing_on_init": { "pref_remove_missing_on_init": {
"message": "Pašalinti trūkstamus parsisiuntimus po restarto", "message": "Šalinti nepavykusius parsisiuntimus po restarto",
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_show_urls": { "pref_show_urls": {
@ -740,7 +740,7 @@
"description": "Window/tab title; Preferences" "description": "Window/tab title; Preferences"
}, },
"queue_finished": { "queue_finished": {
"message": "Parsisiuntimų eilė baigta", "message": "Visi parsisiuntimai baigti",
"description": "Notification text" "description": "Notification text"
}, },
"queued_download": { "queued_download": {
@ -862,11 +862,11 @@
} }
}, },
"remove_missing": { "remove_missing": {
"message": "Išvalyti trūkstamus parsisiuntimus", "message": "Išvalyti nepavykusius parsisiuntimus",
"description": "Menu text" "description": "Menu text"
}, },
"remove_missing_downloads_question": { "remove_missing_downloads_question": {
"message": "Norite išvalyti visus trūkstamus parsisiuntimus?", "message": "Norite išvalyti visus nepavykusius parsisiuntimus?",
"description": "Messagebox text" "description": "Messagebox text"
}, },
"remove_paused_downloads": { "remove_paused_downloads": {

View File

@ -12,7 +12,7 @@
"description": "Action: Add paused" "description": "Action: Add paused"
}, },
"add_download": { "add_download": {
"message": "добавить закачку", "message": "Добавить закачку",
"description": "Action for adding a download" "description": "Action for adding a download"
}, },
"add_new": { "add_new": {
@ -244,7 +244,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": {

File diff suppressed because it is too large Load Diff

1170
_locales/zh_TW/messages.json Normal file

File diff suppressed because it is too large Load Diff

396
data/mime.json Normal file
View 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"
}
}

View File

@ -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";
@ -103,7 +106,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[] :
@ -566,6 +569,43 @@ locale.then(() => {
} }
(async function init() { (async function init() {
const urlBase = runtime.getURL("");
history.onVisited.addListener(({url}: {url: string}) => {
if (!url || !url.startsWith(urlBase)) {
return;
}
history.deleteUrl({url});
});
const results: {url?: string}[] = await history.search({text: urlBase});
for (const {url} of results) {
if (!url) {
continue;
}
history.deleteUrl({url});
}
if (!CHROME) {
const sessionRemover = async () => {
for (const s of await sessions.getRecentlyClosed()) {
if (s.tab) {
if (s.tab.url.startsWith(urlBase)) {
await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId);
}
continue;
}
if (!s.window || !s.window.tabs || s.window.tabs.length > 1) {
continue;
}
const [tab] = s.window.tabs;
if (tab.url.startsWith(urlBase)) {
await sessions.forgetClosedWindow(s.window.sessionId);
}
}
};
sessions.onChanged.addListener(sessionRemover);
await sessionRemover();
}
await Prefs.set("last-run", new Date()); await Prefs.set("last-run", new Date());
Prefs.get("global-turbo", false).then(v => adjustAction(v)); Prefs.get("global-turbo", false).then(v => adjustAction(v));
Prefs.on("global-turbo", (prefs, key, value) => { Prefs.on("global-turbo", (prefs, key, value) => {

View File

@ -39,16 +39,19 @@ export interface RawPort {
postMessage: (message: any) => void; postMessage: (message: any) => void;
} }
export const {extension} = polyfill;
export const {notifications} = polyfill;
export const {browserAction} = polyfill; export const {browserAction} = polyfill;
export const {contextMenus} = polyfill; export const {contextMenus} = polyfill;
export const {downloads} = polyfill; export const {downloads} = polyfill;
export const {extension} = polyfill;
export const {history} = polyfill;
export const {menus} = polyfill; export const {menus} = polyfill;
export const {notifications} = polyfill;
export const {runtime} = polyfill; export const {runtime} = polyfill;
export const {sessions} = polyfill;
export const {storage} = polyfill; export const {storage} = polyfill;
export const {tabs} = polyfill; export const {tabs} = polyfill;
export const {webNavigation} = polyfill; export const {webNavigation} = polyfill;
export const {webRequest} = polyfill;
export const {windows} = polyfill; export const {windows} = polyfill;
export const CHROME = navigator.appVersion.includes("Chrome/"); export const CHROME = navigator.appVersion.includes("Chrome/");

230
lib/cdheaderparser.ts Normal file
View 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(/^[^']*'/, ""));
}
}

View File

@ -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

View File

@ -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";

View File

@ -27,6 +27,9 @@ const SAVEDPROPS = [
"written", "written",
// server stuff // server stuff
"serverName", "serverName",
"browserName",
"mime",
"prerolled",
// other options // other options
"private", "private",
// db // db
@ -39,10 +42,13 @@ const DEFAULTS = {
state: QUEUED, state: QUEUED,
error: "", error: "",
serverName: "", serverName: "",
browserName: "",
fileName: "", fileName: "",
totalSize: 0, totalSize: 0,
written: 0, written: 0,
manId: 0, manId: 0,
mime: "",
prerolled: false
}; };
let sessionId = 0; let sessionId = 0;
@ -59,14 +65,26 @@ export class BaseDownload {
public url: string; public url: string;
public usable: string;
public uReferrer: URLd; public uReferrer: URLd;
public referrer: string; public referrer: string;
public usableReferrer: string;
public startDate: Date; public startDate: Date;
public fileName: string; public fileName: string;
public description?: string;
public title?: string;
public batch: number;
public idx: number;
public error: string; public error: string;
public postData: any; public postData: any;
@ -79,8 +97,13 @@ export class BaseDownload {
public serverName: string; public serverName: string;
public browserName: string;
public mime: string;
public mask: string; public mask: string;
public prerolled: boolean;
constructor(options: any) { constructor(options: any) {
Object.assign(this, DEFAULTS); Object.assign(this, DEFAULTS);
@ -115,6 +138,10 @@ export class BaseDownload {
return this.serverName || this.fileName || this.urlName || "index.html"; return this.serverName || this.fileName || this.urlName || "index.html";
} }
get currentName() {
return this.browserName || this.dest.name || this.finalName;
}
get urlName() { get urlName() {
const path = parsePath(this.uURL); const path = parsePath(this.uURL);
if (path.name) { if (path.name) {
@ -152,6 +179,7 @@ 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;
return rv; return rv;

View File

@ -1,25 +1,26 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import { CHROME, downloads } from "../browser";
import { Prefs } from "../prefs"; import { Prefs } from "../prefs";
import { parsePath, filterInSitu } from "../util";
import {
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
FORCABLE, PAUSABLE, CANCELABLE,
} from "./state";
import { BaseDownload } from "./basedownload";
import { PromiseSerializer } from "../pserializer"; import { PromiseSerializer } from "../pserializer";
import { filterInSitu, parsePath } from "../util";
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { Manager } from "./man"; import { Manager } from "./man";
import { downloads, CHROME } from "../browser"; import Renamer from "./renamer";
import { debounce } from "../../uikit/lib/util"; import {
CANCELABLE,
CANCELED,
const setShelfEnabled = downloads.setShelfEnabled || function() { DONE,
// ignored FORCABLE,
}; MISSING,
PAUSABLE,
const reenableShelf = debounce(() => setShelfEnabled(true), 1000, true); PAUSED,
QUEUED,
RUNNING
} from "./state";
import { Preroller } from "./preroller";
type Header = {name: string; value: string}; type Header = {name: string; value: string};
interface Options { interface Options {
@ -53,6 +54,7 @@ export class Download extends BaseDownload {
} }
markDirty() { markDirty() {
this.renamer = new Renamer(this);
this.manager.setDirty(this); this.manager.setDirty(this);
} }
@ -80,6 +82,11 @@ export class Download extends BaseDownload {
this.updateStateFromBrowser(); this.updateStateFromBrowser();
return; return;
} }
if (state[0].state === "complete") {
this.changeState(DONE);
this.updateStateFromBrowser();
return;
}
if (!state[0].canResume) { if (!state[0].canResume) {
throw new Error("Cannot resume"); throw new Error("Cannot resume");
} }
@ -97,9 +104,22 @@ 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 {
if (!this.prerolled) {
await this.maybePreroll();
if (this.state !== RUNNING) {
// Aborted by preroll
return;
}
}
const options: Options = { const options: Options = {
conflictAction: await Prefs.get("conflict-action"), conflictAction: await Prefs.get("conflict-action"),
filename: this.dest.full, filename: this.dest.full,
@ -124,8 +144,6 @@ export class Download extends BaseDownload {
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 +157,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 +166,42 @@ export class Download extends BaseDownload {
} }
} }
private async maybePreroll() {
try {
if (this.prerolled) {
// Check again, just in case, async and all
return;
}
const roller = new Preroller(this);
if (!roller.shouldPreroll) {
return;
}
const res = await roller.roll();
if (!res) {
return;
}
if (res.mime) {
this.mime = res.mime;
}
if (res.name) {
this.serverName = res.name;
}
if (res.error) {
this.cancel();
this.error = res.error;
}
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
finally {
if (this.state === RUNNING) {
this.prerolled = true;
this.markDirty();
}
}
}
resume(forced = false) { resume(forced = false) {
if (!(FORCABLE & this.state)) { if (!(FORCABLE & this.state)) {
return; return;
@ -181,9 +231,10 @@ export class Download extends BaseDownload {
} }
reset() { reset() {
this.prerolled = false;
this.manId = 0; this.manId = 0;
this.written = this.totalSize = 0; this.written = this.totalSize = 0;
this.serverName = ""; this.mime = this.serverName = this.browserName = "";
} }
async removeFromBrowser() { async removeFromBrowser() {
@ -260,8 +311,11 @@ export class Download extends BaseDownload {
const state = (await downloads.search({id: this.manId})).pop(); const state = (await downloads.search({id: this.manId})).pop();
const {filename, error} = state; const {filename, error} = state;
const path = parsePath(filename); const path = parsePath(filename);
this.serverName = path.name; this.browserName = path.name;
this.adoptSize(state); this.adoptSize(state);
if (!this.mime && state.mime) {
this.mime = state.mime;
}
this.markDirty(); this.markDirty();
switch (state.state) { switch (state.state) {
case "in_progress": case "in_progress":

View File

@ -25,11 +25,14 @@ 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
};
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;
@ -93,7 +96,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 +154,7 @@ export class Manager extends EventEmitter {
} }
const next = await this.scheduler.next(this.running); const next = await this.scheduler.next(this.running);
if (!next) { if (!next) {
this.maybeNotifyFinished(); this.maybeRunFinishActions();
break; break;
} }
if (this.running.has(next) || next.state !== QUEUED) { if (this.running.has(next) || next.state !== QUEUED) {
@ -168,10 +174,31 @@ export class Manager extends EventEmitter {
async startDownload(download: Download) { async startDownload(download: Download) {
// Add to running first, so we don't confuse the scheduler and other parts // Add to running first, so we don't confuse the scheduler and other parts
this.running.add(download); this.running.add(download);
setShelfEnabled(false);
await download.start(); await download.start();
this.notifiedFinished = false; this.notifiedFinished = false;
} }
async maybeRunFinishActions() {
if (this.running.size) {
return;
}
await this.maybeNotifyFinished();
if (this.running.size) {
return;
}
if (this.shouldReload) {
this.saveQueue.trigger();
setTimeout(() => {
if (this.running.size) {
return;
}
runtime.reload();
}, RELOAD_TIMEOUT);
}
setShelfEnabled(true);
}
async maybeNotifyFinished() { async maybeNotifyFinished() {
if (!(await Prefs.get("finish-notification"))) { if (!(await Prefs.get("finish-notification"))) {
return; return;
@ -181,14 +208,6 @@ export class Manager extends EventEmitter {
} }
this.notifiedFinished = true; this.notifiedFinished = true;
new Notification(null, _("queue-finished")); new Notification(null, _("queue-finished"));
if (this.shouldReload) {
setTimeout(() => {
if (this.running.size) {
return;
}
runtime.reload();
}, RELOAD_TIMEOUT);
}
} }
addManId(id: number, download: Download) { addManId(id: number, download: Download) {
@ -236,7 +255,7 @@ export class Manager extends EventEmitter {
this.emit("dirty", items); this.emit("dirty", items);
} }
save(items: Download[]) { private save(items: Download[]) {
DB.saveItems(items.filter(i => !i.removed)). DB.saveItems(items.filter(i => !i.removed)).
catch(console.error); catch(console.error);
} }
@ -361,6 +380,10 @@ export class Manager extends EventEmitter {
} }
this.emit("active", this.active); this.emit("active", this.active);
} }
getMsgItems() {
return this.items.map(e => e.toMsg());
}
} }
let inited: Promise<Manager>; let inited: Promise<Manager>;

View File

@ -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()));
} }
} }

220
lib/manager/preroller.ts Normal file
View File

@ -0,0 +1,220 @@
"use strict";
// License: MIT
import MimeType from "whatwg-mimetype";
// eslint-disable-next-line no-unused-vars
import { Download } from "./download";
import { CHROME, webRequest } from "../browser";
import { CDHeaderParser } from "../cdheaderparser";
import { sanitizePath, parsePath } from "../util";
import { MimeDB } from "../mime";
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
const PREROLL_HOSTS = /4cdn|chan/;
const PREROLL_TIMEOUT = 10000;
const PREROLL_NOPE = new Set<string>();
const PREROLL_SEARCHEXTS = Object.freeze(new Set<string>([
"php",
"asp",
"aspx",
"inc",
"py",
"pl",
"action",
"htm",
"html",
"shtml"
]));
const NAME_TESTER = /\.[a-z0-9]{1,5}$/i;
const CDPARSER = new CDHeaderParser();
export interface PrerollResults {
error?: string;
name?: string;
mime?: string;
finalURL?: string;
}
export class Preroller {
private readonly download: Download
constructor(download: Download) {
this.download = download;
}
get shouldPreroll() {
const {uURL, renamer} = this.download;
const {pathname, search, host} = uURL;
if (PREROLL_NOPE.has(host)) {
return false;
}
if (!renamer.p_ext) {
return true;
}
if (search.length) {
return true;
}
if (uURL.pathname.endsWith("/")) {
return true;
}
if (PREROLL_HEURISTICS.test(pathname)) {
return true;
}
if (PREROLL_HOSTS.test(host)) {
return true;
}
return false;
}
async roll() {
try {
return await (CHROME ? this.prerollChrome() : this.prerollFirefox());
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
return null;
}
private async prerollFirefox() {
const controller = new AbortController();
const {signal} = controller;
const {uURL} = this.download;
const res = await fetch(uURL.toString(), {
method: "GET",
headers: new Headers({
Range: "bytes=0-1",
}),
mode: "same-origin",
signal,
});
if (res.body) {
res.body.cancel();
}
controller.abort();
const {headers} = res;
return this.finalize(headers, res);
}
private async prerollChrome() {
let rid = "";
const {uURL} = this.download;
const rurl = uURL.toString();
let listener: any;
const wr = new Promise<any[]>(resolve => {
listener = (details: any) => {
const {url, requestId, statusCode} = details;
if (rid !== requestId && url !== rurl) {
return;
}
// eslint-disable-next-line no-magic-numbers
if (statusCode >= 300 && statusCode < 400) {
// Redirect, continue tracking;
rid = requestId;
return;
}
resolve(details.responseHeaders);
};
webRequest.onHeadersReceived.addListener(
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
});
const p = Promise.race([
wr,
new Promise<any[]>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
]);
p.finally(() => {
webRequest.onHeadersReceived.removeListener(listener);
});
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(rurl, {
method: "GET",
headers: new Headers({
Range: "bytes=0-1",
}),
signal,
});
if (res.body) {
res.body.cancel();
}
controller.abort();
const headers = await p;
return this.finalize(
new Headers(headers.map(i => [i.name, i.value])), res);
}
private finalize(headers: Headers, res: Response): PrerollResults {
const rv: PrerollResults = {};
const type = MimeType.parse(headers.get("content-type") || "");
if (type) {
rv.mime = type.essence;
}
const {p_ext: ext} = this.download.renamer;
const dispHeader = headers.get("content-disposition");
if (dispHeader) {
const file = CDPARSER.parse(dispHeader);
// Sanitize
rv.name = sanitizePath(file.replace(/[/\\]+/g, "-"));
}
else if (!ext || PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) {
const {searchParams} = this.download.uURL;
let detected = "";
for (const [, value] of searchParams) {
if (!NAME_TESTER.test(value)) {
continue;
}
const p = parsePath(value);
if (!p.base || !p.ext) {
continue;
}
if (!MimeDB.hasExtension(p.ext)) {
continue;
}
if (rv.mime) {
const mime = MimeDB.getMime(rv.mime);
if (mime && !mime.extensions.has(p.ext.toLowerCase())) {
continue;
}
}
const sanitized = sanitizePath(p.name);
if (sanitized.length <= detected.length) {
continue;
}
detected = sanitized;
}
if (detected) {
rv.name = detected;
}
}
rv.finalURL = res.url;
/* eslint-disable no-magic-numbers */
const {status} = res;
if (status === 404) {
rv.error = "SERVER_BAD_CONTENT";
}
else if (status === 403) {
rv.error = "SERVER_FORBIDDEN";
}
else if (status === 402 || status === 407) {
rv.error = "SERVER_UNAUTHORIZED";
}
else if (status === 400 || status === 405 || status === 416) {
PREROLL_NOPE.add(this.download.uURL.host);
if (PREROLL_NOPE.size > 1000) {
PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value);
}
}
else if (status > 400 && status < 500) {
rv.error = "SERVER_FAILED";
}
/* eslint-enable no-magic-numbers */
return rv;
}
}

View File

@ -2,8 +2,12 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import { parsePath, sanitizePath } from "../util";
import { _ } from "../i18n"; import { _ } from "../i18n";
import { MimeDB } from "../mime";
// eslint-disable-next-line no-unused-vars
import { parsePath, PathInfo, sanitizePath } from "../util";
// eslint-disable-next-line no-unused-vars
import { BaseDownload } from "./basedownload";
const REPLACE_EXPR = /\*\w+\*/gi; const REPLACE_EXPR = /\*\w+\*/gi;
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
}); });
export default class Renamer { export default class Renamer {
private readonly d: any; private readonly d: BaseDownload;
constructor(download: any) { private readonly nameinfo: PathInfo;
constructor(download: BaseDownload) {
this.d = download; this.d = download;
const info = parsePath(this.d.finalName);
this.nameinfo = this.fixupExtension(info);
} }
get nameinfo() { private fixupExtension(info: PathInfo): PathInfo {
return parsePath(this.d.finalName); if (!this.d.mime) {
return info;
}
const mime = MimeDB.getMime(this.d.mime);
if (!mime) {
return info;
}
const {ext} = info;
if (mime.major === "image" || mime.major === "video") {
if (ext && mime.extensions.has(ext.toLowerCase())) {
return info;
}
return new PathInfo(info.base, mime.primary, info.path);
}
if (ext) {
return info;
}
return new PathInfo(info.base, mime.primary, info.path);
} }
get ref() { get ref() {
return this.d.uReferrer; return this.d.uReferrer;
} }
get p_name() { get p_name() {
return this.nameinfo.base; return this.nameinfo.base;
} }
@ -184,7 +208,7 @@ export default class Renamer {
(self[prop] || "").trim() : (self[prop] || "").trim() :
type; type;
if (flat) { if (flat) {
return rv.replace(/\/+/g, "-"); return rv.replace(/[/\\]+/g, "-");
} }
return rv; return rv;
})); }));

65
lib/mime.ts Normal file
View 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());
}
}();

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import * as DEFAULT_PREFS from "../data/prefs.json"; import DEFAULT_PREFS from "../data/prefs.json";
import { EventEmitter } from "./events"; import { EventEmitter } from "./events";
import {loadOverlay} from "./objectoverlay"; import { loadOverlay } from "./objectoverlay";
import { storage } from "./browser"; import { storage } from "./browser";
const PREFS = Symbol("PREFS"); const PREFS = Symbol("PREFS");

View File

@ -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)),

View File

@ -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)),

View File

@ -2,8 +2,8 @@
// License: MIT // License: MIT
import * as psl from "psl"; import * as psl from "psl";
import {memoize, identity} from "./memoize"; import { identity, memoize } from "./memoize";
export {debounce} from "../uikit/lib/util"; export { debounce } from "../uikit/lib/util";
export class Promised { export class Promised {
private promise: Promise<any>; private promise: Promise<any>;
@ -96,8 +96,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
export const sanitizePath = identity( export const sanitizePath = identity(
IS_WIN ? sanitizePathWindows : sanitizePathGeneric); IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
export class PathInfo {
private baseField: string;
private extField: string;
private pathField: string;
private nameField: string;
private fullField: string;
constructor(base: string, ext: string, path: string) {
this.baseField = base;
this.extField = ext;
this.pathField = path;
this.update();
}
get base() {
return this.baseField;
}
set base(nv) {
this.baseField = sanitizePath(nv);
this.update();
}
get ext() {
return this.extField;
}
set ext(nv) {
this.extField = sanitizePath(nv);
this.update();
}
get name() {
return this.nameField;
}
get path() {
return this.pathField;
}
set path(nv) {
this.pathField = sanitizePath(nv);
this.update();
}
get full() {
return this.fullField;
}
private update() {
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
}
clone() {
return new PathInfo(this.baseField, this.extField, this.pathField);
}
}
// XXX cleanup + test // XXX cleanup + test
export const parsePath = memoize(function parsePath(path: string | URL) { export const parsePath = memoize(function parsePath(
path: string | URL): PathInfo {
if (path instanceof URL) { if (path instanceof URL) {
path = decodeURIComponent(path.pathname); path = decodeURIComponent(path.pathname);
} }
@ -127,13 +191,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
} }
path = pieces.join("/"); path = pieces.join("/");
return { return new PathInfo(base, ext, path);
path,
name,
base,
ext,
full: path ? `${path}/${name}` : name
};
}); });
export class CoalescedUpdate<T> extends Set<T> { export class CoalescedUpdate<T> extends Set<T> {

View File

@ -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;
} }

View File

@ -3,7 +3,7 @@
import { windows, tabs, runtime } from "../lib/browser"; import { windows, tabs, runtime } from "../lib/browser";
import {getManager} from "./manager/man"; import {getManager} from "./manager/man";
import * as DEFAULT_ICONS from "../data/icons.json"; import DEFAULT_ICONS from "../data/icons.json";
const DONATE_URL = "https://www.downthemall.org/howto/donate/"; const DONATE_URL = "https://www.downthemall.org/howto/donate/";
const MANAGER_URL = "/windows/manager.html"; const MANAGER_URL = "/windows/manager.html";

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "DownThemAll!", "name": "DownThemAll!",
"version": "4.0.8", "version": "4.0.11",
"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,17 @@
"permissions": [ "permissions": [
"<all_urls>", "<all_urls>",
"contextMenus", "contextMenus",
"menus",
"downloads", "downloads",
"downloads.open", "downloads.open",
"downloads.shelf", "downloads.shelf",
"history",
"menus",
"notifications", "notifications",
"sessions",
"storage", "storage",
"tabs", "tabs",
"webNavigation" "webNavigation",
"webRequest"
], ],
"background": { "background": {

View File

@ -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"
} }
} }

View File

@ -295,7 +295,7 @@ class Gatherer {
function gather(msg: any, sender: any, callback: Function) { function gather(msg: any, sender: any, callback: Function) {
try { try {
if (!msg || msg.type !== "DTA:gather" || !callback) { if (!msg || msg.type !== "DTA:gather" || !callback) {
return; return Promise.resolve(null);
} }
const gatherer = new Gatherer(msg); const gatherer = new Gatherer(msg);
const result = { const result = {
@ -313,10 +313,11 @@ function gather(msg: any, sender: any, callback: Function) {
), ),
}; };
urlToUsable(result, result.baseURL); urlToUsable(result, result.baseURL);
callback(result); return Promise.resolve(result);
} }
catch (ex) { catch (ex) {
console.error(ex.toString(), ex.stack, ex); console.error(ex.toString(), ex.stack, ex);
return Promise.resolve(null);
} }
} }

View File

@ -18,12 +18,17 @@
--folder-color: rgb(214, 165, 4); --folder-color: rgb(214, 165, 4);
--maskbutton-color: rgb(236, 185, 16); --maskbutton-color: rgb(236, 185, 16);
--missing-color: rgb(0, 82, 204); --missing-color: rgb(0, 82, 204);
--open-color: rgba(236, 185, 16, 0.8);
} }
html[data-platform="mac"] { html[data-platform="mac"] {
--folder-color: rgb(4, 102, 214); --folder-color: rgb(4, 102, 214);
} }
html, body {
font-size: 10pt !important;
}
@font-face { @font-face {
font-family: 'downthemall'; font-family: 'downthemall';
src: url('downthemall.woff2?75791791') format('woff2'); src: url('downthemall.woff2?75791791') format('woff2');

View File

@ -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;
} }
@ -262,6 +270,7 @@ body > * {
} }
.virtualtable-column-6, .virtualtable-column-6,
.virtualtable-column-4,
.virtualtable-column-3 { .virtualtable-column-3 {
text-align: right; text-align: right;
} }
@ -430,6 +439,8 @@ body > * {
justify-items: stretch; justify-items: stretch;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 2px 6px black; box-shadow: 2px 2px 6px black;
-webkit-user-select: none;
user-select: none;
} }
#tooltip-infos { #tooltip-infos {

View File

@ -232,3 +232,7 @@ body > * {
#maskButton { #maskButton {
justify-self: flex-start; justify-self: flex-start;
} }
#btnDownload {
font-weight: bold;
}

View File

@ -81,3 +81,7 @@ h3 {
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
#btnDownload {
font-weight: bold;
}

View 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
View 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;
});
});

View File

@ -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");

View File

@ -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
View 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;
}

View File

@ -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"))
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
View 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
View 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)

View File

@ -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);
} }
} }

View File

@ -116,7 +116,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 +144,8 @@ export class DownloadItem extends EventEmitter {
private largeIconField?: string; private largeIconField?: string;
public opening: boolean;
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 +161,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 +180,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 +219,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() {
@ -522,8 +524,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",
@ -607,8 +617,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 +751,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) {
@ -777,7 +792,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 +817,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() {
@ -1111,7 +1137,16 @@ export class DownloadTable extends VirtualTable {
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;
} }

View File

@ -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"