292 Commits

Author SHA1 Message Date
82e7361567 Version 4.2.6 2019-11-26 13:13:09 +01:00
5c84493a0f build: cleanup stale bundles 2019-11-26 13:05:16 +01:00
83cb8e32f1 Update Readme.md 2019-11-25 19:14:51 +01:00
c49af54532 Remove compresslevel from build.py
Makes python3.6 choke
2019-11-25 19:08:33 +01:00
eee8c4ea1a Version 4.2.5 2019-11-21 15:07:36 +01:00
858b8f277e Update dev dependencies 2019-11-21 13:25:20 +01:00
6841fdcfc8 New icon 2019-11-21 13:25:20 +01:00
1e8e7ad6ec Use correct grouping 2019-11-21 13:21:02 +01:00
6ed84b9560 Move landing pages into own repo 2019-11-21 12:03:29 +01:00
f739cb789c Adjust some darkmode colors 2019-11-21 11:40:39 +01:00
0d470a7ce0 Handle CRLF in imports
Closes #186
2019-11-21 11:40:39 +01:00
9ec1d46787 Update of Danish translation (#185) 2019-11-21 11:21:47 +01:00
ba283e9221 Version 4.2.4 2019-10-28 11:28:16 +01:00
e549886532 Do not mess up queue when sorting in select
Closes #181
2019-10-28 11:13:19 +01:00
d3b7032229 Version 4.2.3 2019-10-13 13:44:38 +02:00
5586bcb671 Update changelog 2019-10-13 13:44:05 +02:00
bd72c417d2 Remove sounds on Opera
See #125
2019-10-13 13:44:05 +02:00
52643e0bec Update feature_request.md 2019-10-11 17:38:14 +02:00
af59fb60ff Improve bug report template. 2019-10-11 17:37:04 +02:00
ac2bc8cdfd Add privacy notice 2019-10-11 17:26:17 +02:00
c5309a8923 Always import links in the right context
Closes #163
2019-10-11 15:47:16 +02:00
1370723e6d Push raw indexes 2019-10-11 15:47:16 +02:00
4ba827fc15 Make sure we're on background 2019-10-11 15:47:16 +02:00
b2e20b9875 Update zh_CN translation (#161) 2019-10-11 13:39:36 +02:00
ef9cff003d Update ru translation
Closes #160
2019-10-11 13:37:21 +02:00
74b3ce7eb1 Dark mode: improve display of disabled menu items 2019-10-11 13:28:41 +02:00
6528e2118e Show context when no selection
Closes #162
2019-10-11 13:28:18 +02:00
c901438216 Link landing from changelog 2019-10-09 20:56:02 +02:00
7a0718d9cc Version 4.2.2 2019-10-09 19:13:13 +02:00
a7cc3c7fff Fix landing title 2019-10-09 18:59:45 +02:00
9d313f319d Use the correct full path in tooltips 2019-10-09 18:32:22 +02:00
856044c88c Open landing page on installs 2019-10-09 18:26:36 +02:00
de1b13a50f Add landing page 2019-10-09 18:10:40 +02:00
a981b7b8c7 Update nl locale
Closes #157
2019-10-08 22:24:32 +02:00
abe9d82d03 Always notify all observers 2019-10-08 22:24:32 +02:00
49a3f08a9a Update Arabic translation (#158) 2019-10-08 15:12:38 +02:00
afaa75fcdc Update Bulgarian localization (#156) 2019-10-08 15:12:14 +02:00
a5c749412a Work around Firefox window.create({left, top}) issues 2019-10-07 14:55:01 +02:00
e64da40355 Track window state of manager windows
Closes #154
2019-10-07 14:55:00 +02:00
05e7283f9f Update messages.json (#155) 2019-10-07 14:27:19 +02:00
bea8e230fb Update pl translation
Closes #151
2019-10-06 23:04:53 +02:00
23c1ece807 4 more French strings (#152) 2019-10-06 23:02:11 +02:00
539d340f1a Make prefwatcher not stop event chain
See #148
2019-10-06 22:33:23 +02:00
876486bbf5 Update hungarian translation (#149) 2019-10-06 19:34:58 +02:00
9179851c85 Update zh_TW translation (#147) 2019-10-06 08:44:31 +02:00
1e96d7e787 Version 4.2.1 Beta
For translators
2019-10-06 08:12:27 +02:00
612478bcc7 Add license to changelog 2019-10-06 08:12:00 +02:00
23f84fbde0 Use both .currentSrc and .src for image gathering
Works around some lazy-loader issues and make it at least as compatible as v3

Closes #145
2019-10-06 08:00:50 +02:00
b7b4c57034 Update de 2019-10-06 08:00:50 +02:00
872b058d4c Update et (#146) 2019-10-06 07:56:38 +02:00
93ad3e71db Update of lt locale (#144) 2019-10-05 22:39:52 +02:00
7d824bf61e Update Japanese translation 5th (#143) 2019-10-05 22:39:25 +02:00
06228d9ec9 Update messages.json (#141) 2019-10-03 17:04:27 +02:00
f9232ffd96 Update messages.json (#140) 2019-10-03 14:30:31 +02:00
ab3c335bf1 Polish pref window a bit 2019-10-03 09:04:00 +02:00
9142cc023f Add a basic editorconfig 2019-10-03 08:30:45 +02:00
3133a8d8ad Dark theme modals 2019-10-03 08:26:22 +02:00
312f39f7f6 Dark mode scrollbar colors 2019-10-02 22:09:04 +02:00
4ba7bb530d Close unhandled ports 2019-10-02 21:47:38 +02:00
9caad6b3a5 Detect browser theme on Firefox (#139) 2019-10-02 14:54:51 +02:00
5e323db2f0 Make theme configurable (#139) 2019-10-02 14:24:36 +02:00
18daa28cea Initial dark theme (#139) 2019-10-02 13:57:27 +02:00
65c358c01b Update Readme.md 2019-10-02 10:49:09 +02:00
2d14432efe Update TODO.md 2019-10-02 04:43:45 +02:00
19b1cc8856 Add Character batches
Closes #118
2019-10-02 04:37:19 +02:00
883f9a6f0b Add Changelog 2019-10-02 04:05:32 +02:00
e969ba237a Update packages 2019-10-02 04:04:39 +02:00
207248e706 Update ar translation (#90) 2019-10-02 01:55:26 +02:00
9925dec0f4 Use onDeterminingFilename when supported
Closes #132
2019-09-26 08:46:57 +02:00
09b2b4be10 4.1.2 2019-09-26 07:19:43 +02:00
f331de4134 Add Cmd+F shortcut to focus filter box 2019-09-26 07:08:26 +02:00
7760072e8e Avoid the filter box getting an outline in chrome 2019-09-26 07:02:12 +02:00
626bf592de List da locale in all.json 2019-09-26 06:38:49 +02:00
d550de0e27 Add Danish locale
Close #133
2019-09-26 06:37:49 +02:00
367efb4b53 Fix domain parsing in particular IPs
Closes #135
2019-09-26 06:34:23 +02:00
4a82c62958 Update zh_TW translation (#129) 2019-09-23 07:27:39 +02:00
891aa66bff Update zh_CN translation (#126) 2019-09-23 07:24:50 +02:00
2513b60c1b Update ko locale
Closes #127
2019-09-22 17:22:40 +02:00
31cca481f9 Update nl locale
Closes #128
2019-09-22 17:17:24 +02:00
31999bba9f Version 4.1.1 2019-09-21 09:48:56 +02:00
73d90662e3 Remove debug statement 2019-09-21 09:44:38 +02:00
5f5deb09f3 Add audios to DOM to avoid some playback errors 2019-09-21 09:41:40 +02:00
b68245f4f0 Do not treat CRASH as recoverable 2019-09-21 09:34:27 +02:00
34c8537cb5 Update locale list 2019-09-21 09:26:59 +02:00
6b7c9d461d Update ru translation 2019-09-21 09:26:01 +02:00
d2f09ca592 Added Bulgarian localization (#124) 2019-09-21 09:20:55 +02:00
919c6a8f10 more French strings (#120) 2019-09-19 01:52:30 +02:00
dcd7e7cd0e Update pl translation 2019-09-17 13:46:45 +02:00
c545bbab1d Update de locale 2019-09-17 13:46:45 +02:00
dc6e64e690 Add language specific donate URLs 2019-09-17 13:46:45 +02:00
578762db27 Update of lt locale (#117) 2019-09-17 13:39:41 +02:00
e996a2b41a Indonesia translation update (#115) 2019-09-17 03:43:56 +02:00
38496d9161 Hungarian translation update (#114) 2019-09-17 01:17:14 +02:00
4a0756aa26 Updated Spanish translation (#113) 2019-09-16 19:24:57 +02:00
63d0ff22fa Update Italian translation (#112)
Revised and updated translation, which contains work from me and @harbons
2019-09-16 18:31:17 +02:00
5390642978 Update Estonian translation (#111) 2019-09-16 18:20:11 +02:00
2e59dedda3 Japanese translation 4th (#110) 2019-09-16 18:19:33 +02:00
ee649717a2 Update Portuguese translation (#108) 2019-09-16 18:18:41 +02:00
078ce277ce Fix default subfolder suggestion.
Closes #109
2019-09-16 08:12:34 +02:00
7a3cad83b0 Japanese Translation 3rd (#107) 2019-09-15 19:12:53 +02:00
4d953c373f Basic import/export
Closes #64
2019-09-15 12:28:31 +02:00
da9832552f Update Greek translation
Closes #103
2019-09-15 09:51:14 +02:00
5909633a04 Updated Spanish translation (#101) 2019-09-15 09:45:19 +02:00
750fd987bd Update French translation
Closes #102
2019-09-15 09:43:01 +02:00
b87e0d6138 Japanese Translation 2nd (#100) 2019-09-14 21:33:40 +02:00
31cb23923a Add a subfolders dropdown
The dropdown will help users switch between folders easier than using the mask.
Also, the dropdown is more discoverable than masked folders.

See #2
2019-09-14 01:07:29 +02:00
71d98bc603 Use correct rows for tooltip ETA/status 2019-09-14 00:00:20 +02:00
4b09a0db67 Retries
Closes #95
2019-09-14 00:00:11 +02:00
da6c6bcf68 User Canceled error message 2019-09-13 23:09:44 +02:00
84fea3ba35 Allow to open Manager in a popup
Closes #47
2019-09-13 23:09:44 +02:00
33de1cbce9 Allow setting the referrer in select
Closes #78
2019-09-13 23:09:43 +02:00
04b8a981ef Allow to choose button action
Closes #51
2019-09-13 23:09:43 +02:00
58c7955c64 Add Delete Files action
Closes #92
2019-09-13 23:09:43 +02:00
dcf9603da8 Play done sound
Cloes #81
2019-09-13 23:09:43 +02:00
c2b9664b4b Version 4.0.12 2019-09-13 22:31:55 +02:00
e760d2b022 Handle Chrome state changes correct. 2019-09-12 20:54:16 +02:00
1a836d914b Preroll-nope 401, 402
Closes #89
2019-09-12 13:30:06 +02:00
1b0e6eb6c4 Send referrer with prerolls
See #89
2019-09-12 13:26:24 +02:00
39827ad485 Update TODO 2019-09-12 11:22:55 +02:00
79c4d4e98f typos in the de-locale 2019-09-12 10:00:36 +02:00
427bd2f348 Actually add Italian translation 2019-09-11 23:04:39 +02:00
4fefd0e128 Arabic translation
Closes #90
2019-09-11 23:03:25 +02:00
d79060237d Silence window-type errors 2019-09-10 14:43:49 +02:00
2df7a1c592 do not force ascii in addlocale 2019-09-10 14:43:48 +02:00
8c4ceb3e4b Privetize downloads 2019-09-10 12:44:56 +02:00
bf725ece72 Open links in the correct window context 2019-09-10 12:43:47 +02:00
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
6e55ee0745 Version 4.0.8 2019-09-03 13:32:09 +02:00
56098a382e Actually get most recent window 2019-09-03 13:23:00 +02:00
5ba9c7179b Korean locale
Closes #54
2019-09-03 13:07:10 +02:00
be9256ff1f Add Greek locale
Closes #53
2019-09-03 13:00:59 +02:00
92b2c32dd3 Add Russian locale
Closes #52
2019-09-03 12:57:48 +02:00
1935c7f444 Typos 2019-09-03 08:30:17 +02:00
4d48a2c395 Implement locale switcher 2019-09-03 08:24:29 +02:00
3cf30aaf08 Use _ version of language codes
Closes #50
2019-09-03 08:24:29 +02:00
262c3e169b Update TODO.md 2019-09-02 20:31:39 +02:00
2a0cb6720f Update Readme.md 2019-09-02 20:29:57 +02:00
295077dd75 Convert cs to new Chrome-compat
See: #46
2019-09-02 19:32:29 +02:00
c484f82cf5 Added czech translation 2019-09-02 19:29:27 +02:00
05aaac7ed8 Convert id translation to new Chrome-compatible format
See #45
2019-09-02 19:27:57 +02:00
9ef497028c Update text 2019-09-02 19:24:41 +02:00
380325bd43 Indonesian translation 2019-09-02 19:24:41 +02:00
164aa99eca Initial Chrome support
Part of #35
2019-09-02 18:05:06 +02:00
0a9155dcec Implement system icons
Closes #44
2019-09-02 14:52:14 +02:00
0bf9e76441 Fix incorrectly linked remove-missing-on-init 2019-09-02 14:45:31 +02:00
392681c1b7 Fix multi-filters
Closes #43
2019-09-02 13:33:38 +02:00
d4024a16ad Delay updates while queue is running
Closes #42
2019-09-02 13:16:39 +02:00
fba985482c Version 4.0.7 2019-09-02 12:05:13 +02:00
094fb0ee84 Fix additional placeholder in zh-CN 2019-09-02 12:04:39 +02:00
2ccc12de90 Version 4.0.6 2019-09-02 11:53:03 +02:00
28e6866db8 Use currentWindow instead of WINDOW_ID_CURRENT 2019-09-02 04:06:28 +02:00
7185964649 emulate must use correct window
Closes #41
2019-09-02 04:02:50 +02:00
cd1005823d Typo fixes 2019-09-02 03:57:21 +02:00
0afceb9850 Dutch translation
Closes #39
2019-09-02 00:30:45 +02:00
e6dc205b9d Small de improvement 2019-09-02 00:26:35 +02:00
8e473c778b Rename to zh-CN
Related to #38
2019-09-02 00:26:15 +02:00
7a71ae5f37 Also split ui locales by _ 2019-09-02 00:24:11 +02:00
d04f3db22f Add Simplified Chinese translation
Created this translation by using the online translation tool from https://downthemall.github.io/translate/ .
2019-09-02 00:23:51 +02:00
adc6b9dbb2 Add et locale 2019-09-02 00:03:29 +02:00
e8f09c80f3 Do not add empty separator in URLMenuFilter 2019-09-01 15:24:14 +02:00
46c4e66558 webpack config updates 2019-09-01 06:17:05 +02:00
a8ea416a67 Remove smartphone section from issue template 2019-09-01 05:06:30 +02:00
320c1ddafa Add a little margin to allow focus outlines 2019-09-01 05:01:49 +02:00
4c576ba720 Add content_security_policy 2019-09-01 04:52:57 +02:00
7b58779f9e Remove outdated todos 2019-09-01 00:37:10 +02:00
45d835fe19 use correct autoHide HTML spelling 2019-09-01 00:03:53 +02:00
2d2826d192 Correctly toggle URLMenuFilter 2019-09-01 00:03:34 +02:00
4c77ad0f1f Fix popup height flash bug 2019-08-31 23:16:46 +02:00
4d72ac4534 Notifications galore 2019-08-31 23:08:12 +02:00
cdda0835d8 Wrong translation 2019-08-31 18:12:37 +02:00
78e91304eb Version 4.0.5 2019-08-31 15:44:35 +02:00
0702631003 nag a little later 2019-08-31 13:40:54 +02:00
c45bf671fb fixes to polish translation (#34) 2019-08-31 13:38:47 +02:00
33a3e275fc Create messages.json 2019-08-31 08:16:29 +02:00
369514f155 Add lt locale 2019-08-30 17:02:08 +02:00
494479ce1a Version 4.0.4 2019-08-29 23:09:14 +02:00
98ebb160f9 Silence a console.log 2019-08-29 19:37:09 +02:00
d1cc406f05 select: frontend/backend mappings were out of sync
Closes #27
2019-08-29 19:36:06 +02:00
687b6e1aa9 pl locale
Closes #19
2019-08-29 09:52:26 +02:00
c960d48b72 es-ES locale
Closes #26
2019-08-29 08:24:11 +02:00
c8c7506efc Properly translate Filters heading
Reported in #26
2019-08-29 08:14:22 +02:00
91edcee28c FIx filters message names 2019-08-29 08:14:22 +02:00
a425a786ef Do not log "custom" 2019-08-29 08:14:22 +02:00
f023351acc Update Readme.md 2019-08-28 18:39:44 +02:00
c8610eee29 Add some additional file exts to the def filters 2019-08-28 17:59:29 +02:00
f7a70ec2ea Add fr-FR locale 2019-08-28 17:16:34 +02:00
69d8ffe8a5 Minor typos 2019-08-28 05:52:32 +02:00
71240ec1e8 More typos 2019-08-28 05:45:06 +02:00
a6f3c7a647 Some typos 2019-08-28 05:41:41 +02:00
eab9631a11 Fix typo 2019-08-28 04:10:24 +02:00
9e29db911c Update dependencies 2019-08-27 19:37:55 +02:00
8d1040115a Update information about translations 2019-08-27 19:32:11 +02:00
3551545eae add DE locale 2019-08-27 19:12:36 +02:00
01001dc7b2 Add missing i18n and fix some typos 2019-08-27 18:53:31 +02:00
b4e6ab80d2 Add locale descriptions 2019-08-27 16:43:49 +02:00
d0f6c4f7f3 Version 4.0.3 2019-08-27 04:49:37 +02:00
5b05d52886 Remove url hashes 2019-08-27 04:49:37 +02:00
07a3ec3a7b Use correct action in popup 2019-08-27 04:18:51 +02:00
0f15a4a068 Add translation guide link 2019-08-27 04:13:02 +02:00
c1e5f63935 Typos 2019-08-27 03:34:15 +02:00
6e4a338789 Add bits about translations 2019-08-27 03:26:33 +02:00
37a97b73b3 Translations info 2019-08-27 03:23:44 +02:00
fe61f6176c Load custom translations
Part of #14
2019-08-27 03:02:17 +02:00
48bde9cfdb Create contextual menus first 2019-08-26 20:43:34 +02:00
256c091f15 popup: handle right click too 2019-08-26 20:40:20 +02:00
b4ce2d1d75 Drop unused icons 2019-08-26 19:54:57 +02:00
29fd59c8fd Increase recent lists to 15
Because why not?!
Also closes #15
2019-08-26 19:51:30 +02:00
544b7d522c less of any 2019-08-26 19:50:06 +02:00
116d5b9b00 Fix dialogs 2019-08-26 16:12:15 +02:00
976c57c043 i18n: rely less on exceptions 2019-08-26 02:57:06 +02:00
d00b25cbe7 Add language and language_code to locales 2019-08-26 02:47:53 +02:00
572bab27a0 menu handlers are not necessary promises 2019-08-26 02:40:54 +02:00
8235af22db Implement own locale loader
Why?
* Because i18n sucks multi-browser, especially on Chrome.
* What's more, this will allow overlaying "incomplete" locales with
  missing strings over the base locale and get a semi-translated one,
  which probably is better than nothing.
* Additionally we kinda need that implementation anyway for node-based
  tests.
* Also, while not currently implemented, it could allow (hot) reloading
  of locales, or loading external ones, which would help translators,
  or providing an option to the user to choose a locale.
* And finally, not calling i18n will avoid the "context switch" into
  browserland.

Some implementation details:
* Before code can use a locale, it has to be loaded. Sadly sync loading
  is not really supported. So `await locale` or `await localize`.
* Background force reloads locales right now, and caches them in
  localStorage. Windows will look into localStorage for that cache.
* The locale loader will not verify locales other than some rudimentary
  checks. It is assumed that shipped locales where verified before
  check-in.
2019-08-26 02:37:27 +02:00
2bfb3d5363 Always show status in tip 2019-08-25 23:35:07 +02:00
7dc4dd9da6 Better error messages (somewhat) 2019-08-25 23:30:05 +02:00
c7c111e1c0 Option to hide contexts
Closes #4
2019-08-25 23:22:47 +02:00
a9a811d96b Oops 2019-08-25 23:14:54 +02:00
801eaa819b Egalize menu and popup 2019-08-25 23:01:36 +02:00
d6539c5f96 Add .custom to newly created filters too 2019-08-25 22:31:20 +02:00
a89acad0c9 Proper types for localize and $ 2019-08-25 22:30:07 +02:00
70c7e0b0f3 Avoid update races 2019-08-25 21:31:38 +02:00
af8b05d20c Allow the browserAction button to be turbo again
Closes #13
2019-08-25 21:31:38 +02:00
d98c4e318a Clear selection errors on changes
Closes #12
2019-08-25 21:07:12 +02:00
176060183e Select the proper turbo list 2019-08-25 20:55:54 +02:00
26cd8e8a00 Actually store the last-type 2019-08-25 20:55:44 +02:00
a9df98c2f6 Overhault fast filter matching
Closes #11
2019-08-25 20:55:26 +02:00
ab2b6e40af Add License to popup 2019-08-25 14:31:37 +02:00
112d37deb0 Reduce load size of popup 2019-08-25 14:27:54 +02:00
33bde621f9 Remove bit about referrers 2019-08-24 21:19:53 +02:00
958d58a408 Fix button icon positioning (on Windows) 2019-08-24 21:19:00 +02:00
229b5eb968 vcenter icons 2019-08-24 21:08:18 +02:00
2f282d3a4b Implement All Tabs actions
Closes #3
2019-08-24 07:30:34 +02:00
a9f83071dc Cleanup popup 2019-08-24 07:23:56 +02:00
7bfffd7598 Rely on startNext to invoke maybeNotifyFinished
Closes #6
2019-08-24 06:34:37 +02:00
545d78ad61 Implement a popup menu for the browserAction
Closes #10
2019-08-24 06:28:18 +02:00
0463471704 It is Port.sender.id these days 2019-08-24 02:52:48 +02:00
34ea21b3ea memoize getMessage correctly 2019-08-24 02:52:23 +02:00
d3c9f8bc89 send Referer-s
Clsoes #7
2019-08-24 02:52:04 +02:00
4236195ccf Remove the dta tag header 2019-08-24 00:59:45 +02:00
10ff6f1c11 Set error text when failure to fly 2019-08-24 00:56:41 +02:00
151 changed files with 34596 additions and 2220 deletions

View File

@ -1,12 +1,17 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve DownThemAll!
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
**Desktop (please complete the following information):**
- OS: [e.g. Windows 10, macOS, Linux (distribution, desktop environment)]
- Browser and version: [e.g. Firefox 42, Chrome 70, Opera 15, Seamonkey 2.16]
- DownThemAll! version: [e.g. 4.2, 3.0, latest]
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@ -23,16 +28,6 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@ -1,6 +1,6 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest a feature or an idea for DownThemAll!
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''

View File

@ -30,7 +30,7 @@ THE SOFTWARE.
## DownThemAll! uikit ## DownThemAll! uikit
Copyright © 2016-2019 by Nils Maier Copyright © 2016-2019 by Nils Maier
The uikit libraries and assets are licened under the MIT license. The uikit libraries and assets are licensed under the MIT license.
## DownThemAll! interface (.html, .css) ## DownThemAll! interface (.html, .css)
@ -41,7 +41,8 @@ Licensed under GPL2.0; see [LICENSE.gpl-2.0.txt](LICENSE.gpl-2.0.txt).
## DownThemAll! icons, icon-font and graphic assets ## DownThemAll! icons, icon-font and graphic assets
Copyright (C) 2012-2019 by Nils Maier Copyright (C) 2012-2019 by Nils Maier
Licensed under Creative Commons Attribution-ShareAlike 4.0 International Licensed under Creative Commons Attribution-ShareAlike 4.0 International.
The icon font contains icons from Font Awesome. The icon font contains icons from Font Awesome.
See: https://creativecommons.org/licenses/by-sa/4.0/legalcode See: https://creativecommons.org/licenses/by-sa/4.0/legalcode
@ -54,21 +55,35 @@ Copyright © 2010-2019 by Nils Maier, Stefano Verna.
The DownThemAll! name and logo cannot be used without explicit permission The DownThemAll! name and logo cannot be used without explicit permission
in any derivative work, except in credits and license-related notices. in any derivative work, except in credits and license-related notices.
Using the DownThemAll! logo in personal non-distributed non-commerical Using the DownThemAll! logo in personal non-distributed non-commercial
modifications of the software and forks is permitted without explicit modifications of the software and forks is permitted without explicit
permission. permission.
Distributing official DownThemAll! releases without any modifications is allowed without explicit permission.
## Font Awesome ## Font Awesome
Copyright (C) 2016 by Dave Gandy Copyright (C) 2016 by Dave Gandy
License: SIL () License: SIL ()
Homepage: http://fortawesome.github.com/Font-Awesome/ Homepage: http://fortawesome.github.com/Font-Awesome/
## webextension-polyfill ## webextension-polyfill
Lcensed under the Mozilla Public License 2.0. Licensed under the Mozilla Public License 2.0.
## PSL (public-suffix-list) ## PSL (public-suffix-list)
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

@ -1,12 +1,14 @@
DownThemAll! WE
=== ![DownThemAll!](https://raw.githubusercontent.com/downthemall/downthemall/master/style/icon128.png)
# DownThemAll! WE
The DownThemAll! WebExtension. The DownThemAll! WebExtension.
For those still on supported browser: [Non-WebExtension legacy code](https://github.com/downthemall/downthemall-legacy). For those still on supported browser: [Non-WebExtension legacy code](https://github.com/downthemall/downthemall-legacy).
About ## About
---
This is the WebExtension version of DownThemAll!, a complete re-development from scratch. This is the WebExtension version of DownThemAll!, a complete re-development from scratch.
Being a WebExtension it lacks a ton of features the original DownThemAll! had. Sorry, but there is no way around it since Mozilla decided to adopt WebExtensions as the *only* extension type and WebExtensions are extremely limited in what they can do. Being a WebExtension it lacks a ton of features the original DownThemAll! had. Sorry, but there is no way around it since Mozilla decided to adopt WebExtensions as the *only* extension type and WebExtensions are extremely limited in what they can do.
@ -23,19 +25,79 @@ But it is what it is...
**What we *can* do and did do is bring the mass selection, organizing (renaming masks, etc) and queueing tools of DownThemAll! over to the WebExtension, so you can easily queue up hundreds or thousands files at once without the downloads going up in flames because the browser tried to download them all at once.** **What we *can* do and did do is bring the mass selection, organizing (renaming masks, etc) and queueing tools of DownThemAll! over to the WebExtension, so you can easily queue up hundreds or thousands files at once without the downloads going up in flames because the browser tried to download them all at once.**
## Translations
Development If you would like to help out translating DTA, please see our [translation guide](_locales/Readme.md).
---
You will want to `yarn` the development dependencies such as webpack first. ## Development
Afterwards there is two important commands to run ### Requirements
* `yarn watch` - This will run the webpack bundler in watch mode, updating bundles as you change the source. - [node](https://nodejs.org/en/)
* `yarn webext` - This will run the WebExtension in a development profile using the [`web-ext` tool from mozilla](https://www.npmjs.com/package/web-ext) (which you need to install separately). - [yarn](https://yarnpkg.com/)
- [python3](https://www.python.org/) >= 3.6 (to build zips)
- [web-ext](https://www.npmjs.com/package/web-ext) (for development ease)
### Setup
You will want to run `yarn` to install the development dependencies such as webpack first.
### Making changes
Just use your favorite text editor to edit the files.
You will want to run`yarn watch`.
This will run the webpack bundler in watch mode, transpiling the TypeScript to Javascript and updating bundles as you change the source.
Please note: You have to run `yarn watch` or `yarn build` (at least once) as it builds the actual script bundles.
### Running in Firefox
I recommend you install the [`web-ext`](https://www.npmjs.com/package/web-ext) tools from mozilla. It is not listed as a dependency by design at it causes problems with dependency resolution in yarn right now if installed in the same location as the rest of the dependencies.
If you did, then running `yarn webext` (additionally to `yarn watch`) will run the WebExtension in a development profile. This will use the directory `../dtalite.p` to keep a development profile. You might need to create this directory before you use this command. Furthermore `yarn webext` will watch for changes to the sources and automatically reload the extension.
Alternative, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox). Alternatively, you can also `yarn build`, which then builds an *unsigned* zip that you can then install permanently in a browser that does not enforce signing (i.e. Nightly or the Unbranded Firefox with the right about:config preferences).
Before submitting patches, please make sure you run eslint, if this isn't done automatically, and eslint does not report any open issues. Code contributions should favor typescript code over javascript code. External dependencies that would ship with the final product (including all npm/yarn packages) should be kept to a bare minimum. ### Running in Chrome/Chromium/etc
The code base is comparatively large for a WebExtension, with over 10K sloc of typescript and over 14K sloc total. You have to build the bundles first, of course.
Then put your Chrome into Developement Mode on the Extensions page, and Load Unpacked the directory of your downthemall clone.
### Making release zips
To get a basic unofficial set of zips for Firefox and chrome, run `yarn build`.
If you want to generate release builds like the ones that are eventually released in the extension stores, use `python3 util/build.py --mode=release`.
The output is located in `web-ext-artifacts`.
- `-fx.zip` are Firefox builds
- `-crx.zip` are Chrome/Chromium builds
- `-opr.zip` are Opera builds (essentially like the Chrome one, but without sounds)
### The AMO Editors tl;dr guide
1. Install the requirements.
2. `yarn && python3 build/util.py --mode=release`
3. Have a look in `web-ext-artifacts/dta-*-fx.zip`
### Patches
Before submitting patches, please make sure you run eslint (if this isn't done automatically in your text editor/IDE), and eslint does not report any open issues. Code contributions should favor typescript code over javascript code. External dependencies that would ship with the final product (including all npm/yarn packages) should be kept to a bare minimum and need justification.
Please submit your patches as Pull Requests, and rebase your commits onto the current `master` before submitting.
### Code structure
The code base is comparatively large for a WebExtension, with over 11K sloc of typescript.
It isn't as well organized as it should be in some places; hope you don't mind.
* `uikit/` - The base User Interface Kit, which currently consists of
* the `VirtualTable` implementation, aka that interactive HTML table with columns, columns resizing and hiding, etc you see in the Manager, Select and Preferences windows/tabs
* the `ContextMenu` and related classes that drive the HTML-based context menus
* `lib/` - The "backend stuff" and assorted library routines and classes.
* `windows/` - The "frontend stuff" so all the HTML and corresponding code to make that HTML into something interactive
* `style/` - CSS and images

20
TODO.md
View File

@ -1,25 +1,14 @@
TODO TODO
--- ---
aka a lot
P2 P2
=== ===
Planned for later. Planned for later.
* Investigate using an action popup for the browser action
* Soft errors and retry logic
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
* Delete files (well, as far as the browser allows)
* Inter-addon API (basic) * Inter-addon API (basic)
* Add downloads * Add downloads
* Chrome support
* vtable perf: cache column widths * vtable perf: cache column widths
* Localizations
* Settle on system
* Do the de-locale
* Enagage translators
* Download options * Download options
* This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done. * This is a bit more limited, as we cannot modify options of downloads that have been started (and paused) or that are done.
@ -35,12 +24,7 @@ Nice-to-haves.
* Manipulate downloads (e.g. rewrite URLs) * Manipulate downloads (e.g. rewrite URLs)
* Native context menus? * Native context menus?
* Would require massive reworks incl the need for new icon formats, but potentially feasible. * Would require massive reworks incl the need for new icon formats, but potentially feasible.
* Import/Export
* Download priorities (manual scheduling overrides) * Download priorities (manual scheduling overrides)
* Dark Theme support
* os/browser define be default
* overwritable
* Get and cache system icons (because Firefox doesn't allow moz-icon: for WE, but makes them kinda accessible through the downloads API anyway, essentially copying them via a canvas on a privileged hidden page into a data URL... ikr)
* Remove `any` types as possible, and generally improve typescript (new language to me) * Remove `any` types as possible, and generally improve typescript (new language to me)
P4 P4
@ -52,14 +36,10 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list. * Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
* Segmented downloads * Segmented downloads
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations. * Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
* Handle errors, 404 and such
* The Firefox download manager is too stupid and webRequest does not see Downloads, so cannot be done right now.
* Conflicts: ask when a file exists * Conflicts: ask when a file exists
* Not supported by Firefox * Not supported by Firefox
* Speed limiter * Speed limiter
* Cannot be done with the WebExtensions downloads API * Cannot be done with the WebExtensions downloads API
* Actually send referrers for downloads
* Cannot be done with WebExtensions - webRequest does not see Downloads
* contenthandling aka video sniffing, request manipulation? * contenthandling aka video sniffing, request manipulation?
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient * PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
* Checksums/Hashes? * Checksums/Hashes?

33
_locales/Readme.md Normal file
View File

@ -0,0 +1,33 @@
# Translations
Right now we did not standardize on a tool/website/community use for translations
## Website-based Translation
Please go to [https://downthemall.github.io/translate/](https://downthemall.github.io/translate/) for a "good enough" tool to translate DownThemAll! for now. It will load the English locale as a base automatically.
Then you can translate (your progress will be saved in the browser). Once done, you can Download the `messages.json` and test it or submit it for inclusion.
You can also import your or other people's existing translations to modify. This will overwrite any progress you made so far, tho.
## Manual Translation
* Get the [`en/messages.json`](https://github.com/downthemall/downthemall/raw/master/_locales/en/messages.json) as a base.
* Translate the `"message"` items in that file only. Whip our your favorite text editor, JSON editor, special translation tool, what have you.
* Do not translate anything besides the "message" elements. Pay attention to the descriptions.
* Do not remove anything.
* Do not translate `$PLACEHOLDERS$`. Placeholders should appear in your translation with the same spelling and all uppercase.
They will be relaced at runtime with actual values.
* Make sure you save the file in an "utf-8" encoding. If you need double quotes, you need to escape the quotes with a backslash, e.g. `"some \"quoted\" text"`
* You should translate all strings. If you want to skip a string, set it to an empty `""` string. DTA will then use the English string.
## Testing Your Translation
* Go to the DownThemAll! Preferences where you will find a "Load custom translation" button.
* Select your translated `messages.json`. (it doesn't have to be named exactly like that, but should have a `.json` extension)
* If everything was OK, you will be asked to reload the extension (this will only reload DTA not the entire browser).
* See your strings in action once you reloaded DTA (either by answering OK when asked, or disable/enable the extension manually or restart your browser).
## Submitting Your Translation
If you're happy with the result and would like to contribute it back, you can either file a full Pull Request, or just file an issue and post a link to e.g. a [gist](https://gist.github.com/) or paste the translation in the issue text.

24
_locales/all.json Normal file
View File

@ -0,0 +1,24 @@
{
"ar": "العربية [ar]",
"bg": "Български [bg]",
"cs": "Čeština (CZ) [cs]",
"da": "Dansk [da]",
"de": "Deutsch [de]",
"el": "Ελληνικά [el]",
"en": "English (US) [en]",
"es": "Español (España) [es]",
"et": "Eesti keel [et]",
"fr": "Français [fr]",
"hu": "Magyar (HU) [hu]",
"id": "Bahasa Indonesia [id]",
"it": "Italiano [it]",
"ja": "日本語 (JP) [ja]",
"ko": "한국어 [ko]",
"lt": "Lietuvių [lt]",
"nl": "Nederlands [nl]",
"pl": "Polski [pl]",
"pt": "Português (Brasil) [pt]",
"ru": "Русский [ru]",
"zh_CN": "简体中文 [zh_CN]",
"zh_TW": "正體中文 [zh_TW]"
}

1300
_locales/ar/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/bg/messages.json Normal file

File diff suppressed because it is too large Load Diff

1170
_locales/cs/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/da/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/de/messages.json Normal file

File diff suppressed because it is too large Load Diff

1264
_locales/el/messages.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1300
_locales/es/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/et/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/fr/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/hu/messages.json Normal file

File diff suppressed because it is too large Load Diff

1136
_locales/id/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/it/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/ja/messages.json Normal file

File diff suppressed because it is too large Load Diff

1284
_locales/ko/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/lt/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/nl/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/pl/messages.json Executable file

File diff suppressed because it is too large Load Diff

1300
_locales/pt/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/ru/messages.json Executable file

File diff suppressed because it is too large Load Diff

1300
_locales/zh_CN/messages.json Normal file

File diff suppressed because it is too large Load Diff

1300
_locales/zh_TW/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
}, },
"deffilter-aud": { "deffilter-aud": {
"label": "Audio", "label": "Audio",
"expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape)$/i", "expr": "/\\.(?:mp3|wav|og(?:g|a)|flac|midi?|rm|aac|wma|mka|ape|opus)$/i",
"type": 1, "type": 1,
"active": false, "active": false,
"icon": "mp3" "icon": "mp3"
@ -35,7 +35,7 @@
}, },
"deffilter-img": { "deffilter-img": {
"label": "Images", "label": "Images",
"expr": "/\\.(?:jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico)$/i", "expr": "/\\.(?:jp(?:e?g|e|2)|gif|png|tiff?|bmp|ico|heic|heif|webp|jxr|wdp|dng|cr2|arw)$/i",
"type": 3, "type": 3,
"active": false, "active": false,
"icon": "jpg" "icon": "jpg"
@ -63,7 +63,7 @@
}, },
"deffilter-vid": { "deffilter-vid": {
"label": "Videos", "label": "Videos",
"expr": "/\\.(?:mpeg|ra?m|avi|mp(?:g|e|4)|mov|divx|asf|qt|wmv|m\\dv|rv|vob|asx|ogm|ogv|webm|flv|mkv)$/i", "expr": "/\\.(?:mpeg|ra?m|avi|mp(?:g|e|4)|mov|divx|asf|qt|wmv|m\\dv|rv|vob|asx|ogm|ogv|webm|flv|mkv|f4v|m4v)$/i",
"type": 3, "type": 3,
"active": true, "active": true,
"icon": "mkv" "icon": "mkv"

View File

@ -80,7 +80,11 @@
"tif", "tif",
"tiff", "tiff",
"wmf", "wmf",
"webp" "webp",
"heic",
"heif",
"jxr",
"wdp"
], ],
"video": [ "video": [
"3g2", "3g2",

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

@ -1,17 +1,23 @@
{ {
"global-turbo": false, "button-type": "popup",
"manager-in-popup": false,
"concurrent": 4, "concurrent": 4,
"queue-notification": true, "queue-notification": true,
"finish-notification": true, "finish-notification": true,
"sounds": true,
"open-manager-on-queue": true, "open-manager-on-queue": true,
"text-links": true, "text-links": true,
"add-paused": false, "add-paused": false,
"hide-context": false,
"conflict-action": "uniquify", "conflict-action": "uniquify",
"nagging": 0, "nagging": 0,
"nagging-next": 6, "nagging-next": 7,
"tooltip": true, "tooltip": true,
"show-urls": false, "show-urls": false,
"remove-missing-on-init": false, "remove-missing-on-init": false,
"retries": 5,
"retry-time": 10,
"theme": "default",
"limits": [ "limits": [
{ {
"domain": "*", "domain": "*",

View File

@ -5,25 +5,33 @@ import { TYPE_LINK, TYPE_MEDIA } from "./constants";
import { filters } from "./filters"; import { filters } from "./filters";
import { Prefs } from "./prefs"; import { Prefs } from "./prefs";
import { lazy } from "./util"; import { lazy } from "./util";
import { Item, makeUniqueItems } from "./item"; // eslint-disable-next-line no-unused-vars
import { Item, makeUniqueItems, BaseItem } from "./item";
import { getManager } from "./manager/man"; import { getManager } from "./manager/man";
import { select } from "./select"; import { select } from "./select";
import { single } from "./single"; import { single } from "./single";
import { Notification } from "./notifications"; import { Notification } from "./notifications";
import { MASK, FASTFILTER } from "./recentlist"; import { MASK, FASTFILTER, SUBFOLDER } from "./recentlist";
import { openManager } from "./windowutils"; import { openManager } from "./windowutils";
import { _ } from "./i18n"; import { _ } from "./i18n";
const MAX_BATCH = 10000; const MAX_BATCH = 10000;
export const API = new class { export interface QueueOptions {
async filter(arr: any, type: number) { mask?: string;
return (await filters()).filterItemsByType(arr, type); subfolder?: string;
paused?: boolean;
}
export const API = new class APIImpl {
async filter(arr: BaseItem[], type: number) {
return await (await filters()).filterItemsByType(arr, type);
} }
async queue(items: any, options: any) { async queue(items: BaseItem[], options: QueueOptions) {
await MASK.init(); await Promise.all([MASK.init(), SUBFOLDER.init()]);
const {mask = MASK.current} = options; const {mask = MASK.current} = options;
const {subfolder = SUBFOLDER.current} = options;
const {paused = false} = options; const {paused = false} = options;
const defaults: any = { const defaults: any = {
@ -36,13 +44,11 @@ export const API = new class {
fileName: null, fileName: null,
title: "", title: "",
description: "", description: "",
fromMetalink: false,
startDate: new Date(), startDate: new Date(),
hashes: [],
private: false, private: false,
postData: null, postData: null,
cleanRequest: false,
mask, mask,
subfolder,
date: Date.now(), date: Date.now(),
paused paused
}; };
@ -54,7 +60,7 @@ export const API = new class {
} }
return currentBatch; return currentBatch;
}); });
items = items.map((i: any) => { items = items.map(i => {
delete i.idx; delete i.idx;
return new Item(i, defaults); return new Item(i, defaults);
}); });
@ -79,7 +85,7 @@ export const API = new class {
} }
} }
sanity(links: any[], media: any[]) { sanity(links: BaseItem[], media: BaseItem[]) {
if (!links.length && !media.length) { if (!links.length && !media.length) {
new Notification(null, _("no-links")); new Notification(null, _("no-links"));
return false; return false;
@ -87,48 +93,57 @@ export const API = new class {
return true; return true;
} }
async turbo(links: any[], media: any[]) { async turbo(links: BaseItem[], media: BaseItem[]) {
if (!this.sanity(links, media)) { if (!this.sanity(links, media)) {
return false; return false;
} }
const selected = makeUniqueItems([ const type = await Prefs.get("last-type", "links");
await API.filter(links, TYPE_LINK), const items = await (async () => {
await API.filter(media, TYPE_MEDIA), if (type === "links") {
]); return await API.filter(links, TYPE_LINK);
}
return await API.filter(media, TYPE_MEDIA);
})();
const selected = makeUniqueItems([items]);
if (!selected.length) { if (!selected.length) {
return await this.regular(links, media); return await this.regular(links, media);
} }
return await this.queue(selected, {paused: await Prefs.get("add-paused")}); return await this.queue(selected, {paused: await Prefs.get("add-paused")});
} }
async regularInternal(selected: any) { async regularInternal(selected: BaseItem[], options: any) {
if (selected.mask && !selected.maskOnce) { if (options.mask && !options.maskOnce) {
await MASK.init(); await MASK.init();
await MASK.push(selected.mask); await MASK.push(options.mask);
} }
if (typeof selected.fast === "string" && !selected.fastOnce) { if (typeof options.fast === "string" && !options.fastOnce) {
await FASTFILTER.init(); await FASTFILTER.init();
await FASTFILTER.push(selected.fast); await FASTFILTER.push(options.fast);
} }
const {items} = selected; if (typeof options.subfolder === "string" && !options.subfolderOnce) {
delete selected.items; await SUBFOLDER.init();
return await this.queue(items, selected); await SUBFOLDER.push(options.subfolder);
}
if (typeof options.type === "string") {
await Prefs.set("last-type", options.type);
}
return await this.queue(selected, options);
} }
async regular(links: any[], media: any[]) { async regular(links: BaseItem[], media: BaseItem[]) {
if (!this.sanity(links, media)) { if (!this.sanity(links, media)) {
return false; return false;
} }
const selected = await select(links, media); const {items, options} = await select(links, media);
return this.regularInternal(selected); return this.regularInternal(items, options);
} }
async singleTurbo(item: any) { async singleTurbo(item: BaseItem) {
return await this.queue([item], {paused: await Prefs.get("add-paused")}); return await this.queue([item], {paused: await Prefs.get("add-paused")});
} }
async singleRegular(item: any) { async singleRegular(item: BaseItem | null) {
const selected = await single(item); const {items, options} = await single(item);
return this.regularInternal(selected); return this.regularInternal(items, options);
} }
}(); }();

View File

@ -5,7 +5,7 @@ import { ALLOWED_SCHEMES, TRANSFERABLE_PROPERTIES } from "./constants";
import { API } from "./api"; import { API } from "./api";
import { Finisher, makeUniqueItems } from "./item"; import { Finisher, makeUniqueItems } from "./item";
import { Prefs } from "./prefs"; import { Prefs } from "./prefs";
import { _ } from "./i18n"; import { _, locale } from "./i18n";
import { openPrefs, openManager } from "./windowutils"; import { openPrefs, openManager } from "./windowutils";
import { filters } from "./filters"; import { filters } from "./filters";
import { getManager } from "./manager/man"; import { getManager } from "./manager/man";
@ -13,14 +13,46 @@ import {
browserAction as action, browserAction as action,
menus as _menus, contextMenus as _cmenus, menus as _menus, contextMenus as _cmenus,
tabs, tabs,
webNavigation as nav webNavigation as nav,
// eslint-disable-next-line no-unused-vars
Tab,
// eslint-disable-next-line no-unused-vars
MenuClickInfo,
CHROME,
runtime,
history,
sessions,
// eslint-disable-next-line no-unused-vars
OnInstalled,
} from "./browser"; } from "./browser";
import { Bus } from "./bus";
import { filterInSitu } from "./util";
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus; const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
const GATHER = "/bundles/content-gather.js";
async function runContentJob(tab: any, file: string, msg: any) { const CHROME_CONTEXTS = Object.freeze(new Set([
"all",
"audio",
"browser_action",
"editable",
"frame",
"image",
"launcher",
"link",
"page",
"page_action",
"selection",
"video",
]));
async function runContentJob(tab: Tab, file: string, msg: any) {
try { try {
if (tab && tab.incognito && msg) {
msg.private = tab.incognito;
}
const res = await tabs.executeScript(tab.id, { const res = await tabs.executeScript(tab.id, {
file, file,
allFrames: true, allFrames: true,
@ -48,6 +80,14 @@ async function runContentJob(tab: any, file: string, msg: any) {
} }
} }
type SelectionOptions = {
selectionOnly: boolean;
allTabs: boolean;
turbo: boolean;
tab: Tab;
};
class Handler { class Handler {
async processResults(turbo = false, results: any[]) { async processResults(turbo = false, results: any[]) {
const links = this.makeUnique(results, "links"); const links = this.makeUnique(results, "links");
@ -59,305 +99,573 @@ class Handler {
return makeUniqueItems( return makeUniqueItems(
results.filter(e => e[what]).map(e => { results.filter(e => e[what]).map(e => {
const finisher = new Finisher(e); const finisher = new Finisher(e);
return e[what]. return filterInSitu(e[what].
map((item: any) => finisher.finish(item)). map((item: any) => finisher.finish(item)), e => !!e);
filter((i: any) => i);
})); }));
} }
async performSelection(options: SelectionOptions) {
try {
const toptions: any = {
currentWindow: true,
discarded: false,
};
if (!CHROME) {
toptions.hidden = false;
}
const selectedTabs = options.allTabs ?
await tabs.query(toptions) as any[] :
[options.tab];
const textLinks = await Prefs.get("text-links", true);
const goptions = {
type: "DTA:gather",
selectionOnly: options.selectionOnly,
textLinks,
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
};
const results = await Promise.all(selectedTabs.
map((tab: any) => runContentJob(tab, GATHER, goptions)));
await this.processResults(options.turbo, results.flat());
}
catch (ex) {
console.error(ex.toString(), ex.stack, ex);
}
}
} }
new class Action extends Handler { function getMajor(version?: string) {
constructor() { if (!version) {
super(); return "";
this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
} }
const match = version.match(/^\d+\.\d+/);
async onClicked(tab: {id: number}) { if (!match) {
if (!tab.id) { return "";
return;
}
try {
await this.processResults(
await Prefs.get("global-turbo"),
await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly: false,
textLinks: await Prefs.get("text-links", true),
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
}));
}
catch (ex) {
console.error(ex);
}
} }
}(); return match[0];
}
new class Menus extends Handler { runtime.onInstalled.addListener(({reason, previousVersion}: OnInstalled) => {
constructor() { const {version} = runtime.getManifest();
super(); const major = getMajor(version);
this.onClicked = this.onClicked.bind(this); const prevMajor = getMajor(previousVersion);
menus.create({ if (reason === "update" && major !== prevMajor) {
id: "DTARegular", tabs.create({
contexts: ["all", "browser_action", "tools_menu"], url: `https://about.downthemall.org/changelog/?cur=${major}&prev=${prevMajor}`,
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular"),
}); });
menus.create({
id: "DTATurbo",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo"),
});
menus.create({
id: "DTARegularLink",
contexts: ["link"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.link"),
});
menus.create({
id: "DTATurboLink",
contexts: ["link"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.link"),
});
menus.create({
id: "DTARegularImage",
contexts: ["image"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.image"),
});
menus.create({
id: "DTATurboImage",
contexts: ["image"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.image"),
});
menus.create({
id: "DTARegularMedia",
contexts: ["video", "audio"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.media"),
});
menus.create({
id: "DTATurboMedia",
contexts: ["video", "audio"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.media"),
});
menus.create({
id: "DTARegularSelection",
contexts: ["selection"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.selection"),
});
menus.create({
id: "DTATurboSelection",
contexts: ["selection"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.selection"),
});
menus.create({
id: "sep-1",
contexts: ["all", "browser_action", "tools_menu"],
type: "separator"
});
menus.create({
id: "DTAManager",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-manager.png",
32: "/style/button-manager@2x.png",
},
title: _("manager.short"),
});
menus.create({
id: "DTAPrefs",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/settings.svg",
32: "/style/settings.svg",
64: "/style/settings.svg",
128: "/style/settings.svg",
},
title: _("prefs.short"),
});
menus.onClicked.addListener(this.onClicked);
} }
else if (reason === "install") {
tabs.create({
url: `https://about.downthemall.org/4.0/?cur=${major}`,
});
}
});
*makeSingleItemList(url: string, results: any[]) { locale.then(() => {
for (const result of results) { const menuHandler = new class Menus extends Handler {
const finisher = new Finisher(result); constructor() {
for (const list of [result.links, result.media]) { super();
for (const e of list) { this.onClicked = this.onClicked.bind(this);
if (e.url !== url) { const alls = new Map<string, string[]>();
continue; const mcreate = (options: any) => {
if (CHROME) {
delete options.icons;
options.contexts = options.contexts.
filter((e: string) => CHROME_CONTEXTS.has(e));
if (!options.contexts.length) {
return;
} }
const finished = finisher.finish(e); }
if (!finished) { if (options.contexts.includes("all")) {
continue; alls.set(options.id, options.contexts);
}
menus.create(options);
};
mcreate({
id: "DTARegularLink",
contexts: ["link"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.link"),
});
mcreate({
id: "DTATurboLink",
contexts: ["link"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.link"),
});
mcreate({
id: "DTARegularImage",
contexts: ["image"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.image"),
});
mcreate({
id: "DTATurboImage",
contexts: ["image"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.image"),
});
mcreate({
id: "DTARegularMedia",
contexts: ["video", "audio"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.media"),
});
mcreate({
id: "DTATurboMedia",
contexts: ["video", "audio"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.media"),
});
mcreate({
id: "DTARegularSelection",
contexts: ["selection"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.selection"),
});
mcreate({
id: "DTATurboSelection",
contexts: ["selection"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.selection"),
});
mcreate({
id: "DTARegular",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular"),
});
mcreate({
id: "DTATurbo",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo"),
});
mcreate({
id: "sep-1",
contexts: ["all", "browser_action", "tools_menu"],
type: "separator"
});
mcreate({
id: "DTARegularAll",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta-regular-all"),
});
mcreate({
id: "DTATurboAll",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta-turbo-all"),
});
const sep2ctx = menus.ACTION_MENU_TOP_LEVEL_LIMIT === 6 ?
["all", "tools_menu"] :
["all", "browser_action", "tools_menu"];
mcreate({
id: "sep-2",
contexts: sep2ctx,
type: "separator"
});
mcreate({
id: "DTAAdd",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/add.svg",
32: "/style/add.svg",
64: "/style/add.svg",
128: "/style/add.svg",
},
title: _("add-download"),
});
mcreate({
id: "sep-3",
contexts: ["all", "browser_action", "tools_menu"],
type: "separator"
});
mcreate({
id: "DTAManager",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-manager.png",
32: "/style/button-manager@2x.png",
},
title: _("manager.short"),
});
mcreate({
id: "DTAPrefs",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/settings.svg",
32: "/style/settings.svg",
64: "/style/settings.svg",
128: "/style/settings.svg",
},
title: _("prefs.short"),
});
Object.freeze(alls);
const adjustMenus = (v: boolean) => {
for (const [id, contexts] of alls.entries()) {
const adjusted = v ?
contexts.filter(e => e !== "all") :
contexts;
menus.update(id, {
contexts: adjusted
});
}
};
Prefs.get("hide-context", false).then((v: boolean) => {
// This is the initial load, so no need to adjust when visible already
if (!v) {
return;
}
adjustMenus(v);
});
Prefs.on("hide-context", (prefs, key, value: boolean) => {
adjustMenus(value);
});
menus.onClicked.addListener(this.onClicked);
}
*makeSingleItemList(url: string, results: any[]) {
for (const result of results) {
const finisher = new Finisher(result);
for (const list of [result.links, result.media]) {
for (const e of list) {
if (e.url !== url) {
continue;
}
const finished = finisher.finish(e);
if (!finished) {
continue;
}
yield finished;
} }
yield finished;
} }
} }
} }
}
async findSingleItem(tab: any, url: string, turbo = false) { async findSingleItem(tab: Tab, url: string, turbo = false) {
if (!url) { if (!url) {
return; return;
}
const results = await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly: false,
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
});
const found = Array.from(this.makeSingleItemList(url, results));
const unique = makeUniqueItems([found]);
if (!unique.length) {
return;
}
const [item] = unique;
API[turbo ? "singleTurbo" : "singleRegular"](item);
} }
const results = await runContentJob(
tab, "/bundles/content-gather.js", { onClicked(info: MenuClickInfo, tab: Tab) {
type: "DTA:gather", if (!tab.id) {
selectionOnly: false, return;
schemes: Array.from(ALLOWED_SCHEMES.values()), }
transferable: TRANSFERABLE_PROPERTIES, const {menuItemId} = info;
const {[`onClicked${menuItemId}`]: handler}: any = this;
if (!handler) {
console.error("Invalid Handler for", menuItemId);
return;
}
const rv: Promise<void> | void = handler.call(this, info, tab);
if (rv && rv.catch) {
rv.catch(console.error);
}
}
async emulate(action: string) {
const tab = await tabs.query({
active: true,
currentWindow: true,
}); });
const found = Array.from(this.makeSingleItemList(url, results)); if (!tab || !tab.length) {
const unique = makeUniqueItems([found]); return;
if (!unique.length) { }
return; this.onClicked({
menuItemId: action
}, tab[0]);
} }
const [item] = unique;
API[turbo ? "singleTurbo" : "singleRegular"](item);
}
onClicked(info: any, tab: any) { async onClickedDTARegular(info: MenuClickInfo, tab: Tab) {
if (!tab.id) { return await this.performSelection({
return; selectionOnly: false,
allTabs: false,
turbo: false,
tab,
});
} }
const {menuItemId} = info;
const {[`onClicked${menuItemId}`]: handler}: any = this; async onClickedDTARegularAll(info: MenuClickInfo, tab: Tab) {
if (!handler) { return await this.performSelection({
console.error("Invalid Handler for", menuItemId); selectionOnly: false,
return; allTabs: true,
turbo: false,
tab,
});
} }
handler.call(this, info, tab).catch(console.error);
}
async onClickedDTARegularInternal( async onClickedDTARegularSelection(info: MenuClickInfo, tab: Tab) {
selectionOnly: boolean, info: any, tab: any) { return await this.performSelection({
try { selectionOnly: true,
await this.processResults( allTabs: false,
false, turbo: false,
await runContentJob( tab,
tab, "/bundles/content-gather.js", { });
type: "DTA:gather",
selectionOnly,
textLinks: await Prefs.get("text-links", true),
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
}));
} }
catch (ex) {
console.error(ex); async onClickedDTATurbo(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: false,
turbo: true,
tab,
});
} }
}
async onClickedDTARegular(info: any, tab: any) { async onClickedDTATurboAll(info: MenuClickInfo, tab: Tab) {
return await this.onClickedDTARegularInternal(false, info, tab); return await this.performSelection({
} selectionOnly: false,
allTabs: true,
async onClickedDTARegularSelection(info: any, tab: any) { turbo: true,
return await this.onClickedDTARegularInternal(true, info, tab); tab,
} });
async onClickedDTATurboInternal(selectionOnly: boolean, info: any, tab: any) {
try {
await this.processResults(
true,
await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly,
textLinks: await Prefs.get("text-links", true),
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
}));
} }
catch (ex) {
console.error(ex); async onClickedDTATurboSelection(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: true,
allTabs: false,
turbo: true,
tab,
});
} }
}
async onClickedDTATurbo(info: any, tab: any) { async onClickedDTARegularLink(info: MenuClickInfo, tab: Tab) {
return await this.onClickedDTATurboInternal(false, info, tab); if (!info.linkUrl) {
} return;
}
await this.findSingleItem(tab, info.linkUrl, false);
}
async onClickedDTATurboSelection(info: any, tab: any) { async onClickedDTATurboLink(info: MenuClickInfo, tab: Tab) {
return await this.onClickedDTATurboInternal(false, info, tab); if (!info.linkUrl) {
} return;
}
await this.findSingleItem(tab, info.linkUrl, true);
}
async onClickedDTARegularLink(info: any, tab: any) { async onClickedDTARegularImage(info: MenuClickInfo, tab: Tab) {
return await this.findSingleItem(tab, info.linkUrl, false); if (!info.srcUrl) {
} return;
}
await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboLink(info: any, tab: any) { async onClickedDTATurboImage(info: MenuClickInfo, tab: Tab) {
return await this.findSingleItem(tab, info.linkUrl, true); if (!info.srcUrl) {
} return;
}
await this.findSingleItem(tab, info.srcUrl, true);
}
async onClickedDTARegularImage(info: any, tab: any) { async onClickedDTARegularMedia(info: MenuClickInfo, tab: Tab) {
return await this.findSingleItem(tab, info.srcUrl, false); if (!info.srcUrl) {
} return;
}
await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboImage(info: any, tab: any) { async onClickedDTATurboMedia(info: MenuClickInfo, tab: Tab) {
return await this.findSingleItem(tab, info.srcUrl, true); if (!info.srcUrl) {
} return;
}
await this.findSingleItem(tab, info.srcUrl, true);
}
async onClickedDTARegularMedia(info: any, tab: any) { onClickedDTAAdd() {
return await this.findSingleItem(tab, info.srcUrl, false); API.singleRegular(null);
} }
async onClickedDTATurboMedia(info: any, tab: any) { async onClickedDTAManager() {
return await this.findSingleItem(tab, info.srcUrl, true); await openManager();
} }
async onClickedDTAManager() { async onClickedDTAPrefs() {
await openManager(); await openPrefs();
} }
}();
async onClickedDTAPrefs() { new class Action extends Handler {
await openPrefs(); constructor() {
} super();
}(); this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
Prefs.get("button-type", false).then(v => this.adjust(v));
Prefs.on("button-type", (prefs, key, value) => {
this.adjust(value);
});
}
(async function init() { adjust(type: string) {
await Prefs.set("last-run", new Date()); action.setPopup({
await filters(); popup: type !== "popup" ? "" : "/windows/popup.html"
await getManager(); });
})().catch(ex => { let icons;
console.error("Failed to init components", ex.toString(), ex.stack, ex); switch (type) {
case "popup":
icons = {
16: "/style/icon16.png",
32: "/style/icon32.png",
48: "/style/icon48.png",
64: "/style/icon64.png",
128: "/style/icon128.png",
256: "/style/icon256.png"
};
break;
case "dta":
icons = {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
};
break;
case "turbo":
icons = {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
};
break;
case "manager":
icons = {
16: "/style/button-manager.png",
32: "/style/button-manager@2x.png",
};
break;
}
action.setIcon({path: icons});
}
async onClicked() {
switch (await Prefs.get("button-type")) {
case "popup":
break;
case "dta":
menuHandler.emulate("DTARegular");
break;
case "turbo":
menuHandler.emulate("DTATurbo");
break;
case "manager":
menuHandler.emulate("DTAManager");
break;
}
}
}();
Bus.on("do-regular", () => menuHandler.emulate("DTARegular"));
Bus.on("do-regular-all", () => menuHandler.emulate("DTARegularAll"));
Bus.on("do-turbo", () => menuHandler.emulate("DTATurbo"));
Bus.on("do-turbo-all", () => menuHandler.emulate("DTATurboAll"));
Bus.on("do-single", () => API.singleRegular(null));
Bus.on("open-manager", () => openManager(true));
Bus.on("open-prefs", () => openPrefs());
(async function init() {
const urlBase = runtime.getURL("");
history.onVisited.addListener(({url}: {url: string}) => {
if (!url || !url.startsWith(urlBase)) {
return;
}
history.deleteUrl({url});
});
const results: {url?: string}[] = await history.search({text: urlBase});
for (const {url} of results) {
if (!url) {
continue;
}
history.deleteUrl({url});
}
if (!CHROME) {
const sessionRemover = async () => {
for (const s of await sessions.getRecentlyClosed()) {
if (s.tab) {
if (s.tab.url.startsWith(urlBase)) {
await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId);
}
continue;
}
if (!s.window || !s.window.tabs || s.window.tabs.length > 1) {
continue;
}
const [tab] = s.window.tabs;
if (tab.url.startsWith(urlBase)) {
await sessions.forgetClosedWindow(s.window.sessionId);
}
}
};
sessions.onChanged.addListener(sessionRemover);
await sessionRemover();
}
await Prefs.set("last-run", new Date());
await filters();
await getManager();
})().catch(ex => {
console.error("Failed to init components", ex.toString(), ex.stack, ex);
});
}); });

View File

@ -73,7 +73,7 @@ class Numeral implements Generator {
this.digits = dir ? rawpieces[0].length : rawpieces[1].length; this.digits = dir ? rawpieces[0].length : rawpieces[1].length;
this.length = Math.floor( this.length = Math.floor(
(this.stop - this.start + (dir ? 1 : -1)) / this.step); (this.stop - this.start + (dir ? 1 : -1)) / this.step);
this.preview = this[Symbol.iterator]().next().value; this.preview = this[Symbol.iterator]().next().value as string;
Object.freeze(this); Object.freeze(this);
} }
@ -93,14 +93,68 @@ class Numeral implements Generator {
} }
} }
class Character implements Generator {
public readonly start: number;
public readonly stop: number;
public readonly step: number;
public readonly length: number;
public readonly preview: string;
constructor(str: string) {
const rawpieces = str.split(":").map(e => e.trim());
const pieces = rawpieces.map((e, i) => {
if (i === 2) {
return reallyParseInt(e);
}
if (e.length > 1) {
throw new Error("Malformed Character sequence");
}
return e.charCodeAt(0);
});
if (pieces.length < 2) {
throw new Error("Invalid input");
}
const [start, stop, step] = pieces;
if (step === 0) {
throw new Error("Invalid step");
}
this.step = !step ? 1 : step;
const dir = this.step > 0;
if (dir && start > stop) {
throw new Error("Invalid sequence");
}
else if (!dir && start < stop) {
throw new Error("Invalid sequence");
}
this.start = start;
this.stop = stop;
this.length = Math.floor(
(this.stop - this.start + (dir ? 1 : -1)) / this.step);
this.preview = this[Symbol.iterator]().next().value as string;
Object.freeze(this);
}
*[Symbol.iterator]() {
const {start, stop, step} = this;
const dir = step > 0;
for (let i = start; (dir ? i <= stop : i >= stop); i += step) {
yield String.fromCharCode(i);
}
}
}
export class BatchGenerator implements Generator { export class BatchGenerator implements Generator {
private readonly gens: Generator[]; private readonly gens: Generator[];
public readonly hasInvalid: boolean; public readonly hasInvalid: boolean;
public readonly length: any; public readonly length: number;
public readonly preview: any; public readonly preview: string;
constructor(str: string) { constructor(str: string) {
this.gens = []; this.gens = [];
@ -120,9 +174,14 @@ export class BatchGenerator implements Generator {
try { try {
this.gens.push(new Numeral(tok)); this.gens.push(new Numeral(tok));
} }
catch (ex) { catch {
this.gens.push(new Literal(`[${tok}]`)); try {
this.hasInvalid = true; this.gens.push(new Character(tok));
}
catch {
this.gens.push(new Literal(`[${tok}]`));
this.hasInvalid = true;
}
} }
} }
if (str) { if (str) {

View File

@ -3,15 +3,118 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const polyfill = require("webextension-polyfill"); const polyfill = require("webextension-polyfill");
export const {i18n} = polyfill; interface ExtensionListener {
export const {extension} = polyfill; addListener: (listener: Function) => void;
export const {notifications} = polyfill; removeListener: (listener: Function) => void;
}
export interface MessageSender {
readonly tab?: Tab;
readonly frameId?: number;
readonly id?: number;
readonly url?: string;
readonly tlsChannelId?: string;
}
export interface Tab {
readonly id?: number;
readonly incognito?: boolean;
}
export interface MenuClickInfo {
readonly menuItemId: string | number;
readonly button?: number;
readonly linkUrl?: string;
readonly srcUrl?: string;
}
export interface RawPort {
readonly error: any;
readonly name: string;
readonly sender?: MessageSender;
readonly onDisconnect: ExtensionListener;
readonly onMessage: ExtensionListener;
disconnect: () => void;
postMessage: (message: any) => void;
}
interface WebRequestFilter {
urls?: string[];
}
interface WebRequestListener {
addListener(
callback: Function,
filter: WebRequestFilter,
extraInfoSpec: string[]
): void;
removeListener(callback: Function): void;
}
type Header = {name: string; value: string};
export interface DownloadOptions {
conflictAction: string;
filename?: string;
saveAs: boolean;
url: string;
method?: string;
body?: string;
incognito?: boolean;
headers: Header[];
}
export interface DownloadsQuery {
id?: number;
}
interface Downloads {
download(download: DownloadOptions): Promise<number>;
open(manId: number): Promise<void>;
show(manId: number): Promise<void>;
pause(manId: number): Promise<void>;
resume(manId: number): Promise<void>;
cancel(manId: number): Promise<void>;
erase(query: DownloadsQuery): Promise<void>;
search(query: DownloadsQuery): Promise<any[]>;
getFileIcon(id: number, options?: any): Promise<string>;
setShelfEnabled(state: boolean): void;
removeFile(manId: number): Promise<void>;
readonly onCreated: ExtensionListener;
readonly onChanged: ExtensionListener;
readonly onErased: ExtensionListener;
readonly onDeterminingFilename?: ExtensionListener;
}
interface WebRequest {
readonly onBeforeSendHeaders: WebRequestListener;
readonly onSendHeaders: WebRequestListener;
readonly onHeadersReceived: WebRequestListener;
}
export interface OnInstalled {
readonly reason: string;
readonly previousVersion?: string;
readonly temporary: boolean;
}
export const {browserAction} = polyfill; export const {browserAction} = polyfill;
export const {contextMenus} = polyfill; export const {contextMenus} = polyfill;
export const {downloads} = polyfill; export const {downloads}: {downloads: Downloads} = polyfill;
export const {extension} = polyfill;
export const {history} = polyfill;
export const {menus} = polyfill; export const {menus} = polyfill;
export const {notifications} = polyfill;
export const {runtime} = polyfill; export const {runtime} = polyfill;
export const {sessions} = polyfill;
export const {storage} = polyfill; export const {storage} = polyfill;
export const {tabs} = polyfill; export const {tabs} = polyfill;
export const {webNavigation} = polyfill; export const {webNavigation} = polyfill;
export const {webRequest}: {webRequest: WebRequest} = polyfill;
export const {windows} = polyfill; export const {windows} = polyfill;
export const {theme} = polyfill;
export const CHROME = navigator.appVersion.includes("Chrome/");
export const OPERA = navigator.appVersion.includes("OPR/");

View File

@ -2,50 +2,55 @@
// License: MIT // License: MIT
import { EventEmitter } from "./events"; import { EventEmitter } from "./events";
import {runtime, tabs} from "./browser"; // eslint-disable-next-line no-unused-vars
import {runtime, tabs, RawPort, MessageSender} from "./browser";
export class Port extends EventEmitter { export class Port extends EventEmitter {
private port: any; private port: RawPort | null;
constructor(port: any) { private disconnected = false;
constructor(port: RawPort) {
super(); super();
this.port = port; this.port = port;
let disconnected = false;
let tabListener: any;
const disconnect = () => {
if (tabListener) {
tabs.onRemoved.removeListener(tabListener);
tabListener = null;
}
if (disconnected) {
return;
}
disconnected = true;
this.port = null; // Break the cycle
this.emit("disconnect", this, port);
};
// Nasty firefox bug, thus listen for tab removal explicitly // Nasty firefox bug, thus listen for tab removal explicitly
if (port.sender && port.sender.tab && port.sender.tab.id) { if (port.sender && port.sender.tab && port.sender.tab.id) {
const otherTabId = port.sender.tab.id; const otherTabId = port.sender.tab.id;
const tabListener = function(tabId: number) { const tabListener = (tabId: number) => {
if (tabId !== otherTabId) { if (tabId !== otherTabId) {
return; return;
} }
disconnect(); this.disconnect();
}; };
tabs.onRemoved.addListener(tabListener); tabs.onRemoved.addListener(tabListener);
} }
port.onMessage.addListener(this.onMessage.bind(this)); port.onMessage.addListener(this.onMessage.bind(this));
port.onDisconnect.addListener(disconnect); port.onDisconnect.addListener(this.disconnect.bind(this));
}
disconnect() {
if (this.disconnected) {
return;
}
this.disconnected = true;
const {port} = this;
this.port = null; // Break the cycle
this.emit("disconnect", this, port);
} }
get name() { get name() {
if (!this.port) {
return null;
}
return this.port.name; return this.port.name;
} }
get id() { get id() {
return this.port.sender && this.port.sender.extensionId; if (!this.port || !this.port.sender) {
return null;
}
return this.port.sender.id;
} }
get isSelf() { get isSelf() {
@ -53,6 +58,9 @@ export class Port extends EventEmitter {
} }
post(msg: string, ...data: any[]) { post(msg: string, ...data: any[]) {
if (!this.port) {
return;
}
if (!data) { if (!data) {
this.port.postMessage({msg}); this.port.postMessage({msg});
return; return;
@ -64,14 +72,17 @@ export class Port extends EventEmitter {
} }
onMessage(message: any) { onMessage(message: any) {
if (Object.keys(message).includes("msg")) { if (!this.port) {
this.emit(message.msg, message);
return; return;
} }
if (Array.isArray(message)) { if (Array.isArray(message)) {
message.forEach(this.onMessage, this); message.forEach(this.onMessage, this);
return; return;
} }
if (Object.keys(message).includes("msg")) {
this.emit(message.msg, message);
return;
}
if (typeof message === "string") { if (typeof message === "string") {
this.emit(message); this.emit(message);
return; return;
@ -99,7 +110,7 @@ export const Bus = new class extends EventEmitter {
runtime.onConnect.addListener(this.onConnect.bind(this)); runtime.onConnect.addListener(this.onConnect.bind(this));
} }
onMessage(msg: any, sender: any, callback: any) { onMessage(msg: any, sender: MessageSender, callback: any) {
let {type = null} = msg; let {type = null} = msg;
if (!type) { if (!type) {
type = msg; type = msg;
@ -107,11 +118,14 @@ export const Bus = new class extends EventEmitter {
this.emit(type, msg, callback); this.emit(type, msg, callback);
} }
onConnect(port: any) { onConnect(port: RawPort) {
if (!port.name) { if (!port.name) {
port.disconnect(); port.disconnect();
return; return;
} }
this.ports.emit(port.name, new Port(port)); const wrapped = new Port(port);
if (!this.ports.emit(port.name, wrapped)) {
wrapped.disconnect();
}
} }
}(); }();

230
lib/cdheaderparser.ts Normal file
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

@ -1,5 +1,11 @@
"use strict"; "use strict";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
// eslint-disable-next-line no-unused-vars
import { Download } from "./manager/download";
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
// License: MIT // License: MIT
const VERSION = 1; const VERSION = 1;
@ -40,12 +46,12 @@ export const DB = new class DB {
}); });
} }
getAllInternal(resolve: (items: any[]) => void, reject: Function) { getAllInternal(resolve: (items: BaseItem[]) => void, reject: Function) {
if (!this.db) { if (!this.db) {
reject(new Error("db closed")); reject(new Error("db closed"));
return; return;
} }
const items: any[] = []; const items: BaseItem[] = [];
const transaction = this.db.transaction(STORE, "readonly"); const transaction = this.db.transaction(STORE, "readonly");
transaction.onerror = ex => reject(ex); transaction.onerror = ex => reject(ex);
const store = transaction.objectStore(STORE); const store = transaction.objectStore(STORE);
@ -66,7 +72,7 @@ export const DB = new class DB {
return await new Promise(this.getAllInternal); return await new Promise(this.getAllInternal);
} }
saveItemsInternal(items: any[], resolve: Function, reject: Function) { saveItemsInternal(items: Download[], resolve: Function, reject: Function) {
if (!items || !items.length || !this.db) { if (!items || !items.length || !this.db) {
resolve(); resolve();
return; return;
@ -80,9 +86,13 @@ export const DB = new class DB {
if (item.private) { if (item.private) {
continue; continue;
} }
const req = store.put(item.toJSON()); const json = item.toJSON();
if (item.state === RUNNING || item.state === RETRYING) {
json.state = QUEUED;
}
const req = store.put(json);
if (!("dbId" in item) || item.dbId < 0) { if (!("dbId" in item) || item.dbId < 0) {
req.onsuccess = () => item.dbId = req.result; req.onsuccess = () => item.dbId = req.result as number;
} }
} }
} }
@ -91,7 +101,7 @@ export const DB = new class DB {
} }
} }
async saveItems(items: any[]) { async saveItems(items: Download[]) {
await this.init(); await this.init();
return await new Promise(this.saveItemsInternal.bind(this, items)); return await new Promise(this.saveItemsInternal.bind(this, items));
} }

View File

@ -4,13 +4,16 @@
import uuid from "./uuid"; import uuid from "./uuid";
import "./objectoverlay"; import "./objectoverlay";
import { storage, i18n } from "./browser"; import { storage } from "./browser";
import { EventEmitter } from "./events"; import { EventEmitter } from "./events";
import { Prefs } from "./prefs";
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 { _, locale } from "./i18n";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
const REG_ESCAPE = /[{}()[\]\\^$.]/g; const REG_ESCAPE = /[{}()[\]\\^$.]/g;
const REG_FNMATCH = /[*?]/; const REG_FNMATCH = /[*?]/;
@ -91,7 +94,7 @@ function *parseIntoRegexpInternal(str: string): Iterable<RegExp> {
// multi-expression // multi-expression
if (str.includes(",")) { if (str.includes(",")) {
for (const part in str.split(",")) { for (const part of str.split(",")) {
yield *parseIntoRegexpInternal(part); yield *parseIntoRegexpInternal(part);
} }
return; return;
@ -173,25 +176,37 @@ export class Matcher {
} }
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
matchItem(item: any) { matchItem(item: BaseItem) {
const {usable = "", title = "", description = "", fileName = ""} = item; const {usable = "", title = "", description = "", fileName = ""} = item;
return this.match(usable) || this.match(title) || return this.match(usable) || this.match(title) ||
this.match(description) || this.match(fileName); this.match(description) || this.match(fileName);
} }
} }
interface RawFilter extends Object {
active: boolean;
type: number;
label: string;
expr: string;
icon?: string;
custom?: boolean;
isOverridden?: (prop: string) => boolean;
reset?: () => void;
toJSON?: () => any;
}
export class Filter { export class Filter {
private readonly owner: Filters; private readonly owner: Filters;
public readonly id: any; public readonly id: string | symbol;
private readonly raw: any; private readonly raw: RawFilter;
private _label: string; private _label: string;
private _reg: Matcher; private _reg: Matcher;
constructor(owner: Filters, id: any, raw: any) { constructor(owner: Filters, id: string | symbol, raw: RawFilter) {
if (!owner || !id || !raw) { if (!owner || !id || !raw) {
throw new Error("null argument"); throw new Error("null argument");
} }
@ -203,9 +218,11 @@ export class Filter {
init() { init() {
this._label = this.raw.label; this._label = this.raw.label;
if (this.id !== FAST && this.id.startsWith("deffilter-") && if (typeof this.raw.isOverridden !== "undefined" &&
!this.raw.isOverridden("label")) { typeof this.id === "string") {
this._label = i18n.getMessage(this.id) || this._label; if (this.id.startsWith("deffilter-") && !this.raw.isOverridden("label")) {
this._label = _(this.id) || this._label;
}
} }
this._reg = Matcher.fromExpression(this.expr); this._reg = Matcher.fromExpression(this.expr);
Object.seal(this); Object.seal(this);
@ -281,7 +298,7 @@ export class Filter {
} }
async reset() { async reset() {
if (this.raw.custom) { if (!this.raw.reset) {
throw Error("Cannot reset non-default filter"); throw Error("Cannot reset non-default filter");
} }
this.raw.reset(); this.raw.reset();
@ -291,7 +308,10 @@ export class Filter {
async "delete"() { async "delete"() {
if (!this.raw.custom) { if (!this.raw.custom) {
throw Error("Cannot delete default filter"); throw new Error("Cannot delete default filter");
}
if (typeof this.id !== "string") {
throw new Error("Cannot delete symbolized");
} }
await this.owner.delete(this.id); await this.owner.delete(this.id);
} }
@ -300,7 +320,7 @@ export class Filter {
return this._reg.match(str); return this._reg.match(str);
} }
matchItem(item: any) { matchItem(item: BaseItem) {
return this._reg.matchItem(item); return this._reg.matchItem(item);
} }
@ -315,8 +335,7 @@ class FastFilter extends Filter {
throw new Error("Invalid fast filter value"); throw new Error("Invalid fast filter value");
} }
super(owner, FAST, { super(owner, FAST, {
id: FAST, label: "fast",
label: FAST,
type: TYPE_ALL, type: TYPE_ALL,
active: true, active: true,
expr: value, expr: value,
@ -351,8 +370,6 @@ class Filters extends EventEmitter {
private filters: Filter[]; private filters: Filter[];
private fastFilter: string | null;
ignoreNext: boolean; ignoreNext: boolean;
private readonly typeMatchers: Map<number, Matcher>; private readonly typeMatchers: Map<number, Matcher>;
@ -362,10 +379,8 @@ class Filters extends EventEmitter {
this.typeMatchers = new Map(); this.typeMatchers = new Map();
this.loaded = false; this.loaded = false;
this.filters = []; this.filters = [];
this.fastFilter = null;
this.ignoreNext = false; this.ignoreNext = false;
this.regenerate(); this.regenerate();
Prefs.on("fast-filter", this.updateFastFilter.bind(this));
storage.onChanged.addListener(async (changes: any) => { storage.onChanged.addListener(async (changes: any) => {
if (this.ignoreNext) { if (this.ignoreNext) {
this.ignoreNext = false; this.ignoreNext = false;
@ -403,6 +418,7 @@ class Filters extends EventEmitter {
const id = `custom-${uuid()}`; const id = `custom-${uuid()}`;
const filter = new Filter(this, id, { const filter = new Filter(this, id, {
active: true, active: true,
custom: true,
label, label,
expr, expr,
type, type,
@ -411,11 +427,11 @@ class Filters extends EventEmitter {
await this.save(); await this.save();
} }
"get"(id: any) { "get"(id: string | symbol) {
return this.filters.find(e => e.id === id); return this.filters.find(e => e.id === id);
} }
async "delete"(id: any) { async "delete"(id: string) {
const idx = this.filters.findIndex(e => e.id === id); const idx = this.filters.findIndex(e => e.id === id);
if (idx < 0) { if (idx < 0) {
return; return;
@ -438,21 +454,12 @@ class Filters extends EventEmitter {
return new FastFilter(this, value); return new FastFilter(this, value);
} }
getFastFilter() { async getFastFilter() {
if (!this.fastFilter) { await FASTFILTER.init();
throw new Error("Nothing stored"); if (!FASTFILTER.current) {
return null;
} }
return new FastFilter(this, this.fastFilter); return new FastFilter(this, FASTFILTER.current);
}
async setFastFilter(value: string) {
this.fastFilter = value || "";
await Prefs.set("fast-filter", this.fastFilter);
}
updateFastFilter(pref: any, key: string, value: string) {
this.fastFilter = value || null;
this.regenerate();
} }
regenerate() { regenerate() {
@ -480,17 +487,6 @@ class Filters extends EventEmitter {
console.error("Filter", current.label || "unknown", ex); console.error("Filter", current.label || "unknown", ex);
} }
} }
if (this.fastFilter) {
try {
const fastFilter = new FastFilter(this, this.fastFilter);
all.push(fastFilter);
links.push(fastFilter);
media.push(fastFilter);
}
catch (ex) {
console.error("fast filter", this.fastFilter, "is invalid", ex);
}
}
this.typeMatchers.set(TYPE_ALL, new Matcher(all)); this.typeMatchers.set(TYPE_ALL, new Matcher(all));
this.typeMatchers.set(TYPE_LINK, new Matcher(links)); this.typeMatchers.set(TYPE_LINK, new Matcher(links));
this.typeMatchers.set(TYPE_MEDIA, new Matcher(media)); this.typeMatchers.set(TYPE_MEDIA, new Matcher(media));
@ -498,6 +494,7 @@ class Filters extends EventEmitter {
} }
async load() { async load() {
await locale;
const defaultFilters = DEFAULT_FILTERS as any; const defaultFilters = DEFAULT_FILTERS as any;
let savedFilters = (await storage.local.get("userFilters")); let savedFilters = (await storage.local.get("userFilters"));
if (savedFilters && "userFilters" in savedFilters) { if (savedFilters && "userFilters" in savedFilters) {
@ -534,14 +531,17 @@ class Filters extends EventEmitter {
defaultFilters[filter]); defaultFilters[filter]);
this.filters.push(new Filter(this, filter, current)); this.filters.push(new Filter(this, filter, current));
} }
this.fastFilter = await Prefs.get("fast-filter", null);
this.loaded = true; this.loaded = true;
this.regenerate(); this.regenerate();
} }
filterItemsByType(items: any[], type: number) { async filterItemsByType(items: BaseItem[], type: number) {
const matcher = this.typeMatchers.get(type); const matcher = this.typeMatchers.get(type);
const fast = await this.getFastFilter();
return items.filter(function(item) { return items.filter(function(item) {
if (fast && fast.matchItem(item)) {
return true;
}
return matcher && matcher.matchItem(item); return matcher && matcher.matchItem(item);
}); });
} }
@ -562,12 +562,14 @@ class Filters extends EventEmitter {
} }
} }
let _filters: any; let _filters: Filters;
let _loader: Promise<void>;
export async function filters(): Promise<Filters> { export async function filters(): Promise<Filters> {
if (!_filters) { if (!_loader) {
_filters = new Filters(); _filters = new Filters();
await _filters.load(); _loader = _filters.load();
} }
await _loader;
return _filters; return _filters;
} }

View File

@ -2,50 +2,239 @@
// License: MIT // License: MIT
import {memoize} from "./memoize"; import {memoize} from "./memoize";
import langs from "../_locales/all.json";
import { sorted, naturalCaseCompare } from "./sorting";
function load() {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {i18n} = require("webextension-polyfill");
return i18n; export const ALL_LANGS = Object.freeze(new Map<string, string>(
sorted(Object.entries(langs), e => {
return [e[1], e[0]];
}, naturalCaseCompare)));
let CURRENT = "en";
export function getCurrentLanguage() {
return CURRENT;
}
declare let browser: any;
declare let chrome: any;
const CACHE_KEY = "_cached_locales";
const CUSTOM_KEY = "_custom_locale";
const normalizer = /[^A-Za-z0-9_]/g;
interface JSONEntry {
message: string;
placeholders: any;
}
class Entry {
private message: string;
constructor(entry: JSONEntry) {
if (!entry.message.includes("$")) {
throw new Error("Not entry-able");
}
let hit = false;
this.message = entry.message.replace(/\$[A-Z0-9]+\$/g, (r: string) => {
hit = true;
const id = r.substr(1, r.length - 2).toLocaleLowerCase();
const pholder = entry.placeholders[id];
if (!pholder || !pholder.content) {
throw new Error(`Invalid placeholder: ${id}`);
}
return `${pholder.content}$`;
});
if (!hit) {
throw new Error("Not entry-able");
}
} }
catch (ex) {
localize(args: any[]) {
return this.message.replace(/\$\d+\$/g, (r: string) => {
const idx = parseInt(r.substr(1, r.length - 2), 10) - 1;
return args[idx] || "";
});
}
}
class Localization {
private strings: Map<string, Entry | string>;
constructor(baseLanguage: any, ...overlayLanguages: any) {
this.strings = new Map();
const mapLanguage = (lang: any) => {
for (const [id, entry] of Object.entries<JSONEntry>(lang)) {
if (!entry.message) {
continue;
}
try {
if (entry.message.includes("$")) {
this.strings.set(id, new Entry(entry));
}
else {
this.strings.set(id, entry.message);
}
}
catch (ex) {
this.strings.set(id, entry.message);
}
}
};
mapLanguage(baseLanguage);
overlayLanguages.forEach(mapLanguage);
}
localize(id: string, ...args: any[]) {
const entry = this.strings.get(id.replace(normalizer, "_"));
if (!entry) {
return "";
}
if (typeof entry === "string") {
return entry;
}
if (args.length === 1 && Array.isArray(args)) {
[args] = args;
}
return entry.localize(args);
}
}
function checkBrowser() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
if (typeof browser !== "undefined" && browser.i18n) {
return;
}
if (typeof chrome !== "undefined" && chrome.i18n) {
return;
}
throw new Error("not in a webext");
}
async function fetchLanguage(code: string) {
try {
const resp = await fetch(`/_locales/${code}/messages.json`);
return await resp.json();
}
catch {
return null;
}
}
function loadCached() {
if (document.location.pathname.includes("/windows/")) {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
return JSON.parse(cached) as any[];
}
}
return null;
}
async function loadRawLocales() {
// en is the base locale, always to be loaded
// The loader will override string from it with more specific string
// from other locales
const langs = new Set<string>(["en"]);
const uiLang: string = (typeof browser !== "undefined" ? browser : chrome).
i18n.getUILanguage();
// Chrome will only look for underscore versions of locale codes,
// while Firefox will look for both.
// So we better normalize the code to the underscore version.
// However, the API seems to always return the dash-version.
// Add all base locales into ascending order of priority,
// starting with the most unspecific base locale, ending
// with the most specific locale.
// e.g. this will transform ["zh", "CN"] -> ["zh", "zh_CN"]
uiLang.split(/[_-]/g).reduce<string[]>((prev, curr) => {
prev.push(curr);
langs.add(prev.join("_"));
return prev;
}, []);
if (CURRENT && CURRENT !== "default") {
langs.delete(CURRENT);
langs.add(CURRENT);
}
const valid = Array.from(langs).filter(e => ALL_LANGS.has(e));
const fetched = await Promise.all(Array.from(valid, fetchLanguage));
return fetched.filter(e => !!e);
}
async function load(): Promise<Localization> {
try {
checkBrowser();
try {
let currentLang: any = "";
if (typeof browser !== "undefined") {
currentLang = await browser.storage.sync.get("language");
}
else {
currentLang = await new Promise(
resolve => chrome.storage.sync.get("language", resolve));
}
if ("language" in currentLang) {
currentLang = currentLang.language;
}
if (!currentLang || !currentLang.length) {
currentLang = "default";
}
CURRENT = currentLang;
// en is the base locale
let valid = loadCached();
if (!valid) {
valid = await loadRawLocales();
localStorage.setItem(CACHE_KEY, JSON.stringify(valid));
}
if (!valid.length) {
throw new Error("Could not lood ANY of these locales");
}
const custom = localStorage.getItem(CUSTOM_KEY);
if (custom) {
try {
valid.push(JSON.parse(custom));
}
catch (ex) {
console.error(ex);
// ignored
}
}
const base = valid.shift();
const rv = new Localization(base, ...valid);
return rv;
}
catch (ex) {
console.error("Failed to load locale", ex.toString(), ex.stack, ex);
return new Localization({});
}
}
catch {
// We might be running under node for tests // We might be running under node for tests
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const messages = require("../_locales/en/messages.json"); const messages = require("../_locales/en/messages.json");
const map = new Map(); return new Localization(messages);
for (const [k, v] of Object.entries<any>(messages)) {
const {placeholders = {}} = v;
let {message = ""} = v;
for (const [pname, pval] of Object.entries<any>(placeholders)) {
message = message.replace(`$${pname.toUpperCase()}$`, `${pval.content}$`);
}
map.set(k, message);
}
return {
getMessage(id: string, subst: string[]) {
const m = map.get(id);
if (typeof subst === undefined) {
return m;
}
if (!Array.isArray(subst)) {
subst = [subst];
}
return m.replace(/\$\d+\$/g, (r: string) => {
const idx = parseInt(r.substr(1, r.length - 2), 10) - 1;
return subst[idx] || "";
});
}
};
} }
} }
const i18n = load(); type MemoLocalize = (id: string, ...args: any[]) => string;
const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
export const locale = load();
let loc: Localization | null;
let memoLocalize: MemoLocalize | null = null;
locale.then(l => {
loc = l;
memoLocalize = memoize(loc.localize.bind(loc), 10 * 1000, 10);
});
/** /**
* Localize a message * Localize a message
@ -53,22 +242,22 @@ const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
* @param {string[]} [subst] Message substituations * @param {string[]} [subst] Message substituations
* @returns {string} Localized message * @returns {string} Localized message
*/ */
function _(id: string, ...subst: any[]) { export function _(id: string, ...subst: any[]) {
if (!loc || !memoLocalize) {
console.trace("TOO SOON");
throw new Error("Called too soon");
}
if (!subst.length) { if (!subst.length) {
return memoGetMessage(id); return memoLocalize(id);
} }
if (subst.length === 1 && Array.isArray(subst[0])) { return loc.localize(id, subst);
subst = subst.pop();
}
return i18n.getMessage(id, subst);
} }
/** function localize_<T extends HTMLElement | DocumentFragment>(elem: T): T {
* Localize a DOM for (const tmpl of elem.querySelectorAll<HTMLTemplateElement>("template")) {
* @param {Element} elem DOM to localize localize_(tmpl.content);
* @returns {Element} Passed in element (fluent) }
*/
function localize(elem: HTMLElement) {
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) { for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
const {i18n: i} = el.dataset; const {i18n: i} = el.dataset;
if (!i) { if (!i) {
@ -99,8 +288,25 @@ function localize(elem: HTMLElement) {
for (const el of document.querySelectorAll("*[data-l18n]")) { for (const el of document.querySelectorAll("*[data-l18n]")) {
console.error("wrong!", el); console.error("wrong!", el);
} }
return elem; return elem as T;
} }
/**
* Localize a DOM
* @param {Element} elem DOM to localize
* @returns {Element} Passed in element (fluent)
*/
export async function localize<T extends HTMLElement | DocumentFragment>(
elem: T): Promise<T> {
await locale;
return localize_(elem);
}
export {localize, _}; export function saveCustomLocale(data?: string) {
if (!data) {
localStorage.removeItem(CUSTOM_KEY);
return;
}
new Localization(JSON.parse(data));
localStorage.setItem(CUSTOM_KEY, data);
}

162
lib/iconcache.ts Normal file
View File

@ -0,0 +1,162 @@
"use strict";
// License: MIT
import { downloads, CHROME } from "./browser";
import { EventEmitter } from "../uikit/lib/events";
import { PromiseSerializer } from "./pserializer";
const VERSION = 1;
const STORE = "iconcache";
// eslint-disable-next-line no-magic-numbers
const CACHE_SIZES = CHROME ? [16, 32] : [16, 32, 64, 127];
const BLACKLISTED = Object.freeze(new Set([
"",
"ext",
"ico",
"pif",
"scr",
"ani",
"cur",
"ttf",
"otf",
"woff",
"woff2",
"cpl",
"desktop",
"app",
]));
async function getIcon(size: number, manId: number) {
const raw = await downloads.getFileIcon(manId, {size});
const icon = new URL(raw);
if (icon.protocol === "data:") {
const res = await fetch(icon.toString());
const blob = await res.blob();
return {size, icon: blob};
}
return {size, icon};
}
const SYNONYMS = Object.freeze(new Map<string, string>([
["jpe", "jpg"],
["jpeg", "jpg"],
["jfif", "jpg"],
["mpe", "mpg"],
["mpeg", "mpg"],
["m4v", "mp4"],
]));
export const IconCache = new class IconCache extends EventEmitter {
private db: Promise<IDBDatabase>;
private cache: Map<string, string>;
constructor() {
super();
this.db = this.init();
this.cache = new Map();
this.get = PromiseSerializer.wrapNew(8, this, this.get);
this.set = PromiseSerializer.wrapNew(1, this, this.set);
}
private async init() {
return await new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open(STORE, VERSION);
req.onupgradeneeded = evt => {
const db = req.result;
switch (evt.oldVersion) {
case 0: {
db.createObjectStore(STORE);
break;
}
}
};
req.onerror = ex => reject(ex);
req.onsuccess = () => {
resolve(req.result);
};
});
}
private normalize(ext: string) {
ext = ext.toLocaleLowerCase();
return SYNONYMS.get(ext) || ext;
}
// eslint-disable-next-line no-magic-numbers
async get(ext: string, size = 16) {
ext = this.normalize(ext);
if (BLACKLISTED.has(ext)) {
return undefined;
}
const sext = `${ext}-${size}`;
let rv = this.cache.get(sext);
if (rv) {
return rv;
}
const db = await this.db;
rv = this.cache.get(sext);
if (rv) {
return rv;
}
return await new Promise<string | undefined>(resolve => {
const trans = db.transaction(STORE, "readonly");
trans.onerror = () => resolve(undefined);
const store = trans.objectStore(STORE);
const req = store.get(sext);
req.onerror = () => resolve(undefined);
req.onsuccess = () => {
const rv = this.cache.get(sext);
if (rv) {
resolve(rv);
return;
}
let {result} = req;
if (!result) {
resolve(undefined);
return;
}
if (typeof req.result !== "string") {
result = URL.createObjectURL(result).toString();
}
this.cache.set(sext, result);
this.cache.set(ext, "");
resolve(result);
};
});
}
async set(ext: string, manId: number) {
ext = this.normalize(ext);
if (BLACKLISTED.has(ext)) {
return;
}
if (this.cache.has(ext)) {
// already processed in this session
return;
}
// eslint-disable-next-line no-magic-numbers
const urls = await Promise.all(CACHE_SIZES.map(
size => getIcon(size, manId)));
if (this.cache.has(ext)) {
// already processed in this session
return;
}
for (const {size, icon} of urls) {
this.cache.set(`${ext}-${size}`, URL.createObjectURL(icon));
}
this.cache.set(ext, "");
const db = await this.db;
await new Promise((resolve, reject) => {
const trans = db.transaction(STORE, "readwrite");
trans.onerror = reject;
trans.oncomplete = resolve;
const store = trans.objectStore(STORE);
for (const {size, icon} of urls) {
store.put(icon, `${ext}-${size}`);
}
});
this.emit("cached", ext);
}
}();

247
lib/imex.ts Normal file
View File

@ -0,0 +1,247 @@
"use strict";
// License: MIT
import { getTextLinks } from "./textlinks";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
import { ALLOWED_SCHEMES } from "./constants";
export const NS_METALINK_RFC5854 = "urn:ietf:params:xml:ns:metalink";
export const NS_DTA = "http://www.downthemall.net/properties#";
function parseNum(
file: Element,
attr: string,
defaultValue: number,
ns = NS_METALINK_RFC5854) {
const val = file.getAttributeNS(ns, attr);
if (!val) {
return defaultValue + 1;
}
const num = parseInt(val, 10);
if (isFinite(num)) {
return num;
}
return defaultValue + 1;
}
function importMeta4(data: string) {
const parser = new DOMParser();
const document = parser.parseFromString(data, "text/xml");
const {documentElement} = document;
const items: BaseItem[] = [];
let batch = 0;
for (const file of documentElement.querySelectorAll("file")) {
try {
const url = Array.from(file.querySelectorAll("url")).map(u => {
try {
const {textContent} = u;
if (!textContent) {
return null;
}
const url = new URL(textContent);
if (!ALLOWED_SCHEMES.has(url.protocol)) {
return null;
}
const prio = parseNum(u, "priority", 0);
return {
url,
prio
};
}
catch {
return null;
}
}).filter(u => !!u).reduce((p, c) => {
if (!c) {
return null;
}
if (!p || p.prio < c.prio) {
return c;
}
return p;
});
if (!url) {
continue;
}
batch = parseNum(file, "num", batch, NS_DTA);
const idx = parseNum(file, "idx", 0, NS_DTA);
const item: BaseItem = {
url: url.url.toString(),
usable: decodeURIComponent(url.url.toString()),
batch,
idx
};
const ref = file.getAttributeNS(NS_DTA, "referrer");
if (ref) {
item.referrer = ref;
item.usableReferrer = decodeURIComponent(ref);
}
const mask = file.getAttributeNS(NS_DTA, "mask");
if (mask) {
item.mask = mask;
}
items.push(item);
}
catch (ex) {
console.error("Failed to import file", ex);
}
}
return items;
}
function parseKV(current: BaseItem, line: string) {
const [k, v] = line.split("=", 2);
switch (k.toLocaleLowerCase().trim()) {
case "referer": {
const rurls = getTextLinks(v);
if (rurls && rurls.length) {
current.referrer = rurls.pop();
current.usableReferrer = decodeURIComponent(current.referrer || "");
}
break;
}
}
}
export function importText(data: string) {
if (data.includes(NS_METALINK_RFC5854)) {
return importMeta4(data);
}
const splitter = /((?:.|\r)+)\n|(.+)$/g;
const spacer = /^\s+/;
let match;
let current: BaseItem | undefined = undefined;
let idx = 0;
const items = [];
while ((match = splitter.exec(data)) !== null) {
try {
const line = match[0].trimRight();
if (!line) {
continue;
}
if (spacer.test(line)) {
if (!current) {
continue;
}
parseKV(current, line);
continue;
}
const urls = getTextLinks(line);
if (!urls || !urls.length) {
continue;
}
current = {
url: urls[0],
usable: decodeURIComponent(urls[0]),
idx: ++idx
};
items.push(current);
}
catch (ex) {
current = undefined;
console.error("Failed to import", ex);
}
}
return items;
}
export interface Exporter {
fileName: string;
getText(items: BaseItem[]): string;
}
class TextExporter {
readonly fileName: string;
constructor() {
this.fileName = "links.txt";
}
getText(items: BaseItem[]) {
const lines = [];
for (const item of items) {
lines.push(item.url);
}
return lines.join("\n");
}
}
class Aria2Exporter {
readonly fileName: string;
constructor() {
this.fileName = "links.aria2.txt";
}
getText(items: BaseItem[]) {
const lines = [];
for (const item of items) {
lines.push(item.url);
if (item.referrer) {
lines.push(` referer=${item.referrer}`);
}
}
return lines.join("\n");
}
}
class MetalinkExporter {
readonly fileName: string;
constructor() {
this.fileName = "links.meta4";
}
getText(items: BaseItem[]) {
const document = window.document.implementation.
createDocument(NS_METALINK_RFC5854, "metalink", null);
const root = document.documentElement;
root.setAttributeNS(NS_DTA, "generator", "DownThemAll!");
root.appendChild(document.createComment(
"metalink as exported by DownThemAll!",
));
for (const item of items) {
const aitem = item as any;
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
f.setAttribute("name", aitem.currentName);
if (item.batch) {
f.setAttributeNS(NS_DTA, "num", item.batch.toString());
}
if (item.idx) {
f.setAttributeNS(NS_DTA, "idx", item.idx.toString());
}
if (item.referrer) {
f.setAttributeNS(NS_DTA, "referrer", item.referrer);
}
if (item.mask) {
f.setAttributeNS(NS_DTA, "mask", item.mask);
}
if (item.description) {
const n = document.createElementNS(NS_METALINK_RFC5854, "description");
n.textContent = item.description;
f.appendChild(n);
}
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
u.textContent = item.url;
f.appendChild(u);
if (aitem.totalSize > 0) {
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
s.textContent = aitem.totalSize.toString();
f.appendChild(s);
}
root.appendChild(f);
}
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
xml += root.outerHTML;
return xml;
}
}
export const textExporter = new TextExporter();
export const aria2Exporter = new Aria2Exporter();
export const metalinkExporter = new MetalinkExporter();

4
lib/ipreg.ts Normal file
View File

@ -0,0 +1,4 @@
"use strict";
// License: MIT
export const IPReg = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$|^(?:(?:(?:[0-9a-fA-F]{1,4}):){7}(?:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){6}(?:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|:(?:[0-9a-fA-F]{1,4})|:)|(?:(?:[0-9a-fA-F]{1,4}):){5}(?::((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,2}|:)|(?:(?:[0-9a-fA-F]{1,4}):){4}(?:(:(?:[0-9a-fA-F]{1,4})){0,1}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,3}|:)|(?:(?:[0-9a-fA-F]{1,4}):){3}(?:(:(?:[0-9a-fA-F]{1,4})){0,2}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,4}|:)|(?:(?:[0-9a-fA-F]{1,4}):){2}(?:(:(?:[0-9a-fA-F]{1,4})){0,3}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,5}|:)|(?:(?:[0-9a-fA-F]{1,4}):){1}(?:(:(?:[0-9a-fA-F]{1,4})){0,4}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(:(?:[0-9a-fA-F]{1,4})){1,6}|:)|(?::((?::(?:[0-9a-fA-F]{1,4})){0,5}:((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(?::(?:[0-9a-fA-F]{1,4})){1,7}|:)))(%[0-9a-zA-Z]{1,})?$/;

View File

@ -4,18 +4,34 @@
import { ALLOWED_SCHEMES } from "./constants"; import { ALLOWED_SCHEMES } from "./constants";
import { TRANSFERABLE_PROPERTIES } from "./constants"; import { TRANSFERABLE_PROPERTIES } from "./constants";
export interface BaseItem {
url: string;
usable: string;
referrer?: string;
usableReferrer?: string;
description?: string;
title?: string;
fileName?: string;
batch?: number;
idx: number;
mask?: string;
subfolder?: string;
startDate?: number;
private?: boolean;
postData?: string;
paused?: boolean;
}
const OPTIONPROPS = Object.freeze([ const OPTIONPROPS = Object.freeze([
"referrer", "usableReferrer", "referrer", "usableReferrer",
"description", "title", "description", "title",
"fileName", "fileName",
"batch", "idx", "batch", "idx",
"mask", "mask",
"fromMetalink", "subfolder",
"startDate", "startDate",
"hashes",
"private", "private",
"postData", "postData",
"cleanRequest",
"paused" "paused"
]); ]);
@ -34,7 +50,7 @@ function maybeAssign(options: any, what: any) {
this[what] = val; this[what] = val;
} }
export class Item { export class Item implements BaseItem {
public url: string; public url: string;
public usable: string; public usable: string;
@ -43,6 +59,8 @@ export class Item {
public usableReferrer: string; public usableReferrer: string;
public idx: number;
constructor(raw: any, options?: any) { constructor(raw: any, options?: any) {
Object.assign(this, raw); Object.assign(this, raw);
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {})); OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
@ -99,7 +117,7 @@ function transfer(e: any, other: any) {
} }
export function makeUniqueItems(items: any, mapping?: Function) { export function makeUniqueItems(items: any[][], mapping?: Function) {
const known = new Map(); const known = new Map();
const unique = []; const unique = [];
for (const itemlist of items) { for (const itemlist of items) {

View File

@ -5,6 +5,8 @@
import { parsePath, URLd } from "../util"; import { parsePath, URLd } from "../util";
import { QUEUED, RUNNING, PAUSED } from "./state"; import { QUEUED, RUNNING, PAUSED } from "./state";
import Renamer from "./renamer"; import Renamer from "./renamer";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../item";
const SAVEDPROPS = [ const SAVEDPROPS = [
"state", "state",
@ -14,6 +16,7 @@ const SAVEDPROPS = [
"usableReferrer", "usableReferrer",
"fileName", "fileName",
"mask", "mask",
"subfolder",
"date", "date",
// batches // batches
"batch", "batch",
@ -27,10 +30,11 @@ const SAVEDPROPS = [
"written", "written",
// server stuff // server stuff
"serverName", "serverName",
"browserName",
"mime",
"prerolled",
// other options // other options
"private", "private",
"fromMetalink",
"cleanRequest",
// db // db
"manId", "manId",
"dbId", "dbId",
@ -41,10 +45,15 @@ const DEFAULTS = {
state: QUEUED, state: QUEUED,
error: "", error: "",
serverName: "", serverName: "",
browserName: "",
fileName: "", fileName: "",
totalSize: 0, totalSize: 0,
written: 0, written: 0,
manId: 0, manId: 0,
mime: "",
prerolled: false,
retries: 0,
deadline: 0
}; };
let sessionId = 0; let sessionId = 0;
@ -61,14 +70,26 @@ export class BaseDownload {
public url: string; public url: string;
public usable: string;
public uReferrer: URLd; public uReferrer: URLd;
public referrer: string; public referrer: string;
public usableReferrer: string;
public startDate: Date; public startDate: Date;
public fileName: string; public fileName: string;
public description?: string;
public title?: string;
public batch: number;
public idx: number;
public error: string; public error: string;
public postData: any; public postData: any;
@ -81,10 +102,19 @@ export class BaseDownload {
public serverName: string; public serverName: string;
public browserName: string;
public mime: string;
public mask: string; public mask: string;
public subfolder: string;
constructor(options: any) { public prerolled: boolean;
public retries: number;
constructor(options: BaseItem) {
Object.assign(this, DEFAULTS); Object.assign(this, DEFAULTS);
this.assign(options); this.assign(options);
if (this.state === RUNNING) { if (this.state === RUNNING) {
@ -92,14 +122,16 @@ export class BaseDownload {
} }
this.sessionId = ++sessionId; this.sessionId = ++sessionId;
this.renamer = new Renamer(this); this.renamer = new Renamer(this);
this.retries = 0;
} }
assign(options: any) { assign(options: BaseItem) {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this; const self: any = this;
const other: any = options;
for (const prop of SAVEDPROPS) { for (const prop of SAVEDPROPS) {
if (prop in options) { if (prop in options) {
self[prop] = options[prop]; self[prop] = other[prop];
} }
} }
this.uURL = new URL(this.url) as URLd; this.uURL = new URL(this.url) as URLd;
@ -117,6 +149,10 @@ export class BaseDownload {
return this.serverName || this.fileName || this.urlName || "index.html"; return this.serverName || this.fileName || this.urlName || "index.html";
} }
get currentName() {
return this.browserName || this.dest.name || this.finalName;
}
get urlName() { get urlName() {
const path = parsePath(this.uURL); const path = parsePath(this.uURL);
if (path.name) { if (path.name) {
@ -154,7 +190,11 @@ 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.currentFull = `${dest.path}/${rv.currentName}`;
rv.error = this.error; rv.error = this.error;
rv.ext = this.renamer.p_ext;
rv.retries = this.retries;
return rv; return rv;
} }
} }

View File

@ -1,22 +1,42 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import { Prefs } from "../prefs"; // eslint-disable-next-line no-unused-vars
import { parsePath } from "../util"; import { CHROME, downloads, DownloadOptions } from "../browser";
import { import { Prefs, PrefWatcher } from "../prefs";
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
FORCABLE, PAUSABLE, CANCELABLE,
} from "./state";
import { BaseDownload } from "./basedownload";
import { PromiseSerializer } from "../pserializer"; import { PromiseSerializer } from "../pserializer";
import { filterInSitu, parsePath } from "../util";
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { Manager } from "./man"; import { Manager } from "./man";
import { downloads } from "../browser"; import Renamer from "./renamer";
import {
CANCELABLE,
CANCELED,
DONE,
FORCABLE,
MISSING,
PAUSABLE,
PAUSED,
QUEUED,
RUNNING,
RETRYING
} from "./state";
// eslint-disable-next-line no-unused-vars
import { Preroller, PrerollResults } from "./preroller";
function isRecoverable(error: string) {
switch (error) {
case "SERVER_FAILED":
return true;
const setShelfEnabled = downloads.setShelfEnabled || function() { default:
// ignored return error.startsWith("NETWORK_");
}; }
}
const RETRIES = new PrefWatcher("retries", 5);
const RETRY_TIME = new PrefWatcher("retry-time", 5);
export class Download extends BaseDownload { export class Download extends BaseDownload {
public manager: Manager; public manager: Manager;
@ -29,6 +49,10 @@ export class Download extends BaseDownload {
public error: string; public error: string;
public dbId: number;
public deadline: number;
constructor(manager: Manager, options: any) { constructor(manager: Manager, options: any) {
super(options); super(options);
this.manager = manager; this.manager = manager;
@ -38,6 +62,7 @@ export class Download extends BaseDownload {
} }
markDirty() { markDirty() {
this.renamer = new Renamer(this);
this.manager.setDirty(this); this.manager.setDirty(this);
} }
@ -59,22 +84,28 @@ export class Download extends BaseDownload {
if (this.manId) { if (this.manId) {
const {manId: id} = this; const {manId: id} = this;
try { try {
const state = await downloads.search({id}); const state = (await downloads.search({id})).pop() || {};
if (state[0].state === "in_progress") { if (state.state === "in_progress" && !state.error && !state.paused) {
this.changeState(RUNNING); this.changeState(RUNNING);
this.updateStateFromBrowser(); this.updateStateFromBrowser();
return; return;
} }
if (!state[0].canResume) { if (state.state === "complete") {
this.changeState(DONE);
this.updateStateFromBrowser();
return;
}
if (!state.canResume) {
throw new Error("Cannot resume"); throw new Error("Cannot resume");
} }
// Cannot await here // Cannot await here
// Firefox bug: will not return until download is finished // Firefox bug: will not return until download is finished
downloads.resume(id).catch(() => {}); downloads.resume(id).catch(console.error);
this.changeState(RUNNING); this.changeState(RUNNING);
return; return;
} }
catch (ex) { catch (ex) {
console.error("cannot resume", ex);
this.manager.removeManId(this.manId); this.manager.removeManId(this.manId);
this.removeFromBrowser(); this.removeFromBrowser();
} }
@ -82,46 +113,66 @@ 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.dest, this.mask); console.log("starting", this.toString(), this.toMsg());
this.changeState(RUNNING); this.changeState(RUNNING);
// Do NOT await
this.reallyStart();
}
private async reallyStart() {
try { try {
const options: any = { if (!this.prerolled) {
await this.maybePreroll();
if (this.state !== RUNNING) {
// Aborted by preroll
return;
}
}
const options: DownloadOptions = {
conflictAction: await Prefs.get("conflict-action"), conflictAction: await Prefs.get("conflict-action"),
filename: this.dest.full,
saveAs: false, saveAs: false,
url: this.url, url: this.url,
headers: [{ headers: [],
name: "X-DTA-Tag",
value: this.sessionId.toString(),
}],
}; };
if (!CHROME) {
options.filename = this.dest.full;
}
if (!CHROME && this.private) {
options.incognito = true;
}
if (this.postData) { if (this.postData) {
options.body = this.postData; options.body = this.postData;
options.method = "POST"; options.method = "POST";
} }
if (this.private) { if (!CHROME && this.referrer) {
options.incognito = true;
}
/* XXX "forbidden"
Cannot be worked around with webRequest either
as those do not see downloads.
if (this.referrer) {
options.headers.push({ options.headers.push({
name: "Referer", name: "Referer",
value: this.referrer value: this.referrer
}); });
} }
*/ else if (CHROME) {
options.headers.push({
name: "X-DTA-ID",
value: this.sessionId.toString(),
});
}
if (this.manId) { if (this.manId) {
this.manager.removeManId(this.manId); this.manager.removeManId(this.manId);
} }
setShelfEnabled(false);
try { try {
this.manager.addManId( this.manager.addManId(
this.manId = await downloads.download(options), this); this.manId = await downloads.download(options), this);
} }
finally { catch (ex) {
setShelfEnabled(true); if (!this.referrer) {
throw ex;
}
// Re-attempt without referrer
filterInSitu(options.headers, h => h.name !== "Referer");
this.manager.addManId(
this.manId = await downloads.download(options), this);
} }
this.markDirty(); this.markDirty();
} }
@ -132,6 +183,45 @@ export class Download extends BaseDownload {
} }
} }
private async maybePreroll() {
try {
if (this.prerolled) {
// Check again, just in case, async and all
return;
}
const roller = new Preroller(this);
if (!roller.shouldPreroll) {
return;
}
const res = await roller.roll();
if (!res) {
return;
}
this.adoptPrerollResults(res);
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
finally {
if (this.state === RUNNING) {
this.prerolled = true;
this.markDirty();
}
}
}
adoptPrerollResults(res: PrerollResults) {
if (res.mime) {
this.mime = res.mime;
}
if (res.name) {
this.serverName = res.name;
}
if (res.error) {
this.cancelAccordingToError(res.error);
}
}
resume(forced = false) { resume(forced = false) {
if (!(FORCABLE & this.state)) { if (!(FORCABLE & this.state)) {
return; return;
@ -144,26 +234,41 @@ export class Download extends BaseDownload {
} }
} }
async pause() { async pause(retry?: boolean) {
if (!(PAUSABLE & this.state)) { if (!(PAUSABLE & this.state)) {
return; return;
} }
if (!retry) {
this.retries = 0;
this.deadline = 0;
}
else {
// eslint-disable-next-line no-magic-numbers
this.deadline = Date.now() + RETRY_TIME.value * 60 * 1000;
}
if (this.state === RUNNING && this.manId) { if (this.state === RUNNING && this.manId) {
try { try {
await downloads.pause(this.manId); await downloads.pause(this.manId);
} }
catch (ex) { catch (ex) {
console.error("pause", ex.toString(), ex); console.error("pause", ex.toString(), ex);
this.cancel();
return; return;
} }
} }
this.changeState(PAUSED);
this.changeState(retry ? RETRYING : PAUSED);
} }
reset() { reset() {
this.prerolled = false;
this.manId = 0; this.manId = 0;
this.written = this.totalSize = 0; this.written = this.totalSize = 0;
this.serverName = ""; this.mime = this.serverName = this.browserName = "";
this.retries = 0;
this.deadline = 0;
} }
async removeFromBrowser() { async removeFromBrowser() {
@ -196,6 +301,17 @@ export class Download extends BaseDownload {
this.changeState(CANCELED); this.changeState(CANCELED);
} }
async cancelAccordingToError(error: string) {
if (!isRecoverable(error) || ++this.retries > RETRIES.value) {
this.cancel();
this.error = error;
return;
}
await this.pause(true);
this.error = error;
}
setMissing() { setMissing() {
if (this.manId) { if (this.manId) {
this.manager.removeManId(this.manId); this.manager.removeManId(this.manId);
@ -240,14 +356,19 @@ export class Download extends BaseDownload {
const state = (await downloads.search({id: this.manId})).pop(); const state = (await downloads.search({id: this.manId})).pop();
const {filename, error} = state; const {filename, error} = state;
const path = parsePath(filename); const path = parsePath(filename);
this.serverName = path.name; this.browserName = path.name;
this.adoptSize(state); this.adoptSize(state);
if (!this.mime && state.mime) {
this.mime = state.mime;
}
this.markDirty(); this.markDirty();
switch (state.state) { switch (state.state) {
case "in_progress": case "in_progress":
if (error) { if (state.paused) {
this.cancel(); this.changeState(PAUSED);
this.error = error; }
else if (error) {
this.cancelAccordingToError(error);
} }
else { else {
this.changeState(RUNNING); this.changeState(RUNNING);
@ -258,6 +379,9 @@ export class Download extends BaseDownload {
if (state.paused) { if (state.paused) {
this.changeState(PAUSED); this.changeState(PAUSED);
} }
else if (error) {
this.cancelAccordingToError(error);
}
else { else {
this.cancel(); this.cancel();
this.error = error || ""; this.error = error || "";
@ -274,4 +398,27 @@ export class Download extends BaseDownload {
this.setMissing(); this.setMissing();
} }
} }
updatefromSuggestion(state: any) {
const res: PrerollResults = {};
if (state.mime) {
res.mime = state.mime;
}
if (state.filename) {
res.name = state.filename;
}
if (state.finalUrl) {
res.finalURL = state.finalUrl;
const detected = Preroller.maybeFindNameFromSearchParams(this, res);
if (detected) {
res.name = detected;
}
}
try {
this.adoptPrerollResults(res);
}
finally {
this.markDirty();
}
}
} }

View File

@ -4,30 +4,39 @@
import { EventEmitter } from "../events"; import { EventEmitter } from "../events";
import { Notification } from "../notifications"; import { Notification } from "../notifications";
import { DB } from "../db"; import { DB } from "../db";
import { QUEUED, CANCELED, RUNNING } from "./state"; import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { Bus, Port } from "../bus"; import { Bus, Port } from "../bus";
import { sort } from "../sorting"; import { sort } from "../sorting";
import { Prefs } from "../prefs"; import { Prefs, PrefWatcher } from "../prefs";
import { _ } from "../i18n"; import { _ } from "../i18n";
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util"; import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
import { PromiseSerializer } from "../pserializer"; import { PromiseSerializer } from "../pserializer";
import {Download} from "./download"; import { Download } from "./download";
import {ManagerPort} from "./port"; import { ManagerPort } from "./port";
import {Scheduler} from "./scheduler"; import { Scheduler } from "./scheduler";
import {Limits} from "./limits"; import { Limits } from "./limits";
import { downloads } from "../browser"; import { downloads, runtime, webRequest, CHROME, OPERA } from "../browser";
const US = runtime.getURL("");
const AUTOSAVE_TIMEOUT = 2000; const AUTOSAVE_TIMEOUT = 2000;
const DIRTY_TIMEOUT = 100; const DIRTY_TIMEOUT = 100;
// eslint-disable-next-line no-magic-numbers // eslint-disable-next-line no-magic-numbers
const MISSING_TIMEOUT = 12 * 1000; const MISSING_TIMEOUT = 12 * 1000;
const RELOAD_TIMEOUT = 10 * 1000;
const setShelfEnabled = downloads.setShelfEnabled || function() {
// ignored
};
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
const SOUNDS = new PrefWatcher("sounds", false);
export class Manager extends EventEmitter { export class Manager extends EventEmitter {
private items: Download[]; private items: Download[];
private active: boolean; public active: boolean;
private notifiedFinished: boolean; private notifiedFinished: boolean;
@ -43,27 +52,43 @@ export class Manager extends EventEmitter {
private readonly running: Set<Download>; private readonly running: Set<Download>;
private readonly retrying: Set<Download>;
private scheduler: Scheduler | null; private scheduler: Scheduler | null;
private shouldReload: boolean;
private deadlineTimer: number;
constructor() { constructor() {
if (!document.location.href.includes("background")) {
throw new Error("Not on background");
}
super(); super();
this.active = true; this.active = true;
this.shouldReload = false;
this.notifiedFinished = true; this.notifiedFinished = true;
this.items = []; this.items = [];
this.saveQueue = new CoalescedUpdate( this.saveQueue = new CoalescedUpdate(
AUTOSAVE_TIMEOUT, this.save.bind(this)); AUTOSAVE_TIMEOUT, this.save.bind(this));
this.dirty = new CoalescedUpdate( this.dirty = new CoalescedUpdate(
DIRTY_TIMEOUT, this.processDirty.bind(this)); DIRTY_TIMEOUT, this.processDirty.bind(this));
this.processDeadlines = this.processDeadlines.bind(this);
this.sids = new Map(); this.sids = new Map();
this.manIds = new Map(); this.manIds = new Map();
this.ports = new Set(); this.ports = new Set();
this.scheduler = null; this.scheduler = null;
this.running = new Set(); this.running = new Set();
this.retrying = new Set();
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext); this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
downloads.onChanged.addListener(this.onChanged.bind(this)); downloads.onChanged.addListener(this.onChanged.bind(this));
downloads.onErased.addListener(this.onErased.bind(this)); downloads.onErased.addListener(this.onErased.bind(this));
if (CHROME && downloads.onDeterminingFilename) {
downloads.onDeterminingFilename.addListener(
this.onDeterminingFilename.bind(this));
}
Bus.onPort("manager", (port: Port) => { Bus.onPort("manager", (port: Port) => {
const mport = new ManagerPort(this, port); const mport = new ManagerPort(this, port);
@ -71,10 +96,19 @@ export class Manager extends EventEmitter {
this.ports.delete(mport); this.ports.delete(mport);
}); });
this.ports.add(mport); this.ports.add(mport);
return true;
}); });
Limits.on("changed", () => { Limits.on("changed", () => {
this.resetScheduler(); this.resetScheduler();
}); });
if (CHROME) {
webRequest.onBeforeSendHeaders.addListener(
this.stuffReferrer.bind(this),
{urls: ["<all_urls>"]},
["blocking", "requestHeaders", "extraHeaders"]
);
}
} }
async init() { async init() {
@ -88,9 +122,19 @@ 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(() => {
if (this.running.size) {
this.shouldReload = true;
return;
}
runtime.reload();
});
return this; return this;
} }
@ -121,6 +165,20 @@ export class Manager extends EventEmitter {
this.manIds.delete(downloadId); this.manIds.delete(downloadId);
} }
onDeterminingFilename(state: any, suggest: Function) {
const download = this.manIds.get(state.id);
if (!download) {
return;
}
try {
download.updatefromSuggestion(state);
}
finally {
const suggestion = {filename: download.dest.full};
suggest(suggestion);
}
}
async resetScheduler() { async resetScheduler() {
this.scheduler = null; this.scheduler = null;
await this.startNext(); await this.startNext();
@ -136,7 +194,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) {
@ -147,6 +205,7 @@ export class Manager extends EventEmitter {
} }
catch (ex) { catch (ex) {
next.changeState(CANCELED); next.changeState(CANCELED);
next.error = ex.toString();
console.error(ex.toString(), ex); console.error(ex.toString(), ex);
} }
} }
@ -155,19 +214,43 @@ export class Manager extends EventEmitter {
async startDownload(download: Download) { async startDownload(download: Download) {
// Add to running first, so we don't confuse the scheduler and other parts // Add to running first, so we don't confuse the scheduler and other parts
this.running.add(download); this.running.add(download);
setShelfEnabled(false);
await download.start(); await download.start();
this.notifiedFinished = false; this.notifiedFinished = false;
} }
async maybeNotifyFinished() { maybeRunFinishActions() {
if (!(await Prefs.get("finish-notification"))) { if (this.running.size) {
return; return;
} }
if (this.notifiedFinished || this.running.size) { this.maybeNotifyFinished();
if (this.shouldReload) {
this.saveQueue.trigger();
setTimeout(() => {
if (this.running.size) {
return;
}
runtime.reload();
}, RELOAD_TIMEOUT);
}
setShelfEnabled(true);
}
maybeNotifyFinished() {
if (this.notifiedFinished || this.running.size || this.retrying.size) {
return; return;
} }
if (SOUNDS.value && !OPERA) {
const audio = new Audio(runtime.getURL("/style/done.opus"));
audio.addEventListener("canplaythrough", () => audio.play());
audio.addEventListener("ended", () => document.body.removeChild(audio));
audio.addEventListener("error", () => document.body.removeChild(audio));
document.body.appendChild(audio);
}
if (FINISH_NOTIFICATION.value) {
new Notification(null, _("queue-finished"));
}
this.notifiedFinished = true; this.notifiedFinished = true;
new Notification(null, _("queue-finished"));
} }
addManId(id: number, download: Download) { addManId(id: number, download: Download) {
@ -215,7 +298,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);
} }
@ -265,7 +348,10 @@ export class Manager extends EventEmitter {
changedState(download: Download, oldState: number, newState: number) { changedState(download: Download, oldState: number, newState: number) {
if (oldState === RUNNING) { if (oldState === RUNNING) {
this.running.delete(download); this.running.delete(download);
this.maybeNotifyFinished(); }
else if (oldState === RETRYING) {
this.retrying.delete(download);
this.findDeadline();
} }
if (newState === QUEUED) { if (newState === QUEUED) {
this.resetScheduler(); this.resetScheduler();
@ -278,7 +364,53 @@ export class Manager extends EventEmitter {
this.running.add(download); this.running.add(download);
} }
else { else {
this.startNext(); if (newState === RETRYING) {
this.addRetry(download);
}
this.startNext().catch(console.error);
}
}
addRetry(download: Download) {
this.retrying.add(download);
this.findDeadline();
}
private findDeadline() {
let deadline = Array.from(this.retrying).
reduce<number>((deadline, item) => {
if (deadline) {
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
}
return item.deadline;
}, 0);
if (deadline <= 0) {
return;
}
deadline -= Date.now();
if (deadline <= 0) {
return;
}
if (this.deadlineTimer) {
window.clearTimeout(this.deadlineTimer);
}
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
}
private processDeadlines() {
this.deadlineTimer = 0;
try {
const now = Date.now();
this.items.forEach(item => {
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
this.retrying.delete(item);
item.resume(false);
}
});
}
finally {
this.findDeadline();
} }
} }
@ -341,6 +473,35 @@ export class Manager extends EventEmitter {
} }
this.emit("active", this.active); this.emit("active", this.active);
} }
getMsgItems() {
return this.items.map(e => e.toMsg());
}
stuffReferrer(details: any): any {
if (details.tabId > 0 && !US.startsWith(details.initiator)) {
return undefined;
}
const sidx = details.requestHeaders.findIndex(
(e: any) => e.name.toLowerCase() === "x-dta-id");
if (sidx < 0) {
return undefined;
}
const sid = parseInt(details.requestHeaders[sidx].value, 10);
details.requestHeaders.splice(sidx, 1);
const item = this.sids.get(sid);
if (!item) {
return undefined;
}
details.requestHeaders.push({
name: "Referer",
value: (item.uReferrer || item.uURL).toString()
});
const rv: any = {
requestHeaders: details.requestHeaders
};
return rv;
}
} }
let inited: Promise<Manager>; let inited: Promise<Manager>;

View File

@ -5,6 +5,12 @@ 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";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../item";
type SID = {sid: number}; type SID = {sid: number};
type SIDS = { type SIDS = {
@ -13,9 +19,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;
@ -38,6 +44,9 @@ export class ManagerPort {
port.on("prefs", () => { port.on("prefs", () => {
openPrefs(); openPrefs();
}); });
port.on("import", ({items}: {items: BaseItem[]}) => {
API.regular(items, []);
});
port.on("all", () => this.sendAll()); port.on("all", () => this.sendAll());
port.on("removeSids", this.onMsgRemoveSids); port.on("removeSids", this.onMsgRemoveSids);
port.on("showSingle", async () => { port.on("showSingle", async () => {
@ -61,6 +70,7 @@ export class ManagerPort {
delete this.manager; delete this.manager;
delete this.port; delete this.port;
}); });
this.port.post("active", this.manager.active); this.port.post("active", this.manager.active);
this.sendAll(); this.sendAll();
} }
@ -78,7 +88,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()));
} }
} }

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

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

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;
} }
@ -169,24 +193,24 @@ export default class Renamer {
} }
toString() { toString() {
const {mask} = this.d; const {mask, subfolder} = this.d;
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this; const self: any = this;
// XXX flat const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) { return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
let prop = type.substr(1, type.length - 2); let prop = type.substr(1, type.length - 2);
const flat = prop.startsWith("flat"); const flat = prop.startsWith("flat");
if (flat) { if (flat) {
prop = prop.substr(4); prop = prop.substr(4);
} }
prop = `p_${prop}`; prop = `p_${prop}`;
const rv = (prop in self) ? let rv = (prop in self) ?
(self[prop] || "").trim() : (self[prop] || "").trim() :
type; type;
if (flat) { if (flat) {
return rv.replace(/\/+/g, "-"); rv = rv.replace(/[/\\]+/g, "-");
} }
return rv; return rv.replace(/\/{2,}/g, "/");
})); }));
} }
} }

View File

@ -8,8 +8,9 @@ export const PAUSED = 1 << 3;
export const DONE = 1 << 4; export const DONE = 1 << 4;
export const CANCELED = 1 << 5; export const CANCELED = 1 << 5;
export const MISSING = 1 << 6; export const MISSING = 1 << 6;
export const RETRYING = 1 << 7;
export const RESUMABLE = PAUSED | CANCELED; export const RESUMABLE = PAUSED | CANCELED | RETRYING;
export const FORCABLE = PAUSED | QUEUED | CANCELED; export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
export const PAUSABLE = QUEUED | CANCELED | RUNNING; export const PAUSABLE = QUEUED | CANCELED | RUNNING | RETRYING;
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING; export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING | RETRYING;

65
lib/mime.ts Normal file
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

@ -12,13 +12,20 @@ const DEFAULTS = {
message: "message", message: "message",
}; };
const TIMEOUT = 4000;
let gid = 1;
export class Notification extends EventEmitter { export class Notification extends EventEmitter {
private notification: any; private notification: any;
private readonly generated: boolean;
constructor(id: string | null, options = {}) { constructor(id: string | null, options = {}) {
super(); super();
id = id || "DownThemAll-notification"; this.generated = !id;
id = id || `DownThemAll-notification${++gid}`;
if (typeof options === "string") { if (typeof options === "string") {
options = {message: options}; options = {message: options};
} }
@ -39,11 +46,16 @@ export class Notification extends EventEmitter {
opened(notification: any) { opened(notification: any) {
this.notification = notification; this.notification = notification;
this.emit("opened", this); this.emit("opened", this);
if (this.generated) {
setTimeout(() => {
notifications.clear(notification);
}, TIMEOUT);
}
} }
clicked(notification: any, button?: number) { clicked(notification: any, button?: number) {
// We can only be clicked, when we were opened, at which point the // We can only be clicked, when we were opened, at which point the
// notification id is availablfalse // notification id is available
if (notification !== this.notification) { if (notification !== this.notification) {
return; return;
} }
@ -52,6 +64,7 @@ export class Notification extends EventEmitter {
return; return;
} }
this.emit("clicked", this); this.emit("clicked", this);
console.log("clicked", notification);
} }
async closed(notification: any) { async closed(notification: any) {

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");
@ -99,6 +99,5 @@ export class PrefWatcher {
changed(prefs: any, key: string, value: any) { changed(prefs: any, key: string, value: any) {
this.value = value; this.value = value;
return true;
} }
} }

View File

@ -36,7 +36,7 @@ export class RecentList {
this.pref = `savedlist-${pref}`; this.pref = `savedlist-${pref}`;
this.defaults = Array.from(defaults); this.defaults = Array.from(defaults);
this[LIST] = []; this[LIST] = [];
this.limit = 5; this.limit = 15;
} }
get values() { get values() {
@ -116,3 +116,9 @@ export const FASTFILTER = new RecentList("fastfilter", [
"*.z??, *.css, *.html" "*.z??, *.css, *.html"
]); ]);
FASTFILTER.init().catch(console.error); FASTFILTER.init().catch(console.error);
export const SUBFOLDER = new RecentList("subfolder", [
"",
"downthemall",
]);
SUBFOLDER.init().catch(console.error);

View File

@ -9,12 +9,28 @@ import { donate, openPrefs, openUrls } from "./windowutils";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { filters, FAST, Filter } from "./filters"; import { filters, FAST, Filter } from "./filters";
import { WindowStateTracker } from "./windowstatetracker"; import { WindowStateTracker } from "./windowstatetracker";
import { windows } from "./browser"; import { windows, CHROME } from "./browser";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
interface BaseMatchedItem extends BaseItem {
sidx?: number;
matched?: string | null;
prevMatched?: string | null;
}
function computeSelection(filters: any[], items: any[], onlyFast: boolean) { export interface ItemDelta {
let ws = items.map((item: any, idx: number) => { idx: number;
item.idx = idx; matched?: string | null;
}
function computeSelection(
filters: Filter[],
items: BaseMatchedItem[],
onlyFast: boolean): ItemDelta[] {
let ws = items.map((item, idx: number) => {
item.idx = item.idx || idx;
item.sidx = item.sidx || idx;
const {matched = null} = item; const {matched = null} = item;
item.prevMatched = matched; item.prevMatched = matched;
item.matched = null; item.matched = null;
@ -23,16 +39,22 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
for (const filter of filters) { for (const filter of filters) {
ws = ws.filter(item => { ws = ws.filter(item => {
if (filter.matchItem(item)) { if (filter.matchItem(item)) {
item.matched = filter.id === FAST ? if (filter.id === FAST) {
"fast" : item.matched = "fast";
(onlyFast ? null : filter.id); }
else if (!onlyFast && typeof filter.id === "string") {
item.matched = filter.id;
}
else {
item.matched = null;
}
} }
return !item.matched; return !item.matched;
}); });
} }
return items.filter(item => item.prevMatched !== item.matched). map(item => { return items.filter(item => item.prevMatched !== item.matched).map(item => {
return { return {
idx: item.idx, idx: item.sidx as number,
matched: item.matched matched: item.matched
}; };
}); });
@ -41,6 +63,9 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
function *computeActiveFiltersGen( function *computeActiveFiltersGen(
filters: Filter[], activeOverrides: Map<string, boolean>) { filters: Filter[], activeOverrides: Map<string, boolean>) {
for (const filter of filters) { for (const filter of filters) {
if (typeof filter.id !== "string") {
continue;
}
const override = activeOverrides.get(filter.id); const override = activeOverrides.get(filter.id);
if (typeof override === "boolean") { if (typeof override === "boolean") {
if (override) { if (override) {
@ -59,11 +84,11 @@ function computeActiveFilters(
return Array.from(computeActiveFiltersGen(filters, activeOverrides)); return Array.from(computeActiveFiltersGen(filters, activeOverrides));
} }
function filtersToDescs(filters: any[]) { function filtersToDescs(filters: Filter[]) {
return filters.map(f => f.descriptor); return filters.map(f => f.descriptor);
} }
export async function select(links: any[], media: any[]) { export async function select(links: BaseItem[], media: BaseItem[]) {
const fm = await filters(); const fm = await filters();
const tracker = new WindowStateTracker("select", { const tracker = new WindowStateTracker("select", {
minWidth: 700, minWidth: 700,
@ -75,9 +100,16 @@ export async function select(links: any[], media: any[]) {
type: "popup", type: "popup",
}); });
const window = await windows.create(windowOptions); const window = await windows.create(windowOptions);
tracker.track(window.id);
try { try {
if (!CHROME) {
windows.update(window.id, tracker.getOptions({}));
}
const port = await Promise.race<Port>([ const port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("select", resolve)), new Promise<Port>(resolve => Bus.oncePort("select", port => {
resolve(port);
return true;
})),
timeout<Port>(5 * 1000)]); timeout<Port>(5 * 1000)]);
if (!port.isSelf) { if (!port.isSelf) {
throw Error("Invalid sender connected"); throw Error("Invalid sender connected");
@ -85,26 +117,26 @@ export async function select(links: any[], media: any[]) {
tracker.track(window.id, port); tracker.track(window.id, port);
const overrides = new Map(); const overrides = new Map();
let fast: any = null; let fast: Filter | null = null;
let onlyFast: false; let onlyFast: false;
try { try {
fast = fm.getFastFilter(); fast = await fm.getFastFilter();
} }
catch (ex) { catch (ex) {
// ignored // ignored
} }
const sendFilters = function(delta = false) { const sendFilters = function(delta = false) {
let {linkFilters, mediaFilters} = fm; const {linkFilters, mediaFilters} = fm;
const alink = computeActiveFilters(linkFilters, overrides); const alink = computeActiveFilters(linkFilters, overrides);
const amedia = computeActiveFilters(mediaFilters, overrides); const amedia = computeActiveFilters(mediaFilters, overrides);
const sactiveFilters = new Set<any>(); const sactiveFilters = new Set<any>();
[alink, amedia].forEach( [alink, amedia].forEach(
a => a.forEach(filter => sactiveFilters.add(filter.id))); a => a.forEach(filter => sactiveFilters.add(filter.id)));
const activeFilters = Array.from(sactiveFilters); const activeFilters = Array.from(sactiveFilters);
linkFilters = filtersToDescs(linkFilters); const linkFilterDescs = filtersToDescs(linkFilters);
mediaFilters = filtersToDescs(mediaFilters); const mediaFilterDescs = filtersToDescs(mediaFilters);
port.post("filters", {linkFilters, mediaFilters, activeFilters}); port.post("filters", {linkFilterDescs, mediaFilterDescs, activeFilters});
if (fast) { if (fast) {
alink.unshift(fast); alink.unshift(fast);
@ -128,9 +160,6 @@ export async function select(links: any[], media: any[]) {
}); });
port.on("queue", (msg: any) => { port.on("queue", (msg: any) => {
const selected = new Set<number>(msg.items);
const items = (msg.type === "links" ? links : media);
msg.items = items.filter((item: any, idx: number) => selected.has(idx));
done.resolve(msg); done.resolve(msg);
}); });
@ -166,8 +195,8 @@ export async function select(links: any[], media: any[]) {
openPrefs(); openPrefs();
}); });
port.on("openUrls", ({urls}) => { port.on("openUrls", ({urls, incognito}) => {
openUrls(urls); openUrls(urls, incognito);
}); });
try { try {
@ -175,7 +204,11 @@ export async function select(links: any[], media: any[]) {
sendFilters(false); sendFilters(false);
const type = await Prefs.get("last-type", "links"); const type = await Prefs.get("last-type", "links");
port.post("items", {type, links, media}); port.post("items", {type, links, media});
const result = await done; const {items, options} = await done;
const selectedIndexes = new Set<number>(items);
const selectedList = (options.type === "links" ? links : media);
const selectedItems = selectedList.filter(
(item: BaseItem, idx: number) => selectedIndexes.has(idx));
for (const [filter, override] of overrides) { for (const [filter, override] of overrides) {
const f = fm.get(filter); const f = fm.get(filter);
if (f) { if (f) {
@ -183,7 +216,7 @@ export async function select(links: any[], media: any[]) {
} }
} }
await fm.save(); await fm.save();
return result; return {items: selectedItems, options};
} }
finally { finally {
fm.off("changed", sendFilters); fm.off("changed", sendFilters);

View File

@ -6,9 +6,11 @@ import { Bus, Port } from "./bus";
import { WindowStateTracker } from "./windowstatetracker"; import { WindowStateTracker } from "./windowstatetracker";
import { Promised, timeout } from "./util"; import { Promised, timeout } from "./util";
import { donate } from "./windowutils"; import { donate } from "./windowutils";
import { windows } from "./browser"; import { windows, CHROME } from "./browser";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
export async function single(item: any) { export async function single(item: BaseItem | null) {
const tracker = new WindowStateTracker("single", { const tracker = new WindowStateTracker("single", {
minWidth: 700, minWidth: 700,
minHeight: 460 minHeight: 460
@ -19,9 +21,16 @@ export async function single(item: any) {
type: "popup", type: "popup",
}); });
const window = await windows.create(windowOptions); const window = await windows.create(windowOptions);
tracker.track(window.id);
try { try {
if (!CHROME) {
windows.update(window.id, tracker.getOptions({}));
}
const port: Port = await Promise.race<Port>([ const port: Port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("single", resolve)), new Promise<Port>(resolve => Bus.oncePort("single", port => {
resolve(port);
return true;
})),
timeout<Port>(5 * 1000)]); timeout<Port>(5 * 1000)]);
if (!port.isSelf) { if (!port.isSelf) {
throw Error("Invalid sender connected"); throw Error("Invalid sender connected");
@ -46,7 +55,9 @@ export async function single(item: any) {
donate(); donate();
}); });
port.post("item", item); if (item) {
port.post("item", {item});
}
return await done; return await done;
} }
finally { finally {

View File

@ -2,8 +2,9 @@
// 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"; import { IPReg } from "./ipreg";
export { debounce } from "../uikit/lib/util";
export class Promised { export class Promised {
private promise: Promise<any>; private promise: Promise<any>;
@ -96,8 +97,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 +192,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> {
@ -179,7 +238,10 @@ export interface URLd extends URL {
Object.defineProperty(URL.prototype, "domain", { Object.defineProperty(URL.prototype, "domain", {
get() { get() {
try { try {
return hostToDomain(this.host) || this.host; const {hostname} = this;
return IPReg.test(hostname) ?
hostname :
hostToDomain(hostname) || hostname;
} }
catch (ex) { catch (ex) {
console.error(ex); console.error(ex);
@ -299,3 +361,20 @@ export function mapFilterInSitu<TRes, T>(
export function randint(min: number, max: number) { export function randint(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min; return Math.floor(Math.random() * (max - min)) + min;
} }
export function validateSubFolder(folder: string) {
if (!folder) {
return;
}
folder = folder.replace(/[/\\]+/g, "/");
if (folder.startsWith("/")) {
throw new Error("error.noabsolutepath");
}
if (/^[a-z]:\//i.test(folder)) {
throw new Error("error.noabsolutepath");
}
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
throw new Error("error.nodotsinpath");
}
}

View File

@ -3,6 +3,8 @@
import { Prefs } from "./prefs"; import { Prefs } from "./prefs";
import { windows } from "./browser"; import { windows } from "./browser";
// eslint-disable-next-line no-unused-vars
import { Port } from "./bus";
const VALID_WINDOW_STATES = Object.freeze(new Set(["normal", "maximized"])); const VALID_WINDOW_STATES = Object.freeze(new Set(["normal", "maximized"]));
@ -55,13 +57,15 @@ 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 (this.top >= 0) { if (result.state !== "maximized") {
result.top = this.top; result.width = this.width;
result.left = this.left; result.height = this.height;
if (this.top >= 0) {
result.top = this.top;
result.left = this.left;
}
} }
return result; return result;
} }
@ -78,34 +82,48 @@ export class WindowStateTracker {
if (!this.windowId) { if (!this.windowId) {
return; return;
} }
const window = await windows.get(this.windowId); try {
if (!VALID_WINDOW_STATES.has(window.state)) { const window = await windows.get(this.windowId);
return; if (!VALID_WINDOW_STATES.has(window.state)) {
return;
}
const previous = JSON.stringify(this);
this.width = window.width;
this.height = window.height;
this.left = window.left;
this.top = window.top;
this.state = window.state;
this.validate();
if (previous === JSON.stringify(this)) {
// Nothing changed
return;
}
await this.save();
} }
const previous = JSON.stringify(this); catch {
this.width = window.width; // ignored
this.height = window.height;
this.left = window.left;
this.top = window.top;
this.state = window.state;
this.validate();
if (previous === JSON.stringify(this)) {
// Nothing changed
return;
} }
await this.save();
} }
track(windowId: number, port: any) { track(windowId: number, port?: Port) {
if (port) { if (port) {
port.on("resized", this.update); port.on("resized", this.update);
port.on("unload", e => this.finalize(e));
port.on("disconnect", this.finalize.bind(this));
} }
this.windowId = windowId; this.windowId = windowId;
} }
async finalize() { async finalize(state?: any) {
if (state) {
this.left = state.left;
this.top = state.top;
}
await this.update(); await this.update();
this.windowId = 0; this.windowId = 0;
if (state) {
await this.save();
}
} }
async save() { async save() {

View File

@ -1,30 +1,61 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import { windows, tabs, runtime } from "../lib/browser"; import { windows, tabs, runtime, CHROME } from "../lib/browser";
import {getManager} from "./manager/man"; import { getManager } from "./manager/man";
import * as DEFAULT_ICONS from "../data/icons.json"; import DEFAULT_ICONS from "../data/icons.json";
import { Prefs } from "./prefs";
import { _ } from "./i18n";
import { WindowStateTracker } from "./windowstatetracker";
// eslint-disable-next-line no-unused-vars
import { Port, Bus } from "./bus";
import { timeout } from "./util";
const DONATE_URL = "https://www.downthemall.org/howto/donate/"; const DONATE_URL = "https://www.downthemall.org/howto/donate/";
const DONATE_LANG_URLS = Object.freeze(new Map([
["de", "https://www.downthemall.org/howto/donate/spenden/"],
]));
const MANAGER_URL = "/windows/manager.html"; const MANAGER_URL = "/windows/manager.html";
const IS_CHROME = navigator && navigator.userAgent.includes("Chrome"); export async function mostRecentBrowser(incognito: boolean): Promise<any> {
let window;
try {
export async function mostRecentBrowser(): Promise<any> { window = await windows.getCurrent();
let window = Array.from(await windows.getAll({windowTypes: ["normal"]})). if (window.type !== "normal") {
filter((w: any) => w.type === "normal").pop(); throw new Error("not a normal window");
}
if (incognito && !window.incognito) {
throw new Error("Not incognito");
}
}
catch {
try {
window = await windows.getlastFocused();
if (window.type !== "normal") {
throw new Error("not a normal window");
}
if (incognito && !window.incognito) {
throw new Error("Not incognito");
}
}
catch {
window = Array.from(await windows.getAll({windowTypes: ["normal"]})).
filter(
(w: any) => w.type === "normal" && !!w.incognito === !!incognito).
pop();
}
}
if (!window) { if (!window) {
window = await windows.create({ window = await windows.create({
url: DONATE_URL, incognito: !!incognito,
type: "normal", type: "normal",
}); });
} }
return window; return window;
} }
export async function openInTab(url: string) { export async function openInTab(url: string, incognito: boolean) {
const window = await mostRecentBrowser(); const window = await mostRecentBrowser(incognito);
await tabs.create({ await tabs.create({
active: true, active: true,
url, url,
@ -33,7 +64,7 @@ export async function openInTab(url: string) {
await windows.update(window.id, {focused: true}); await windows.update(window.id, {focused: true});
} }
export async function openInTabOrFocus(url: string) { export async function openInTabOrFocus(url: string, incognito: boolean) {
const etabs = await tabs.query({ const etabs = await tabs.query({
url url
}); });
@ -43,21 +74,22 @@ export async function openInTabOrFocus(url: string) {
await windows.update(tab.windowId, {focused: true}); await windows.update(tab.windowId, {focused: true});
return; return;
} }
await openInTab(url); await openInTab(url, incognito);
} }
export async function maybeOpenInTab(url: string) { export async function maybeOpenInTab(url: string, incognito: boolean) {
const etabs = await tabs.query({ const etabs = await tabs.query({
url url
}); });
if (etabs.length) { if (etabs.length) {
return; return;
} }
await openInTab(url); await openInTab(url, incognito);
} }
export async function donate() { export async function donate() {
await openInTab(DONATE_URL); const url = DONATE_LANG_URLS.get(_("language_code")) || DONATE_URL;
await openInTab(url, false);
} }
export async function openPrefs() { export async function openPrefs() {
@ -71,16 +103,64 @@ export async function openManager(focus = true) {
catch (ex) { catch (ex) {
console.error(ex.toString(), ex); console.error(ex.toString(), ex);
} }
const url = runtime.getURL(MANAGER_URL);
const openInPopup = await Prefs.get("manager-in-popup");
if (openInPopup) {
const etabs = await tabs.query({
url
});
if (etabs.length) {
if (!focus) {
return;
}
const tab = etabs.pop();
await tabs.update(tab.id, {active: true});
await windows.update(tab.windowId, {focused: true});
return;
}
const tracker = new WindowStateTracker("manager", {
minWidth: 700,
minHeight: 500,
});
await tracker.init();
const windowOptions = tracker.getOptions({
url,
type: "popup",
});
const window = await windows.create(windowOptions);
tracker.track(window.id);
try {
if (!CHROME) {
windows.update(window.id, tracker.getOptions({}));
}
const port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("manager", port => {
resolve(port);
return true;
})),
timeout<Port>(5 * 1000)]);
if (!port.isSelf) {
throw Error("Invalid sender connected");
}
tracker.track(window.id, port);
}
catch (ex) {
console.error("couldn't track manager", ex);
}
return;
}
if (focus) { if (focus) {
await openInTabOrFocus(await runtime.getURL(MANAGER_URL)); await openInTabOrFocus(runtime.getURL(MANAGER_URL), false);
} }
else { else {
await maybeOpenInTab(await runtime.getURL(MANAGER_URL)); await maybeOpenInTab(runtime.getURL(MANAGER_URL), false);
} }
} }
export async function openUrls(urls: string) { export async function openUrls(urls: string, incognito: boolean) {
const window = await mostRecentBrowser(); const window = await mostRecentBrowser(incognito);
for (const url of urls) { for (const url of urls) {
try { try {
await tabs.create({ await tabs.create({
@ -106,32 +186,10 @@ const ICONS = Object.freeze((() => {
return new Map<string, string>(rv); return new Map<string, string>(rv);
})()); })());
let iconForPathPlatform: Function; export const DEFAULT_ICON_SIZE = 16;
if (IS_CHROME) {
const FOUR = 128;
const DOUBLE = 64;
iconForPathPlatform = function(icon: string, size: number) {
let scale = "1x";
if (size > FOUR) {
// wishful thinking at this point
scale = "4x";
}
else if (size > DOUBLE) {
scale = "2x";
}
return `chrome://fileicon/${icon}?scale=${scale}`;
};
}
else {
// eslint-disable-next-line no-unused-vars
iconForPathPlatform = function(icon: string, size: number) {
return ICONS.get(icon) || "icon-file-generic";
};
}
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-magic-numbers export function iconForPath(path: string, size = DEFAULT_ICON_SIZE) {
export function iconForPath(path: string, size = 16) {
const web = /^https?:\/\//.test(path); const web = /^https?:\/\//.test(path);
let file = path.split(/[\\/]/).pop(); let file = path.split(/[\\/]/).pop();
if (file) { if (file) {
@ -152,7 +210,7 @@ export function iconForPath(path: string, size = 16) {
file = "file"; file = "file";
} }
} }
return iconForPathPlatform(file, size); return ICONS.get(file) || "icon-file-generic";
} }
/** /**

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "DownThemAll!", "name": "DownThemAll!",
"version": "4.0.2", "version": "4.2.6",
"description": "__MSG_extensionDescription__", "description": "__MSG_extensionDescription__",
"homepage_url": "https://downthemall.org/", "homepage_url": "https://downthemall.org/",
@ -9,12 +9,13 @@
"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: http: https: 'self'; default-src 'self'",
"icons": { "icons": {
"16": "style/icon16.png", "16": "style/icon16.png",
"32": "style/icon32.png", "32": "style/icon32.png",
"48": "style/icon48.png", "48": "style/icon48.png",
"64": "style/icon64.png", "64": "style/icon64.png",
"96": "style/icon96.png",
"128": "style/icon128.png", "128": "style/icon128.png",
"256": "style/icon256.png" "256": "style/icon256.png"
}, },
@ -22,14 +23,19 @@
"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" "theme",
"webNavigation",
"webRequest",
"webRequestBlocking"
], ],
"background": { "background": {
@ -40,13 +46,13 @@
}, },
"browser_action": { "browser_action": {
"browser_style": false, "browser_style": true,
"default_popup": "windows/popup.html",
"default_icon": { "default_icon": {
"16": "style/icon16.png", "16": "style/icon16.png",
"32": "style/icon32.png", "32": "style/icon32.png",
"48": "style/icon48.png", "48": "style/icon48.png",
"64": "style/icon64.png", "64": "style/icon64.png",
"96": "style/icon96.png",
"128": "style/icon128.png", "128": "style/icon128.png",
"256": "style/icon256.png" "256": "style/icon256.png"
}, },

View File

@ -8,6 +8,7 @@
}, },
"scripts": { "scripts": {
"build": "util/build.py", "build": "util/build.py",
"build:cleanup": "rm -rf bundles",
"build:bundles": "webpack", "build:bundles": "webpack",
"build:regexps": "node util/makexregexps.js > data/xregexps.json", "build:regexps": "node util/makexregexps.js > data/xregexps.json",
"stats": "cloc --vcs=git --exclude-lang=Markdown,SVG", "stats": "cloc --vcs=git --exclude-lang=Markdown,SVG",
@ -18,22 +19,24 @@
"author": "Nils Maier", "author": "Nils Maier",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^12.7.2", "@types/node": "^12.7.8",
"@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/eslint-plugin": "^2.3.2",
"@typescript-eslint/parser": "^2.0.0", "@typescript-eslint/parser": "^2.3.2",
"chai": "^4.1.2", "chai": "^4.1.2",
"eslint": "^6.1.0", "eslint": "^6.5.1",
"mocha": "^6.2.0", "mocha": "^6.2.1",
"ts-loader": "^6.0.4", "ts-loader": "^6.2.0",
"ts-node": "^8.3.0", "ts-node": "^8.4.1",
"typescript": "^3.5.3", "typescript": "^3.6.3",
"webpack": "^4.39.2", "webpack": "^4.41.0",
"webpack-cli": "^3.3.6", "webpack-cli": "^3.3.9",
"xregexp": "^4.2.4" "xregexp": "^4.2.4"
}, },
"dependencies": { "dependencies": {
"@types/psl": "^1.1.0", "@types/psl": "^1.1.0",
"psl": "^1.3.0", "@types/whatwg-mimetype": "^2.1.0",
"webextension-polyfill": "^0.4.0" "psl": "^1.4.0",
"webextension-polyfill": "^0.5.0",
"whatwg-mimetype": "^2.3.0"
} }
} }

View File

@ -28,7 +28,9 @@ return url;
}(); }();
function makeURL(url: string) { function makeURL(url: string) {
return new URL(url, baseURL); const rv = new URL(url, baseURL);
rv.hash = "";
return rv;
} }
function sanitize(str: string | null | undefined) { function sanitize(str: string | null | undefined) {
@ -75,6 +77,8 @@ function urlToUsable(e: any, u: string) {
} }
class Gatherer { class Gatherer {
private: boolean;
textLinks: boolean; textLinks: boolean;
selectionOnly: boolean; selectionOnly: boolean;
@ -86,6 +90,7 @@ class Gatherer {
transferable: string[]; transferable: string[];
constructor(options: any) { constructor(options: any) {
this.private = !!options.private;
this.textLinks = options.textLinks; this.textLinks = options.textLinks;
this.selectionOnly = options.selectionOnly; this.selectionOnly = options.selectionOnly;
this.selection = options.selectionOnly ? getSelection() : null; this.selection = options.selectionOnly ? getSelection() : null;
@ -116,30 +121,43 @@ class Gatherer {
*collectImageInternal(img: HTMLImageElement) { *collectImageInternal(img: HTMLImageElement) {
try { try {
const src = img.currentSrc || img.src; {
const item = this.makeItem(src, img); const {src} = img;
if (item) { const item = this.makeItem(src, img);
item.fileName = "";
item.description = item.title;
yield item;
}
const {srcset} = img;
if (!srcset) {
return;
}
const imgs = srcset.split(",").flatMap(e => {
const idx = e.lastIndexOf(" ");
return (idx > 0 ? e.slice(0, idx) : e).trim();
});
for (const i of imgs) {
const item = this.makeItem(i, img);
if (item) { if (item) {
item.fileName = ""; item.fileName = "";
item.description = item.title; item.description = item.title;
yield item; yield item;
} }
} }
{
const {currentSrc} = img;
const item = this.makeItem(currentSrc, img);
if (item) {
item.fileName = "";
item.description = item.title;
yield item;
}
}
{
const {srcset} = img;
if (!srcset) {
return;
}
const imgs = srcset.split(",").flatMap(e => {
const idx = e.lastIndexOf(" ");
return (idx > 0 ? e.slice(0, idx) : e).trim();
});
for (const i of imgs) {
const item = this.makeItem(i, img);
if (item) {
item.fileName = "";
item.description = item.title;
yield item;
}
}
}
} }
catch (ex) { catch (ex) {
console.error("oops image", ex.toString(), ex.stack, ex); console.error("oops image", ex.toString(), ex.stack, ex);
@ -253,6 +271,7 @@ class Gatherer {
return { return {
url: url.href, url: url.href,
title, title,
private: this.private
}; };
} }
catch (ex) { catch (ex) {
@ -293,7 +312,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 = {
@ -311,10 +330,11 @@ function gather(msg: any, sender: any, callback: Function) {
), ),
}; };
urlToUsable(result, result.baseURL); urlToUsable(result, result.baseURL);
callback(result); return Promise.resolve(result);
} }
catch (ex) { catch (ex) {
console.error(ex.toString(), ex.stack, ex); console.error(ex.toString(), ex.stack, ex);
return Promise.resolve(null);
} }
} }

BIN
sounds/done.wav Normal file

Binary file not shown.

BIN
sounds/error.wav Normal file

Binary file not shown.

4
style/add.svg Executable file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 16 16" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path d="m8 0a8 8 0 0 0-8 8 8 8 0 0 0 8 8 8 8 0 0 0 8-8 8 8 0 0 0-8-8zm-1.5 3h3v3.5h3.5v3h-3.5v3.5h-3v-3.5h-3.5v-3h3.5v-3.5z" fill="#000080" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -2,14 +2,29 @@
/* License: gpl-v2 */ /* License: gpl-v2 */
:root { :root {
--general-color: #2a2a2e;
--general-bgcolor: rgb(249, 249, 250);
--general-border-color: lightgray;
--general-input-color: black;
--general-input-bgcolor: white;
--general-button-color: black;
--general-button-bgcolor: rgb(246, 246, 246);
--general-button-bgcolor-hover: white;
--general-button-shadow: 0px 0px 5px 1px rgba(128, 128, 128, 0.5);
--menu-bgcolor: white;
--menu-bgcolor-hover: #2283fb;
--table-bgcolor: white;
--table-head-bgcolor: white;
--toolbar-bg-color: rgb(248, 134, 6); --toolbar-bg-color: rgb(248, 134, 6);
--toolbar-active-border-color: #478de7; --toolbar-active-border-color: #478de7;
--toolbar-hover-border-color: red; --toolbar-hover-border-color: red;
--toolbar-hover-background: rgb(247, 149, 37); --toolbar-hover-background: rgb(247, 149, 37);
--toolbar-border-width: 2px; --toolbar-border-width: 2px;
--toolbar-border: 1px solid rgba(255, 255, 255, 0.5);
--add-color: navy; --add-color: navy;
--queue-color: gray; --queue-color: gray;
--pause-color: #ffa318; --pause-color: #ffa318;
--retry-color: rgb(0, 112, 204);
--error-color: rgb(160, 13, 42); --error-color: rgb(160, 13, 42);
--running-color: #aae061; --running-color: #aae061;
--finishing-color: #57cc12; --finishing-color: #57cc12;
@ -18,100 +33,279 @@
--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);
--status-icon-color: #363636;
--status-icon-color-hover: #6e6d6d;
--tile-url: url(tile.png);
--file-icon-image-color: rgb(17, 107, 163);
--popup-bgcolor: #fff;
--popup-color: #0c0c0d;
--modal-color: black;
--modal-bgcolor: white;
}
html.dark {
--add-color: lightblue;
--error-color: rgb(130, 3, 22);
--running-color: #67a041;
--finishing-color: #4bb111;
--done-color: #006f00;
--pause-color: #cf9308;
--general-bgcolor: #2a2a2e;
--general-border-color: rgb(85, 85, 85);
--general-button-bgcolor-hover: black;
--general-button-bgcolor: rgb(36, 36, 36);
--general-button-color: white;
--general-color: rgb(249, 249, 250);
--menu-bgcolor: black;
--menu-bgcolor-hover: #1a6bce;
--table-bgcolor: #1a1a1e;
--table-head-bgcolor: #3a3a3e;
--toolbar-bg-color: rgb(202, 108, 0);
--status-icon-color: #b9b9b9;
--status-icon-color-hover: #e2e2e2;
--tile-url: url(tile-dark.png?3);
--toolbar-border: 1px solid rgba(30, 30, 30, 0.5);
--file-icon-image-color: rgb(21, 130, 197);
--popup-bgcolor: #4a4a4f;
--popup-color: rgb(249, 249, 250);
--general-button-shadow: 0px 0px 7px 1px rgba(128, 128, 128, 0.8);
--modal-color: white;
--modal-bgcolor: #333;
scrollbar-color: rgba(249, 249, 250, 0.4) rgba(20, 20, 25, 0.3);
}
html.dark a {
color: lightblue;
}
html.dark ::-webkit-scrollbar {
background: rgba(20, 20, 25, 0.3);
}
html.dark ::-webkit-scrollbar-thumb {
background: rgba(249, 249, 250, 0.4);
}
html.dark ::-webkit-scrollbar-corner {
background: #000;
} }
html[data-platform="mac"] { html[data-platform="mac"] {
--folder-color: rgb(4, 102, 214); --folder-color: rgb(4, 102, 214);
} }
html,
body {
font-size: 10pt !important;
}
@font-face { @font-face {
font-family: 'downthemall'; font-family: "downthemall";
src: url('downthemall.woff2?75791791') format('woff2'); src: url("downthemall.woff2?75791791") format("woff2");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
[class^="icon-"]:before, [class*=" icon-"]:before { [class^="icon-"]:before,
[class*=" icon-"]:before {
font-family: "downthemall"; font-family: "downthemall";
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
text-decoration: inherit; text-decoration: inherit;
width: 1em; width: 1em;
text-align: center; text-align: center;
font-variant: normal; font-variant: normal;
text-transform: none; text-transform: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-add:before { content: '\e800'; } /* '' */ .icon-add:before {
.icon-addsegment:before { content: '\e801'; } /* '' */ content: "\e800";
.icon-bottom:before { content: '\e802'; } /* '' */ } /* '' */
.icon-picture:before { content: '\e803'; } /* '' */ .icon-addsegment:before {
.icon-circle:before { content: '\e804'; } /* '' */ content: "\e801";
.icon-delete:before { content: '\e805'; } /* '' */ } /* '' */
.icon-done:before { content: '\e806'; } /* '' */ .icon-bottom:before {
.icon-down:before { content: '\e807'; } /* '' */ content: "\e802";
.icon-download:before { content: '\e808'; } /* '' */ } /* '' */
.icon-dupe:before { content: '\e809'; } /* '' */ .icon-picture:before {
.icon-error:before { content: '\e80a'; } /* '' */ content: "\e803";
.icon-failed:before { content: '\e80b'; } /* '' */ } /* '' */
.icon-file:before { content: '\e80c'; } /* '' */ .icon-circle:before {
.icon-find:before { content: '\e80d'; } /* '' */ content: "\e804";
.icon-folder:before { content: '\e80e'; } /* '' */ } /* '' */
.icon-force:before { content: '\e80f'; } /* '' */ .icon-delete:before {
.icon-go:before { content: '\e810'; } /* '' */ content: "\e805";
.icon-import:before { content: '\e811'; } /* '' */ } /* '' */
.icon-info:before { content: '\e812'; } /* '' */ .icon-done:before {
.icon-launch:before { content: '\e813'; } /* '' */ content: "\e806";
.icon-missing:before { content: '\e814'; } /* '' */ } /* '' */
.icon-network-off:before { content: '\e815'; } /* '' */ .icon-down:before {
.icon-network-on:before { content: '\e816'; } /* '' */ content: "\e807";
.icon-pause:before { content: '\e817'; } /* '' */ } /* '' */
.icon-remsegment:before { content: '\e818'; } /* '' */ .icon-download:before {
.icon-rename:before { content: '\e819'; } /* '' */ content: "\e808";
.icon-save:before { content: '\e81a'; } /* '' */ } /* '' */
.icon-settings:before { content: '\e81b'; } /* '' */ .icon-dupe:before {
.icon-top:before { content: '\e81c'; } /* '' */ content: "\e809";
.icon-unchecked:before { content: '\e81d'; } /* '' */ } /* '' */
.icon-unlimited:before { content: '\e81e'; } /* '' */ .icon-error:before {
.icon-link:before { content: '\e81f'; } /* '' */ content: "\e80a";
.icon-up:before { content: '\e820'; } /* '' */ } /* '' */
.icon-privacy:before { content: '\e821'; } /* '' */ .icon-failed:before {
.icon-tags:before { content: '\e822'; } /* '' */ content: "\e80b";
.icon-attention:before { content: '\e823'; } /* '' */ } /* '' */
.icon-notification:before { content: '\e824'; } /* '' */ .icon-file:before {
.icon-file-video:before { content: '\e825'; } /* '' */ content: "\e80c";
.icon-file-generic:before { content: '\e826'; } /* '' */ } /* '' */
.icon-question-dark:before { content: '\e827'; } /* '' */ .icon-find:before {
.icon-filter:before { content: '\f0b0'; } /* '' */ content: "\e80d";
.icon-donate:before { content: '\f0d6'; } /* '' */ } /* '' */
.icon-file-doc:before { content: '\f0f6'; } /* '' */ .icon-folder:before {
.icon-interface:before { content: '\f108'; } /* '' */ content: "\e80e";
.icon-folder-1:before { content: '\f115'; } /* '' */ } /* '' */
.icon-sort-asc:before { content: '\f15d'; } /* '' */ .icon-force:before {
.icon-sort-desc:before { content: '\f15e'; } /* '' */ content: "\e80f";
.icon-file-pdf:before { content: '\f1c1'; } /* '' */ } /* '' */
.icon-file-word:before { content: '\f1c2'; } /* '' */ .icon-go:before {
.icon-file-image:before { content: '\f1c5'; } /* '' */ content: "\e810";
.icon-file-archive:before { content: '\f1c6'; } /* '' */ } /* '' */
.icon-file-audio:before { content: '\f1c7'; } /* '' */ .icon-import:before {
.icon-toggle:before { content: '\f205'; } /* '' */ content: "\e811";
.icon-server:before { content: '\f233'; } /* '' */ } /* '' */
.icon-question-light:before { content: '\f29c'; } /* '' */ .icon-info:before {
content: "\e812";
} /* '' */
.icon-launch:before {
content: "\e813";
} /* '' */
.icon-missing:before {
content: "\e814";
} /* '' */
.icon-network-off:before {
content: "\e815";
} /* '' */
.icon-network-on:before {
content: "\e816";
} /* '' */
.icon-pause:before {
content: "\e817";
} /* '' */
.icon-remsegment:before {
content: "\e818";
} /* '' */
.icon-rename:before {
content: "\e819";
} /* '' */
.icon-save:before {
content: "\e81a";
} /* '' */
.icon-settings:before {
content: "\e81b";
} /* '' */
.icon-top:before {
content: "\e81c";
} /* '' */
.icon-unchecked:before {
content: "\e81d";
} /* '' */
.icon-unlimited:before {
content: "\e81e";
} /* '' */
.icon-link:before {
content: "\e81f";
} /* '' */
.icon-up:before {
content: "\e820";
} /* '' */
.icon-privacy:before {
content: "\e821";
} /* '' */
.icon-tags:before {
content: "\e822";
} /* '' */
.icon-attention:before {
content: "\e823";
} /* '' */
.icon-notification:before {
content: "\e824";
} /* '' */
.icon-file-video:before {
content: "\e825";
} /* '' */
.icon-file-generic:before {
content: "\e826";
} /* '' */
.icon-question-dark:before {
content: "\e827";
} /* '' */
.icon-forward:before {
content: "\e828";
} /* '' */
.icon-filter:before {
content: "\f0b0";
} /* '' */
.icon-donate:before {
content: "\f0d6";
} /* '' */
.icon-file-doc:before {
content: "\f0f6";
} /* '' */
.icon-interface:before {
content: "\f108";
} /* '' */
.icon-folder-1:before {
content: "\f115";
} /* '' */
.icon-sort-asc:before {
content: "\f15d";
} /* '' */
.icon-sort-desc:before {
content: "\f15e";
} /* '' */
.icon-file-pdf:before {
content: "\f1c1";
} /* '' */
.icon-file-word:before {
content: "\f1c2";
} /* '' */
.icon-file-image:before {
content: "\f1c5";
} /* '' */
.icon-file-archive:before {
content: "\f1c6";
} /* '' */
.icon-file-audio:before {
content: "\f1c7";
} /* '' */
.icon-toggle-off:before {
content: "\f204";
} /* '' */
.icon-toggle-on:before {
content: "\f205";
} /* '' */
.icon-server:before {
content: "\f233";
} /* '' */
.icon-question-light:before {
content: "\f29c";
} /* '' */
@media (min-resolution: 144dpi) { @media (min-resolution: 144dpi) {
[class^="icon-file-"]:before, [class*=" icon-file-"]:before { [class^="icon-file-"]:before,
[class*=" icon-file-"]:before {
font-weight: bold !important; font-weight: bold !important;
} }
} }
.icon-file-image { .icon-file-image {
color: rgb(17, 107, 163); color: var(--file-icon-image-color);
} }
.icon-file-pdf, .icon-file-pdf,
@ -132,18 +326,29 @@ html[data-platform="mac"] {
color: rgb(202, 81, 198); color: rgb(202, 81, 198);
} }
body, html { body,
background: #F6F6F8; html {
color: #0C0C0D;
font: message-box; font: message-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Ubuntu",
"Helvetica Neue", sans-serif;
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
color: var(--general-color);
background: var(--general-bgcolor);
} }
h1, h2, h3, h4 { html#popup,
html#popup > body {
color: var(--popup-color);
background: var(--popup-bgcolor);
}
h1,
h2,
h3,
h4 {
font: caption; font: caption;
font-weight: bold; font-weight: bold;
} }
@ -183,7 +388,11 @@ section {
} }
.virtualtable-column:active { .virtualtable-column:active {
background-image: linear-gradient(to top, rgba(0,0,0,0.03), rgba(128,128,128,0.1)); background-image: linear-gradient(
to top,
rgba(0, 0, 0, 0.03),
rgba(128, 128, 128, 0.1)
);
} }
th.virtualtable { th.virtualtable {
@ -210,8 +419,12 @@ td.virtualtable {
font-size: 12px; font-size: 12px;
align-items: stretch; align-items: stretch;
justify-items: center; justify-items: center;
background: linear-gradient(to bottom, rgba(128,128,128,0.1) 0%,rgba(0,0,0,0) 100%); background: linear-gradient(
border-top: 1px solid rgba(128,128,128,0.6); to bottom,
rgba(128, 128, 128, 0.1) 0%,
rgba(0, 0, 0, 0) 100%
);
border-top: 1px solid rgba(128, 128, 128, 0.6);
display: flex; display: flex;
margin-bottom: 1ex; margin-bottom: 1ex;
overflow: auto; overflow: auto;
@ -238,15 +451,15 @@ td.virtualtable {
flex-grow: 3; flex-grow: 3;
margin-right: 2ex; margin-right: 2ex;
padding-right: 1ex; padding-right: 1ex;
border-right: 1px dotted rgba(128,128,128,0.6); border-right: 1px dotted rgba(128, 128, 128, 0.6);
} }
#statusPrefs { #statusPrefs {
cursor: pointer; cursor: pointer;
color: #363636; color: var(--status-icon-color);
} }
#statusPrefs:hover { #statusPrefs:hover {
color: #6e6d6d; color: var(--status-icon-color-hover);
} }
.dropdown { .dropdown {
@ -264,13 +477,14 @@ td.virtualtable {
outline: none; outline: none;
position: absolute; position: absolute;
top: 0; top: 0;
width:100%; width: 100%;
} }
.dropdown input { .dropdown input {
-moz-appearance: none; -moz-appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
color: black;
background: white; background: white;
border: none; border: none;
bottom: 2px; bottom: 2px;
@ -293,7 +507,6 @@ td.virtualtable {
padding-bottom: 1ex; padding-bottom: 1ex;
} }
@supports (not (-moz-appearance: none)) { @supports (not (-moz-appearance: none)) {
.dropdown select { .dropdown select {
background: white; background: white;
@ -362,4 +575,55 @@ td.virtualtable {
#maskButton { #maskButton {
color: var(--maskbutton-color); color: var(--maskbutton-color);
} }
table.virtualtable,
.virtualtable-body {
color: var(--general-color);
background: var(--table-bgcolor);
}
.virtualtable-head,
.virtualtable-head > table {
background: var(--table-head-bgcolor) !important;
}
.virtualtable-column {
border-right: 1px solid var(--general-border-color);
}
.virtualtable-cell {
border-right: 1px dotted var(--general-border-color);
}
.virtualtable-head,
.virtualtable-body {
border-bottom: 1px solid var(--general-border-color);
}
ul.context-menu,
ul.context-menu ul {
color: var(--general-color);
background: var(--menu-bgcolor);
}
.context-menu-item:hover:not(.context-menu-seperator),
.context-menu-item:hover:not(.context-menu-seperator) > * {
background: var(--menu-bgcolor-hover);
}
html.dark .context-menu-item.disabled,
html.dark .context-menu-item.disabled > * {
opacity: 0.7;
}
input {
color: var(--general-input-color);
background: var(--general-input-bgcolor);
border: inherit;
padding: 2px;
}
.modal-dialog {
color: var(--modal-color);
background: var(--modal-bgcolor);
}

BIN
style/done.opus Normal file

Binary file not shown.

BIN
style/downthemall.woff2 Normal file → Executable file

Binary file not shown.

BIN
style/error.opus Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -16,7 +16,7 @@ body > * {
#toolbar { #toolbar {
display: flex; display: flex;
margin: 0; margin: 0;
background: var(--toolbar-bg-color) url(tile.png) repeat-x; background: var(--toolbar-bg-color) var(--tile-url) repeat-x;
} }
#toolbar .spacer { #toolbar .spacer {
@ -42,10 +42,15 @@ body > * {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
box-shadow: 0px 0px 5px 1px rgba(128,128,128,0.5); box-shadow: var(--general-button-shadow);
background: rgb(246,246,246); background: var(--general-button-bgcolor);
color: black; color: var(--general-button-color);
transition: box-shadow 0.5s, background 1s; transition: box-shadow 0.5s, background 1s;
font-size: 24px;
line-height: 24px;
}
#toolbar > .button > span:before {
display: block;
} }
#toolbar > .button.disabled { #toolbar > .button.disabled {
@ -55,7 +60,7 @@ body > * {
} }
#toolbar > .button:hover:not(.disabled) { #toolbar > .button:hover:not(.disabled) {
background: white; background: var(--general-button-bgcolor-hover);
box-shadow: 0px 0px 7px 2px rgba(70,70,70,0.75); box-shadow: 0px 0px 7px 2px rgba(70,70,70,0.75);
} }
@ -63,13 +68,6 @@ body > * {
box-shadow: 0px 0px 7px 2px rgba(220,220,220,0.75); box-shadow: 0px 0px 7px 2px rgba(220,220,220,0.75);
} }
#toolbar > .button > span {
display: block;
flex-grow: 0;
width: 24px;
line-height: 24px;
}
#toolbar > .button > .icon-add { #toolbar > .button > .icon-add {
color: var(--add-color); color: var(--add-color);
} }
@ -90,14 +88,14 @@ body > * {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
display: grid; display: grid;
background: white; background: var(--general-bgcolor);
} }
#loading { #loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: rgba(255,255,255,0.9); background: var(--general-button-bgcolor);
font-weight: bolder; font-weight: bolder;
font-size: 200%; font-size: 200%;
z-index: 10; z-index: 10;
@ -110,11 +108,11 @@ body > * {
} }
#colURL { #colURL {
width: 38%; width: 42%;
} }
#colPercent { #colPercent {
width: 3em; width: 4em;
min-width: 3em; min-width: 3em;
} }
@ -123,11 +121,11 @@ body > * {
} }
#colSize { #colSize {
width: 15em; width: 14em;
} }
#colSpeed { #colSpeed {
width: 6em; width: 7em;
} }
#colDomain, #colDomain,
@ -156,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;
} }
@ -196,6 +202,23 @@ body > * {
); );
} }
.retrying .virtualtable-column-2 .virtualtable-icon {
color: var(--retry-color);
}
.retrying .virtualtable-column-2 .virtualtable-progress-bar {
background: var(--retry-color);
}
.retrying .virtualtable-column-2 .virtualtable-progress-undetermined {
background: repeating-linear-gradient(
45deg,
var(--retry-color),
var(--retry-color) 6px,
transparent 6px,
transparent 12px
);
}
.missing .virtualtable-column-2 .virtualtable-icon, .missing .virtualtable-column-2 .virtualtable-icon,
.canceled .virtualtable-column-2 .virtualtable-icon { .canceled .virtualtable-column-2 .virtualtable-icon {
color: var(--error-color); color: var(--error-color);
@ -264,6 +287,7 @@ body > * {
} }
.virtualtable-column-6, .virtualtable-column-6,
.virtualtable-column-4,
.virtualtable-column-3 { .virtualtable-column-3 {
text-align: right; text-align: right;
} }
@ -287,7 +311,7 @@ body > * {
color: crimson; color: crimson;
} }
#statusNetwork.icon-network-on { #statusNetwork.icon-network-on {
color: navy; color: var(--add-color);
} }
#statusFilter { #statusFilter {
@ -320,6 +344,7 @@ body > * {
height: 16px; height: 16px;
-moz-appearance: none; -moz-appearance: none;
border: 0; border: 0;
outline: 0;
background: transparent; background: transparent;
width: calc(100% - 28px); width: calc(100% - 28px);
} }
@ -379,7 +404,7 @@ body > * {
font-size: 10pt !important; font-size: 10pt !important;
} }
#nagging { #nagging {
border-top: 1px solid lightgray; border-top: 1px solid var(--general-border-color);
display: grid; display: grid;
grid-template-columns: 1fr auto auto auto; grid-template-columns: 1fr auto auto auto;
align-content: center; align-content: center;
@ -432,6 +457,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 {
@ -444,8 +471,13 @@ body > * {
} }
#tooltip-icon { #tooltip-icon {
font-size: 48px; height: 64px;
line-height: 48px; width: 64px;
background-size: 64px 64px;
background-repeat: no-repeat;
background-position: center center;
font-size: 64px;
line-height: 64px;
padding: 6px; padding: 6px;
text-align: center; text-align: center;
grid-row: 1/-1; grid-row: 1/-1;
@ -497,4 +529,24 @@ body > * {
height: 100%; height: 100%;
width: 100%; width: 100%;
background: var(--done-color); background: var(--done-color);
}
#tooltip-eta.single {
font-weight: bold;
grid-column-end: span 2;
}
.deletefiles-list {
padding-left: 1ex;
padding-right: 1.5ex;
border: 1px solid lightgray;
border-radius: 6px;
background-color: rgba(128,128,128,0.1);
max-height: 8em;
overflow-y: auto;
}
.deletefiles-list > li {
list-style-type: none;
padding: 0;
margin: 0;
} }

View File

@ -1,8 +1,8 @@
/* License: gpl-v2 */ /* License: gpl-v2 */
@import 'common.css'; @import "common.css";
html, body { html,
background: transparent !important; body {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@ -19,16 +19,17 @@ article {
#tabs { #tabs {
display: flex; display: flex;
background: url(icon64.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color); background: url(icon64.png) 1em 50%/32px 32px no-repeat,
var(--tile-url) repeat-x, var(--toolbar-bg-color);
padding-left: calc(2em + 32px); padding-left: calc(2em + 32px);
color: white; color: var(--general-bgcolor);
} }
input.tab { input.tab {
display: none; display: none;
} }
#tabs > label{ #tabs > label {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
font-size: 150%; font-size: 150%;
@ -54,9 +55,10 @@ input.tab {
#tabsel-general:checked ~ #tabs #tabel-general, #tabsel-general:checked ~ #tabs #tabel-general,
#tabsel-filters:checked ~ #tabs #tabel-filters, #tabsel-filters:checked ~ #tabs #tabel-filters,
#tabsel-network:checked ~ #tabs #tabel-network { #tabsel-network:checked ~ #tabs #tabel-network {
color: black !important; color: var(--general-color) !important;
background: white; background: var(--general-bgcolor);
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color); border-top: var(--toolbar-border-width) solid
var(--toolbar-active-border-color);
} }
#tabs > label { #tabs > label {
@ -64,13 +66,14 @@ input.tab {
border-top: var(--toolbar-border-width) solid transparent; border-top: var(--toolbar-border-width) solid transparent;
border-left: 1px solid transparent; border-left: 1px solid transparent;
border-right: 1px solid transparent; border-right: 1px solid transparent;
border-left: 1px solid rgba(255, 255, 255, 0.5); border-left: var(--toolbar-border);
border-right: 1px solid rgba(255, 255, 255, 0.5); border-right: var(--toolbar-border);
background: var(--toolbar-bg-color); background: var(--toolbar-bg-color);
} }
#tabs > label:hover:not(:checked) { #tabs > label:hover:not(:checked) {
border-top: var(--toolbar-border-width) solid var(--toolbar-hover-border-color); border-top: var(--toolbar-border-width) solid
var(--toolbar-hover-border-color);
background: var(--toolbar-hover-background); background: var(--toolbar-hover-background);
} }
@ -102,7 +105,7 @@ input.tab {
text-align: center; text-align: center;
} }
.buttons > button{ .buttons > button {
margin: 0 2em; margin: 0 2em;
} }
@ -113,15 +116,27 @@ input.tab {
fieldset { fieldset {
display: flex; display: flex;
margin-bottom: 1em; margin-bottom: 1em;
border: 1px solid lightgray; border: 1px solid var(--general-border-color);
border-radius: 6px; border-radius: 6px;
box-shadow: 1px 1px 6px lightgray; box-shadow: 1px 1px 6px var(--general-border-color);
background: rgba(128, 128, 128, 0.05); background: rgba(128, 128, 128, 0.05);
flex-direction: column; flex-direction: column;
max-width: 60em; max-width: 60em;
padding: 1.2em; padding: 1.2em;
} }
.optiongroups,
fieldset > label {
display: flex;
align-items: center;
}
fieldset > label > input,
fieldset > label > select {
margin-left: 1ex;
margin-right: 1ex;
}
legend { legend {
font-weight: bold; font-weight: bold;
font-size: 120%; font-size: 120%;
@ -134,8 +149,36 @@ legend {
} }
.virtualtable-container { .virtualtable-container {
border: 1px solid lightgray; border: 1px solid var(--general-border-color);
border-radius: 6px; border-radius: 6px;
background: rgba(128, 128, 128, 0.05); background: rgba(128, 128, 128, 0.05);
box-shadow: 1px 1px 6px lightgray; box-shadow: 1px 1px 6px var(--general-border-color);
} }
#network-general {
display: grid;
grid-template-columns: auto 1fr;
grid-column-gap: 1em;
grid-row-gap: 1ex;
}
.optiongroups {
display: grid;
grid-template-columns: auto 1fr;
grid-column-gap: 1em;
grid-row-gap: 1ex;
}
.optiongroups > div,
.optiongroups > div > label {
display: flex;
align-items: center;
}
.optiongroups input {
margin-left: 1em;
margin-right: 0.7ex;
}
hr {
width: 100%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -107,7 +107,7 @@ body > * {
padding: 0; padding: 0;
padding-left: calc(2em + 32px); padding-left: calc(2em + 32px);
color: black; color: black;
background: url(icon32.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color); background: url(icon32.png) 1em 0/32px 32px no-repeat, var(--tile-url) repeat-x, var(--toolbar-bg-color);
font: caption; font: caption;
font-size: 150%; font-size: 150%;
font-weight: bold; font-weight: bold;
@ -117,7 +117,7 @@ body > * {
} }
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) { @media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
#tabs { #tabs {
background: url(icon64.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color); background: url(icon64.png) 1em 50%/32px 32px no-repeat, var(--tile-url) repeat-x, var(--toolbar-bg-color);
} }
} }
@ -145,27 +145,27 @@ body > * {
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
background: var(--toolbar-bg-color); background: var(--toolbar-bg-color);
color: white; color: var(--general-color);
min-width: 10em; min-width: 10em;
padding: 1.2ex; padding: 1ex;
padding-left: 1em; padding-left: 1em;
cursor: pointer; cursor: pointer;
border: 0; border: 0;
border-top: var(--toolbar-border-width) solid transparent; border-top: var(--toolbar-border-width) solid transparent;
border-left: 1px solid rgba(255,255,255,0.3); border-left: var(--toolbar-border);
border-right: 1px solid rgba(255,255,255,0.3); border-right: var(--toolbar-border);
transition: border 1s; transition: border 1s;
} }
.tab:not(.active):not(.disabled):hover { .tab:not(.active):not(.disabled):hover {
border-top: var(--toolbar-border-width) solid var(--toolbar-hover-border-color); border-top: var(--toolbar-border-width) solid var(--toolbar-hover-border-color);
color: rgb(255, 226, 167); color: var(--general-color);
background: var(--toolbar-hover-background); background: var(--toolbar-hover-background);
} }
.tab.active { .tab.active {
color: black; color: var(--general-color);
background: white; background: var(--table-head-bgcolor);
border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color); border-top: var(--toolbar-border-width) solid var(--toolbar-active-border-color);
border-left: 1px solid transparent; border-left: 1px solid transparent;
border-right: 1px solid transparent; border-right: 1px solid transparent;
@ -232,3 +232,7 @@ body > * {
#maskButton { #maskButton {
justify-self: flex-start; justify-self: flex-start;
} }
#btnDownload {
font-weight: bold;
}

View File

@ -49,7 +49,7 @@ p.example {
} }
#options > * { #options > * {
margin: 0; margin: 2px;
} }
#options > input { #options > input {
@ -63,6 +63,7 @@ p.example {
align-items: center; align-items: center;
} }
#options > #subfolderOptions,
#options > #maskOptions { #options > #maskOptions {
display: grid; display: grid;
grid-template-columns: 2fr auto auto; grid-template-columns: 2fr auto auto;
@ -81,3 +82,7 @@ h3 {
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
#btnDownload {
font-weight: bold;
}

BIN
style/tile-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

10
tests/.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
tab_width = 2
trim_trailing_whitespace = true

Some files were not shown because too many files have changed in this diff Show More