70 Commits

Author SHA1 Message Date
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
82 changed files with 9556 additions and 1341 deletions

View File

@ -30,7 +30,7 @@ THE SOFTWARE.
## DownThemAll! uikit
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)
@ -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
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.
See: https://creativecommons.org/licenses/by-sa/4.0/legalcode
@ -54,21 +55,25 @@ Copyright © 2010-2019 by Nils Maier, Stefano Verna.
The DownThemAll! name and logo cannot be used without explicit permission
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
permission.
Distributing official DownThemAll! releases without any modifications is allowed without explicit permission.
## Font Awesome
Copyright (C) 2016 by Dave Gandy
License: SIL ()
Homepage: http://fortawesome.github.com/Font-Awesome/
## webextension-polyfill
Lcensed under the Mozilla Public License 2.0.
Licensed under the Mozilla Public License 2.0.
## PSL (public-suffix-list)
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.

View File

@ -23,6 +23,10 @@ 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.**
Translations
---
If you would like to help out translating DTA, please see our [translation guide](_locales/Readme.md).
Development
---
@ -32,7 +36,9 @@ You will want to `yarn` the development dependencies such as webpack first.
Afterwards there is two important commands to run
* `yarn watch` - This will run the webpack bundler in watch mode, updating bundles as you change the source.
* `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 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). This will use the directory `../dtalite.p` to keep a development profile. You might need to create this directory before you use this command first.
Please note: You have to run `yarn watch` (at least once) as it builds the actual script bundles.
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).

View File

@ -8,7 +8,6 @@ P2
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)
@ -58,8 +57,6 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
* Not supported by Firefox
* Speed limiter
* 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?
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
* 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.

1170
_locales/de/messages.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1170
_locales/es/messages.json Normal file

File diff suppressed because it is too large Load Diff

1178
_locales/fr/messages.json Normal file

File diff suppressed because it is too large Load Diff

1170
_locales/lt/messages.json Normal file

File diff suppressed because it is too large Load Diff

1170
_locales/pl/messages.json Executable file

File diff suppressed because it is too large Load Diff

1160
_locales/pt/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
},
"deffilter-aud": {
"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,
"active": false,
"icon": "mp3"
@ -35,7 +35,7 @@
},
"deffilter-img": {
"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,
"active": false,
"icon": "jpg"
@ -63,7 +63,7 @@
},
"deffilter-vid": {
"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,
"active": true,
"icon": "mkv"

View File

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

View File

@ -6,9 +6,10 @@
"open-manager-on-queue": true,
"text-links": true,
"add-paused": false,
"hide-context": false,
"conflict-action": "uniquify",
"nagging": 0,
"nagging-next": 6,
"nagging-next": 7,
"tooltip": true,
"show-urls": false,
"remove-missing-on-init": false,

View File

@ -5,7 +5,8 @@ import { TYPE_LINK, TYPE_MEDIA } from "./constants";
import { filters } from "./filters";
import { Prefs } from "./prefs";
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 { select } from "./select";
import { single } from "./single";
@ -16,12 +17,17 @@ import { _ } from "./i18n";
const MAX_BATCH = 10000;
export const API = new class {
async filter(arr: any, type: number) {
return (await filters()).filterItemsByType(arr, type);
export interface QueueOptions {
mask?: 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();
const {mask = MASK.current} = options;
@ -36,12 +42,9 @@ export const API = new class {
fileName: null,
title: "",
description: "",
fromMetalink: false,
startDate: new Date(),
hashes: [],
private: false,
postData: null,
cleanRequest: false,
mask,
date: Date.now(),
paused
@ -54,7 +57,7 @@ export const API = new class {
}
return currentBatch;
});
items = items.map((i: any) => {
items = items.map(i => {
delete i.idx;
return new Item(i, defaults);
});
@ -79,7 +82,7 @@ export const API = new class {
}
}
sanity(links: any[], media: any[]) {
sanity(links: BaseItem[], media: BaseItem[]) {
if (!links.length && !media.length) {
new Notification(null, _("no-links"));
return false;
@ -87,48 +90,53 @@ export const API = new class {
return true;
}
async turbo(links: any[], media: any[]) {
async turbo(links: BaseItem[], media: BaseItem[]) {
if (!this.sanity(links, media)) {
return false;
}
const selected = makeUniqueItems([
await API.filter(links, TYPE_LINK),
await API.filter(media, TYPE_MEDIA),
]);
const type = await Prefs.get("last-type", "links");
const items = await (async () => {
if (type === "links") {
return await API.filter(links, TYPE_LINK);
}
return await API.filter(media, TYPE_MEDIA);
})();
const selected = makeUniqueItems([items]);
if (!selected.length) {
return await this.regular(links, media);
}
return await this.queue(selected, {paused: await Prefs.get("add-paused")});
}
async regularInternal(selected: any) {
if (selected.mask && !selected.maskOnce) {
async regularInternal(selected: BaseItem[], options: any) {
if (options.mask && !options.maskOnce) {
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.push(selected.fast);
await FASTFILTER.push(options.fast);
}
const {items} = selected;
delete selected.items;
return await this.queue(items, selected);
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)) {
return false;
}
const selected = await select(links, media);
return this.regularInternal(selected);
const {items, options} = await select(links, media);
return this.regularInternal(items, options);
}
async singleTurbo(item: any) {
async singleTurbo(item: BaseItem) {
return await this.queue([item], {paused: await Prefs.get("add-paused")});
}
async singleRegular(item: any) {
const selected = await single(item);
return this.regularInternal(selected);
async singleRegular(item: BaseItem | null) {
const {items, options} = await single(item);
return this.regularInternal(items, options);
}
}();

View File

@ -5,7 +5,7 @@ import { ALLOWED_SCHEMES, TRANSFERABLE_PROPERTIES } from "./constants";
import { API } from "./api";
import { Finisher, makeUniqueItems } from "./item";
import { Prefs } from "./prefs";
import { _ } from "./i18n";
import { _, locale } from "./i18n";
import { openPrefs, openManager } from "./windowutils";
import { filters } from "./filters";
import { getManager } from "./manager/man";
@ -13,13 +13,22 @@ import {
browserAction as action,
menus as _menus, contextMenus as _cmenus,
tabs,
webNavigation as nav
webNavigation as nav,
// eslint-disable-next-line no-unused-vars
Tab,
// eslint-disable-next-line no-unused-vars
MenuClickInfo
} from "./browser";
import { Bus } from "./bus";
import { filterInSitu } from "./util";
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
const GATHER = "/bundles/content-gather.js";
async function runContentJob(tab: any, file: string, msg: any) {
async function runContentJob(tab: Tab, file: string, msg: any) {
try {
const res = await tabs.executeScript(tab.id, {
file,
@ -48,6 +57,14 @@ async function runContentJob(tab: any, file: string, msg: any) {
}
}
type SelectionOptions = {
selectionOnly: boolean;
allTabs: boolean;
turbo: boolean;
tab: Tab;
};
class Handler {
async processResults(turbo = false, results: any[]) {
const links = this.makeUnique(results, "links");
@ -59,305 +76,466 @@ class Handler {
return makeUniqueItems(
results.filter(e => e[what]).map(e => {
const finisher = new Finisher(e);
return e[what].
map((item: any) => finisher.finish(item)).
filter((i: any) => i);
return filterInSitu(e[what].
map((item: any) => finisher.finish(item)), e => !!e);
}));
}
async performSelection(options: SelectionOptions) {
try {
const selectedTabs = options.allTabs ?
await tabs.query({
currentWindow: true,
discarded: false,
hidden: false}) 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 {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
}
async onClicked(tab: {id: number}) {
if (!tab.id) {
return;
locale.then(() => {
new class Action extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
}
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);
}
}
}();
new class Menus extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
menus.create({
id: "DTARegular",
contexts: ["all", "browser_action", "tools_menu"],
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);
}
async onClicked(tab: {id: number}) {
if (!tab.id) {
return;
}
try {
await this.processResults(
true,
await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly: false,
textLinks: await Prefs.get("text-links", true),
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
}));
}
catch (ex) {
console.error(ex);
}
}
}();
*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 menuHandler = new class Menus extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
const alls = new Map<string, string[]>();
const mcreate = (options: any) => {
if (options.contexts.includes("all")) {
alls.set(options.id, options.contexts);
}
return 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;
}
const finished = finisher.finish(e);
if (!finished) {
continue;
}
yield finished;
}
}
}
}
async findSingleItem(tab: any, url: string, turbo = false) {
if (!url) {
return;
async findSingleItem(tab: Tab, url: string, turbo = false) {
if (!url) {
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", {
type: "DTA:gather",
onClicked(info: MenuClickInfo, tab: Tab) {
if (!tab.id) {
return;
}
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 enumulate(action: string) {
const tab = await tabs.query({active: true});
if (!tab || !tab.length) {
return;
}
this.onClicked({
menuItemId: action
}, tab[0]);
}
async onClickedDTARegular(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
allTabs: false,
turbo: false,
tab,
});
const found = Array.from(this.makeSingleItemList(url, results));
const unique = makeUniqueItems([found]);
if (!unique.length) {
return;
}
const [item] = unique;
API[turbo ? "singleTurbo" : "singleRegular"](item);
}
onClicked(info: any, tab: any) {
if (!tab.id) {
return;
async onClickedDTARegularAll(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: true,
turbo: false,
tab,
});
}
const {menuItemId} = info;
const {[`onClicked${menuItemId}`]: handler}: any = this;
if (!handler) {
console.error("Invalid Handler for", menuItemId);
return;
async onClickedDTARegularSelection(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: true,
allTabs: false,
turbo: false,
tab,
});
}
handler.call(this, info, tab).catch(console.error);
}
async onClickedDTARegularInternal(
selectionOnly: boolean, info: any, tab: any) {
try {
await this.processResults(
false,
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,
}));
async onClickedDTATurbo(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: false,
turbo: true,
tab,
});
}
catch (ex) {
console.error(ex);
async onClickedDTATurboAll(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: true,
turbo: true,
tab,
});
}
}
async onClickedDTARegular(info: any, tab: any) {
return await this.onClickedDTARegularInternal(false, info, tab);
}
async onClickedDTARegularSelection(info: any, tab: any) {
return await this.onClickedDTARegularInternal(true, info, 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,
}));
async onClickedDTATurboSelection(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: true,
allTabs: false,
turbo: true,
tab,
});
}
catch (ex) {
console.error(ex);
async onClickedDTARegularLink(info: MenuClickInfo, tab: Tab) {
if (!info.linkUrl) {
return;
}
await this.findSingleItem(tab, info.linkUrl, false);
}
async onClickedDTATurboLink(info: MenuClickInfo, tab: Tab) {
if (!info.linkUrl) {
return;
}
await this.findSingleItem(tab, info.linkUrl, true);
}
async onClickedDTARegularImage(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboImage(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, true);
}
async onClickedDTARegularMedia(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboMedia(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, true);
}
onClickedDTAAdd() {
API.singleRegular(null);
}
async onClickedDTAManager() {
await openManager();
}
async onClickedDTAPrefs() {
await openPrefs();
}
}();
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
Bus.on("do-single", () => API.singleRegular(null));
Bus.on("open-manager", () => openManager(true));
Bus.on("open-prefs", () => openPrefs());
function adjustAction(globalTurbo: boolean) {
action.setPopup({
popup: globalTurbo ? "" : null
});
action.setIcon({
path: globalTurbo ? {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
} : null
});
}
async onClickedDTATurbo(info: any, tab: any) {
return await this.onClickedDTATurboInternal(false, info, tab);
}
async onClickedDTATurboSelection(info: any, tab: any) {
return await this.onClickedDTATurboInternal(false, info, tab);
}
async onClickedDTARegularLink(info: any, tab: any) {
return await this.findSingleItem(tab, info.linkUrl, false);
}
async onClickedDTATurboLink(info: any, tab: any) {
return await this.findSingleItem(tab, info.linkUrl, true);
}
async onClickedDTARegularImage(info: any, tab: any) {
return await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboImage(info: any, tab: any) {
return await this.findSingleItem(tab, info.srcUrl, true);
}
async onClickedDTARegularMedia(info: any, tab: any) {
return await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboMedia(info: any, tab: any) {
return await this.findSingleItem(tab, info.srcUrl, true);
}
async onClickedDTAManager() {
await openManager();
}
async onClickedDTAPrefs() {
await openPrefs();
}
}();
(async function init() {
await Prefs.set("last-run", new Date());
await filters();
await getManager();
})().catch(ex => {
console.error("Failed to init components", ex.toString(), ex.stack, ex);
(async function init() {
await Prefs.set("last-run", new Date());
Prefs.get("global-turbo", false).then(v => adjustAction(v));
Prefs.on("global-turbo", (prefs, key, value) => {
adjustAction(value);
});
await filters();
await getManager();
})().catch(ex => {
console.error("Failed to init components", ex.toString(), ex.stack, ex);
});
});

View File

@ -98,9 +98,9 @@ export class BatchGenerator implements Generator {
public readonly hasInvalid: boolean;
public readonly length: any;
public readonly length: number;
public readonly preview: any;
public readonly preview: string;
constructor(str: string) {
this.gens = [];

View File

@ -3,7 +3,42 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const polyfill = require("webextension-polyfill");
export const {i18n} = polyfill;
interface ExtensionListener {
addListener: (listener: Function) => void;
removeListener: (listener: Function) => void;
}
export interface MessageSender {
tab?: Tab;
frameId?: number;
id?: number;
url?: string;
tlsChannelId?: string;
}
export interface Tab {
id?: number;
}
export interface MenuClickInfo {
menuItemId: string | number;
button?: number;
linkUrl?: string;
srcUrl?: string;
}
export interface RawPort {
error: any;
name: string;
onDisconnect: ExtensionListener;
onMessage: ExtensionListener;
sender?: MessageSender;
disconnect: () => void;
postMessage: (message: any) => void;
}
export const {extension} = polyfill;
export const {notifications} = polyfill;
export const {browserAction} = polyfill;

View File

@ -2,22 +2,18 @@
// License: MIT
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 {
private port: any;
private port: RawPort | null;
constructor(port: any) {
constructor(port: RawPort) {
super();
this.port = port;
let disconnected = false;
let tabListener: any;
const disconnect = () => {
if (tabListener) {
tabs.onRemoved.removeListener(tabListener);
tabListener = null;
}
if (disconnected) {
return;
}
@ -41,11 +37,17 @@ export class Port extends EventEmitter {
}
get name() {
if (!this.port) {
return null;
}
return this.port.name;
}
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() {
@ -53,6 +55,9 @@ export class Port extends EventEmitter {
}
post(msg: string, ...data: any[]) {
if (!this.port) {
return;
}
if (!data) {
this.port.postMessage({msg});
return;
@ -64,14 +69,17 @@ export class Port extends EventEmitter {
}
onMessage(message: any) {
if (Object.keys(message).includes("msg")) {
this.emit(message.msg, message);
if (!this.port) {
return;
}
if (Array.isArray(message)) {
message.forEach(this.onMessage, this);
return;
}
if (Object.keys(message).includes("msg")) {
this.emit(message.msg, message);
return;
}
if (typeof message === "string") {
this.emit(message);
return;
@ -99,7 +107,7 @@ export const Bus = new class extends EventEmitter {
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;
if (!type) {
type = msg;
@ -107,7 +115,7 @@ export const Bus = new class extends EventEmitter {
this.emit(type, msg, callback);
}
onConnect(port: any) {
onConnect(port: RawPort) {
if (!port.name) {
port.disconnect();
return;

View File

@ -1,5 +1,8 @@
"use strict";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
// License: MIT
const VERSION = 1;
@ -40,12 +43,12 @@ export const DB = new class DB {
});
}
getAllInternal(resolve: (items: any[]) => void, reject: Function) {
getAllInternal(resolve: (items: BaseItem[]) => void, reject: Function) {
if (!this.db) {
reject(new Error("db closed"));
return;
}
const items: any[] = [];
const items: BaseItem[] = [];
const transaction = this.db.transaction(STORE, "readonly");
transaction.onerror = ex => reject(ex);
const store = transaction.objectStore(STORE);

View File

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

View File

@ -3,49 +3,188 @@
import {memoize} from "./memoize";
function load() {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {i18n} = require("webextension-polyfill");
declare let browser: any;
declare let chrome: any;
return i18n;
const CACHE_KEY = "_cached_locales";
const CUSTOM_KEY = "_custom_locale";
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);
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
const langs = new Set<string>(["en"]);
const ui = (browser.i18n || chrome.i18n).getUILanguage();
langs.add(ui);
if (ui.includes("-")) {
// Try the base too
langs.add(ui.split(/[_-]+/)[0]);
}
const fetched = await Promise.all(Array.from(langs, fetchLanguage));
return fetched.filter(e => !!e);
}
async function load(): Promise<Localization> {
try {
checkBrowser();
try {
// 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
// eslint-disable-next-line @typescript-eslint/no-var-requires
const messages = require("../_locales/en/messages.json");
const map = new Map();
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] || "";
});
}
};
return new Localization(messages);
}
}
const i18n = load();
const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
type MemoLocalize = (id: string, ...args: any[]) => string;
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
@ -53,22 +192,22 @@ const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
* @param {string[]} [subst] Message substituations
* @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) {
return memoGetMessage(id);
return memoLocalize(id);
}
if (subst.length === 1 && Array.isArray(subst[0])) {
subst = subst.pop();
}
return i18n.getMessage(id, subst);
return loc.localize(id, subst);
}
/**
* Localize a DOM
* @param {Element} elem DOM to localize
* @returns {Element} Passed in element (fluent)
*/
function localize(elem: HTMLElement) {
function localize_<T extends HTMLElement | DocumentFragment>(elem: T): T {
for (const tmpl of elem.querySelectorAll<HTMLTemplateElement>("template")) {
localize_(tmpl.content);
}
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
const {i18n: i} = el.dataset;
if (!i) {
@ -99,8 +238,25 @@ function localize(elem: HTMLElement) {
for (const el of document.querySelectorAll("*[data-l18n]")) {
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);
}

View File

@ -4,18 +4,32 @@
import { ALLOWED_SCHEMES } 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;
startDate?: number;
private?: boolean;
postData?: string;
paused?: boolean;
}
const OPTIONPROPS = Object.freeze([
"referrer", "usableReferrer",
"description", "title",
"fileName",
"batch", "idx",
"mask",
"fromMetalink",
"startDate",
"hashes",
"private",
"postData",
"cleanRequest",
"paused"
]);
@ -34,7 +48,7 @@ function maybeAssign(options: any, what: any) {
this[what] = val;
}
export class Item {
export class Item implements BaseItem {
public url: string;
public usable: string;
@ -43,6 +57,8 @@ export class Item {
public usableReferrer: string;
public idx: number;
constructor(raw: any, options?: any) {
Object.assign(this, raw);
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
@ -99,7 +115,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 unique = [];
for (const itemlist of items) {

View File

@ -29,8 +29,6 @@ const SAVEDPROPS = [
"serverName",
// other options
"private",
"fromMetalink",
"cleanRequest",
// db
"manId",
"dbId",

View File

@ -2,7 +2,7 @@
// License: MIT
import { Prefs } from "../prefs";
import { parsePath } from "../util";
import { parsePath, filterInSitu } from "../util";
import {
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
FORCABLE, PAUSABLE, CANCELABLE,
@ -18,6 +18,18 @@ const setShelfEnabled = downloads.setShelfEnabled || function() {
// ignored
};
type Header = {name: string; value: string};
interface Options {
conflictAction: string;
filename: string;
saveAs: boolean;
url: string;
method?: string;
body?: string;
incognito: boolean;
headers: Header[];
}
export class Download extends BaseDownload {
public manager: Manager;
@ -82,43 +94,46 @@ export class Download extends BaseDownload {
if (this.state !== QUEUED) {
throw new Error("invalid state");
}
console.trace("starting", this.toString(), this.dest, this.mask);
console.trace("starting", this.toString(), this.toMsg());
this.changeState(RUNNING);
try {
const options: any = {
const options: Options = {
conflictAction: await Prefs.get("conflict-action"),
filename: this.dest.full,
saveAs: false,
url: this.url,
headers: [{
name: "X-DTA-Tag",
value: this.sessionId.toString(),
}],
headers: [],
incognito: this.private
};
if (this.postData) {
options.body = this.postData;
options.method = "POST";
}
if (this.private) {
options.incognito = true;
}
/* XXX "forbidden"
Cannot be worked around with webRequest either
as those do not see downloads.
if (this.referrer) {
options.headers.push({
name: "Referer",
value: this.referrer
});
}
*/
if (this.manId) {
this.manager.removeManId(this.manId);
}
setShelfEnabled(false);
try {
this.manager.addManId(
this.manId = await downloads.download(options), this);
try {
this.manager.addManId(
this.manId = await downloads.download(options), this);
}
catch (ex) {
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);
}
}
finally {
setShelfEnabled(true);

View File

@ -147,6 +147,7 @@ export class Manager extends EventEmitter {
}
catch (ex) {
next.changeState(CANCELED);
next.error = ex.toString();
console.error(ex.toString(), ex);
}
}
@ -265,7 +266,6 @@ export class Manager extends EventEmitter {
changedState(download: Download, oldState: number, newState: number) {
if (oldState === RUNNING) {
this.running.delete(download);
this.maybeNotifyFinished();
}
if (newState === QUEUED) {
this.resetScheduler();
@ -278,7 +278,7 @@ export class Manager extends EventEmitter {
this.running.add(download);
}
else {
this.startNext();
this.startNext().catch(console.error);
}
}

View File

@ -36,7 +36,7 @@ export class RecentList {
this.pref = `savedlist-${pref}`;
this.defaults = Array.from(defaults);
this[LIST] = [];
this.limit = 5;
this.limit = 15;
}
get values() {

View File

@ -10,10 +10,24 @@ import { donate, openPrefs, openUrls } from "./windowutils";
import { filters, FAST, Filter } from "./filters";
import { WindowStateTracker } from "./windowstatetracker";
import { windows } from "./browser";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
interface BaseMatchedItem extends BaseItem {
matched?: string | null;
prevMatched?: string | null;
}
function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
let ws = items.map((item: any, idx: number) => {
export interface ItemDelta {
idx: number;
matched?: string | null;
}
function computeSelection(
filters: Filter[],
items: BaseMatchedItem[],
onlyFast: boolean): ItemDelta[] {
let ws = items.map((item, idx: number) => {
item.idx = idx;
const {matched = null} = item;
item.prevMatched = matched;
@ -23,14 +37,20 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
for (const filter of filters) {
ws = ws.filter(item => {
if (filter.matchItem(item)) {
item.matched = filter.id === FAST ?
"fast" :
(onlyFast ? null : filter.id);
if (filter.id === FAST) {
item.matched = "fast";
}
else if (!onlyFast && typeof filter.id === "string") {
item.matched = filter.id;
}
else {
item.matched = null;
}
}
return !item.matched;
});
}
return items.filter(item => item.prevMatched !== item.matched). map(item => {
return items.filter(item => item.prevMatched !== item.matched).map(item => {
return {
idx: item.idx,
matched: item.matched
@ -41,6 +61,9 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
function *computeActiveFiltersGen(
filters: Filter[], activeOverrides: Map<string, boolean>) {
for (const filter of filters) {
if (typeof filter.id !== "string") {
continue;
}
const override = activeOverrides.get(filter.id);
if (typeof override === "boolean") {
if (override) {
@ -59,11 +82,11 @@ function computeActiveFilters(
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
}
function filtersToDescs(filters: any[]) {
function filtersToDescs(filters: Filter[]) {
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 tracker = new WindowStateTracker("select", {
minWidth: 700,
@ -85,26 +108,26 @@ export async function select(links: any[], media: any[]) {
tracker.track(window.id, port);
const overrides = new Map();
let fast: any = null;
let fast: Filter | null = null;
let onlyFast: false;
try {
fast = fm.getFastFilter();
fast = await fm.getFastFilter();
}
catch (ex) {
// ignored
}
const sendFilters = function(delta = false) {
let {linkFilters, mediaFilters} = fm;
const {linkFilters, mediaFilters} = fm;
const alink = computeActiveFilters(linkFilters, overrides);
const amedia = computeActiveFilters(mediaFilters, overrides);
const sactiveFilters = new Set<any>();
[alink, amedia].forEach(
a => a.forEach(filter => sactiveFilters.add(filter.id)));
const activeFilters = Array.from(sactiveFilters);
linkFilters = filtersToDescs(linkFilters);
mediaFilters = filtersToDescs(mediaFilters);
port.post("filters", {linkFilters, mediaFilters, activeFilters});
const linkFilterDescs = filtersToDescs(linkFilters);
const mediaFilterDescs = filtersToDescs(mediaFilters);
port.post("filters", {linkFilterDescs, mediaFilterDescs, activeFilters});
if (fast) {
alink.unshift(fast);
@ -128,9 +151,6 @@ export async function select(links: any[], media: 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);
});
@ -175,7 +195,11 @@ export async function select(links: any[], media: any[]) {
sendFilters(false);
const type = await Prefs.get("last-type", "links");
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) {
const f = fm.get(filter);
if (f) {
@ -183,7 +207,7 @@ export async function select(links: any[], media: any[]) {
}
}
await fm.save();
return result;
return {items: selectedItems, options};
}
finally {
fm.off("changed", sendFilters);

View File

@ -7,8 +7,10 @@ import { WindowStateTracker } from "./windowstatetracker";
import { Promised, timeout } from "./util";
import { donate } from "./windowutils";
import { windows } 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", {
minWidth: 700,
minHeight: 460
@ -46,7 +48,9 @@ export async function single(item: any) {
donate();
});
port.post("item", item);
if (item) {
port.post("item", {item});
}
return await done;
}
finally {

View File

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "DownThemAll!",
"version": "4.0.2",
"version": "4.0.5",
"description": "__MSG_extensionDescription__",
"homepage_url": "https://downthemall.org/",
@ -40,7 +40,8 @@
},
"browser_action": {
"browser_style": false,
"browser_style": true,
"default_popup": "windows/popup.html",
"default_icon": {
"16": "style/icon16.png",
"32": "style/icon32.png",

View File

@ -22,13 +22,13 @@
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
"chai": "^4.1.2",
"eslint": "^6.1.0",
"eslint": "^6.2.2",
"mocha": "^6.2.0",
"ts-loader": "^6.0.4",
"ts-node": "^8.3.0",
"typescript": "^3.5.3",
"webpack": "^4.39.2",
"webpack-cli": "^3.3.6",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"xregexp": "^4.2.4"
},
"dependencies": {

View File

@ -28,7 +28,9 @@ return url;
}();
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) {

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

@ -46,6 +46,11 @@ body > * {
background: rgb(246,246,246);
color: black;
transition: box-shadow 0.5s, background 1s;
font-size: 24px;
line-height: 24px;
}
#toolbar > .button > span:before {
display: block;
}
#toolbar > .button.disabled {
@ -63,13 +68,6 @@ body > * {
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 {
color: var(--add-color);
}

View File

@ -19,7 +19,7 @@ article {
#tabs {
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, url(tile.png) repeat-x, var(--toolbar-bg-color);
padding-left: calc(2em + 32px);
color: white;
}
@ -28,7 +28,7 @@ input.tab {
display: none;
}
#tabs > label{
#tabs > label {
display: inline-block;
cursor: pointer;
font-size: 150%;

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

@ -117,7 +117,7 @@ body > * {
}
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
#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, url(tile.png) repeat-x, var(--toolbar-bg-color);
}
}
@ -147,7 +147,7 @@ body > * {
background: var(--toolbar-bg-color);
color: white;
min-width: 10em;
padding: 1.2ex;
padding: 1ex;
padding-left: 1em;
cursor: pointer;
border: 0;

View File

@ -14,6 +14,8 @@ import {
} from "./tablesymbols";
import { InvalidatedSet, UpdateRecord } from "./tableutil";
import { addClass, clampUInt, IS_MAC } from "./util";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "./config";
const ROWS_SMALL_UPDATE = 5;
const PIXEL_PREC = 5;
@ -79,7 +81,7 @@ export class BaseTable extends AbstractTable {
[COLS]: Columns;
constructor(elem: any, config: any, version?: number) {
constructor(elem: any, config: TableConfig | null, version?: number) {
config = (config && config.version === version && config) || {};
super();
@ -121,9 +123,9 @@ export class BaseTable extends AbstractTable {
this.makeDOM(config);
}
makeDOM(config: any) {
makeDOM(config: TableConfig) {
const configColumns = "columns" in config ? config.columns : null;
const cols = this[COLS] = new Columns(this, configColumns);
const cols = this[COLS] = new Columns(this, configColumns || null);
const container = document.createElement("div");
const thead = document.createElement("div");
@ -241,7 +243,7 @@ export class BaseTable extends AbstractTable {
return new SelectionRange(firstIdx, lastIdx);
}
get config() {
get config(): TableConfig {
return {
version: this.version,
columns: this.columnConfig

View File

@ -1,14 +1,12 @@
"use strict";
// License: MIT
/* eslint-disable no-unused-vars */
import { TableEvents } from "./tableevents";
import {addClass, debounce, sum} from "./util";
import {EventEmitter} from "./events";
import {APOOL} from "./animationpool";
/* eslint-enable no-unused-vars */
// License: MIT
import { ColumnConfig, ColumnConfigs } from "./config";
const PIXLIT_WIDTH = 2;
const MIN_COL_WIDTH = 16;
@ -55,8 +53,7 @@ export class Column extends EventEmitter {
columns: Columns,
col: HTMLTableHeaderCellElement,
id: number,
config: any) {
config = config || {};
config: ColumnConfig | null) {
super();
this.columns = columns;
this.elem = col;
@ -89,7 +86,7 @@ export class Column extends EventEmitter {
this.elem.appendChild(containerElem);
if ("visible" in config) {
if (config) {
this.visible = config.visible;
}
this.initWidths(config);
@ -148,18 +145,18 @@ export class Column extends EventEmitter {
return Math.max(0, this.currentWidth - this.minWidth);
}
get config() {
get config(): ColumnConfig {
return {
visible: this.visible,
width: this.currentWidth,
};
}
initWidths(config: any) {
initWidths(config: ColumnConfig | null) {
const style = getComputedStyle(this.elem, null);
this.minWidth = toPixel(style.getPropertyValue("min-width"), MIN_COL_WIDTH);
this.maxWidth = toPixel(style.getPropertyValue("max-width"), 0);
const width = config.width || this.baseWidth;
const width = (config && config.width) || this.baseWidth;
this.setWidth(width);
}
@ -236,7 +233,7 @@ export class Columns extends EventEmitter {
public visible: Column[];
constructor(table: any, config: any) {
constructor(table: any, config: ColumnConfigs | null) {
config = config || {};
super();
this.table = table;
@ -247,7 +244,9 @@ export class Columns extends EventEmitter {
this.named = new Map<string, Column>();
this.cols = Array.from(table.elem.querySelectorAll("th")).
map((colEl: HTMLTableHeaderCellElement, colid: number) => {
const columnConfig = colEl.id in config ? config[colEl.id] : null;
const columnConfig = config && colEl.id in config ?
config[colEl.id] :
null;
const col = new Column(this, colEl, colid, columnConfig);
col.on("gripmoved", this.gripmoved);
this.named.set(colEl.id, col);
@ -261,7 +260,7 @@ export class Columns extends EventEmitter {
Object.seal(this);
}
get config() {
get config(): ColumnConfigs {
const rv: any = {};
for (const c of this.cols) {
rv[c.elem.id] = c.config;

13
uikit/lib/config.ts Normal file
View File

@ -0,0 +1,13 @@
"use strict";
// License: MIT
export interface ColumnConfig {
visible: boolean;
width: number;
}
export type ColumnConfigs ={ [name: string]: ColumnConfig };
export interface TableConfig {
version?: number;
columns?: ColumnConfigs;
}

View File

@ -10,7 +10,7 @@ const MENU_OPEN_BOUNCE = 500;
let ids = 0;
const Keys = new Map([
export const Keys = new Map([
["ACCEL", IS_MAC ? "⌘" : "Ctrl"],
["CTRL", "Ctrl"],
["ALT", IS_MAC ? "⌥" : "Alt"],
@ -33,6 +33,14 @@ export interface MenuPosition {
clientY: number;
}
interface MenuOptions {
disabled?: string;
allowClick?: string;
icon?: string;
key?: string;
autoHide?: string;
}
export class MenuItemBase {
public readonly owner: ContextMenu;
@ -44,7 +52,7 @@ export class MenuItemBase {
public readonly key: string;
public readonly autohide: boolean;
public readonly autoHide: boolean;
public readonly elem: HTMLLIElement;
@ -54,18 +62,16 @@ export class MenuItemBase {
public readonly keyElem: HTMLSpanElement;
constructor(owner: ContextMenu, id = "", text = "", {
key = "", icon = "", autohide = Object()
}) {
constructor(owner: ContextMenu, id = "", text = "", options: MenuOptions) {
this.owner = owner;
if (!id) {
id = `contextmenu-${++ids}`;
}
this.id = id;
this.text = text || "";
this.icon = icon || "";
this.key = key || "";
this.autohide = autohide !== "false" && autohide !== false;
this.icon = options.icon || "";
this.key = options.key || "";
this.autoHide = options.autoHide !== "false";
this.elem = document.createElement("li");
this.elem.id = this.id;
@ -96,13 +102,14 @@ export class MenuItemBase {
}
export class MenuItem extends MenuItemBase {
constructor(owner: ContextMenu, id = "", text = "", options: any = {}) {
constructor(
owner: ContextMenu, id = "", text = "", options: MenuOptions = {}) {
options = options || {};
super(owner, id, text, options);
this.disabled = !!options.disabled;
this.disabled = options.disabled === "true";
this.elem.setAttribute("aria-role", "menuitem");
this.elem.addEventListener(
"click", () => this.owner.emit("clicked", this.id, this.autohide));
"click", () => this.owner.emit("clicked", this.id, this.autoHide));
}
get disabled() {
@ -132,7 +139,8 @@ export class SubMenuItem extends MenuItemBase {
public readonly expandElem: HTMLSpanElement;
constructor(owner: ContextMenu, id = "", text = "", options: any = {}) {
constructor(
owner: ContextMenu, id = "", text = "", options: MenuOptions = {}) {
super(owner, id, text, options);
this.elem.setAttribute("aria-role", "menuitem");
this.elem.setAttribute("aria-haspopup", "true");
@ -145,8 +153,8 @@ export class SubMenuItem extends MenuItemBase {
this.expandElem.textContent = "►";
this.elem.appendChild(this.expandElem);
this.elem.addEventListener("click", event => {
if (options.allowClick) {
this.owner.emit("clicked", this.id, this.autohide);
if (options.allowClick === "true") {
this.owner.emit("clicked", this.id, this.autoHide);
}
event.stopPropagation();
event.preventDefault();
@ -160,7 +168,7 @@ export class SubMenuItem extends MenuItemBase {
this.owner.on("showing", () => {
this.menu.dismiss();
});
this.menu.on("clicked", (...args: any) => {
this.menu.on("clicked", (...args: any[]) => {
this.owner.emit("clicked", ...args);
});
}
@ -215,7 +223,7 @@ export class SubMenuItem extends MenuItemBase {
export class ContextMenu extends EventEmitter {
id: string;
items: any[];
items: MenuItemBase[];
itemMap: Map<string, MenuItemBase>;
@ -223,7 +231,7 @@ export class ContextMenu extends EventEmitter {
showing: boolean;
_maybeDismiss: any;
_maybeDismiss: (this: Window, ev: MouseEvent) => any;
constructor(el?: any) {
super();
@ -348,10 +356,12 @@ export class ContextMenu extends EventEmitter {
return this.itemMap.get(id);
}
add(item: MenuItemBase, before: any = "") {
add(item: MenuItemBase, before: MenuItemBase | string = "") {
let idx = this.items.length;
if (before) {
before = before.id || before;
if (typeof before !== "string") {
before = before.id;
}
const ni = this.items.findIndex(i => i.id === before);
if (ni >= 0) {
idx = ni;
@ -366,8 +376,8 @@ export class ContextMenu extends EventEmitter {
this.itemMap.set(item.id, item);
}
remove(item: any) {
const id = item.id || item;
remove(item: MenuItemBase | string) {
const id = typeof item === "string" ? item : item.id;
const idx = this.items.findIndex(i => i.id === id);
if (idx >= 0) {
this.items.splice(idx, 1);

View File

@ -1,15 +1,20 @@
"use strict";
// License: MIT
interface ModalButton {
export interface ModalButton {
title: string;
value: string;
default?: boolean;
dismiss?: boolean;
}
export default class ModalDialog {
private _showing: any;
interface Promised {
resolve: Function;
reject: Function;
}
export default abstract class ModalDialog {
private _showing: Promised | null;
private _dismiss: HTMLButtonElement | null;
@ -23,7 +28,7 @@ export default class ModalDialog {
this._default = null;
}
_makeEl() {
async _makeEl() {
this._dismiss = null;
this._default = null;
@ -35,7 +40,7 @@ export default class ModalDialog {
const body = document.createElement("article");
body.classList.add("modal-body");
body.appendChild(this.content);
body.appendChild(await this.getContent());
cont.appendChild(body);
const footer = document.createElement("footer");
@ -87,9 +92,8 @@ export default class ModalDialog {
return el;
}
get content(): DocumentFragment | HTMLElement {
throw new Error("Not implemented");
}
abstract getContent():
Promise<DocumentFragment | HTMLElement> | HTMLElement | DocumentFragment;
get buttons(): ModalButton[] {
return [
@ -108,7 +112,10 @@ export default class ModalDialog {
];
}
done(button: any) {
done(button: ModalButton) {
if (!this._showing) {
return;
}
const value = this.convertValue(button.value);
if (button.dismiss) {
this._showing.reject(new Error(value));
@ -131,7 +138,7 @@ export default class ModalDialog {
}
async show() {
async show(): Promise<any> {
if (this._showing) {
throw new Error("Double show");
}
@ -160,7 +167,7 @@ export default class ModalDialog {
return;
};
document.body.appendChild(this.el = this._makeEl());
document.body.appendChild(this.el = await this._makeEl());
this.shown();
addEventListener("keydown", escapeHandler);
addEventListener("keydown", enterHandler);
@ -205,7 +212,7 @@ export default class ModalDialog {
*/
static async inform(title: string, text: string, oktext: string) {
const dialog = new class extends ModalDialog {
get content() {
getContent() {
const rv = document.createDocumentFragment();
const h = document.createElement("h1");
h.textContent = title || "Information";
@ -241,7 +248,7 @@ export default class ModalDialog {
static async confirm(title: string, text: string) {
const dialog = new class extends ModalDialog {
get content() {
getContent() {
const rv = document.createDocumentFragment();
const h = document.createElement("h1");
h.textContent = title || "Confirm";
@ -280,7 +287,7 @@ export default class ModalDialog {
const dialog = new class extends ModalDialog {
_input: HTMLInputElement;
get content() {
getContent() {
const rv = document.createDocumentFragment();
const h = document.createElement("h1");
h.textContent = title || "Confirm";

View File

@ -38,7 +38,7 @@ class Hover {
private hovering: boolean;
private timer: any;
private timer: number | null;
constructor(row: Row) {
this.row = row;
@ -62,7 +62,7 @@ class Hover {
this.elem.addEventListener("mousemove", this.onmove, {passive: true});
this.x = evt.clientX;
this.y = evt.clientY;
this.timer = setTimeout(this.onhover, HOVER_TIME);
this.timer = window.setTimeout(this.onhover, HOVER_TIME);
}
onleave() {
@ -93,7 +93,7 @@ class Hover {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(this.onhover, HOVER_TIME);
this.timer = window.setTimeout(this.onhover, HOVER_TIME);
}
}

View File

@ -12,6 +12,8 @@ import {Row} from "./row";
import {APOOL} from "./animationpool";
import {COLS, ROWCACHE, VISIBLE} from "./tablesymbols";
import {ContextMenu, MenuItem} from "./contextmenu";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "./config";
const RESIZE_DEBOUNCE = 500;
const SCROLL_DEBOUNCE = 250;
@ -19,7 +21,7 @@ const SCROLL_DEBOUNCE = 250;
export class TableEvents extends BaseTable {
private oldVisibleTop: number;
constructor(elem: any, config?: any, version?: number) {
constructor(elem: any, config: TableConfig | null, version?: number) {
super(elem, config, version);
const {selection} = this;
selection.on("selection-added", this.selectionAdded.bind(this));
@ -172,7 +174,7 @@ export class TableEvents extends BaseTable {
ctx,
id,
col.spanElem.textContent || "",
{autohide: "false"});
{autoHide: "false"});
ctx.add(item);
item.iconElem.textContent = col.visible ? "✓" : " ";
ctx.on(id, async () => {

View File

@ -8,6 +8,8 @@ import { Row } from "./row";
import {APOOL} from "./animationpool";
import {ROW_CACHE_SIZE, ROW_REUSE_SIZE} from "./constants";
import {clampUInt} from "./util";
// eslint-disable-next-line no-unused-vars
import { BaseTable } from "./basetable";
export class InvalidatedSet<T> extends Set<T> {
@ -70,7 +72,7 @@ export class UpdateRecord {
bottom: number;
constructor(table: any, cols: Column[]) {
constructor(table: BaseTable, cols: Column[]) {
this.rowCount = table.rowCount;
this.scrollTop = table.visibleTop;
this.rowHeight = table.rowHeight;

View File

@ -13,14 +13,21 @@ export function addClass(elem: HTMLElement, ...cls: string[]) {
}
}
interface Timer {
args: any[];
}
export function debounce(fn: Function, to: number) {
let timer: any;
let timer: Timer | null;
return function(...args: any[]) {
if (timer) {
timer.args = args;
return;
}
setTimeout(function() {
if (!timer) {
return;
}
const {args} = timer;
timer = null;
try {
@ -38,7 +45,7 @@ function sumreduce(p: number, c: number) {
return p + c;
}
export function sum(arr: any[]) {
export function sum(arr: number[]) {
return arr.reduce(sumreduce, 0);
}

View File

@ -25,6 +25,7 @@ module.exports = {
"select": "./windows/select.ts",
"single": "./windows/single.ts",
"prefs": "./windows/prefs.ts",
"content-popup": "./windows/popup.ts",
"content-gather": "./scripts/gather.ts",
},
externals(context, request, callback) {

View File

@ -1,9 +1,8 @@
"use strict";
// License: MIT
import {Keys} from "./keys";
const $ = document.querySelector.bind(document);
import { Keys } from "./keys";
import { $ } from "./winutil";
export class Broadcaster {
private readonly els: HTMLElement[];
@ -38,7 +37,7 @@ export class Broadcaster {
}
onkey(evt: KeyboardEvent) {
const {localName} = evt.target as HTMLElement;
const { localName } = evt.target as HTMLElement;
if (localName === "input" || localName === "textarea") {
return undefined;
}

22
windows/contextmenu.ts Normal file
View File

@ -0,0 +1,22 @@
"use strict";
// License: MIT
export * from "../uikit/lib/contextmenu";
import { Keys } from "../uikit/lib/contextmenu";
import { IS_MAC } from "../uikit/lib/util";
import { locale, _ } from "../lib/i18n";
locale.then(() => {
Keys.clear();
[
["ACCEL", IS_MAC ? "⌘" : _("key-ctrl")],
["CTRL", _("key-ctrl")],
["ALT", IS_MAC ? "⌥" : _("key-alt")],
["DELETE", _("key-delete")],
["PAGEUP", _("key-pageup")],
["PAGEDOWN", _("key-pagedown")],
["HOME", _("key-home")],
["END", _("key-end")],
["SHIFT", "⇧"],
].forEach(([k, v]) => Keys.set(k, v));
});

View File

@ -13,7 +13,7 @@ export class Dropdown extends EventEmitter {
select: HTMLSelectElement;
constructor(el: string, options: any[] = []) {
constructor(el: string, options: string[] = []) {
super();
let input = document.querySelector(el);
if (!input || !input.parentElement) {

View File

@ -3,7 +3,7 @@
import {EventEmitter} from "../lib/events";
// eslint-disable-next-line no-unused-vars
import { ContextMenu } from "../uikit/lib/contextmenu";
import { ContextMenu } from "./contextmenu";
import { runtime } from "../lib/browser";
export const Keys = new class extends EventEmitter {

View File

@ -128,8 +128,8 @@
<template id="menufilter-template">
<ul>
<li id="ctx-menufilter-seperator">-</li>
<li id="ctx-menufilter-invert" data-autohide="false">Invert</li>
<li id="ctx-menufilter-clear" data-autohide="false">Clear</li>
<li id="ctx-menufilter-invert" data-autoHide="false">Invert</li>
<li id="ctx-menufilter-clear" data-autoHide="false">Clear</li>
<li>-</li>
<li id="ctx-menufilter-sort-ascending" data-icon="icon-sort-asc">Sort ascending</li>
<li id="ctx-menufilter-sort-descending" data-icon="icon-sort-desc">Sort descending</li>

View File

@ -6,6 +6,8 @@ import {_, localize} from "../lib/i18n";
import {Prefs} from "../lib/prefs";
import PORT from "./manager/port";
import { runtime } from "../lib/browser";
import { Promised } from "../lib/util";
import { PromiseSerializer } from "../lib/pserializer";
const $ = document.querySelector.bind(document);
@ -18,37 +20,6 @@ const LOADED = new Promise(resolve => {
});
});
LOADED.then(async () => {
const nag = await Prefs.get("nagging", 0);
const nagnext = await Prefs.get("nagging-next", 6);
const next = Math.ceil(Math.log2(Math.max(1, nag)));
const el = $("#nagging");
const remove = () => {
el.parentElement.removeChild(el);
};
if (next <= nagnext) {
return;
}
setTimeout(() => {
$("#nagging-donate").addEventListener("click", () => {
PORT.post("donate");
Prefs.set("nagging-next", next);
remove();
});
$("#nagging-later").addEventListener("click", () => {
Prefs.set("nagging-next", next);
remove();
});
$("#nagging-never").addEventListener("click", () => {
Prefs.set("nagging-next", Number.MAX_SAFE_INTEGER);
remove();
});
$("#nagging-message").textContent = _(
"nagging-message", nag.toLocaleString());
$("#nagging").classList.remove("hidden");
}, 2 * 1000);
});
addEventListener("DOMContentLoaded", function dom() {
removeEventListener("DOMContentLoaded", dom);
@ -67,9 +38,41 @@ addEventListener("DOMContentLoaded", function dom() {
}
})();
const loaded = Promise.all([LOADED, platformed]);
const tabled = new Promised();
const localized = localize(document.documentElement);
const loaded = Promise.all([LOADED, platformed, localized]);
const fullyloaded = Promise.all([LOADED, platformed, tabled, localized]);
fullyloaded.then(async () => {
const nag = await Prefs.get("nagging", 0);
const nagnext = await Prefs.get("nagging-next", 7);
const next = Math.ceil(Math.log2(Math.max(1, nag)));
const el = $("#nagging");
const remove = () => {
el.parentElement.removeChild(el);
};
if (next <= nagnext) {
return;
}
setTimeout(() => {
$("#nagging-donate").addEventListener("click", () => {
PORT.post("donate");
Prefs.set("nagging-next", next);
remove();
});
$("#nagging-later").addEventListener("click", () => {
Prefs.set("nagging-next", next);
remove();
});
$("#nagging-never").addEventListener("click", () => {
Prefs.set("nagging-next", Number.MAX_SAFE_INTEGER);
remove();
});
$("#nagging-message").textContent = _(
"nagging-message", nag.toLocaleString());
$("#nagging").classList.remove("hidden");
}, 2 * 1000);
});
localize(document.documentElement);
$("#donate").addEventListener("click", () => {
PORT.post("donate");
});
@ -85,24 +88,29 @@ addEventListener("DOMContentLoaded", function dom() {
Table.init();
const loading = $("#loading");
loading.parentElement.removeChild(loading);
tabled.resolve();
}
Table.setItems(items);
});
});
PORT.on("dirty", async items => {
await loaded;
// Updates
const serializer = new PromiseSerializer(1);
PORT.on("dirty", serializer.wrap(this, async (items: any[]) => {
await fullyloaded;
Table.updateItems(items);
});
PORT.on("removed", async sids => {
await loaded;
}));
PORT.on("removed", serializer.wrap(this, async (sids: number[]) => {
await fullyloaded;
Table.removedItems(sids);
});
}));
const statusNetwork = $("#statusNetwork");
statusNetwork.addEventListener("click", () => {
PORT.post("toggle-active");
});
PORT.on("active", active => {
PORT.on("active", async (active: boolean) => {
await loaded;
if (active) {
statusNetwork.className = "icon-network-on";
statusNetwork.setAttribute("title", _("statusNetwork-active.title"));

View File

@ -2,8 +2,7 @@
// License: MIT
import { EventEmitter } from "../../lib/events";
const $ = document.querySelector.bind(document);
import { $ } from "../winutil";
export class Buttons extends EventEmitter {
private readonly parent: HTMLElement;

View File

@ -10,7 +10,7 @@ import {
MenuItemBase,
// eslint-disable-next-line no-unused-vars
MenuPosition,
} from "../../uikit/lib/contextmenu";
} from "../contextmenu";
import {EventEmitter} from "../../lib/events";
// eslint-disable-next-line no-unused-vars
import {filters, Matcher, Filter} from "../../lib/filters";
@ -19,12 +19,10 @@ import {sort, defaultCompare, naturalCaseCompare} from "../../lib/sorting";
import {DownloadItem, DownloadTable} from "./table";
import {formatSize} from "../../lib/formatters";
import {_} from "../../lib/i18n";
import {StateTexts} from "./state";
import {$} from "../winutil";
const TIMEOUT_SEARCH = 750;
const $ = document.querySelector.bind(document);
class ItemFilter {
public readonly id: string;
@ -43,7 +41,7 @@ export class TextFilter extends ItemFilter {
private box: HTMLInputElement;
private timer: any;
private timer: number | null;
private current: string;
@ -60,7 +58,7 @@ export class TextFilter extends ItemFilter {
if (this.timer) {
return;
}
this.timer = setTimeout(() => this.update(), TIMEOUT_SEARCH);
this.timer = window.setTimeout(() => this.update(), TIMEOUT_SEARCH);
});
this.box.addEventListener("keydown", e => {
if (e.key !== "Escape") {
@ -119,8 +117,10 @@ export class MenuFilter extends ItemFilter {
constructor(id: string) {
super(id);
this.items = new Map();
const tmpl = $<HTMLTemplateElement>("#menufilter-template").
content.cloneNode(true);
this.menu = new ContextMenu(
$("#menufilter-template").content.cloneNode(true).firstElementChild);
(tmpl as HTMLElement).firstElementChild);
this.menu.on("clicked", this.onclicked.bind(this));
this.menu.on("ctx-menufilter-invert", () => this.invert());
this.menu.on("ctx-menufilter-clear", () => this.clear());
@ -152,7 +152,7 @@ export class MenuFilter extends ItemFilter {
return;
}
const item = new MenuItem(this.menu, id, text, {
autohide: false,
autoHide: "false",
});
item.iconElem.textContent = checked ? "✓" : "";
this.items.set(id, {item, callback});
@ -202,16 +202,24 @@ export class MenuFilter extends ItemFilter {
}
}
type ChainedFunction = (item: DownloadItem) => boolean;
interface ChainedItem {
text: string;
fn: ChainedFunction;
}
class FixedMenuFilter extends MenuFilter {
collection: FilteredCollection;
selected: Set<any>;
selected: Set<ChainedItem>;
fixed: Set<any>;
fixed: Set<ChainedItem>;
chain: any;
chain: ChainedFunction | null;
constructor(id: string, collection: FilteredCollection, items: any[]) {
constructor(
id: string, collection: FilteredCollection, items: ChainedItem[]) {
super(id);
this.collection = collection;
this.selected = new Set();
@ -226,7 +234,7 @@ class FixedMenuFilter extends MenuFilter {
});
}
toggle(item: MenuItem) {
toggle(item: ChainedItem) {
if (this.selected.has(item)) {
this.selected.delete(item);
}
@ -241,14 +249,18 @@ class FixedMenuFilter extends MenuFilter {
this.collection.removeFilter(this);
return;
}
this.chain = Array.from(this.selected).reduce((prev, curr) => {
return (item: DownloadItem) => curr.fn(item) || (prev && prev(item));
}, null);
this.chain = null;
this.chain = Array.from(this.selected).reduce(
(prev: ChainedFunction | null, curr) => {
return (item: DownloadItem) => {
return curr.fn(item) || (prev !== null && prev(item));
};
}, this.chain);
this.collection.addFilter(this);
}
allow(item: DownloadItem) {
return this.chain(item);
return this.chain !== null && this.chain(item);
}
clear() {
@ -259,7 +271,9 @@ class FixedMenuFilter extends MenuFilter {
}
export class StateMenuFilter extends FixedMenuFilter {
constructor(collection: FilteredCollection) {
constructor(
collection: FilteredCollection,
StateTexts: Readonly<Map<number, string>>) {
const items = Array.from(StateTexts.entries()).map(([state, text]) => {
return {
state,
@ -339,7 +353,7 @@ export class UrlMenuFilter extends MenuFilter {
});
}
toggleRegularFilter(filter: any) {
toggleRegularFilter(filter: Filter) {
if (this.filters.has(filter)) {
this.filters.delete(filter);
}

View File

@ -2,14 +2,18 @@
// License: MIT
import { EventEmitter } from "../../lib/events";
import { runtime } from "../../lib/browser";
// eslint-disable-next-line no-unused-vars
import { runtime, RawPort } from "../../lib/browser";
const PORT = new class Port extends EventEmitter {
port: any;
port: RawPort | null;
constructor() {
super();
this.port = runtime.connect(null, { name: "manager" });
if (!this.port) {
throw new Error("Could not connect");
}
this.port.onMessage.addListener((msg: any) => {
if (typeof msg === "string") {
this.emit(msg);
@ -23,10 +27,16 @@ const PORT = new class Port extends EventEmitter {
}
post(msg: string, data?: any) {
if (!this.port) {
return;
}
this.port.postMessage(Object.assign({msg}, data));
}
disconnect() {
if (!this.port) {
return;
}
this.port.disconnect();
this.port = null;
}

View File

@ -5,8 +5,7 @@ import ModalDialog from "../../uikit/lib/modal";
import { _, localize } from "../../lib/i18n";
import { Prefs } from "../../lib/prefs";
import { Keys } from "../keys";
const $ = document.querySelector.bind(document);
import { $ } from "../winutil";
export default class RemovalModalDialog extends ModalDialog {
private readonly text: string;
@ -22,11 +21,12 @@ export default class RemovalModalDialog extends ModalDialog {
this.check = null;
}
get content() {
const content = $("#removal-template").content.cloneNode(true);
localize(content);
async getContent() {
const content = $<HTMLTemplateElement>("#removal-template").
content.cloneNode(true) as DocumentFragment;
await localize(content);
this.check = content.querySelector(".removal-remember");
content.querySelector(".removal-text").textContent = this.text;
$(".removal-text", content).textContent = this.text;
return content;
}

View File

@ -2,11 +2,11 @@
// License: MIT
import * as _DownloadState from "../../lib/manager/state";
import { _ } from "../../lib/i18n";
import { _, locale } from "../../lib/i18n";
export const DownloadState = _DownloadState;
export const StateTexts = Object.freeze(new Map([
export const StateTexts = locale.then(() => Object.freeze(new Map([
[DownloadState.QUEUED, _("queued")],
[DownloadState.RUNNING, _("running")],
[DownloadState.FINISHING, _("finishing")],
@ -14,7 +14,7 @@ export const StateTexts = Object.freeze(new Map([
[DownloadState.DONE, _("done")],
[DownloadState.CANCELED, _("canceled")],
[DownloadState.MISSING, _("missing")],
]));
])));
export const StateClasses = Object.freeze(new Map([
[DownloadState.QUEUED, "queued"],

View File

@ -7,11 +7,11 @@ import {
MenuItem,
// eslint-disable-next-line no-unused-vars
SubMenuItem
} from "../../uikit/lib/contextmenu";
} from "../contextmenu";
import { iconForPath } from "../../lib/windowutils";
import { formatSpeed, formatSize, formatTimeDelta } from "../../lib/formatters";
import { filters } from "../../lib/filters";
import { _, localize } from "../../lib/i18n";
import { _ } from "../../lib/i18n";
import { EventEmitter } from "../../lib/events";
import { Prefs, PrefWatcher } from "../../lib/prefs";
// eslint-disable-next-line no-unused-vars
@ -34,6 +34,9 @@ import { Tooltip } from "./tooltip";
import "../../lib/util";
import { CellTypes } from "../../uikit/lib/constants";
import { downloads } from "../../lib/browser";
import { $ } from "../winutil";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "../../uikit/lib/config";
const TREE_CONFIG_VERSION = 2;
const RUNNING_TIMEOUT = 1000;
@ -51,9 +54,11 @@ const COL_SEGS = 8;
const ICON_BASE_SIZE = 16;
const TEXT_SIZE_UNKNOWM = _("size-unknown");
const $ = document.querySelector.bind(document);
let TEXT_SIZE_UNKNOWM = "unknown";
let REAL_STATE_TEXTS = Object.freeze(new Map<number, string>());
StateTexts.then(v => {
REAL_STATE_TEXTS = v;
});
const prettyNumber = (function() {
const rv = new Intl.NumberFormat(undefined, {
@ -189,9 +194,9 @@ export class DownloadItem extends EventEmitter {
return this.eta;
}
if (this.error) {
return this.error;
return _(this.error) || this.error;
}
return StateTexts.get(this.state);
return REAL_STATE_TEXTS.get(this.state) || "";
}
get fmtSpeed() {
@ -308,9 +313,9 @@ export class DownloadTable extends VirtualTable {
public readonly showUrls: ShowUrlsWatcher;
private runningTimer: any;
private runningTimer: number | null;
private sizesTimer: any;
private sizesTimer: number | null;
private readonly globalStats: Stats;
@ -326,7 +331,7 @@ export class DownloadTable extends VirtualTable {
private readonly openFileAction: Broadcaster;
private readonly openDirectoryAction: any;
private readonly openDirectoryAction: Broadcaster;
private readonly moveTopAction: Broadcaster;
@ -336,13 +341,15 @@ export class DownloadTable extends VirtualTable {
private readonly moveBottomAction: Broadcaster;
private readonly disableSet: Set<any>;
private readonly disableSet: Set<Broadcaster>;
private tooltip: Tooltip | null;
constructor(treeConfig: any) {
constructor(treeConfig: TableConfig | null) {
super("#items", treeConfig, TREE_CONFIG_VERSION);
TEXT_SIZE_UNKNOWM = _("size-unknown");
this.finished = 0;
this.running = new Set();
this.runningTimer = null;
@ -363,7 +370,7 @@ export class DownloadTable extends VirtualTable {
new TextFilter(this.downloads);
const menufilters = new Map<string, MenuFilter>([
["colURL", new UrlMenuFilter(this.downloads)],
["colETA", new StateMenuFilter(this.downloads)],
["colETA", new StateMenuFilter(this.downloads, REAL_STATE_TEXTS)],
["colSize", new SizeMenuFilter(this.downloads)],
]);
this.on("column-clicked", (id, evt, col) => {
@ -403,7 +410,6 @@ export class DownloadTable extends VirtualTable {
this.sids = new Map<number, DownloadItem>();
this.icons = new Icons($("#icons"));
localize($("#table-context").content);
const ctx = this.contextMenu = new ContextMenu("#table-context");
Keys.adoptContext(ctx);
Keys.adoptButtons($("#toolbar"));
@ -617,7 +623,7 @@ export class DownloadTable extends VirtualTable {
filter(e => e.startsWith(prefix)).
forEach(e => rem.remove(e));
for (const filt of filts.all) {
if (filt.id === "deffilter-all") {
if (typeof filt.id !== "string" || filt.id === "deffilter-all") {
continue;
}
const mi = new MenuItem(rem, `${prefix}-${filt.id}`, filt.label, {
@ -914,7 +920,7 @@ export class DownloadTable extends VirtualTable {
}
const filter = (await filters()).get(id);
if (!filter) {
if (!filter || typeof filter.id !== "string") {
return;
}
await new RemovalModalDialog(
@ -962,7 +968,7 @@ export class DownloadTable extends VirtualTable {
switch (oldState) {
case DownloadState.RUNNING:
this.running.delete(item);
if (!this.running.size && this.runningTimer) {
if (!this.running.size && this.runningTimer && this.sizesTimer) {
clearInterval(this.runningTimer);
this.runningTimer = null;
clearInterval(this.sizesTimer);
@ -979,9 +985,9 @@ export class DownloadTable extends VirtualTable {
case DownloadState.RUNNING:
this.running.add(item);
if (!this.runningTimer) {
this.runningTimer = setInterval(
this.runningTimer = window.setInterval(
this.updateRunning.bind(this), RUNNING_TIMEOUT);
this.sizesTimer = setInterval(
this.sizesTimer = window.setInterval(
this.updateSizes.bind(this), SIZES_TIMEOUT);
this.updateRunning();
this.updateSizes();

View File

@ -2,7 +2,7 @@
"use strict";
// License: MIT
import { _, localize } from "../../lib/i18n";
import { _ } from "../../lib/i18n";
import { formatSpeed } from "../../lib/formatters";
import { DownloadState } from "./state";
import { Rect } from "../../uikit/lib/rect";
@ -155,7 +155,7 @@ export class Tooltip {
if (!el) {
throw new Error("invalid template");
}
this.elem = localize(el.cloneNode(true) as HTMLElement);
this.elem = el.cloneNode(true) as HTMLElement;
this.adjust(pos);
// eslint-disable-next-line @typescript-eslint/no-this-alias
@ -184,12 +184,13 @@ export class Tooltip {
this.from.textContent = item.usable;
this.size.textContent = item.fmtSize;
this.date.textContent = new Date(item.startDate).toLocaleString();
this.eta.textContent = item.fmtETA;
const running = item.state === DownloadState.RUNNING;
const hidden = this.eta.classList.contains("hidden");
const hidden = this.speedbox.classList.contains("hidden");
if (!running && !hidden) {
this.eta.classList.add("hidden");
this.eta.style.fontWeight = "bold";
this.etalabel.classList.add("hidden");
this.speedbox.classList.add("hidden");
this.progressbar.classList.add("hidden");
@ -199,14 +200,13 @@ export class Tooltip {
return;
}
if (hidden) {
this.eta.classList.remove("hidden");
this.eta.style.fontWeight = "auto";
this.etalabel.classList.remove("hidden");
this.speedbox.classList.remove("hidden");
this.progressbar.classList.remove("hidden");
this.adjust(null);
}
this.progress.style.width = `${item.percent * 100}%`;
this.eta.textContent = item.fmtETA;
this.current.textContent = formatSpeed(item.stats.current);
this.average.textContent = formatSpeed(item.stats.avg);
this.drawSpeeds();

100
windows/popup.html Normal file
View File

@ -0,0 +1,100 @@
<!doctype html>
<head>
<!-- License: GPL-v2 -->
<meta charset="utf-8">
<style>
html {
box-sizing: content-box !important;
}
ul {
margin: 1.5ex;
margin-right: 2ex;
padding: 0;
display: grid;
grid-template-columns: auto;
list-style-type: none;
min-width: 14em;
}
li {
display: flex;
margin: 0;
padding: 1ex;
font-size: 110%;
vertical-align: center;
align-items: center;
border-radius: 4px;
}
li.sep {
display: inline-block;
padding: 0;
}
li:not(.sep):hover {
background: Highlight;
color: HighlightText;
}
li>.icon,
li>img {
font-size: 16px;
line-height: 16px;
margin-right: 1ex;
width: 16px;
height: 16px;
object-fit: contain;
}
#single:not(:hover)>.icon-add {
color: var(--add-color);
}
</style>
<style>
@import url(/style/common.css);
</style>
<script defer src="/bundles/content-popup.js"></script>
</head>
<body>
<ul>
<li id="regular" data-action="do-regular">
<img srcset="/style/button-regular.png, /style/button-regular@2x.png 2x">
<span data-i18n="dta.regular"></span>
</li>
<li id="turbo" data-action="do-turbo">
<img srcset="/style/button-turbo.png, /style/button-turbo@2x.png 2x">
<span data-i18n="dta.turbo"></span>
</li>
<li class="sep">
<hr>
</li>
<li id="regular-all" data-action="do-regular-all">
<img srcset="/style/button-regular.png, /style/button-regular@2x.png 2x">
<span data-i18n="dta-regular-all"></span>
</li>
<li id="turbo" data-action="do-turbo-all">
<img srcset="/style/button-turbo.png, /style/button-turbo@2x.png 2x">
<span data-i18n="dta-turbo-all"></span>
</li>
<li class="sep">
<hr>
</li>
<li id="single" data-action="do-single">
<span class="icon icon-add"></span>
<span data-i18n="add-download"></span>
</li>
<li class="sep">
<hr>
</li>
<li id="manager" data-action="open-manager">
<img srcset="/style/button-manager.png, /style/button-manager@2x.png 2x">
<span data-i18n="manager.short"></span>
</li>
<li id="prefs" data-action="open-prefs">
<span class="icon icon-settings"></span>
<span data-i18n="prefs.short">Preferences</span>
</li>
</ul>
</body>

34
windows/popup.ts Normal file
View File

@ -0,0 +1,34 @@
"use strict";
// License: MIT
import { localize } from "../lib/i18n";
declare let browser: any;
declare let chrome: any;
const runtime = browser !== "undefined" ? browser.runtime : chrome.runtime;
function handler(e: Event) {
e.preventDefault();
let target = e.target as HTMLElement;
if (!target) {
return;
}
while (target) {
const {action} = target.dataset;
if (!action) {
target = target.parentElement as HTMLElement;
continue;
}
runtime.sendMessage(action);
close();
return;
}
}
addEventListener("DOMContentLoaded", () => {
localize(document.documentElement);
document.body.addEventListener("contextmenu", handler);
document.body.addEventListener("click", handler);
});

View File

@ -45,6 +45,7 @@
<legend data-i18n="pref.ui">UI</legend>
<label><input type="checkbox" id="pref-global-turbo"> <span data-i18n="pref-global-turbo">Global turbo</span></label>
<label><input type="checkbox" id="pref-finish-notification"> <span data-i18n="pref-finish-notification"></span></label>
<label><input type="checkbox" id="pref-hide-context"> <span data-i18n="pref-hide-context"></span></label>
<div style="margin-top: 1em;">
<button id="reset-confirmations" data-i18n="reset-confirmations"></button>
<button id="reset-layout" data-i18n="reset-layouts"></button>
@ -64,10 +65,19 @@
<label><input type="checkbox" id="pref-remove-missing-on-init"> <span data-i18n="pref-remove-missing-on-init"></span></label>
</fieldset>
<fieldset id="pref-conflict-action">
<legend>When a file exists</legend>
<label><input type="radio" name="pref-conflict-action" value="overwrite"> <span>Overwrite</span></label>
<label><input type="radio" name="pref-conflict-action" value="uniquify"> <span>Rename</span></label>
<!--<label><input type="radio" name="pref-conflict-action" value="prompt"> <span>Prompt</span></label>-->
<legend data-i18n="prefs.conflicts">When a file exists</legend>
<label><input type="radio" name="pref-conflict-action" value="overwrite"> <span data-i18n="conflict-overwrite">Overwrite</span></label>
<label><input type="radio" name="pref-conflict-action" value="uniquify"> <span data-i18n="conflict-rename">Rename</span></label>
<!--<label><input type="radio" name="pref-conflict-action" value="prompt"> <span data-i18n="conflict-prompt">Prompt</span></label>-->
</fieldset>
<fieldset>
<legend>Translations</legend>
<div>
<button id="loadCustomLocale">Load custom translation</button>
<button id="clearCustomLocale">Clear custom translation</button>
<label>See the <a href="https://github.com/downthemall/downthemall/blob/master/_locales/Readme.md" rel="noopener noreferrer" target="_blank">Translation guide</a>.</label>
</div>
<input type="file" style="display: none;" id="customLocale" accept=".json">
</fieldset>
</article>

View File

@ -1,22 +1,24 @@
"use strict";
// License: MIT
import { _, localize } from "../lib/i18n";
import { _, localize, saveCustomLocale } from "../lib/i18n";
import { Prefs, PrefWatcher } from "../lib/prefs";
import { hostToDomain } from "../lib/util";
import { filters } from "../lib/filters";
import {Limits} from "../lib/manager/limits";
import ModalDialog from "../uikit/lib/modal";
// eslint-disable-next-line no-unused-vars
import ModalDialog, { ModalButton } from "../uikit/lib/modal";
import { TYPE_LINK, TYPE_MEDIA } from "../lib/constants";
import { iconForPath, visible } from "../lib/windowutils";
import { VirtualTable } from "../uikit/lib/table";
import { Icons } from "./icons";
import { $ } from "./winutil";
import { runtime } from "../lib/browser";
const ICON_BASE_SIZE = 16;
const $ = document.querySelector.bind(document);
class UIPref<T> extends PrefWatcher {
class UIPref<T extends HTMLElement> extends PrefWatcher {
id: string;
pref: string;
@ -101,27 +103,28 @@ class OptionPref extends UIPref<HTMLElement> {
}
class CreateFilterDialog extends ModalDialog {
label: any;
label: HTMLInputElement;
expr: any;
expr: HTMLInputElement;
link: any;
link: HTMLInputElement;
media: any;
media: HTMLInputElement;
get content() {
const rv = localize($("#create-filter-template").content.cloneNode(true));
this.label = rv.querySelector("#filter-create-label");
this.expr = rv.querySelector("#filter-create-expr");
this.link = rv.querySelector("#filter-create-type-link");
this.media = rv.querySelector("#filter-create-type-media");
getContent() {
const rv = $<HTMLTemplateElement>("#create-filter-template").
content.cloneNode(true) as DocumentFragment;
this.label = $("#filter-create-label", rv);
this.expr = $("#filter-create-expr", rv);
this.link = $("#filter-create-type-link", rv);
this.media = $("#filter-create-type-media", rv);
return rv;
}
get buttons() {
return [
{
title: "Create",
title: _("create-filter"),
value: "ok",
default: true
},
@ -137,7 +140,7 @@ class CreateFilterDialog extends ModalDialog {
this.label.focus();
}
done(b: any) {
done(b: ModalButton) {
if (!b || !b.default) {
return super.done(b);
}
@ -209,7 +212,7 @@ class FiltersUI extends VirtualTable {
ignoreNext: boolean;
constructor() {
super("#filters");
super("#filters", null);
this.filters = [];
this.icons = new Icons($("#icons"));
const filter: any = null;
@ -402,7 +405,7 @@ class LimitsUI extends VirtualTable {
};
constructor() {
super("#limits");
super("#limits", null);
this.limits = [];
Limits.on("changed", () => {
this.limits = Array.from(Limits);
@ -548,6 +551,7 @@ addEventListener("DOMContentLoaded", () => {
new BoolPref("pref-global-turbo", "global-turbo");
new BoolPref("pref-queue-notification", "queue-notification");
new BoolPref("pref-finish-notification", "finish-notification");
new BoolPref("pref-hide-context", "hide-context");
new BoolPref("pref-tooltip", "tooltip");
new BoolPref("pref-open-manager-on-queue", "open-manager-on-queue");
new BoolPref("pref-text-links", "text-links");
@ -580,7 +584,7 @@ addEventListener("DOMContentLoaded", () => {
await Prefs.reset(k);
}
await ModalDialog.inform(
_("information.title"), _("reset-confirmations.done"), _("ok"));
_("information.title"), _("reset-layouts.done"), _("ok"));
});
// Filters
@ -590,4 +594,40 @@ addEventListener("DOMContentLoaded", () => {
new IntPref("pref-concurrent-downloads", "concurrent");
visible("#limits").then(() => new LimitsUI());
const customLocale = $<HTMLInputElement>("#customLocale");
$<HTMLInputElement>("#loadCustomLocale").addEventListener("click", () => {
customLocale.click();
});
$<HTMLInputElement>("#clearCustomLocale").addEventListener("click", () => {
saveCustomLocale(undefined);
runtime.reload();
});
customLocale.addEventListener("change", async () => {
if (!customLocale.files || !customLocale.files.length) {
return;
}
const [file] = customLocale.files;
if (!file || file.size > (5 << 20)) {
return;
}
try {
const text = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = reject;
reader.readAsText(file);
});
saveCustomLocale(text);
if (confirm("Imported your file.\nWant to realod the extension now?")) {
runtime.reload();
}
}
catch (ex) {
console.error(ex);
alert(`Could not load your translation file:\n${ex.toString()}`);
}
});
});

View File

@ -44,7 +44,7 @@
</table>
</div>
<section id="filters" class="collapsable">
<h2>Filters</h2>
<h2 data-i18n="options-filters"></h2>
<div class="filters-container" id="linksFilters"></div>
<div class="filters-container" id="mediaFilters"></div>
</section>
@ -89,7 +89,7 @@
<li id="ctx-select-filtered" data-key="ACCEL-KeyF" data-i18n="select-checked">Select Checked</li>
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
<li>-</li>
<li id="ctx-open-selected" data-icon="icon-launch" data-key="ACCEL-KeyO">Open</li>
<li id="ctx-open-selected" data-icon="icon-launch" data-key="ACCEL-KeyO" data-i18n="open-link">Open</li>
</ul>
</template>
<template id="paused-template">

View File

@ -3,7 +3,7 @@
import { VirtualTable } from "../uikit/lib/table";
import ModalDialog from "../uikit/lib/modal";
import { ContextMenu } from "../uikit/lib/contextmenu";
import { ContextMenu } from "./contextmenu";
import { iconForPath } from "../lib/windowutils";
import { _, localize } from "../lib/i18n";
import { Prefs } from "../lib/prefs";
@ -15,10 +15,17 @@ import { Icons } from "./icons";
import { sort, naturalCaseCompare } from "../lib/sorting";
import { hookButton } from "../lib/manager/renamer";
import { CellTypes } from "../uikit/lib/constants";
import { runtime } from "../lib/browser";
// eslint-disable-next-line no-unused-vars
import { runtime, RawPort } from "../lib/browser";
import { $ } from "./winutil";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../lib/item";
// eslint-disable-next-line no-unused-vars
import { ItemDelta } from "../lib/select";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "../uikit/lib/config";
const PORT = runtime.connect(null, { name: "select" });
const $ = document.querySelector.bind(document);
const PORT: RawPort = runtime.connect(null, { name: "select" });
const TREE_CONFIG_VERSION = 1;
@ -37,16 +44,28 @@ let Mask: Dropdown;
let FastFilter: Dropdown;
type DELTAS = {deltaLinks: any[]; deltaMedia: any[]};
type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]};
function matched(item: any) {
interface BaseMatchedItem extends BaseItem {
matched?: string | null;
rowid: number;
}
function cleaErrors() {
const not = $("#notification");
not.textContent = "";
not.style.display = "none";
}
function matched(item: BaseMatchedItem) {
return item && item.matched && item.matched !== "unmanual";
}
class PausedModalDialog extends ModalDialog {
get content() {
const content = $("#paused-template").content.cloneNode(true);
localize(content);
getContent() {
const tmpl = $<HTMLTemplateElement>("#paused-template");
const content = tmpl.content.cloneNode(true) as DocumentFragment;
return content;
}
@ -78,8 +97,6 @@ class PausedModalDialog extends ModalDialog {
}
}
class CheckClasser extends Map<string, string> {
gen: IterableIterator<string>;
@ -106,38 +123,105 @@ class CheckClasser extends Map<string, string> {
}
}
type KeyFn = (item: BaseMatchedItem) => any;
class ItemCollection {
private items: BaseMatchedItem[];
private indexes: Map<number, BaseMatchedItem>;
constructor(items: BaseMatchedItem[]) {
this.items = items;
this.assignRows();
this.indexes = new Map(items.map(i => [i.idx, i]));
}
assignRows() {
this.items.forEach((item, idx) => item.rowid = idx);
}
get length() {
return this.items.length;
}
get checked() {
const rv: number[] = [];
this.items.forEach(function (item, idx) {
if (item.matched && item.matched !== "unmanual") {
rv.push(idx);
}
});
return rv;
}
get checkedIndexes() {
const rv: number[] = [];
this.items.forEach(function (item) {
if (item.matched && item.matched !== "unmanual") {
rv.push(item.idx);
}
});
return rv;
}
at(idx: number) {
return this.items[idx];
}
byIndex(idx: number) {
return this.indexes.get(idx);
}
sort(keyFn: KeyFn) {
sort(this.items, keyFn, naturalCaseCompare);
this.assignRows();
}
reverse() {
this.items.reverse();
this.assignRows();
}
filter(fn: (item: BaseMatchedItem, idx: number) => boolean) {
return this.items.filter(fn);
}
}
class SelectionTable extends VirtualTable {
checkClasser: CheckClasser;
icons: Icons;
links: any[];
links: ItemCollection;
media: any[];
media: ItemCollection;
items: ItemCollection;
type: string;
items: any[];
status: HTMLElement;
status: any;
linksTab: HTMLElement;
linksTab: any;
mediaTab: HTMLElement;
mediaTab: any;
linksFilters: HTMLElement;
linksFilters: any;
mediaFilters: any;
mediaFilters: HTMLElement;
contextMenu: ContextMenu;
sortcol: any;
sortcol: number | null;
sortasc: boolean;
keyfns: Map<string, (item: any) => any>;
keyfns: Map<string, KeyFn>;
constructor(treeConfig: any, type: string, links: any[], media: any[]) {
constructor(
treeConfig: TableConfig | null, type: string,
links: BaseMatchedItem[], media: BaseMatchedItem[]) {
if (type === "links" && !links.length) {
type = "media";
}
@ -147,11 +231,11 @@ class SelectionTable extends VirtualTable {
super("#items", treeConfig, TREE_CONFIG_VERSION);
this.checkClasser = new CheckClasser(NUM_FILTER_CLASSES);
this.icons = new Icons($("#icons"));
this.links = links;
this.media = media;
this.icons = new Icons($("#icons") as HTMLStyleElement);
this.links = new ItemCollection(links);
this.media = new ItemCollection(media);
this.type = type;
this.items = (this as any)[type];
this.items = type === "links" ? this.links : this.media;
this.status = $("#statusItems");
this.linksTab = $("#linksTab");
@ -174,13 +258,13 @@ class SelectionTable extends VirtualTable {
this.linksFilters = $("#linksFilters");
this.mediaFilters = $("#mediaFilters");
localize($("#table-context").content);
localize(($("#table-context") as HTMLTemplateElement).content);
this.contextMenu = new ContextMenu("#table-context");
Keys.adoptContext(this.contextMenu);
this.sortcol = null;
this.sortasc = true;
this.keyfns = new Map([
this.keyfns = new Map<string, KeyFn>([
["colDownload", item => item.usable],
["colTitle", item => [item.title, item.usable]],
["colDescription", item => [item.description, item.usable]],
@ -195,8 +279,8 @@ class SelectionTable extends VirtualTable {
if (!keyfn) {
return false;
}
sort(this.links, keyfn, naturalCaseCompare);
sort(this.media, keyfn, naturalCaseCompare);
this.links.sort(keyfn);
this.media.sort(keyfn);
const elem = document.querySelector<HTMLElement>(`#${colid}`);
const oldelem = (this.sortcol && document.querySelector<HTMLElement>(`#${this.sortcol}`));
if (this.sortcol === colid && this.sortasc) {
@ -258,19 +342,19 @@ class SelectionTable extends VirtualTable {
}
let oldmask = "";
for (const r of this.selection) {
const m = this.items[r].mask;
const m = this.items.at(r).mask;
if (oldmask && m !== oldmask) {
oldmask = "";
break;
}
oldmask = m;
oldmask = m || oldmask;
}
try {
Keys.suppressed = true;
const newmask = await ModalDialog.prompt(
"Renaming mask", "Set new renaming mask", oldmask);
for (const r of this.selection) {
this.items[r].mask = newmask;
this.items.at(r).mask = newmask;
this.invalidateRow(r);
}
}
@ -295,16 +379,6 @@ class SelectionTable extends VirtualTable {
this.switchTab(type);
}
get checkedIndexes() {
const rv: number[] = [];
this.items.forEach(function (item, idx) {
if (item.matched && item.matched !== "unmanual") {
rv.push(idx);
}
});
return rv;
}
get rowCount() {
return this.items.length;
}
@ -314,7 +388,7 @@ class SelectionTable extends VirtualTable {
return false;
}
for (const rowid of this.selection) {
const item = this.items[rowid];
const item = this.items.at(rowid);
if (!state) {
state = matched(item) ? "unmanual" : "manual";
}
@ -340,7 +414,7 @@ class SelectionTable extends VirtualTable {
selectChecked() {
this.selection.clear();
let min = null;
for (const ci of this.checkedIndexes) {
for (const ci of this.items.checked) {
this.selection.add(ci);
min = min === null ? ci : Math.min(min, ci);
}
@ -359,7 +433,7 @@ class SelectionTable extends VirtualTable {
if (this.focusRow < 0) {
return;
}
items.push(this.items[this.focusRow]);
items.push(this.items.at(this.focusRow));
}
PORT.postMessage({
msg: "openUrls",
@ -367,14 +441,14 @@ class SelectionTable extends VirtualTable {
});
}
applyDeltaTo(delta: any[], items: any[]) {
applyDeltaTo(delta: ItemDelta[], items: ItemCollection) {
const active = items === this.items;
for (const d of delta) {
const {idx = -1, matched = null} = d;
if (idx < 0) {
continue;
}
const item = items[idx];
const item = items.byIndex(idx);
if (!item) {
continue;
}
@ -388,7 +462,7 @@ class SelectionTable extends VirtualTable {
}
item.matched = matched;
if (active) {
this.invalidateRow(idx);
this.invalidateRow(item.rowid);
}
}
}
@ -413,25 +487,26 @@ class SelectionTable extends VirtualTable {
}
updateStatus() {
const selected = this.checkedIndexes.length;
const selected = this.items.checked.length;
if (!selected) {
this.status.textContent = _("noitems.label");
}
else {
this.status.textContent = _("numitems.label", [selected]);
}
cleaErrors();
}
getRowClasses(rowid: number) {
const item = this.items[rowid];
if (!item || !matched(item)) {
const item = this.items.at(rowid);
if (!item || !matched(item) || !item.matched) {
return null;
}
return ["filtered", this.checkClasser.get(item.matched)];
}
getCellIcon(rowid: number, colid: number) {
const item = this.items[rowid];
const item = this.items.at(rowid);
if (item && colid === COL_DOWNLOAD) {
return this.icons.get(iconForPath(item.url, ICON_BASE_SIZE));
}
@ -448,7 +523,7 @@ class SelectionTable extends VirtualTable {
}
getDownloadText(idx: number) {
const item = this.items[idx];
const item = this.items.at(idx);
if (!item) {
return "";
}
@ -459,7 +534,7 @@ class SelectionTable extends VirtualTable {
}
getText(prop: string, idx: number) {
const item = this.items[idx];
const item: any = this.items.at(idx);
if (!item || !(prop in item) || !item[prop]) {
return "";
}
@ -467,7 +542,7 @@ class SelectionTable extends VirtualTable {
}
getMaskText(idx: number) {
const item = this.items[idx];
const item = this.items.at(idx);
if (item) {
return item.mask;
}
@ -498,13 +573,13 @@ class SelectionTable extends VirtualTable {
getCellCheck(rowid: number, colid: number) {
if (colid === COL_CHECK) {
return matched(this.items[rowid]);
return !!matched(this.items.at(rowid));
}
return false;
}
setCellCheck(rowid: number, colid: number, value: boolean) {
this.items[rowid].matched = value ? "manual" : "unmanual";
this.items.at(rowid).matched = value ? "manual" : "unmanual";
this.invalidateRow(rowid);
this.updateStatus();
}
@ -516,7 +591,7 @@ async function download(paused = false) {
if (!mask) {
throw new Error("error.invalidMask");
}
const items = Table.checkedIndexes;
const items = Table.items.checkedIndexes;
if (!items.length) {
throw new Error("error.noItemsSelected");
}
@ -541,13 +616,15 @@ async function download(paused = false) {
}
PORT.postMessage({
msg: "queue",
type: Table.type,
items,
paused,
mask,
maskOnce: $("#maskOnceCheck").checked,
fast: FastFilter.value,
fastOnce: $("#fastOnceCheck").checked,
options: {
type: Table.type,
paused,
mask,
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
fast: FastFilter.value,
fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
}
});
}
catch (ex) {
@ -559,17 +636,17 @@ async function download(paused = false) {
}
class Filter {
active: any;
container: any;
elem: HTMLLabelElement;
label: any;
active: boolean;
checkElem: HTMLInputElement;
id: any;
container: HTMLElement;
elem: HTMLLabelElement;
id: string;
label: string;
constructor(container: HTMLElement, raw: any, active = false) {
Object.assign(this, raw);
@ -606,10 +683,14 @@ function setFiltersInternal(
}
function setFilters(filters: any) {
const {linkFilters = [], mediaFilters = [], activeFilters = []} = filters;
const {
linkFilterDescs = [],
mediaFilterDescs = [],
activeFilters = []
} = filters;
const active: Set<string> = new Set(activeFilters);
setFiltersInternal("#linksFilters", linkFilters, active);
setFiltersInternal("#mediaFilters", mediaFilters, active);
setFiltersInternal("#linksFilters", linkFilterDescs, active);
setFiltersInternal("#mediaFilters", mediaFilterDescs, active);
}
function cancel() {
@ -620,6 +701,7 @@ function cancel() {
async function init() {
await Promise.all([MASK.init(), FASTFILTER.init()]);
Mask = new Dropdown("#mask", MASK.values);
Mask.on("changed", cleaErrors);
FastFilter = new Dropdown("#fast", FASTFILTER.values);
FastFilter.on("changed", () => {
PORT.postMessage({
@ -661,7 +743,7 @@ addEventListener("DOMContentLoaded", function dom() {
$("#fastDisableOthers").addEventListener("change", () => {
PORT.postMessage({
msg: "onlyfast",
fast: $("#fastDisableOthers").checked
fast: $<HTMLInputElement>("#fastDisableOthers").checked
});
});

View File

@ -28,17 +28,17 @@
<p class="example">../mygallery[0:9]/photo[01:10][03:1:-1].jpg</p>
</section>
<section id="options">
<h2>Download</h2>
<h2 data-i18n="download">Download</h2>
<input type="text" id="URL">
<h2>Custom Filename</h2>
<h2 data-i18n="custom-filename">Custom Filename</h2>
<input type="text" id="filename">
<h3>Referring page</h3>
<h3 data-i18n="referrer">Referring page</h3>
<input type="text" id="referrer">
<h3>Title</h3>
<h3 data-i18n="title">Title</h3>
<input type="text" id="title">
<h3>Description</h3>
<h3 data-i18n="description">Description</h3>
<input type="text" id="description">
<h3>Mask</h3>
<h3 data-i18n="mask">Mask</h3>
<div id="maskOptions">
<input type="text" id="mask">
<button id="maskButton" class="icon-tags"></button>

View File

@ -4,7 +4,8 @@
import ModalDialog from "../uikit/lib/modal";
import { _, localize } from "../lib/i18n";
import { Item } from "../lib/item";
// eslint-disable-next-line no-unused-vars
import { Item, BaseItem } from "../lib/item";
import { MASK } from "../lib/recentlist";
import { BatchGenerator } from "../lib/batches";
import { WindowState } from "./windowstate";
@ -12,11 +13,11 @@ import { Dropdown } from "./dropdown";
import { Keys } from "./keys";
import { hookButton } from "../lib/manager/renamer";
import { runtime } from "../lib/browser";
import { $ } from "./winutil";
const PORT = runtime.connect(null, { name: "single" });
const $ = document.querySelector.bind(document);
let ITEM: any;
let ITEM: BaseItem;
let Mask: Dropdown;
class BatchModalDialog extends ModalDialog {
@ -27,12 +28,11 @@ class BatchModalDialog extends ModalDialog {
this.gen = gen;
}
get content() {
const content = $("#batch-template").content.cloneNode(true);
localize(content);
const $$ = content.querySelector.bind(content);
$$(".batch-items").textContent = this.gen.length.toLocaleString();
$$(".batch-preview").textContent = this.gen.preview;
getContent() {
const tmpl = $("#batch-template") as HTMLTemplateElement;
const content = tmpl.content.cloneNode(true) as DocumentFragment;
$(".batch-items", content).textContent = this.gen.length.toLocaleString();
$(".batch-preview", content).textContent = this.gen.preview;
return content;
}
@ -60,7 +60,7 @@ class BatchModalDialog extends ModalDialog {
}
}
function setItem(item: any) {
function setItem(item: BaseItem) {
if (!item) {
return;
}
@ -73,11 +73,11 @@ function setItem(item: any) {
usableReferrer = "",
mask = ""
} = item;
$("#URL").value = usable;
$("#filename").value = fileName;
$("#title").value = title;
$("#description").value = description;
$("#referrer").value = usableReferrer;
$<HTMLInputElement>("#URL").value = usable;
$<HTMLInputElement>("#filename").value = fileName;
$<HTMLInputElement>("#title").value = title;
$<HTMLInputElement>("#description").value = description;
$<HTMLInputElement>("#referrer").value = usableReferrer;
if (mask) {
Mask.value = mask;
}
@ -90,7 +90,7 @@ function displayError(err: string) {
}
async function downloadInternal(paused: boolean) {
let usable = $("#URL").value.trim();
let usable = $<HTMLInputElement>("#URL").value.trim();
let url;
try {
url = new URL(usable).toString();
@ -98,7 +98,7 @@ async function downloadInternal(paused: boolean) {
catch (ex) {
try {
url = new URL(`https://${usable}`).toString();
$("#URL").value = usable = `https://${usable}`;
$<HTMLInputElement>("#URL").value = usable = `https://${usable}`;
}
catch (ex) {
return displayError("error.invalidURL");
@ -107,7 +107,7 @@ async function downloadInternal(paused: boolean) {
const gen = new BatchGenerator(usable);
const usableReferrer = $("#referrer").value.trim();
const usableReferrer = $<HTMLInputElement>("#referrer").value.trim();
let referrer;
try {
referrer = usableReferrer ? new URL(usableReferrer).toString() : "";
@ -116,9 +116,9 @@ async function downloadInternal(paused: boolean) {
return displayError("error.invalidReferrer");
}
const fileName = $("#filename").value.trim();
const title = $("#title").value.trim();
const description = $("#description").value.trim();
const fileName = $<HTMLInputElement>("#filename").value.trim();
const title = $<HTMLInputElement>("#title").value.trim();
const description = $<HTMLInputElement>("#description").value.trim();
const mask = Mask.value.trim();
if (!mask) {
return displayError("error.invalidMask");
@ -180,10 +180,12 @@ async function downloadInternal(paused: boolean) {
PORT.postMessage({
msg: "queue",
paused,
items,
mask,
maskOnce: $("#maskOnceCheck").checked,
options: {
paused,
mask,
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
}
});
return null;
}
@ -198,15 +200,35 @@ function cancel() {
}
async function init() {
await localize(document.documentElement);
await Promise.all([MASK.init()]);
Mask = new Dropdown("#mask", MASK.values);
}
addEventListener("DOMContentLoaded", function dom() {
addEventListener("DOMContentLoaded", async function dom() {
removeEventListener("DOMContentLoaded", dom);
init().catch(console.error);
localize(document.documentElement);
const inited = init();
PORT.onMessage.addListener(async (msg: any) => {
try {
switch (msg.msg) {
case "item": {
await inited;
setItem(msg.data.item);
return;
}
default:
throw Error("Unhandled message");
}
}
catch (ex) {
console.error("Failed to process message", msg, ex);
}
});
await inited;
$("#btnDownload").addEventListener("click", () => download(false));
$("#btnPaused").addEventListener("click", () => download(true));
$("#btnCancel").addEventListener(
@ -225,27 +247,10 @@ addEventListener("DOMContentLoaded", function dom() {
return true;
});
PORT.onMessage.addListener((msg: any) => {
try {
switch (msg.msg) {
case "item": {
setItem(msg.data);
return;
}
default:
throw Error("Unhandled message");
}
}
catch (ex) {
console.error("Failed to process message", msg, ex);
}
});
hookButton($("#maskButton"));
});
addEventListener("load", function() {
addEventListener("load", function () {
$("#URL").focus();
});
@ -259,7 +264,7 @@ addEventListener("contextmenu", event => {
return false;
});
addEventListener("beforeunload", function() {
addEventListener("beforeunload", function () {
PORT.disconnect();
});

View File

@ -1,10 +1,14 @@
"use strict";
// eslint-disable-next-line no-unused-vars
import { RawPort } from "../lib/browser";
// License: MIT
export class WindowState {
private readonly port: any;
private readonly port: RawPort;
constructor(port: any) {
constructor(port: RawPort) {
this.port = port;
this.update = this.update.bind(this);
addEventListener("resize", this.update);

10
windows/winutil.ts Normal file
View File

@ -0,0 +1,10 @@
"use strict";
// License: MIT
export function $<T extends HTMLElement>(
q: string, el?: HTMLElement | DocumentFragment): T {
if (!el) {
el = document;
}
return el.querySelector<T>(q) as T;
}

819
yarn.lock

File diff suppressed because it is too large Load Diff