Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
d0f6c4f7f3 | |||
5b05d52886 | |||
07a3ec3a7b | |||
0f15a4a068 | |||
c1e5f63935 | |||
6e4a338789 | |||
37a97b73b3 | |||
fe61f6176c | |||
48bde9cfdb | |||
256c091f15 | |||
b4ce2d1d75 | |||
29fd59c8fd | |||
544b7d522c | |||
116d5b9b00 | |||
976c57c043 | |||
d00b25cbe7 | |||
572bab27a0 | |||
8235af22db | |||
2bfb3d5363 | |||
7dc4dd9da6 | |||
c7c111e1c0 | |||
a9a811d96b | |||
801eaa819b | |||
d6539c5f96 | |||
a89acad0c9 | |||
70c7e0b0f3 | |||
af8b05d20c | |||
d98c4e318a | |||
176060183e | |||
26cd8e8a00 | |||
a9df98c2f6 | |||
ab2b6e40af | |||
112d37deb0 | |||
33bde621f9 | |||
958d58a408 | |||
229b5eb968 | |||
2f282d3a4b | |||
a9f83071dc | |||
7bfffd7598 | |||
545d78ad61 | |||
0463471704 | |||
34ea21b3ea | |||
d3c9f8bc89 | |||
4236195ccf | |||
10ff6f1c11 |
@ -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.**
|
**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
|
Development
|
||||||
---
|
---
|
||||||
|
3
TODO.md
@ -8,7 +8,6 @@ P2
|
|||||||
|
|
||||||
Planned for later.
|
Planned for later.
|
||||||
|
|
||||||
* Investigate using an action popup for the browser action
|
|
||||||
* Soft errors and retry logic
|
* 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.
|
* 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)
|
* 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
|
* Not supported by Firefox
|
||||||
* Speed limiter
|
* Speed limiter
|
||||||
* Cannot be done with the WebExtensions downloads API
|
* Cannot be done with the WebExtensions downloads API
|
||||||
* Actually send referrers for downloads
|
|
||||||
* Cannot be done with WebExtensions - webRequest does not see Downloads
|
|
||||||
* contenthandling aka video sniffing, request manipulation?
|
* contenthandling aka video sniffing, request manipulation?
|
||||||
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
|
* PITA and/or infeasible - Essentially cannot be done for a large part and the other prt is extemely inefficient
|
||||||
* Checksums/Hashes?
|
* Checksums/Hashes?
|
||||||
|
20
_locales/Readme.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Translations
|
||||||
|
|
||||||
|
Right now we did not standardize on a tool to translate, so feel free to whip our your favorite text edits, JSON editor, special translation tool, what have you.
|
||||||
|
|
||||||
|
To make a translation of DownThemAll! in your language, please:
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
* Do not translate anything other.
|
||||||
|
* 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.
|
||||||
|
* Once you are at a point you want to test things:
|
||||||
|
* 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).
|
||||||
|
* 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.
|
@ -1,4 +1,32 @@
|
|||||||
{
|
{
|
||||||
|
"CRASH": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Interal Browser Error"
|
||||||
|
},
|
||||||
|
"FILE_FAILED": {
|
||||||
|
"description": "",
|
||||||
|
"message": "File Access Error"
|
||||||
|
},
|
||||||
|
"NETWORK_FAILED": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Network Failure"
|
||||||
|
},
|
||||||
|
"SERVER_BAD_CONTENT": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Not Found"
|
||||||
|
},
|
||||||
|
"SERVER_FAILED": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Server Error"
|
||||||
|
},
|
||||||
|
"SERVER_FORBIDDEN": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Forbidden"
|
||||||
|
},
|
||||||
|
"SERVER_UNAUTHORIZED": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Unauthorized"
|
||||||
|
},
|
||||||
"add-download": {
|
"add-download": {
|
||||||
"description": "Action for adding a download",
|
"description": "Action for adding a download",
|
||||||
"message": "Add Download"
|
"message": "Add Download"
|
||||||
@ -183,6 +211,14 @@
|
|||||||
"description": "Download (verb/action)",
|
"description": "Download (verb/action)",
|
||||||
"message": "Download"
|
"message": "Download"
|
||||||
},
|
},
|
||||||
|
"dta-regular-all": {
|
||||||
|
"description": "",
|
||||||
|
"message": "DownThemAll! - All Tabs"
|
||||||
|
},
|
||||||
|
"dta-turbo-all": {
|
||||||
|
"description": "",
|
||||||
|
"message": "OneClick! - All Tabs"
|
||||||
|
},
|
||||||
"dta.regular": {
|
"dta.regular": {
|
||||||
"description": "Regular dta action",
|
"description": "Regular dta action",
|
||||||
"message": "DownThemAll!"
|
"message": "DownThemAll!"
|
||||||
@ -204,8 +240,8 @@
|
|||||||
"message": "Save Selection with DownThemAll!"
|
"message": "Save Selection with DownThemAll!"
|
||||||
},
|
},
|
||||||
"dta.turbo": {
|
"dta.turbo": {
|
||||||
"description": "Turbo dta action (please keep the case of dTa!)",
|
"description": "",
|
||||||
"message": "dTa! One Click"
|
"message": "OneClick!"
|
||||||
},
|
},
|
||||||
"dta.turbo.image": {
|
"dta.turbo.image": {
|
||||||
"description": "",
|
"description": "",
|
||||||
@ -299,6 +335,14 @@
|
|||||||
"description": "Label",
|
"description": "Label",
|
||||||
"message": "Invert selection"
|
"message": "Invert selection"
|
||||||
},
|
},
|
||||||
|
"language": {
|
||||||
|
"description": "lanuage name",
|
||||||
|
"message": "English (US)"
|
||||||
|
},
|
||||||
|
"language_code": {
|
||||||
|
"description": "language code",
|
||||||
|
"message": "en"
|
||||||
|
},
|
||||||
"limited-to": {
|
"limited-to": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"message": "Limited to"
|
"message": "Limited to"
|
||||||
@ -449,6 +493,10 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"message": "Browser button should be OneClick!"
|
"message": "Browser button should be OneClick!"
|
||||||
},
|
},
|
||||||
|
"pref-hide-context": {
|
||||||
|
"description": "",
|
||||||
|
"message": "Do not show general context menu items"
|
||||||
|
},
|
||||||
"pref-manager-tooltip": {
|
"pref-manager-tooltip": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"message": "Show tooltips in Manager tabs"
|
"message": "Show tooltips in Manager tabs"
|
||||||
@ -489,6 +537,10 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"message": "User Interface"
|
"message": "User Interface"
|
||||||
},
|
},
|
||||||
|
"prefs.conflicts": {
|
||||||
|
"description": "",
|
||||||
|
"message": "When a file exists"
|
||||||
|
},
|
||||||
"prefs.short": {
|
"prefs.short": {
|
||||||
"description": "Preferences",
|
"description": "Preferences",
|
||||||
"message": "Preferences"
|
"message": "Preferences"
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"open-manager-on-queue": true,
|
"open-manager-on-queue": true,
|
||||||
"text-links": true,
|
"text-links": true,
|
||||||
"add-paused": false,
|
"add-paused": false,
|
||||||
|
"hide-context": false,
|
||||||
"conflict-action": "uniquify",
|
"conflict-action": "uniquify",
|
||||||
"nagging": 0,
|
"nagging": 0,
|
||||||
"nagging-next": 6,
|
"nagging-next": 6,
|
||||||
|
69
lib/api.ts
@ -5,7 +5,8 @@ import { TYPE_LINK, TYPE_MEDIA } from "./constants";
|
|||||||
import { filters } from "./filters";
|
import { filters } from "./filters";
|
||||||
import { Prefs } from "./prefs";
|
import { Prefs } from "./prefs";
|
||||||
import { lazy } from "./util";
|
import { lazy } from "./util";
|
||||||
import { Item, makeUniqueItems } from "./item";
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Item, makeUniqueItems, BaseItem } from "./item";
|
||||||
import { getManager } from "./manager/man";
|
import { getManager } from "./manager/man";
|
||||||
import { select } from "./select";
|
import { select } from "./select";
|
||||||
import { single } from "./single";
|
import { single } from "./single";
|
||||||
@ -16,12 +17,17 @@ import { _ } from "./i18n";
|
|||||||
|
|
||||||
const MAX_BATCH = 10000;
|
const MAX_BATCH = 10000;
|
||||||
|
|
||||||
export const API = new class {
|
export interface QueueOptions {
|
||||||
async filter(arr: any, type: number) {
|
mask?: string;
|
||||||
return (await filters()).filterItemsByType(arr, type);
|
paused?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API = new class APIImpl {
|
||||||
|
async filter(arr: BaseItem[], type: number) {
|
||||||
|
return await (await filters()).filterItemsByType(arr, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queue(items: any, options: any) {
|
async queue(items: BaseItem[], options: QueueOptions) {
|
||||||
await MASK.init();
|
await MASK.init();
|
||||||
const {mask = MASK.current} = options;
|
const {mask = MASK.current} = options;
|
||||||
|
|
||||||
@ -36,12 +42,9 @@ export const API = new class {
|
|||||||
fileName: null,
|
fileName: null,
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
fromMetalink: false,
|
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
hashes: [],
|
|
||||||
private: false,
|
private: false,
|
||||||
postData: null,
|
postData: null,
|
||||||
cleanRequest: false,
|
|
||||||
mask,
|
mask,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
paused
|
paused
|
||||||
@ -54,7 +57,7 @@ export const API = new class {
|
|||||||
}
|
}
|
||||||
return currentBatch;
|
return currentBatch;
|
||||||
});
|
});
|
||||||
items = items.map((i: any) => {
|
items = items.map(i => {
|
||||||
delete i.idx;
|
delete i.idx;
|
||||||
return new Item(i, defaults);
|
return new Item(i, defaults);
|
||||||
});
|
});
|
||||||
@ -79,7 +82,7 @@ export const API = new class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sanity(links: any[], media: any[]) {
|
sanity(links: BaseItem[], media: BaseItem[]) {
|
||||||
if (!links.length && !media.length) {
|
if (!links.length && !media.length) {
|
||||||
new Notification(null, _("no-links"));
|
new Notification(null, _("no-links"));
|
||||||
return false;
|
return false;
|
||||||
@ -87,48 +90,54 @@ export const API = new class {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async turbo(links: any[], media: any[]) {
|
async turbo(links: BaseItem[], media: BaseItem[]) {
|
||||||
if (!this.sanity(links, media)) {
|
if (!this.sanity(links, media)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const selected = makeUniqueItems([
|
const type = await Prefs.get("last-type", "links");
|
||||||
await API.filter(links, TYPE_LINK),
|
const items = await (async () => {
|
||||||
await API.filter(media, TYPE_MEDIA),
|
if (type === "links") {
|
||||||
]);
|
return await API.filter(links, TYPE_LINK);
|
||||||
|
}
|
||||||
|
return await API.filter(media, TYPE_MEDIA);
|
||||||
|
})();
|
||||||
|
const selected = makeUniqueItems([items]);
|
||||||
if (!selected.length) {
|
if (!selected.length) {
|
||||||
return await this.regular(links, media);
|
return await this.regular(links, media);
|
||||||
}
|
}
|
||||||
return await this.queue(selected, {paused: await Prefs.get("add-paused")});
|
return await this.queue(selected, {paused: await Prefs.get("add-paused")});
|
||||||
}
|
}
|
||||||
|
|
||||||
async regularInternal(selected: any) {
|
async regularInternal(selected: BaseItem[], options: any) {
|
||||||
if (selected.mask && !selected.maskOnce) {
|
if (options.mask && !options.maskOnce) {
|
||||||
await MASK.init();
|
await MASK.init();
|
||||||
await MASK.push(selected.mask);
|
await MASK.push(options.mask);
|
||||||
}
|
}
|
||||||
if (typeof selected.fast === "string" && !selected.fastOnce) {
|
if (typeof options.fast === "string" && !options.fastOnce) {
|
||||||
await FASTFILTER.init();
|
await FASTFILTER.init();
|
||||||
await FASTFILTER.push(selected.fast);
|
await FASTFILTER.push(options.fast);
|
||||||
}
|
}
|
||||||
const {items} = selected;
|
if (typeof options.type === "string") {
|
||||||
delete selected.items;
|
await Prefs.set("last-type", options.type);
|
||||||
return await this.queue(items, selected);
|
}
|
||||||
|
return await this.queue(selected, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async regular(links: any[], media: any[]) {
|
async regular(links: BaseItem[], media: BaseItem[]) {
|
||||||
if (!this.sanity(links, media)) {
|
if (!this.sanity(links, media)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const selected = await select(links, media);
|
const {items, options} = await select(links, media);
|
||||||
return this.regularInternal(selected);
|
console.log(items, options);
|
||||||
|
return this.regularInternal(items, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async singleTurbo(item: any) {
|
async singleTurbo(item: BaseItem) {
|
||||||
return await this.queue([item], {paused: await Prefs.get("add-paused")});
|
return await this.queue([item], {paused: await Prefs.get("add-paused")});
|
||||||
}
|
}
|
||||||
|
|
||||||
async singleRegular(item: any) {
|
async singleRegular(item: BaseItem | null) {
|
||||||
const selected = await single(item);
|
const {items, options} = await single(item);
|
||||||
return this.regularInternal(selected);
|
return this.regularInternal(items, options);
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
@ -5,7 +5,7 @@ import { ALLOWED_SCHEMES, TRANSFERABLE_PROPERTIES } from "./constants";
|
|||||||
import { API } from "./api";
|
import { API } from "./api";
|
||||||
import { Finisher, makeUniqueItems } from "./item";
|
import { Finisher, makeUniqueItems } from "./item";
|
||||||
import { Prefs } from "./prefs";
|
import { Prefs } from "./prefs";
|
||||||
import { _ } from "./i18n";
|
import { _, locale } from "./i18n";
|
||||||
import { openPrefs, openManager } from "./windowutils";
|
import { openPrefs, openManager } from "./windowutils";
|
||||||
import { filters } from "./filters";
|
import { filters } from "./filters";
|
||||||
import { getManager } from "./manager/man";
|
import { getManager } from "./manager/man";
|
||||||
@ -13,13 +13,22 @@ import {
|
|||||||
browserAction as action,
|
browserAction as action,
|
||||||
menus as _menus, contextMenus as _cmenus,
|
menus as _menus, contextMenus as _cmenus,
|
||||||
tabs,
|
tabs,
|
||||||
webNavigation as nav
|
webNavigation as nav,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Tab,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
MenuClickInfo
|
||||||
} from "./browser";
|
} from "./browser";
|
||||||
|
import { Bus } from "./bus";
|
||||||
|
import { filterInSitu } from "./util";
|
||||||
|
|
||||||
|
|
||||||
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
|
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
|
||||||
|
|
||||||
|
const GATHER = "/bundles/content-gather.js";
|
||||||
|
|
||||||
async function runContentJob(tab: any, file: string, msg: any) {
|
|
||||||
|
async function runContentJob(tab: Tab, file: string, msg: any) {
|
||||||
try {
|
try {
|
||||||
const res = await tabs.executeScript(tab.id, {
|
const res = await tabs.executeScript(tab.id, {
|
||||||
file,
|
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 {
|
class Handler {
|
||||||
async processResults(turbo = false, results: any[]) {
|
async processResults(turbo = false, results: any[]) {
|
||||||
const links = this.makeUnique(results, "links");
|
const links = this.makeUnique(results, "links");
|
||||||
@ -59,14 +76,42 @@ class Handler {
|
|||||||
return makeUniqueItems(
|
return makeUniqueItems(
|
||||||
results.filter(e => e[what]).map(e => {
|
results.filter(e => e[what]).map(e => {
|
||||||
const finisher = new Finisher(e);
|
const finisher = new Finisher(e);
|
||||||
return e[what].
|
return filterInSitu(e[what].
|
||||||
map((item: any) => finisher.finish(item)).
|
map((item: any) => finisher.finish(item)), e => !!e);
|
||||||
filter((i: any) => i);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async performSelection(options: SelectionOptions) {
|
||||||
|
try {
|
||||||
|
const 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 {
|
locale.then(() => {
|
||||||
|
new class Action extends Handler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onClicked = this.onClicked.bind(this);
|
this.onClicked = this.onClicked.bind(this);
|
||||||
@ -79,7 +124,7 @@ new class Action extends Handler {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.processResults(
|
await this.processResults(
|
||||||
await Prefs.get("global-turbo"),
|
true,
|
||||||
await runContentJob(
|
await runContentJob(
|
||||||
tab, "/bundles/content-gather.js", {
|
tab, "/bundles/content-gather.js", {
|
||||||
type: "DTA:gather",
|
type: "DTA:gather",
|
||||||
@ -93,31 +138,20 @@ new class Action extends Handler {
|
|||||||
console.error(ex);
|
console.error(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
new class Menus extends Handler {
|
const menuHandler = new class Menus extends Handler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onClicked = this.onClicked.bind(this);
|
this.onClicked = this.onClicked.bind(this);
|
||||||
menus.create({
|
const alls = new Map<string, string[]>();
|
||||||
id: "DTARegular",
|
const mcreate = (options: any) => {
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
if (options.contexts.includes("all")) {
|
||||||
icons: {
|
alls.set(options.id, options.contexts);
|
||||||
16: "/style/button-regular.png",
|
}
|
||||||
32: "/style/button-regular@2x.png",
|
return menus.create(options);
|
||||||
},
|
};
|
||||||
title: _("dta.regular"),
|
mcreate({
|
||||||
});
|
|
||||||
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",
|
id: "DTARegularLink",
|
||||||
contexts: ["link"],
|
contexts: ["link"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -126,7 +160,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.link"),
|
title: _("dta.regular.link"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboLink",
|
id: "DTATurboLink",
|
||||||
contexts: ["link"],
|
contexts: ["link"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -135,7 +169,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.link"),
|
title: _("dta.turbo.link"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTARegularImage",
|
id: "DTARegularImage",
|
||||||
contexts: ["image"],
|
contexts: ["image"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -144,7 +178,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.image"),
|
title: _("dta.regular.image"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboImage",
|
id: "DTATurboImage",
|
||||||
contexts: ["image"],
|
contexts: ["image"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -153,7 +187,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.image"),
|
title: _("dta.turbo.image"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTARegularMedia",
|
id: "DTARegularMedia",
|
||||||
contexts: ["video", "audio"],
|
contexts: ["video", "audio"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -162,7 +196,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.media"),
|
title: _("dta.regular.media"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboMedia",
|
id: "DTATurboMedia",
|
||||||
contexts: ["video", "audio"],
|
contexts: ["video", "audio"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -171,7 +205,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.media"),
|
title: _("dta.turbo.media"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTARegularSelection",
|
id: "DTARegularSelection",
|
||||||
contexts: ["selection"],
|
contexts: ["selection"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -180,7 +214,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.regular.selection"),
|
title: _("dta.regular.selection"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTATurboSelection",
|
id: "DTATurboSelection",
|
||||||
contexts: ["selection"],
|
contexts: ["selection"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -189,12 +223,72 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("dta.turbo.selection"),
|
title: _("dta.turbo.selection"),
|
||||||
});
|
});
|
||||||
menus.create({
|
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",
|
id: "sep-1",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
menus.create({
|
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",
|
id: "DTAManager",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -203,7 +297,7 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("manager.short"),
|
title: _("manager.short"),
|
||||||
});
|
});
|
||||||
menus.create({
|
mcreate({
|
||||||
id: "DTAPrefs",
|
id: "DTAPrefs",
|
||||||
contexts: ["all", "browser_action", "tools_menu"],
|
contexts: ["all", "browser_action", "tools_menu"],
|
||||||
icons: {
|
icons: {
|
||||||
@ -214,6 +308,29 @@ new class Menus extends Handler {
|
|||||||
},
|
},
|
||||||
title: _("prefs.short"),
|
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);
|
menus.onClicked.addListener(this.onClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +352,7 @@ new class Menus extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findSingleItem(tab: any, url: string, turbo = false) {
|
async findSingleItem(tab: Tab, url: string, turbo = false) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -255,7 +372,7 @@ new class Menus extends Handler {
|
|||||||
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked(info: any, tab: any) {
|
onClicked(info: MenuClickInfo, tab: Tab) {
|
||||||
if (!tab.id) {
|
if (!tab.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -265,84 +382,120 @@ new class Menus extends Handler {
|
|||||||
console.error("Invalid Handler for", menuItemId);
|
console.error("Invalid Handler for", menuItemId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handler.call(this, info, tab).catch(console.error);
|
const rv: Promise<void> | void = handler.call(this, info, tab);
|
||||||
}
|
if (rv && rv.catch) {
|
||||||
|
rv.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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegular(info: any, tab: any) {
|
async enumulate(action: string) {
|
||||||
return await this.onClickedDTARegularInternal(false, info, tab);
|
const tab = await tabs.query({active: true});
|
||||||
|
if (!tab || !tab.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onClicked({
|
||||||
|
menuItemId: action
|
||||||
|
}, tab[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularSelection(info: any, tab: any) {
|
async onClickedDTARegular(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.onClickedDTARegularInternal(true, info, tab);
|
return await this.performSelection({
|
||||||
|
selectionOnly: false,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: false,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboInternal(selectionOnly: boolean, info: any, tab: any) {
|
async onClickedDTARegularAll(info: MenuClickInfo, tab: Tab) {
|
||||||
try {
|
return await this.performSelection({
|
||||||
await this.processResults(
|
selectionOnly: false,
|
||||||
true,
|
allTabs: true,
|
||||||
await runContentJob(
|
turbo: false,
|
||||||
tab, "/bundles/content-gather.js", {
|
tab,
|
||||||
type: "DTA:gather",
|
});
|
||||||
selectionOnly,
|
|
||||||
textLinks: await Prefs.get("text-links", true),
|
|
||||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
|
||||||
transferable: TRANSFERABLE_PROPERTIES,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurbo(info: any, tab: any) {
|
async onClickedDTARegularSelection(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.onClickedDTATurboInternal(false, info, tab);
|
return await this.performSelection({
|
||||||
|
selectionOnly: true,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: false,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboSelection(info: any, tab: any) {
|
async onClickedDTATurbo(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.onClickedDTATurboInternal(false, info, tab);
|
return await this.performSelection({
|
||||||
|
selectionOnly: false,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularLink(info: any, tab: any) {
|
async onClickedDTATurboAll(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.linkUrl, false);
|
return await this.performSelection({
|
||||||
|
selectionOnly: false,
|
||||||
|
allTabs: true,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboLink(info: any, tab: any) {
|
async onClickedDTATurboSelection(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.linkUrl, true);
|
return await this.performSelection({
|
||||||
|
selectionOnly: true,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularImage(info: any, tab: any) {
|
async onClickedDTARegularLink(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
if (!info.linkUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.linkUrl, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboImage(info: any, tab: any) {
|
async onClickedDTATurboLink(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
if (!info.linkUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.linkUrl, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularMedia(info: any, tab: any) {
|
async onClickedDTARegularImage(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
if (!info.srcUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.srcUrl, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTATurboMedia(info: any, tab: any) {
|
async onClickedDTATurboImage(info: MenuClickInfo, tab: Tab) {
|
||||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
if (!info.srcUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.findSingleItem(tab, info.srcUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTARegularMedia(info: 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() {
|
async onClickedDTAManager() {
|
||||||
@ -352,12 +505,37 @@ new class Menus extends Handler {
|
|||||||
async onClickedDTAPrefs() {
|
async onClickedDTAPrefs() {
|
||||||
await openPrefs();
|
await openPrefs();
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
(async function init() {
|
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 function init() {
|
||||||
await Prefs.set("last-run", new Date());
|
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 filters();
|
||||||
await getManager();
|
await getManager();
|
||||||
})().catch(ex => {
|
})().catch(ex => {
|
||||||
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -98,9 +98,9 @@ export class BatchGenerator implements Generator {
|
|||||||
|
|
||||||
public readonly hasInvalid: boolean;
|
public readonly hasInvalid: boolean;
|
||||||
|
|
||||||
public readonly length: any;
|
public readonly length: number;
|
||||||
|
|
||||||
public readonly preview: any;
|
public readonly preview: string;
|
||||||
|
|
||||||
constructor(str: string) {
|
constructor(str: string) {
|
||||||
this.gens = [];
|
this.gens = [];
|
||||||
|
@ -3,7 +3,42 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const polyfill = require("webextension-polyfill");
|
const polyfill = require("webextension-polyfill");
|
||||||
|
|
||||||
export const {i18n} = polyfill;
|
interface ExtensionListener {
|
||||||
|
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 {extension} = polyfill;
|
||||||
export const {notifications} = polyfill;
|
export const {notifications} = polyfill;
|
||||||
export const {browserAction} = polyfill;
|
export const {browserAction} = polyfill;
|
||||||
|
34
lib/bus.ts
@ -2,22 +2,18 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import {runtime, tabs} from "./browser";
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import {runtime, tabs, RawPort, MessageSender} from "./browser";
|
||||||
|
|
||||||
export class Port extends EventEmitter {
|
export class Port extends EventEmitter {
|
||||||
private port: any;
|
private port: RawPort | null;
|
||||||
|
|
||||||
constructor(port: any) {
|
constructor(port: RawPort) {
|
||||||
super();
|
super();
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
let disconnected = false;
|
let disconnected = false;
|
||||||
let tabListener: any;
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
if (tabListener) {
|
|
||||||
tabs.onRemoved.removeListener(tabListener);
|
|
||||||
tabListener = null;
|
|
||||||
}
|
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,11 +37,17 @@ export class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
|
if (!this.port) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return this.port.name;
|
return this.port.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return this.port.sender && this.port.sender.extensionId;
|
if (!this.port || !this.port.sender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.port.sender.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isSelf() {
|
get isSelf() {
|
||||||
@ -53,6 +55,9 @@ export class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post(msg: string, ...data: any[]) {
|
post(msg: string, ...data: any[]) {
|
||||||
|
if (!this.port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
this.port.postMessage({msg});
|
this.port.postMessage({msg});
|
||||||
return;
|
return;
|
||||||
@ -64,14 +69,17 @@ export class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMessage(message: any) {
|
onMessage(message: any) {
|
||||||
if (Object.keys(message).includes("msg")) {
|
if (!this.port) {
|
||||||
this.emit(message.msg, message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(message)) {
|
if (Array.isArray(message)) {
|
||||||
message.forEach(this.onMessage, this);
|
message.forEach(this.onMessage, this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Object.keys(message).includes("msg")) {
|
||||||
|
this.emit(message.msg, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof message === "string") {
|
if (typeof message === "string") {
|
||||||
this.emit(message);
|
this.emit(message);
|
||||||
return;
|
return;
|
||||||
@ -99,7 +107,7 @@ export const Bus = new class extends EventEmitter {
|
|||||||
runtime.onConnect.addListener(this.onConnect.bind(this));
|
runtime.onConnect.addListener(this.onConnect.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(msg: any, sender: any, callback: any) {
|
onMessage(msg: any, sender: MessageSender, callback: any) {
|
||||||
let {type = null} = msg;
|
let {type = null} = msg;
|
||||||
if (!type) {
|
if (!type) {
|
||||||
type = msg;
|
type = msg;
|
||||||
@ -107,7 +115,7 @@ export const Bus = new class extends EventEmitter {
|
|||||||
this.emit(type, msg, callback);
|
this.emit(type, msg, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect(port: any) {
|
onConnect(port: RawPort) {
|
||||||
if (!port.name) {
|
if (!port.name) {
|
||||||
port.disconnect();
|
port.disconnect();
|
||||||
return;
|
return;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
const VERSION = 1;
|
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) {
|
if (!this.db) {
|
||||||
reject(new Error("db closed"));
|
reject(new Error("db closed"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const items: any[] = [];
|
const items: BaseItem[] = [];
|
||||||
const transaction = this.db.transaction(STORE, "readonly");
|
const transaction = this.db.transaction(STORE, "readonly");
|
||||||
transaction.onerror = ex => reject(ex);
|
transaction.onerror = ex => reject(ex);
|
||||||
const store = transaction.objectStore(STORE);
|
const store = transaction.objectStore(STORE);
|
||||||
|
102
lib/filters.ts
@ -4,13 +4,16 @@
|
|||||||
import uuid from "./uuid";
|
import uuid from "./uuid";
|
||||||
|
|
||||||
import "./objectoverlay";
|
import "./objectoverlay";
|
||||||
import { storage, i18n } from "./browser";
|
import { storage } from "./browser";
|
||||||
import { EventEmitter } from "./events";
|
import { EventEmitter } from "./events";
|
||||||
import { Prefs } from "./prefs";
|
|
||||||
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Overlayable } from "./objectoverlay";
|
import { Overlayable } from "./objectoverlay";
|
||||||
import * as DEFAULT_FILTERS from "../data/filters.json";
|
import * 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_ESCAPE = /[{}()[\]\\^$.]/g;
|
||||||
const REG_FNMATCH = /[*?]/;
|
const REG_FNMATCH = /[*?]/;
|
||||||
@ -173,25 +176,37 @@ export class Matcher {
|
|||||||
}
|
}
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
matchItem(item: any) {
|
matchItem(item: BaseItem) {
|
||||||
const {usable = "", title = "", description = "", fileName = ""} = item;
|
const {usable = "", title = "", description = "", fileName = ""} = item;
|
||||||
return this.match(usable) || this.match(title) ||
|
return this.match(usable) || this.match(title) ||
|
||||||
this.match(description) || this.match(fileName);
|
this.match(description) || this.match(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RawFilter extends Object {
|
||||||
|
active: boolean;
|
||||||
|
type: number;
|
||||||
|
label: string;
|
||||||
|
expr: string;
|
||||||
|
icon?: string;
|
||||||
|
custom?: boolean;
|
||||||
|
isOverridden?: (prop: string) => boolean;
|
||||||
|
reset?: () => void;
|
||||||
|
toJSON?: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
export class Filter {
|
export class Filter {
|
||||||
private readonly owner: Filters;
|
private readonly owner: Filters;
|
||||||
|
|
||||||
public readonly id: any;
|
public readonly id: string | symbol;
|
||||||
|
|
||||||
private readonly raw: any;
|
private readonly raw: RawFilter;
|
||||||
|
|
||||||
private _label: string;
|
private _label: string;
|
||||||
|
|
||||||
private _reg: Matcher;
|
private _reg: Matcher;
|
||||||
|
|
||||||
constructor(owner: Filters, id: any, raw: any) {
|
constructor(owner: Filters, id: string | symbol, raw: RawFilter) {
|
||||||
if (!owner || !id || !raw) {
|
if (!owner || !id || !raw) {
|
||||||
throw new Error("null argument");
|
throw new Error("null argument");
|
||||||
}
|
}
|
||||||
@ -203,9 +218,11 @@ export class Filter {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._label = this.raw.label;
|
this._label = this.raw.label;
|
||||||
if (this.id !== FAST && this.id.startsWith("deffilter-") &&
|
if (typeof this.raw.isOverridden !== "undefined" &&
|
||||||
!this.raw.isOverridden("label")) {
|
typeof this.id === "string") {
|
||||||
this._label = i18n.getMessage(this.id) || this._label;
|
if (this.id.startsWith("deffilter-") && !this.raw.isOverridden("label")) {
|
||||||
|
this._label = _(this.id) || this._label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._reg = Matcher.fromExpression(this.expr);
|
this._reg = Matcher.fromExpression(this.expr);
|
||||||
Object.seal(this);
|
Object.seal(this);
|
||||||
@ -281,7 +298,7 @@ export class Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reset() {
|
async reset() {
|
||||||
if (this.raw.custom) {
|
if (!this.raw.reset) {
|
||||||
throw Error("Cannot reset non-default filter");
|
throw Error("Cannot reset non-default filter");
|
||||||
}
|
}
|
||||||
this.raw.reset();
|
this.raw.reset();
|
||||||
@ -291,7 +308,10 @@ export class Filter {
|
|||||||
|
|
||||||
async "delete"() {
|
async "delete"() {
|
||||||
if (!this.raw.custom) {
|
if (!this.raw.custom) {
|
||||||
throw Error("Cannot delete default filter");
|
throw new Error("Cannot delete default filter");
|
||||||
|
}
|
||||||
|
if (typeof this.id !== "string") {
|
||||||
|
throw new Error("Cannot delete symbolized");
|
||||||
}
|
}
|
||||||
await this.owner.delete(this.id);
|
await this.owner.delete(this.id);
|
||||||
}
|
}
|
||||||
@ -300,7 +320,7 @@ export class Filter {
|
|||||||
return this._reg.match(str);
|
return this._reg.match(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
matchItem(item: any) {
|
matchItem(item: BaseItem) {
|
||||||
return this._reg.matchItem(item);
|
return this._reg.matchItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,8 +335,7 @@ class FastFilter extends Filter {
|
|||||||
throw new Error("Invalid fast filter value");
|
throw new Error("Invalid fast filter value");
|
||||||
}
|
}
|
||||||
super(owner, FAST, {
|
super(owner, FAST, {
|
||||||
id: FAST,
|
label: "fast",
|
||||||
label: FAST,
|
|
||||||
type: TYPE_ALL,
|
type: TYPE_ALL,
|
||||||
active: true,
|
active: true,
|
||||||
expr: value,
|
expr: value,
|
||||||
@ -351,8 +370,6 @@ class Filters extends EventEmitter {
|
|||||||
|
|
||||||
private filters: Filter[];
|
private filters: Filter[];
|
||||||
|
|
||||||
private fastFilter: string | null;
|
|
||||||
|
|
||||||
ignoreNext: boolean;
|
ignoreNext: boolean;
|
||||||
|
|
||||||
private readonly typeMatchers: Map<number, Matcher>;
|
private readonly typeMatchers: Map<number, Matcher>;
|
||||||
@ -362,10 +379,8 @@ class Filters extends EventEmitter {
|
|||||||
this.typeMatchers = new Map();
|
this.typeMatchers = new Map();
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.filters = [];
|
this.filters = [];
|
||||||
this.fastFilter = null;
|
|
||||||
this.ignoreNext = false;
|
this.ignoreNext = false;
|
||||||
this.regenerate();
|
this.regenerate();
|
||||||
Prefs.on("fast-filter", this.updateFastFilter.bind(this));
|
|
||||||
storage.onChanged.addListener(async (changes: any) => {
|
storage.onChanged.addListener(async (changes: any) => {
|
||||||
if (this.ignoreNext) {
|
if (this.ignoreNext) {
|
||||||
this.ignoreNext = false;
|
this.ignoreNext = false;
|
||||||
@ -403,6 +418,7 @@ class Filters extends EventEmitter {
|
|||||||
const id = `custom-${uuid()}`;
|
const id = `custom-${uuid()}`;
|
||||||
const filter = new Filter(this, id, {
|
const filter = new Filter(this, id, {
|
||||||
active: true,
|
active: true,
|
||||||
|
custom: true,
|
||||||
label,
|
label,
|
||||||
expr,
|
expr,
|
||||||
type,
|
type,
|
||||||
@ -411,11 +427,11 @@ class Filters extends EventEmitter {
|
|||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
"get"(id: any) {
|
"get"(id: string | symbol) {
|
||||||
return this.filters.find(e => e.id === id);
|
return this.filters.find(e => e.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async "delete"(id: any) {
|
async "delete"(id: string) {
|
||||||
const idx = this.filters.findIndex(e => e.id === id);
|
const idx = this.filters.findIndex(e => e.id === id);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
return;
|
return;
|
||||||
@ -438,21 +454,12 @@ class Filters extends EventEmitter {
|
|||||||
return new FastFilter(this, value);
|
return new FastFilter(this, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFastFilter() {
|
async getFastFilter() {
|
||||||
if (!this.fastFilter) {
|
await FASTFILTER.init();
|
||||||
throw new Error("Nothing stored");
|
if (!FASTFILTER.current) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return new FastFilter(this, this.fastFilter);
|
return new FastFilter(this, FASTFILTER.current);
|
||||||
}
|
|
||||||
|
|
||||||
async setFastFilter(value: string) {
|
|
||||||
this.fastFilter = value || "";
|
|
||||||
await Prefs.set("fast-filter", this.fastFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFastFilter(pref: any, key: string, value: string) {
|
|
||||||
this.fastFilter = value || null;
|
|
||||||
this.regenerate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
regenerate() {
|
regenerate() {
|
||||||
@ -480,17 +487,6 @@ class Filters extends EventEmitter {
|
|||||||
console.error("Filter", current.label || "unknown", ex);
|
console.error("Filter", current.label || "unknown", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.fastFilter) {
|
|
||||||
try {
|
|
||||||
const fastFilter = new FastFilter(this, this.fastFilter);
|
|
||||||
all.push(fastFilter);
|
|
||||||
links.push(fastFilter);
|
|
||||||
media.push(fastFilter);
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error("fast filter", this.fastFilter, "is invalid", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.typeMatchers.set(TYPE_ALL, new Matcher(all));
|
this.typeMatchers.set(TYPE_ALL, new Matcher(all));
|
||||||
this.typeMatchers.set(TYPE_LINK, new Matcher(links));
|
this.typeMatchers.set(TYPE_LINK, new Matcher(links));
|
||||||
this.typeMatchers.set(TYPE_MEDIA, new Matcher(media));
|
this.typeMatchers.set(TYPE_MEDIA, new Matcher(media));
|
||||||
@ -498,6 +494,7 @@ class Filters extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
await locale;
|
||||||
const defaultFilters = DEFAULT_FILTERS as any;
|
const defaultFilters = DEFAULT_FILTERS as any;
|
||||||
let savedFilters = (await storage.local.get("userFilters"));
|
let savedFilters = (await storage.local.get("userFilters"));
|
||||||
if (savedFilters && "userFilters" in savedFilters) {
|
if (savedFilters && "userFilters" in savedFilters) {
|
||||||
@ -534,14 +531,17 @@ class Filters extends EventEmitter {
|
|||||||
defaultFilters[filter]);
|
defaultFilters[filter]);
|
||||||
this.filters.push(new Filter(this, filter, current));
|
this.filters.push(new Filter(this, filter, current));
|
||||||
}
|
}
|
||||||
this.fastFilter = await Prefs.get("fast-filter", null);
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.regenerate();
|
this.regenerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
filterItemsByType(items: any[], type: number) {
|
async filterItemsByType(items: BaseItem[], type: number) {
|
||||||
const matcher = this.typeMatchers.get(type);
|
const matcher = this.typeMatchers.get(type);
|
||||||
|
const fast = await this.getFastFilter();
|
||||||
return items.filter(function(item) {
|
return items.filter(function(item) {
|
||||||
|
if (fast && fast.matchItem(item)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return matcher && matcher.matchItem(item);
|
return matcher && matcher.matchItem(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -562,12 +562,14 @@ class Filters extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _filters: any;
|
let _filters: Filters;
|
||||||
|
let _loader: Promise<void>;
|
||||||
|
|
||||||
export async function filters(): Promise<Filters> {
|
export async function filters(): Promise<Filters> {
|
||||||
if (!_filters) {
|
if (!_loader) {
|
||||||
_filters = new Filters();
|
_filters = new Filters();
|
||||||
await _filters.load();
|
_loader = _filters.load();
|
||||||
}
|
}
|
||||||
|
await _loader;
|
||||||
return _filters;
|
return _filters;
|
||||||
}
|
}
|
||||||
|
249
lib/i18n.ts
@ -3,49 +3,189 @@
|
|||||||
|
|
||||||
import {memoize} from "./memoize";
|
import {memoize} from "./memoize";
|
||||||
|
|
||||||
function load() {
|
declare let browser: any;
|
||||||
try {
|
declare let chrome: any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const {i18n} = require("webextension-polyfill");
|
|
||||||
|
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
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);
|
||||||
|
console.log("custom", custom);
|
||||||
|
if (custom) {
|
||||||
|
try {
|
||||||
|
valid.push(JSON.parse(custom));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = valid.shift();
|
||||||
|
const rv = new Localization(base, ...valid);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to load locale", ex.toString(), ex.stack, ex);
|
||||||
|
return new Localization({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
// We might be running under node for tests
|
// We might be running under node for tests
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const messages = require("../_locales/en/messages.json");
|
const messages = require("../_locales/en/messages.json");
|
||||||
|
|
||||||
const map = new Map();
|
return new Localization(messages);
|
||||||
for (const [k, v] of Object.entries<any>(messages)) {
|
|
||||||
const {placeholders = {}} = v;
|
|
||||||
let {message = ""} = v;
|
|
||||||
for (const [pname, pval] of Object.entries<any>(placeholders)) {
|
|
||||||
message = message.replace(`$${pname.toUpperCase()}$`, `${pval.content}$`);
|
|
||||||
}
|
|
||||||
map.set(k, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getMessage(id: string, subst: string[]) {
|
|
||||||
const m = map.get(id);
|
|
||||||
if (typeof subst === undefined) {
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(subst)) {
|
|
||||||
subst = [subst];
|
|
||||||
}
|
|
||||||
return m.replace(/\$\d+\$/g, (r: string) => {
|
|
||||||
const idx = parseInt(r.substr(1, r.length - 2), 10) - 1;
|
|
||||||
return subst[idx] || "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = load();
|
type MemoLocalize = (id: string, ...args: any[]) => string;
|
||||||
const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
|
|
||||||
|
export const locale = load();
|
||||||
|
let loc: Localization | null;
|
||||||
|
let memoLocalize: MemoLocalize | null = null;
|
||||||
|
locale.then(l => {
|
||||||
|
loc = l;
|
||||||
|
memoLocalize = memoize(loc.localize.bind(loc), 10 * 1000, 10);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Localize a message
|
* Localize a message
|
||||||
@ -53,22 +193,22 @@ const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 0);
|
|||||||
* @param {string[]} [subst] Message substituations
|
* @param {string[]} [subst] Message substituations
|
||||||
* @returns {string} Localized message
|
* @returns {string} Localized message
|
||||||
*/
|
*/
|
||||||
function _(id: string, ...subst: any[]) {
|
export function _(id: string, ...subst: any[]) {
|
||||||
|
if (!loc || !memoLocalize) {
|
||||||
|
console.trace("TOO SOON");
|
||||||
|
throw new Error("Called too soon");
|
||||||
|
}
|
||||||
if (!subst.length) {
|
if (!subst.length) {
|
||||||
return memoGetMessage(id);
|
return memoLocalize(id);
|
||||||
}
|
}
|
||||||
if (subst.length === 1 && Array.isArray(subst[0])) {
|
return loc.localize(id, subst);
|
||||||
subst = subst.pop();
|
|
||||||
}
|
|
||||||
return i18n.getMessage(id, subst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function localize_<T extends HTMLElement | DocumentFragment>(elem: T): T {
|
||||||
* Localize a DOM
|
for (const tmpl of elem.querySelectorAll<HTMLTemplateElement>("template")) {
|
||||||
* @param {Element} elem DOM to localize
|
localize_(tmpl.content);
|
||||||
* @returns {Element} Passed in element (fluent)
|
}
|
||||||
*/
|
|
||||||
function localize(elem: HTMLElement) {
|
|
||||||
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
|
for (const el of elem.querySelectorAll<HTMLElement>("*[data-i18n]")) {
|
||||||
const {i18n: i} = el.dataset;
|
const {i18n: i} = el.dataset;
|
||||||
if (!i) {
|
if (!i) {
|
||||||
@ -99,8 +239,25 @@ function localize(elem: HTMLElement) {
|
|||||||
for (const el of document.querySelectorAll("*[data-l18n]")) {
|
for (const el of document.querySelectorAll("*[data-l18n]")) {
|
||||||
console.error("wrong!", el);
|
console.error("wrong!", el);
|
||||||
}
|
}
|
||||||
return elem;
|
return elem as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localize a DOM
|
||||||
|
* @param {Element} elem DOM to localize
|
||||||
|
* @returns {Element} Passed in element (fluent)
|
||||||
|
*/
|
||||||
|
export async function localize<T extends HTMLElement | DocumentFragment>(
|
||||||
|
elem: T): Promise<T> {
|
||||||
|
await locale;
|
||||||
|
return localize_(elem);
|
||||||
|
}
|
||||||
|
|
||||||
export {localize, _};
|
export function saveCustomLocale(data?: string) {
|
||||||
|
if (!data) {
|
||||||
|
localStorage.removeItem(CUSTOM_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new Localization(JSON.parse(data));
|
||||||
|
localStorage.setItem(CUSTOM_KEY, data);
|
||||||
|
}
|
||||||
|
26
lib/item.ts
@ -4,18 +4,32 @@
|
|||||||
import { ALLOWED_SCHEMES } from "./constants";
|
import { ALLOWED_SCHEMES } from "./constants";
|
||||||
import { TRANSFERABLE_PROPERTIES } from "./constants";
|
import { TRANSFERABLE_PROPERTIES } from "./constants";
|
||||||
|
|
||||||
|
export interface BaseItem {
|
||||||
|
url: string;
|
||||||
|
usable: string;
|
||||||
|
referrer?: string;
|
||||||
|
usableReferrer?: string;
|
||||||
|
description?: string;
|
||||||
|
title?: string;
|
||||||
|
fileName?: string;
|
||||||
|
batch?: number;
|
||||||
|
idx: number;
|
||||||
|
mask?: string;
|
||||||
|
startDate?: number;
|
||||||
|
private?: boolean;
|
||||||
|
postData?: string;
|
||||||
|
paused?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const OPTIONPROPS = Object.freeze([
|
const OPTIONPROPS = Object.freeze([
|
||||||
"referrer", "usableReferrer",
|
"referrer", "usableReferrer",
|
||||||
"description", "title",
|
"description", "title",
|
||||||
"fileName",
|
"fileName",
|
||||||
"batch", "idx",
|
"batch", "idx",
|
||||||
"mask",
|
"mask",
|
||||||
"fromMetalink",
|
|
||||||
"startDate",
|
"startDate",
|
||||||
"hashes",
|
|
||||||
"private",
|
"private",
|
||||||
"postData",
|
"postData",
|
||||||
"cleanRequest",
|
|
||||||
"paused"
|
"paused"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -34,7 +48,7 @@ function maybeAssign(options: any, what: any) {
|
|||||||
this[what] = val;
|
this[what] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Item {
|
export class Item implements BaseItem {
|
||||||
public url: string;
|
public url: string;
|
||||||
|
|
||||||
public usable: string;
|
public usable: string;
|
||||||
@ -43,6 +57,8 @@ export class Item {
|
|||||||
|
|
||||||
public usableReferrer: string;
|
public usableReferrer: string;
|
||||||
|
|
||||||
|
public idx: number;
|
||||||
|
|
||||||
constructor(raw: any, options?: any) {
|
constructor(raw: any, options?: any) {
|
||||||
Object.assign(this, raw);
|
Object.assign(this, raw);
|
||||||
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
|
OPTIONPROPS.forEach(maybeAssign.bind(this, options || {}));
|
||||||
@ -99,7 +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 known = new Map();
|
||||||
const unique = [];
|
const unique = [];
|
||||||
for (const itemlist of items) {
|
for (const itemlist of items) {
|
||||||
|
@ -29,8 +29,6 @@ const SAVEDPROPS = [
|
|||||||
"serverName",
|
"serverName",
|
||||||
// other options
|
// other options
|
||||||
"private",
|
"private",
|
||||||
"fromMetalink",
|
|
||||||
"cleanRequest",
|
|
||||||
// db
|
// db
|
||||||
"manId",
|
"manId",
|
||||||
"dbId",
|
"dbId",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs } from "../prefs";
|
||||||
import { parsePath } from "../util";
|
import { parsePath, filterInSitu } from "../util";
|
||||||
import {
|
import {
|
||||||
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
|
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
|
||||||
FORCABLE, PAUSABLE, CANCELABLE,
|
FORCABLE, PAUSABLE, CANCELABLE,
|
||||||
@ -18,6 +18,18 @@ const setShelfEnabled = downloads.setShelfEnabled || function() {
|
|||||||
// ignored
|
// 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 {
|
export class Download extends BaseDownload {
|
||||||
public manager: Manager;
|
public manager: Manager;
|
||||||
|
|
||||||
@ -82,44 +94,47 @@ export class Download extends BaseDownload {
|
|||||||
if (this.state !== QUEUED) {
|
if (this.state !== QUEUED) {
|
||||||
throw new Error("invalid state");
|
throw new Error("invalid state");
|
||||||
}
|
}
|
||||||
console.trace("starting", this.toString(), this.dest, this.mask);
|
console.trace("starting", this.toString(), this.toMsg());
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
try {
|
try {
|
||||||
const options: any = {
|
const options: Options = {
|
||||||
conflictAction: await Prefs.get("conflict-action"),
|
conflictAction: await Prefs.get("conflict-action"),
|
||||||
filename: this.dest.full,
|
filename: this.dest.full,
|
||||||
saveAs: false,
|
saveAs: false,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
headers: [{
|
headers: [],
|
||||||
name: "X-DTA-Tag",
|
incognito: this.private
|
||||||
value: this.sessionId.toString(),
|
|
||||||
}],
|
|
||||||
};
|
};
|
||||||
if (this.postData) {
|
if (this.postData) {
|
||||||
options.body = this.postData;
|
options.body = this.postData;
|
||||||
options.method = "POST";
|
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) {
|
if (this.referrer) {
|
||||||
options.headers.push({
|
options.headers.push({
|
||||||
name: "Referer",
|
name: "Referer",
|
||||||
value: this.referrer
|
value: this.referrer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShelfEnabled(false);
|
setShelfEnabled(false);
|
||||||
|
try {
|
||||||
try {
|
try {
|
||||||
this.manager.addManId(
|
this.manager.addManId(
|
||||||
this.manId = await downloads.download(options), this);
|
this.manId = await downloads.download(options), this);
|
||||||
}
|
}
|
||||||
|
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 {
|
finally {
|
||||||
setShelfEnabled(true);
|
setShelfEnabled(true);
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,7 @@ export class Manager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
next.changeState(CANCELED);
|
next.changeState(CANCELED);
|
||||||
|
next.error = ex.toString();
|
||||||
console.error(ex.toString(), ex);
|
console.error(ex.toString(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,7 +266,6 @@ export class Manager extends EventEmitter {
|
|||||||
changedState(download: Download, oldState: number, newState: number) {
|
changedState(download: Download, oldState: number, newState: number) {
|
||||||
if (oldState === RUNNING) {
|
if (oldState === RUNNING) {
|
||||||
this.running.delete(download);
|
this.running.delete(download);
|
||||||
this.maybeNotifyFinished();
|
|
||||||
}
|
}
|
||||||
if (newState === QUEUED) {
|
if (newState === QUEUED) {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
@ -278,7 +278,7 @@ export class Manager extends EventEmitter {
|
|||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.startNext();
|
this.startNext().catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export class RecentList {
|
|||||||
this.pref = `savedlist-${pref}`;
|
this.pref = `savedlist-${pref}`;
|
||||||
this.defaults = Array.from(defaults);
|
this.defaults = Array.from(defaults);
|
||||||
this[LIST] = [];
|
this[LIST] = [];
|
||||||
this.limit = 5;
|
this.limit = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
get values() {
|
get values() {
|
||||||
|
@ -10,10 +10,24 @@ import { donate, openPrefs, openUrls } from "./windowutils";
|
|||||||
import { filters, FAST, Filter } from "./filters";
|
import { filters, FAST, Filter } from "./filters";
|
||||||
import { WindowStateTracker } from "./windowstatetracker";
|
import { WindowStateTracker } from "./windowstatetracker";
|
||||||
import { windows } from "./browser";
|
import { windows } 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) {
|
export interface ItemDelta {
|
||||||
let ws = items.map((item: any, idx: number) => {
|
idx: number;
|
||||||
|
matched?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSelection(
|
||||||
|
filters: Filter[],
|
||||||
|
items: BaseMatchedItem[],
|
||||||
|
onlyFast: boolean): ItemDelta[] {
|
||||||
|
let ws = items.map((item, idx: number) => {
|
||||||
item.idx = idx;
|
item.idx = idx;
|
||||||
const {matched = null} = item;
|
const {matched = null} = item;
|
||||||
item.prevMatched = matched;
|
item.prevMatched = matched;
|
||||||
@ -23,9 +37,15 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
|||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
ws = ws.filter(item => {
|
ws = ws.filter(item => {
|
||||||
if (filter.matchItem(item)) {
|
if (filter.matchItem(item)) {
|
||||||
item.matched = filter.id === FAST ?
|
if (filter.id === FAST) {
|
||||||
"fast" :
|
item.matched = "fast";
|
||||||
(onlyFast ? null : filter.id);
|
}
|
||||||
|
else if (!onlyFast && typeof filter.id === "string") {
|
||||||
|
item.matched = filter.id;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item.matched = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return !item.matched;
|
return !item.matched;
|
||||||
});
|
});
|
||||||
@ -41,6 +61,9 @@ function computeSelection(filters: any[], items: any[], onlyFast: boolean) {
|
|||||||
function *computeActiveFiltersGen(
|
function *computeActiveFiltersGen(
|
||||||
filters: Filter[], activeOverrides: Map<string, boolean>) {
|
filters: Filter[], activeOverrides: Map<string, boolean>) {
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
|
if (typeof filter.id !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const override = activeOverrides.get(filter.id);
|
const override = activeOverrides.get(filter.id);
|
||||||
if (typeof override === "boolean") {
|
if (typeof override === "boolean") {
|
||||||
if (override) {
|
if (override) {
|
||||||
@ -59,11 +82,11 @@ function computeActiveFilters(
|
|||||||
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
|
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
|
||||||
}
|
}
|
||||||
|
|
||||||
function filtersToDescs(filters: any[]) {
|
function filtersToDescs(filters: Filter[]) {
|
||||||
return filters.map(f => f.descriptor);
|
return filters.map(f => f.descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function select(links: any[], media: any[]) {
|
export async function select(links: BaseItem[], media: BaseItem[]) {
|
||||||
const fm = await filters();
|
const fm = await filters();
|
||||||
const tracker = new WindowStateTracker("select", {
|
const tracker = new WindowStateTracker("select", {
|
||||||
minWidth: 700,
|
minWidth: 700,
|
||||||
@ -85,26 +108,26 @@ export async function select(links: any[], media: any[]) {
|
|||||||
tracker.track(window.id, port);
|
tracker.track(window.id, port);
|
||||||
|
|
||||||
const overrides = new Map();
|
const overrides = new Map();
|
||||||
let fast: any = null;
|
let fast: Filter | null = null;
|
||||||
let onlyFast: false;
|
let onlyFast: false;
|
||||||
try {
|
try {
|
||||||
fast = fm.getFastFilter();
|
fast = await fm.getFastFilter();
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendFilters = function(delta = false) {
|
const sendFilters = function(delta = false) {
|
||||||
let {linkFilters, mediaFilters} = fm;
|
const {linkFilters, mediaFilters} = fm;
|
||||||
const alink = computeActiveFilters(linkFilters, overrides);
|
const alink = computeActiveFilters(linkFilters, overrides);
|
||||||
const amedia = computeActiveFilters(mediaFilters, overrides);
|
const amedia = computeActiveFilters(mediaFilters, overrides);
|
||||||
const sactiveFilters = new Set<any>();
|
const sactiveFilters = new Set<any>();
|
||||||
[alink, amedia].forEach(
|
[alink, amedia].forEach(
|
||||||
a => a.forEach(filter => sactiveFilters.add(filter.id)));
|
a => a.forEach(filter => sactiveFilters.add(filter.id)));
|
||||||
const activeFilters = Array.from(sactiveFilters);
|
const activeFilters = Array.from(sactiveFilters);
|
||||||
linkFilters = filtersToDescs(linkFilters);
|
const linkFilterDescs = filtersToDescs(linkFilters);
|
||||||
mediaFilters = filtersToDescs(mediaFilters);
|
const mediaFilterDescs = filtersToDescs(mediaFilters);
|
||||||
port.post("filters", {linkFilters, mediaFilters, activeFilters});
|
port.post("filters", {linkFilterDescs, mediaFilterDescs, activeFilters});
|
||||||
|
|
||||||
if (fast) {
|
if (fast) {
|
||||||
alink.unshift(fast);
|
alink.unshift(fast);
|
||||||
@ -128,9 +151,6 @@ export async function select(links: any[], media: any[]) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
port.on("queue", (msg: any) => {
|
port.on("queue", (msg: any) => {
|
||||||
const selected = new Set<number>(msg.items);
|
|
||||||
const items = (msg.type === "links" ? links : media);
|
|
||||||
msg.items = items.filter((item: any, idx: number) => selected.has(idx));
|
|
||||||
done.resolve(msg);
|
done.resolve(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,7 +195,11 @@ export async function select(links: any[], media: any[]) {
|
|||||||
sendFilters(false);
|
sendFilters(false);
|
||||||
const type = await Prefs.get("last-type", "links");
|
const type = await Prefs.get("last-type", "links");
|
||||||
port.post("items", {type, links, media});
|
port.post("items", {type, links, media});
|
||||||
const result = await done;
|
const {items, options} = await done;
|
||||||
|
const selectedIndexes = new Set<number>(items);
|
||||||
|
const selectedList = (options.type === "links" ? links : media);
|
||||||
|
const selectedItems = selectedList.filter(
|
||||||
|
(item: BaseItem, idx: number) => selectedIndexes.has(idx));
|
||||||
for (const [filter, override] of overrides) {
|
for (const [filter, override] of overrides) {
|
||||||
const f = fm.get(filter);
|
const f = fm.get(filter);
|
||||||
if (f) {
|
if (f) {
|
||||||
@ -183,7 +207,7 @@ export async function select(links: any[], media: any[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await fm.save();
|
await fm.save();
|
||||||
return result;
|
return {items: selectedItems, options};
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
fm.off("changed", sendFilters);
|
fm.off("changed", sendFilters);
|
||||||
|
@ -7,8 +7,10 @@ import { WindowStateTracker } from "./windowstatetracker";
|
|||||||
import { Promised, timeout } from "./util";
|
import { Promised, timeout } from "./util";
|
||||||
import { donate } from "./windowutils";
|
import { donate } from "./windowutils";
|
||||||
import { windows } from "./browser";
|
import { windows } from "./browser";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
|
||||||
export async function single(item: any) {
|
export async function single(item: BaseItem | null) {
|
||||||
const tracker = new WindowStateTracker("single", {
|
const tracker = new WindowStateTracker("single", {
|
||||||
minWidth: 700,
|
minWidth: 700,
|
||||||
minHeight: 460
|
minHeight: 460
|
||||||
@ -46,7 +48,9 @@ export async function single(item: any) {
|
|||||||
donate();
|
donate();
|
||||||
});
|
});
|
||||||
|
|
||||||
port.post("item", item);
|
if (item) {
|
||||||
|
port.post("item", {item});
|
||||||
|
}
|
||||||
return await done;
|
return await done;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "DownThemAll!",
|
"name": "DownThemAll!",
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
|
|
||||||
"description": "__MSG_extensionDescription__",
|
"description": "__MSG_extensionDescription__",
|
||||||
"homepage_url": "https://downthemall.org/",
|
"homepage_url": "https://downthemall.org/",
|
||||||
@ -40,7 +40,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"browser_style": false,
|
"browser_style": true,
|
||||||
|
"default_popup": "windows/popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"16": "style/icon16.png",
|
"16": "style/icon16.png",
|
||||||
"32": "style/icon32.png",
|
"32": "style/icon32.png",
|
||||||
|
@ -28,7 +28,9 @@ return url;
|
|||||||
}();
|
}();
|
||||||
|
|
||||||
function makeURL(url: string) {
|
function makeURL(url: string) {
|
||||||
return new URL(url, baseURL);
|
const rv = new URL(url, baseURL);
|
||||||
|
rv.hash = "";
|
||||||
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitize(str: string | null | undefined) {
|
function sanitize(str: string | null | undefined) {
|
||||||
|
4
style/add.svg
Executable 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 |
Before Width: | Height: | Size: 767 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 773 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 776 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 1.5 KiB |
@ -46,6 +46,11 @@ body > * {
|
|||||||
background: rgb(246,246,246);
|
background: rgb(246,246,246);
|
||||||
color: black;
|
color: black;
|
||||||
transition: box-shadow 0.5s, background 1s;
|
transition: box-shadow 0.5s, background 1s;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
#toolbar > .button > span:before {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolbar > .button.disabled {
|
#toolbar > .button.disabled {
|
||||||
@ -63,13 +68,6 @@ body > * {
|
|||||||
box-shadow: 0px 0px 7px 2px rgba(220,220,220,0.75);
|
box-shadow: 0px 0px 7px 2px rgba(220,220,220,0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolbar > .button > span {
|
|
||||||
display: block;
|
|
||||||
flex-grow: 0;
|
|
||||||
width: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar > .button > .icon-add {
|
#toolbar > .button > .icon-add {
|
||||||
color: var(--add-color);
|
color: var(--add-color);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ article {
|
|||||||
|
|
||||||
#tabs {
|
#tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: url(icon64.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon64.png) 1em 50%/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
||||||
padding-left: calc(2em + 32px);
|
padding-left: calc(2em + 32px);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ input.tab {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabs > label{
|
#tabs > label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
|
Before Width: | Height: | Size: 923 B |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 2.0 KiB |
@ -117,7 +117,7 @@ body > * {
|
|||||||
}
|
}
|
||||||
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 124.8dpi) {
|
||||||
#tabs {
|
#tabs {
|
||||||
background: url(icon64.png) 1em 0/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
background: url(icon64.png) 1em 50%/32px 32px no-repeat, url(tile.png) repeat-x, var(--toolbar-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ body > * {
|
|||||||
background: var(--toolbar-bg-color);
|
background: var(--toolbar-bg-color);
|
||||||
color: white;
|
color: white;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
padding: 1.2ex;
|
padding: 1ex;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -14,6 +14,8 @@ import {
|
|||||||
} from "./tablesymbols";
|
} from "./tablesymbols";
|
||||||
import { InvalidatedSet, UpdateRecord } from "./tableutil";
|
import { InvalidatedSet, UpdateRecord } from "./tableutil";
|
||||||
import { addClass, clampUInt, IS_MAC } from "./util";
|
import { addClass, clampUInt, IS_MAC } from "./util";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { TableConfig } from "./config";
|
||||||
|
|
||||||
const ROWS_SMALL_UPDATE = 5;
|
const ROWS_SMALL_UPDATE = 5;
|
||||||
const PIXEL_PREC = 5;
|
const PIXEL_PREC = 5;
|
||||||
@ -79,7 +81,7 @@ export class BaseTable extends AbstractTable {
|
|||||||
|
|
||||||
[COLS]: Columns;
|
[COLS]: Columns;
|
||||||
|
|
||||||
constructor(elem: any, config: any, version?: number) {
|
constructor(elem: any, config: TableConfig | null, version?: number) {
|
||||||
config = (config && config.version === version && config) || {};
|
config = (config && config.version === version && config) || {};
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -121,9 +123,9 @@ export class BaseTable extends AbstractTable {
|
|||||||
this.makeDOM(config);
|
this.makeDOM(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
makeDOM(config: any) {
|
makeDOM(config: TableConfig) {
|
||||||
const configColumns = "columns" in config ? config.columns : null;
|
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 container = document.createElement("div");
|
||||||
const thead = document.createElement("div");
|
const thead = document.createElement("div");
|
||||||
@ -241,7 +243,7 @@ export class BaseTable extends AbstractTable {
|
|||||||
return new SelectionRange(firstIdx, lastIdx);
|
return new SelectionRange(firstIdx, lastIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config(): TableConfig {
|
||||||
return {
|
return {
|
||||||
version: this.version,
|
version: this.version,
|
||||||
columns: this.columnConfig
|
columns: this.columnConfig
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
import { TableEvents } from "./tableevents";
|
import { TableEvents } from "./tableevents";
|
||||||
import {addClass, debounce, sum} from "./util";
|
import {addClass, debounce, sum} from "./util";
|
||||||
import {EventEmitter} from "./events";
|
import {EventEmitter} from "./events";
|
||||||
import {APOOL} from "./animationpool";
|
import {APOOL} from "./animationpool";
|
||||||
|
import { ColumnConfig, ColumnConfigs } from "./config";
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
|
|
||||||
// License: MIT
|
|
||||||
|
|
||||||
const PIXLIT_WIDTH = 2;
|
const PIXLIT_WIDTH = 2;
|
||||||
const MIN_COL_WIDTH = 16;
|
const MIN_COL_WIDTH = 16;
|
||||||
@ -55,8 +53,7 @@ export class Column extends EventEmitter {
|
|||||||
columns: Columns,
|
columns: Columns,
|
||||||
col: HTMLTableHeaderCellElement,
|
col: HTMLTableHeaderCellElement,
|
||||||
id: number,
|
id: number,
|
||||||
config: any) {
|
config: ColumnConfig | null) {
|
||||||
config = config || {};
|
|
||||||
super();
|
super();
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
this.elem = col;
|
this.elem = col;
|
||||||
@ -89,7 +86,7 @@ export class Column extends EventEmitter {
|
|||||||
|
|
||||||
this.elem.appendChild(containerElem);
|
this.elem.appendChild(containerElem);
|
||||||
|
|
||||||
if ("visible" in config) {
|
if (config) {
|
||||||
this.visible = config.visible;
|
this.visible = config.visible;
|
||||||
}
|
}
|
||||||
this.initWidths(config);
|
this.initWidths(config);
|
||||||
@ -148,18 +145,18 @@ export class Column extends EventEmitter {
|
|||||||
return Math.max(0, this.currentWidth - this.minWidth);
|
return Math.max(0, this.currentWidth - this.minWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config(): ColumnConfig {
|
||||||
return {
|
return {
|
||||||
visible: this.visible,
|
visible: this.visible,
|
||||||
width: this.currentWidth,
|
width: this.currentWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initWidths(config: any) {
|
initWidths(config: ColumnConfig | null) {
|
||||||
const style = getComputedStyle(this.elem, null);
|
const style = getComputedStyle(this.elem, null);
|
||||||
this.minWidth = toPixel(style.getPropertyValue("min-width"), MIN_COL_WIDTH);
|
this.minWidth = toPixel(style.getPropertyValue("min-width"), MIN_COL_WIDTH);
|
||||||
this.maxWidth = toPixel(style.getPropertyValue("max-width"), 0);
|
this.maxWidth = toPixel(style.getPropertyValue("max-width"), 0);
|
||||||
const width = config.width || this.baseWidth;
|
const width = (config && config.width) || this.baseWidth;
|
||||||
this.setWidth(width);
|
this.setWidth(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +233,7 @@ export class Columns extends EventEmitter {
|
|||||||
|
|
||||||
public visible: Column[];
|
public visible: Column[];
|
||||||
|
|
||||||
constructor(table: any, config: any) {
|
constructor(table: any, config: ColumnConfigs | null) {
|
||||||
config = config || {};
|
config = config || {};
|
||||||
super();
|
super();
|
||||||
this.table = table;
|
this.table = table;
|
||||||
@ -247,7 +244,9 @@ export class Columns extends EventEmitter {
|
|||||||
this.named = new Map<string, Column>();
|
this.named = new Map<string, Column>();
|
||||||
this.cols = Array.from(table.elem.querySelectorAll("th")).
|
this.cols = Array.from(table.elem.querySelectorAll("th")).
|
||||||
map((colEl: HTMLTableHeaderCellElement, colid: number) => {
|
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);
|
const col = new Column(this, colEl, colid, columnConfig);
|
||||||
col.on("gripmoved", this.gripmoved);
|
col.on("gripmoved", this.gripmoved);
|
||||||
this.named.set(colEl.id, col);
|
this.named.set(colEl.id, col);
|
||||||
@ -261,7 +260,7 @@ export class Columns extends EventEmitter {
|
|||||||
Object.seal(this);
|
Object.seal(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config(): ColumnConfigs {
|
||||||
const rv: any = {};
|
const rv: any = {};
|
||||||
for (const c of this.cols) {
|
for (const c of this.cols) {
|
||||||
rv[c.elem.id] = c.config;
|
rv[c.elem.id] = c.config;
|
||||||
|
13
uikit/lib/config.ts
Normal 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;
|
||||||
|
}
|
@ -33,6 +33,14 @@ export interface MenuPosition {
|
|||||||
clientY: number;
|
clientY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MenuOptions {
|
||||||
|
disabled?: string;
|
||||||
|
allowClick?: string;
|
||||||
|
icon?: string;
|
||||||
|
key?: string;
|
||||||
|
autoHide?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class MenuItemBase {
|
export class MenuItemBase {
|
||||||
public readonly owner: ContextMenu;
|
public readonly owner: ContextMenu;
|
||||||
|
|
||||||
@ -44,7 +52,7 @@ export class MenuItemBase {
|
|||||||
|
|
||||||
public readonly key: string;
|
public readonly key: string;
|
||||||
|
|
||||||
public readonly autohide: boolean;
|
public readonly autoHide: boolean;
|
||||||
|
|
||||||
public readonly elem: HTMLLIElement;
|
public readonly elem: HTMLLIElement;
|
||||||
|
|
||||||
@ -54,18 +62,16 @@ export class MenuItemBase {
|
|||||||
|
|
||||||
public readonly keyElem: HTMLSpanElement;
|
public readonly keyElem: HTMLSpanElement;
|
||||||
|
|
||||||
constructor(owner: ContextMenu, id = "", text = "", {
|
constructor(owner: ContextMenu, id = "", text = "", options: MenuOptions) {
|
||||||
key = "", icon = "", autohide = Object()
|
|
||||||
}) {
|
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
id = `contextmenu-${++ids}`;
|
id = `contextmenu-${++ids}`;
|
||||||
}
|
}
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.text = text || "";
|
this.text = text || "";
|
||||||
this.icon = icon || "";
|
this.icon = options.icon || "";
|
||||||
this.key = key || "";
|
this.key = options.key || "";
|
||||||
this.autohide = autohide !== "false" && autohide !== false;
|
this.autoHide = options.autoHide !== "false";
|
||||||
|
|
||||||
this.elem = document.createElement("li");
|
this.elem = document.createElement("li");
|
||||||
this.elem.id = this.id;
|
this.elem.id = this.id;
|
||||||
@ -96,13 +102,14 @@ export class MenuItemBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MenuItem extends MenuItemBase {
|
export class MenuItem extends MenuItemBase {
|
||||||
constructor(owner: ContextMenu, id = "", text = "", options: any = {}) {
|
constructor(
|
||||||
|
owner: ContextMenu, id = "", text = "", options: MenuOptions = {}) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
super(owner, id, text, options);
|
super(owner, id, text, options);
|
||||||
this.disabled = !!options.disabled;
|
this.disabled = options.disabled === "true";
|
||||||
this.elem.setAttribute("aria-role", "menuitem");
|
this.elem.setAttribute("aria-role", "menuitem");
|
||||||
this.elem.addEventListener(
|
this.elem.addEventListener(
|
||||||
"click", () => this.owner.emit("clicked", this.id, this.autohide));
|
"click", () => this.owner.emit("clicked", this.id, this.autoHide));
|
||||||
}
|
}
|
||||||
|
|
||||||
get disabled() {
|
get disabled() {
|
||||||
@ -132,7 +139,8 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
|
|
||||||
public readonly expandElem: HTMLSpanElement;
|
public readonly expandElem: HTMLSpanElement;
|
||||||
|
|
||||||
constructor(owner: ContextMenu, id = "", text = "", options: any = {}) {
|
constructor(
|
||||||
|
owner: ContextMenu, id = "", text = "", options: MenuOptions = {}) {
|
||||||
super(owner, id, text, options);
|
super(owner, id, text, options);
|
||||||
this.elem.setAttribute("aria-role", "menuitem");
|
this.elem.setAttribute("aria-role", "menuitem");
|
||||||
this.elem.setAttribute("aria-haspopup", "true");
|
this.elem.setAttribute("aria-haspopup", "true");
|
||||||
@ -145,8 +153,8 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
this.expandElem.textContent = "►";
|
this.expandElem.textContent = "►";
|
||||||
this.elem.appendChild(this.expandElem);
|
this.elem.appendChild(this.expandElem);
|
||||||
this.elem.addEventListener("click", event => {
|
this.elem.addEventListener("click", event => {
|
||||||
if (options.allowClick) {
|
if (options.allowClick === "true") {
|
||||||
this.owner.emit("clicked", this.id, this.autohide);
|
this.owner.emit("clicked", this.id, this.autoHide);
|
||||||
}
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -160,7 +168,7 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
this.owner.on("showing", () => {
|
this.owner.on("showing", () => {
|
||||||
this.menu.dismiss();
|
this.menu.dismiss();
|
||||||
});
|
});
|
||||||
this.menu.on("clicked", (...args: any) => {
|
this.menu.on("clicked", (...args: any[]) => {
|
||||||
this.owner.emit("clicked", ...args);
|
this.owner.emit("clicked", ...args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -215,7 +223,7 @@ export class SubMenuItem extends MenuItemBase {
|
|||||||
export class ContextMenu extends EventEmitter {
|
export class ContextMenu extends EventEmitter {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
items: any[];
|
items: MenuItemBase[];
|
||||||
|
|
||||||
itemMap: Map<string, MenuItemBase>;
|
itemMap: Map<string, MenuItemBase>;
|
||||||
|
|
||||||
@ -223,7 +231,7 @@ export class ContextMenu extends EventEmitter {
|
|||||||
|
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
|
|
||||||
_maybeDismiss: any;
|
_maybeDismiss: (this: Window, ev: MouseEvent) => any;
|
||||||
|
|
||||||
constructor(el?: any) {
|
constructor(el?: any) {
|
||||||
super();
|
super();
|
||||||
@ -348,10 +356,12 @@ export class ContextMenu extends EventEmitter {
|
|||||||
return this.itemMap.get(id);
|
return this.itemMap.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(item: MenuItemBase, before: any = "") {
|
add(item: MenuItemBase, before: MenuItemBase | string = "") {
|
||||||
let idx = this.items.length;
|
let idx = this.items.length;
|
||||||
if (before) {
|
if (before) {
|
||||||
before = before.id || before;
|
if (typeof before !== "string") {
|
||||||
|
before = before.id;
|
||||||
|
}
|
||||||
const ni = this.items.findIndex(i => i.id === before);
|
const ni = this.items.findIndex(i => i.id === before);
|
||||||
if (ni >= 0) {
|
if (ni >= 0) {
|
||||||
idx = ni;
|
idx = ni;
|
||||||
@ -366,8 +376,8 @@ export class ContextMenu extends EventEmitter {
|
|||||||
this.itemMap.set(item.id, item);
|
this.itemMap.set(item.id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(item: any) {
|
remove(item: MenuItemBase | string) {
|
||||||
const id = item.id || item;
|
const id = typeof item === "string" ? item : item.id;
|
||||||
const idx = this.items.findIndex(i => i.id === id);
|
const idx = this.items.findIndex(i => i.id === id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this.items.splice(idx, 1);
|
this.items.splice(idx, 1);
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
interface ModalButton {
|
export interface ModalButton {
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value: string;
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
dismiss?: boolean;
|
dismiss?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ModalDialog {
|
interface Promised {
|
||||||
private _showing: any;
|
resolve: Function;
|
||||||
|
reject: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default abstract class ModalDialog {
|
||||||
|
private _showing: Promised | null;
|
||||||
|
|
||||||
private _dismiss: HTMLButtonElement | null;
|
private _dismiss: HTMLButtonElement | null;
|
||||||
|
|
||||||
@ -23,7 +28,7 @@ export default class ModalDialog {
|
|||||||
this._default = null;
|
this._default = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeEl() {
|
async _makeEl() {
|
||||||
this._dismiss = null;
|
this._dismiss = null;
|
||||||
this._default = null;
|
this._default = null;
|
||||||
|
|
||||||
@ -35,7 +40,7 @@ export default class ModalDialog {
|
|||||||
|
|
||||||
const body = document.createElement("article");
|
const body = document.createElement("article");
|
||||||
body.classList.add("modal-body");
|
body.classList.add("modal-body");
|
||||||
body.appendChild(this.content);
|
body.appendChild(await this.getContent());
|
||||||
cont.appendChild(body);
|
cont.appendChild(body);
|
||||||
|
|
||||||
const footer = document.createElement("footer");
|
const footer = document.createElement("footer");
|
||||||
@ -87,9 +92,8 @@ export default class ModalDialog {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content(): DocumentFragment | HTMLElement {
|
abstract getContent():
|
||||||
throw new Error("Not implemented");
|
Promise<DocumentFragment | HTMLElement> | HTMLElement | DocumentFragment;
|
||||||
}
|
|
||||||
|
|
||||||
get buttons(): ModalButton[] {
|
get buttons(): ModalButton[] {
|
||||||
return [
|
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);
|
const value = this.convertValue(button.value);
|
||||||
if (button.dismiss) {
|
if (button.dismiss) {
|
||||||
this._showing.reject(new Error(value));
|
this._showing.reject(new Error(value));
|
||||||
@ -131,7 +138,7 @@ export default class ModalDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async show() {
|
async show(): Promise<any> {
|
||||||
if (this._showing) {
|
if (this._showing) {
|
||||||
throw new Error("Double show");
|
throw new Error("Double show");
|
||||||
}
|
}
|
||||||
@ -160,7 +167,7 @@ export default class ModalDialog {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.appendChild(this.el = this._makeEl());
|
document.body.appendChild(this.el = await this._makeEl());
|
||||||
this.shown();
|
this.shown();
|
||||||
addEventListener("keydown", escapeHandler);
|
addEventListener("keydown", escapeHandler);
|
||||||
addEventListener("keydown", enterHandler);
|
addEventListener("keydown", enterHandler);
|
||||||
@ -205,7 +212,7 @@ export default class ModalDialog {
|
|||||||
*/
|
*/
|
||||||
static async inform(title: string, text: string, oktext: string) {
|
static async inform(title: string, text: string, oktext: string) {
|
||||||
const dialog = new class extends ModalDialog {
|
const dialog = new class extends ModalDialog {
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = document.createDocumentFragment();
|
const rv = document.createDocumentFragment();
|
||||||
const h = document.createElement("h1");
|
const h = document.createElement("h1");
|
||||||
h.textContent = title || "Information";
|
h.textContent = title || "Information";
|
||||||
@ -241,7 +248,7 @@ export default class ModalDialog {
|
|||||||
|
|
||||||
static async confirm(title: string, text: string) {
|
static async confirm(title: string, text: string) {
|
||||||
const dialog = new class extends ModalDialog {
|
const dialog = new class extends ModalDialog {
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = document.createDocumentFragment();
|
const rv = document.createDocumentFragment();
|
||||||
const h = document.createElement("h1");
|
const h = document.createElement("h1");
|
||||||
h.textContent = title || "Confirm";
|
h.textContent = title || "Confirm";
|
||||||
@ -280,7 +287,7 @@ export default class ModalDialog {
|
|||||||
const dialog = new class extends ModalDialog {
|
const dialog = new class extends ModalDialog {
|
||||||
_input: HTMLInputElement;
|
_input: HTMLInputElement;
|
||||||
|
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = document.createDocumentFragment();
|
const rv = document.createDocumentFragment();
|
||||||
const h = document.createElement("h1");
|
const h = document.createElement("h1");
|
||||||
h.textContent = title || "Confirm";
|
h.textContent = title || "Confirm";
|
||||||
|
@ -38,7 +38,7 @@ class Hover {
|
|||||||
|
|
||||||
private hovering: boolean;
|
private hovering: boolean;
|
||||||
|
|
||||||
private timer: any;
|
private timer: number | null;
|
||||||
|
|
||||||
constructor(row: Row) {
|
constructor(row: Row) {
|
||||||
this.row = row;
|
this.row = row;
|
||||||
@ -62,7 +62,7 @@ class Hover {
|
|||||||
this.elem.addEventListener("mousemove", this.onmove, {passive: true});
|
this.elem.addEventListener("mousemove", this.onmove, {passive: true});
|
||||||
this.x = evt.clientX;
|
this.x = evt.clientX;
|
||||||
this.y = evt.clientY;
|
this.y = evt.clientY;
|
||||||
this.timer = setTimeout(this.onhover, HOVER_TIME);
|
this.timer = window.setTimeout(this.onhover, HOVER_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
onleave() {
|
onleave() {
|
||||||
@ -93,7 +93,7 @@ class Hover {
|
|||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
}
|
}
|
||||||
this.timer = setTimeout(this.onhover, HOVER_TIME);
|
this.timer = window.setTimeout(this.onhover, HOVER_TIME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import {Row} from "./row";
|
|||||||
import {APOOL} from "./animationpool";
|
import {APOOL} from "./animationpool";
|
||||||
import {COLS, ROWCACHE, VISIBLE} from "./tablesymbols";
|
import {COLS, ROWCACHE, VISIBLE} from "./tablesymbols";
|
||||||
import {ContextMenu, MenuItem} from "./contextmenu";
|
import {ContextMenu, MenuItem} from "./contextmenu";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { TableConfig } from "./config";
|
||||||
|
|
||||||
const RESIZE_DEBOUNCE = 500;
|
const RESIZE_DEBOUNCE = 500;
|
||||||
const SCROLL_DEBOUNCE = 250;
|
const SCROLL_DEBOUNCE = 250;
|
||||||
@ -19,7 +21,7 @@ const SCROLL_DEBOUNCE = 250;
|
|||||||
export class TableEvents extends BaseTable {
|
export class TableEvents extends BaseTable {
|
||||||
private oldVisibleTop: number;
|
private oldVisibleTop: number;
|
||||||
|
|
||||||
constructor(elem: any, config?: any, version?: number) {
|
constructor(elem: any, config: TableConfig | null, version?: number) {
|
||||||
super(elem, config, version);
|
super(elem, config, version);
|
||||||
const {selection} = this;
|
const {selection} = this;
|
||||||
selection.on("selection-added", this.selectionAdded.bind(this));
|
selection.on("selection-added", this.selectionAdded.bind(this));
|
||||||
@ -172,7 +174,7 @@ export class TableEvents extends BaseTable {
|
|||||||
ctx,
|
ctx,
|
||||||
id,
|
id,
|
||||||
col.spanElem.textContent || "",
|
col.spanElem.textContent || "",
|
||||||
{autohide: "false"});
|
{autoHide: "false"});
|
||||||
ctx.add(item);
|
ctx.add(item);
|
||||||
item.iconElem.textContent = col.visible ? "✓" : " ";
|
item.iconElem.textContent = col.visible ? "✓" : " ";
|
||||||
ctx.on(id, async () => {
|
ctx.on(id, async () => {
|
||||||
|
@ -8,6 +8,8 @@ import { Row } from "./row";
|
|||||||
import {APOOL} from "./animationpool";
|
import {APOOL} from "./animationpool";
|
||||||
import {ROW_CACHE_SIZE, ROW_REUSE_SIZE} from "./constants";
|
import {ROW_CACHE_SIZE, ROW_REUSE_SIZE} from "./constants";
|
||||||
import {clampUInt} from "./util";
|
import {clampUInt} from "./util";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseTable } from "./basetable";
|
||||||
|
|
||||||
|
|
||||||
export class InvalidatedSet<T> extends Set<T> {
|
export class InvalidatedSet<T> extends Set<T> {
|
||||||
@ -70,7 +72,7 @@ export class UpdateRecord {
|
|||||||
|
|
||||||
bottom: number;
|
bottom: number;
|
||||||
|
|
||||||
constructor(table: any, cols: Column[]) {
|
constructor(table: BaseTable, cols: Column[]) {
|
||||||
this.rowCount = table.rowCount;
|
this.rowCount = table.rowCount;
|
||||||
this.scrollTop = table.visibleTop;
|
this.scrollTop = table.visibleTop;
|
||||||
this.rowHeight = table.rowHeight;
|
this.rowHeight = table.rowHeight;
|
||||||
|
@ -13,14 +13,21 @@ export function addClass(elem: HTMLElement, ...cls: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Timer {
|
||||||
|
args: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export function debounce(fn: Function, to: number) {
|
export function debounce(fn: Function, to: number) {
|
||||||
let timer: any;
|
let timer: Timer | null;
|
||||||
return function(...args: any[]) {
|
return function(...args: any[]) {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
timer.args = args;
|
timer.args = args;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
if (!timer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const {args} = timer;
|
const {args} = timer;
|
||||||
timer = null;
|
timer = null;
|
||||||
try {
|
try {
|
||||||
@ -38,7 +45,7 @@ function sumreduce(p: number, c: number) {
|
|||||||
return p + c;
|
return p + c;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sum(arr: any[]) {
|
export function sum(arr: number[]) {
|
||||||
return arr.reduce(sumreduce, 0);
|
return arr.reduce(sumreduce, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ module.exports = {
|
|||||||
"select": "./windows/select.ts",
|
"select": "./windows/select.ts",
|
||||||
"single": "./windows/single.ts",
|
"single": "./windows/single.ts",
|
||||||
"prefs": "./windows/prefs.ts",
|
"prefs": "./windows/prefs.ts",
|
||||||
|
"content-popup": "./windows/popup.ts",
|
||||||
"content-gather": "./scripts/gather.ts",
|
"content-gather": "./scripts/gather.ts",
|
||||||
},
|
},
|
||||||
externals(context, request, callback) {
|
externals(context, request, callback) {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import {Keys} from "./keys";
|
import { Keys } from "./keys";
|
||||||
|
import { $ } from "./winutil";
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
export class Broadcaster {
|
export class Broadcaster {
|
||||||
private readonly els: HTMLElement[];
|
private readonly els: HTMLElement[];
|
||||||
@ -38,7 +37,7 @@ export class Broadcaster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onkey(evt: KeyboardEvent) {
|
onkey(evt: KeyboardEvent) {
|
||||||
const {localName} = evt.target as HTMLElement;
|
const { localName } = evt.target as HTMLElement;
|
||||||
if (localName === "input" || localName === "textarea") {
|
if (localName === "input" || localName === "textarea") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export class Dropdown extends EventEmitter {
|
|||||||
|
|
||||||
select: HTMLSelectElement;
|
select: HTMLSelectElement;
|
||||||
|
|
||||||
constructor(el: string, options: any[] = []) {
|
constructor(el: string, options: string[] = []) {
|
||||||
super();
|
super();
|
||||||
let input = document.querySelector(el);
|
let input = document.querySelector(el);
|
||||||
if (!input || !input.parentElement) {
|
if (!input || !input.parentElement) {
|
||||||
|
@ -128,8 +128,8 @@
|
|||||||
<template id="menufilter-template">
|
<template id="menufilter-template">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="ctx-menufilter-seperator">-</li>
|
<li id="ctx-menufilter-seperator">-</li>
|
||||||
<li id="ctx-menufilter-invert" data-autohide="false">Invert</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-clear" data-autoHide="false">Clear</li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
<li id="ctx-menufilter-sort-ascending" data-icon="icon-sort-asc">Sort ascending</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>
|
<li id="ctx-menufilter-sort-descending" data-icon="icon-sort-desc">Sort descending</li>
|
||||||
|
@ -6,6 +6,8 @@ import {_, localize} from "../lib/i18n";
|
|||||||
import {Prefs} from "../lib/prefs";
|
import {Prefs} from "../lib/prefs";
|
||||||
import PORT from "./manager/port";
|
import PORT from "./manager/port";
|
||||||
import { runtime } from "../lib/browser";
|
import { runtime } from "../lib/browser";
|
||||||
|
import { Promised } from "../lib/util";
|
||||||
|
import { PromiseSerializer } from "../lib/pserializer";
|
||||||
|
|
||||||
const $ = document.querySelector.bind(document);
|
const $ = document.querySelector.bind(document);
|
||||||
|
|
||||||
@ -18,7 +20,29 @@ const LOADED = new Promise(resolve => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
LOADED.then(async () => {
|
addEventListener("DOMContentLoaded", function dom() {
|
||||||
|
removeEventListener("DOMContentLoaded", dom);
|
||||||
|
|
||||||
|
const platformed = (async () => {
|
||||||
|
try {
|
||||||
|
const platform = (await runtime.getPlatformInfo()).os;
|
||||||
|
document.documentElement.dataset.platform = platform;
|
||||||
|
if (platform === "mac") {
|
||||||
|
const ctx = $("#table-context").content;
|
||||||
|
ctx.querySelector("#ctx-open-file").dataset.key = "ACCEL-KeyO";
|
||||||
|
ctx.querySelector("#ctx-open-directory").dataset.key = "ALT-ACCEL-KeyO";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("failed to setup platform", ex.toString(), ex.stack, ex);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
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 nag = await Prefs.get("nagging", 0);
|
||||||
const nagnext = await Prefs.get("nagging-next", 6);
|
const nagnext = await Prefs.get("nagging-next", 6);
|
||||||
const next = Math.ceil(Math.log2(Math.max(1, nag)));
|
const next = Math.ceil(Math.log2(Math.max(1, nag)));
|
||||||
@ -47,29 +71,8 @@ LOADED.then(async () => {
|
|||||||
"nagging-message", nag.toLocaleString());
|
"nagging-message", nag.toLocaleString());
|
||||||
$("#nagging").classList.remove("hidden");
|
$("#nagging").classList.remove("hidden");
|
||||||
}, 2 * 1000);
|
}, 2 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("DOMContentLoaded", function dom() {
|
|
||||||
removeEventListener("DOMContentLoaded", dom);
|
|
||||||
|
|
||||||
const platformed = (async () => {
|
|
||||||
try {
|
|
||||||
const platform = (await runtime.getPlatformInfo()).os;
|
|
||||||
document.documentElement.dataset.platform = platform;
|
|
||||||
if (platform === "mac") {
|
|
||||||
const ctx = $("#table-context").content;
|
|
||||||
ctx.querySelector("#ctx-open-file").dataset.key = "ACCEL-KeyO";
|
|
||||||
ctx.querySelector("#ctx-open-directory").dataset.key = "ALT-ACCEL-KeyO";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.error("failed to setup platform", ex.toString(), ex.stack, ex);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
const loaded = Promise.all([LOADED, platformed]);
|
|
||||||
|
|
||||||
localize(document.documentElement);
|
|
||||||
$("#donate").addEventListener("click", () => {
|
$("#donate").addEventListener("click", () => {
|
||||||
PORT.post("donate");
|
PORT.post("donate");
|
||||||
});
|
});
|
||||||
@ -85,24 +88,29 @@ addEventListener("DOMContentLoaded", function dom() {
|
|||||||
Table.init();
|
Table.init();
|
||||||
const loading = $("#loading");
|
const loading = $("#loading");
|
||||||
loading.parentElement.removeChild(loading);
|
loading.parentElement.removeChild(loading);
|
||||||
|
tabled.resolve();
|
||||||
}
|
}
|
||||||
Table.setItems(items);
|
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);
|
Table.updateItems(items);
|
||||||
});
|
}));
|
||||||
PORT.on("removed", async sids => {
|
PORT.on("removed", serializer.wrap(this, async (sids: number[]) => {
|
||||||
await loaded;
|
await fullyloaded;
|
||||||
Table.removedItems(sids);
|
Table.removedItems(sids);
|
||||||
});
|
}));
|
||||||
|
|
||||||
const statusNetwork = $("#statusNetwork");
|
const statusNetwork = $("#statusNetwork");
|
||||||
statusNetwork.addEventListener("click", () => {
|
statusNetwork.addEventListener("click", () => {
|
||||||
PORT.post("toggle-active");
|
PORT.post("toggle-active");
|
||||||
});
|
});
|
||||||
PORT.on("active", active => {
|
PORT.on("active", async (active: boolean) => {
|
||||||
|
await loaded;
|
||||||
if (active) {
|
if (active) {
|
||||||
statusNetwork.className = "icon-network-on";
|
statusNetwork.className = "icon-network-on";
|
||||||
statusNetwork.setAttribute("title", _("statusNetwork-active.title"));
|
statusNetwork.setAttribute("title", _("statusNetwork-active.title"));
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { EventEmitter } from "../../lib/events";
|
import { EventEmitter } from "../../lib/events";
|
||||||
|
import { $ } from "../winutil";
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
export class Buttons extends EventEmitter {
|
export class Buttons extends EventEmitter {
|
||||||
private readonly parent: HTMLElement;
|
private readonly parent: HTMLElement;
|
||||||
|
@ -19,12 +19,10 @@ import {sort, defaultCompare, naturalCaseCompare} from "../../lib/sorting";
|
|||||||
import {DownloadItem, DownloadTable} from "./table";
|
import {DownloadItem, DownloadTable} from "./table";
|
||||||
import {formatSize} from "../../lib/formatters";
|
import {formatSize} from "../../lib/formatters";
|
||||||
import {_} from "../../lib/i18n";
|
import {_} from "../../lib/i18n";
|
||||||
import {StateTexts} from "./state";
|
import {$} from "../winutil";
|
||||||
|
|
||||||
const TIMEOUT_SEARCH = 750;
|
const TIMEOUT_SEARCH = 750;
|
||||||
|
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
class ItemFilter {
|
class ItemFilter {
|
||||||
public readonly id: string;
|
public readonly id: string;
|
||||||
|
|
||||||
@ -43,7 +41,7 @@ export class TextFilter extends ItemFilter {
|
|||||||
|
|
||||||
private box: HTMLInputElement;
|
private box: HTMLInputElement;
|
||||||
|
|
||||||
private timer: any;
|
private timer: number | null;
|
||||||
|
|
||||||
private current: string;
|
private current: string;
|
||||||
|
|
||||||
@ -60,7 +58,7 @@ export class TextFilter extends ItemFilter {
|
|||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.timer = setTimeout(() => this.update(), TIMEOUT_SEARCH);
|
this.timer = window.setTimeout(() => this.update(), TIMEOUT_SEARCH);
|
||||||
});
|
});
|
||||||
this.box.addEventListener("keydown", e => {
|
this.box.addEventListener("keydown", e => {
|
||||||
if (e.key !== "Escape") {
|
if (e.key !== "Escape") {
|
||||||
@ -119,8 +117,10 @@ export class MenuFilter extends ItemFilter {
|
|||||||
constructor(id: string) {
|
constructor(id: string) {
|
||||||
super(id);
|
super(id);
|
||||||
this.items = new Map();
|
this.items = new Map();
|
||||||
|
const tmpl = $<HTMLTemplateElement>("#menufilter-template").
|
||||||
|
content.cloneNode(true);
|
||||||
this.menu = new ContextMenu(
|
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("clicked", this.onclicked.bind(this));
|
||||||
this.menu.on("ctx-menufilter-invert", () => this.invert());
|
this.menu.on("ctx-menufilter-invert", () => this.invert());
|
||||||
this.menu.on("ctx-menufilter-clear", () => this.clear());
|
this.menu.on("ctx-menufilter-clear", () => this.clear());
|
||||||
@ -152,7 +152,7 @@ export class MenuFilter extends ItemFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = new MenuItem(this.menu, id, text, {
|
const item = new MenuItem(this.menu, id, text, {
|
||||||
autohide: false,
|
autoHide: "false",
|
||||||
});
|
});
|
||||||
item.iconElem.textContent = checked ? "✓" : "";
|
item.iconElem.textContent = checked ? "✓" : "";
|
||||||
this.items.set(id, {item, callback});
|
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 {
|
class FixedMenuFilter extends MenuFilter {
|
||||||
collection: FilteredCollection;
|
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);
|
super(id);
|
||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
this.selected = new Set();
|
this.selected = new Set();
|
||||||
@ -226,7 +234,7 @@ class FixedMenuFilter extends MenuFilter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(item: MenuItem) {
|
toggle(item: ChainedItem) {
|
||||||
if (this.selected.has(item)) {
|
if (this.selected.has(item)) {
|
||||||
this.selected.delete(item);
|
this.selected.delete(item);
|
||||||
}
|
}
|
||||||
@ -241,14 +249,18 @@ class FixedMenuFilter extends MenuFilter {
|
|||||||
this.collection.removeFilter(this);
|
this.collection.removeFilter(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.chain = Array.from(this.selected).reduce((prev, curr) => {
|
this.chain = null;
|
||||||
return (item: DownloadItem) => curr.fn(item) || (prev && prev(item));
|
this.chain = Array.from(this.selected).reduce(
|
||||||
}, null);
|
(prev: ChainedFunction | null, curr) => {
|
||||||
|
return (item: DownloadItem) => {
|
||||||
|
return curr.fn(item) || (prev !== null && prev(item));
|
||||||
|
};
|
||||||
|
}, this.chain);
|
||||||
this.collection.addFilter(this);
|
this.collection.addFilter(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
allow(item: DownloadItem) {
|
allow(item: DownloadItem) {
|
||||||
return this.chain(item);
|
return this.chain !== null && this.chain(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@ -259,7 +271,9 @@ class FixedMenuFilter extends MenuFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StateMenuFilter extends FixedMenuFilter {
|
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]) => {
|
const items = Array.from(StateTexts.entries()).map(([state, text]) => {
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
@ -339,7 +353,7 @@ export class UrlMenuFilter extends MenuFilter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleRegularFilter(filter: any) {
|
toggleRegularFilter(filter: Filter) {
|
||||||
if (this.filters.has(filter)) {
|
if (this.filters.has(filter)) {
|
||||||
this.filters.delete(filter);
|
this.filters.delete(filter);
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,18 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { EventEmitter } from "../../lib/events";
|
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 {
|
const PORT = new class Port extends EventEmitter {
|
||||||
port: any;
|
port: RawPort | null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.port = runtime.connect(null, { name: "manager" });
|
this.port = runtime.connect(null, { name: "manager" });
|
||||||
|
if (!this.port) {
|
||||||
|
throw new Error("Could not connect");
|
||||||
|
}
|
||||||
this.port.onMessage.addListener((msg: any) => {
|
this.port.onMessage.addListener((msg: any) => {
|
||||||
if (typeof msg === "string") {
|
if (typeof msg === "string") {
|
||||||
this.emit(msg);
|
this.emit(msg);
|
||||||
@ -23,10 +27,16 @@ const PORT = new class Port extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post(msg: string, data?: any) {
|
post(msg: string, data?: any) {
|
||||||
|
if (!this.port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.port.postMessage(Object.assign({msg}, data));
|
this.port.postMessage(Object.assign({msg}, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
if (!this.port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.port.disconnect();
|
this.port.disconnect();
|
||||||
this.port = null;
|
this.port = null;
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ import ModalDialog from "../../uikit/lib/modal";
|
|||||||
import { _, localize } from "../../lib/i18n";
|
import { _, localize } from "../../lib/i18n";
|
||||||
import { Prefs } from "../../lib/prefs";
|
import { Prefs } from "../../lib/prefs";
|
||||||
import { Keys } from "../keys";
|
import { Keys } from "../keys";
|
||||||
|
import { $ } from "../winutil";
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
export default class RemovalModalDialog extends ModalDialog {
|
export default class RemovalModalDialog extends ModalDialog {
|
||||||
private readonly text: string;
|
private readonly text: string;
|
||||||
@ -22,11 +21,12 @@ export default class RemovalModalDialog extends ModalDialog {
|
|||||||
this.check = null;
|
this.check = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content() {
|
async getContent() {
|
||||||
const content = $("#removal-template").content.cloneNode(true);
|
const content = $<HTMLTemplateElement>("#removal-template").
|
||||||
localize(content);
|
content.cloneNode(true) as DocumentFragment;
|
||||||
|
await localize(content);
|
||||||
this.check = content.querySelector(".removal-remember");
|
this.check = content.querySelector(".removal-remember");
|
||||||
content.querySelector(".removal-text").textContent = this.text;
|
$(".removal-text", content).textContent = this.text;
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import * as _DownloadState from "../../lib/manager/state";
|
import * as _DownloadState from "../../lib/manager/state";
|
||||||
import { _ } from "../../lib/i18n";
|
import { _, locale } from "../../lib/i18n";
|
||||||
|
|
||||||
export const DownloadState = _DownloadState;
|
export const DownloadState = _DownloadState;
|
||||||
|
|
||||||
export const StateTexts = Object.freeze(new Map([
|
export const StateTexts = locale.then(() => Object.freeze(new Map([
|
||||||
[DownloadState.QUEUED, _("queued")],
|
[DownloadState.QUEUED, _("queued")],
|
||||||
[DownloadState.RUNNING, _("running")],
|
[DownloadState.RUNNING, _("running")],
|
||||||
[DownloadState.FINISHING, _("finishing")],
|
[DownloadState.FINISHING, _("finishing")],
|
||||||
@ -14,7 +14,7 @@ export const StateTexts = Object.freeze(new Map([
|
|||||||
[DownloadState.DONE, _("done")],
|
[DownloadState.DONE, _("done")],
|
||||||
[DownloadState.CANCELED, _("canceled")],
|
[DownloadState.CANCELED, _("canceled")],
|
||||||
[DownloadState.MISSING, _("missing")],
|
[DownloadState.MISSING, _("missing")],
|
||||||
]));
|
])));
|
||||||
|
|
||||||
export const StateClasses = Object.freeze(new Map([
|
export const StateClasses = Object.freeze(new Map([
|
||||||
[DownloadState.QUEUED, "queued"],
|
[DownloadState.QUEUED, "queued"],
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import { iconForPath } from "../../lib/windowutils";
|
import { iconForPath } from "../../lib/windowutils";
|
||||||
import { formatSpeed, formatSize, formatTimeDelta } from "../../lib/formatters";
|
import { formatSpeed, formatSize, formatTimeDelta } from "../../lib/formatters";
|
||||||
import { filters } from "../../lib/filters";
|
import { filters } from "../../lib/filters";
|
||||||
import { _, localize } from "../../lib/i18n";
|
import { _ } from "../../lib/i18n";
|
||||||
import { EventEmitter } from "../../lib/events";
|
import { EventEmitter } from "../../lib/events";
|
||||||
import { Prefs, PrefWatcher } from "../../lib/prefs";
|
import { Prefs, PrefWatcher } from "../../lib/prefs";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
@ -34,6 +34,9 @@ import { Tooltip } from "./tooltip";
|
|||||||
import "../../lib/util";
|
import "../../lib/util";
|
||||||
import { CellTypes } from "../../uikit/lib/constants";
|
import { CellTypes } from "../../uikit/lib/constants";
|
||||||
import { downloads } from "../../lib/browser";
|
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 TREE_CONFIG_VERSION = 2;
|
||||||
const RUNNING_TIMEOUT = 1000;
|
const RUNNING_TIMEOUT = 1000;
|
||||||
@ -51,9 +54,11 @@ const COL_SEGS = 8;
|
|||||||
|
|
||||||
const ICON_BASE_SIZE = 16;
|
const ICON_BASE_SIZE = 16;
|
||||||
|
|
||||||
const TEXT_SIZE_UNKNOWM = _("size-unknown");
|
let TEXT_SIZE_UNKNOWM = "unknown";
|
||||||
|
let REAL_STATE_TEXTS = Object.freeze(new Map<number, string>());
|
||||||
const $ = document.querySelector.bind(document);
|
StateTexts.then(v => {
|
||||||
|
REAL_STATE_TEXTS = v;
|
||||||
|
});
|
||||||
|
|
||||||
const prettyNumber = (function() {
|
const prettyNumber = (function() {
|
||||||
const rv = new Intl.NumberFormat(undefined, {
|
const rv = new Intl.NumberFormat(undefined, {
|
||||||
@ -189,9 +194,9 @@ export class DownloadItem extends EventEmitter {
|
|||||||
return this.eta;
|
return this.eta;
|
||||||
}
|
}
|
||||||
if (this.error) {
|
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() {
|
get fmtSpeed() {
|
||||||
@ -308,9 +313,9 @@ export class DownloadTable extends VirtualTable {
|
|||||||
|
|
||||||
public readonly showUrls: ShowUrlsWatcher;
|
public readonly showUrls: ShowUrlsWatcher;
|
||||||
|
|
||||||
private runningTimer: any;
|
private runningTimer: number | null;
|
||||||
|
|
||||||
private sizesTimer: any;
|
private sizesTimer: number | null;
|
||||||
|
|
||||||
private readonly globalStats: Stats;
|
private readonly globalStats: Stats;
|
||||||
|
|
||||||
@ -326,7 +331,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
|
|
||||||
private readonly openFileAction: Broadcaster;
|
private readonly openFileAction: Broadcaster;
|
||||||
|
|
||||||
private readonly openDirectoryAction: any;
|
private readonly openDirectoryAction: Broadcaster;
|
||||||
|
|
||||||
private readonly moveTopAction: Broadcaster;
|
private readonly moveTopAction: Broadcaster;
|
||||||
|
|
||||||
@ -336,13 +341,15 @@ export class DownloadTable extends VirtualTable {
|
|||||||
|
|
||||||
private readonly moveBottomAction: Broadcaster;
|
private readonly moveBottomAction: Broadcaster;
|
||||||
|
|
||||||
private readonly disableSet: Set<any>;
|
private readonly disableSet: Set<Broadcaster>;
|
||||||
|
|
||||||
private tooltip: Tooltip | null;
|
private tooltip: Tooltip | null;
|
||||||
|
|
||||||
constructor(treeConfig: any) {
|
constructor(treeConfig: TableConfig | null) {
|
||||||
super("#items", treeConfig, TREE_CONFIG_VERSION);
|
super("#items", treeConfig, TREE_CONFIG_VERSION);
|
||||||
|
|
||||||
|
TEXT_SIZE_UNKNOWM = _("size-unknown");
|
||||||
|
|
||||||
this.finished = 0;
|
this.finished = 0;
|
||||||
this.running = new Set();
|
this.running = new Set();
|
||||||
this.runningTimer = null;
|
this.runningTimer = null;
|
||||||
@ -363,7 +370,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
new TextFilter(this.downloads);
|
new TextFilter(this.downloads);
|
||||||
const menufilters = new Map<string, MenuFilter>([
|
const menufilters = new Map<string, MenuFilter>([
|
||||||
["colURL", new UrlMenuFilter(this.downloads)],
|
["colURL", new UrlMenuFilter(this.downloads)],
|
||||||
["colETA", new StateMenuFilter(this.downloads)],
|
["colETA", new StateMenuFilter(this.downloads, REAL_STATE_TEXTS)],
|
||||||
["colSize", new SizeMenuFilter(this.downloads)],
|
["colSize", new SizeMenuFilter(this.downloads)],
|
||||||
]);
|
]);
|
||||||
this.on("column-clicked", (id, evt, col) => {
|
this.on("column-clicked", (id, evt, col) => {
|
||||||
@ -403,7 +410,6 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.sids = new Map<number, DownloadItem>();
|
this.sids = new Map<number, DownloadItem>();
|
||||||
this.icons = new Icons($("#icons"));
|
this.icons = new Icons($("#icons"));
|
||||||
|
|
||||||
localize($("#table-context").content);
|
|
||||||
const ctx = this.contextMenu = new ContextMenu("#table-context");
|
const ctx = this.contextMenu = new ContextMenu("#table-context");
|
||||||
Keys.adoptContext(ctx);
|
Keys.adoptContext(ctx);
|
||||||
Keys.adoptButtons($("#toolbar"));
|
Keys.adoptButtons($("#toolbar"));
|
||||||
@ -617,7 +623,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
filter(e => e.startsWith(prefix)).
|
filter(e => e.startsWith(prefix)).
|
||||||
forEach(e => rem.remove(e));
|
forEach(e => rem.remove(e));
|
||||||
for (const filt of filts.all) {
|
for (const filt of filts.all) {
|
||||||
if (filt.id === "deffilter-all") {
|
if (typeof filt.id !== "string" || filt.id === "deffilter-all") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mi = new MenuItem(rem, `${prefix}-${filt.id}`, filt.label, {
|
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);
|
const filter = (await filters()).get(id);
|
||||||
if (!filter) {
|
if (!filter || typeof filter.id !== "string") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await new RemovalModalDialog(
|
await new RemovalModalDialog(
|
||||||
@ -962,7 +968,7 @@ export class DownloadTable extends VirtualTable {
|
|||||||
switch (oldState) {
|
switch (oldState) {
|
||||||
case DownloadState.RUNNING:
|
case DownloadState.RUNNING:
|
||||||
this.running.delete(item);
|
this.running.delete(item);
|
||||||
if (!this.running.size && this.runningTimer) {
|
if (!this.running.size && this.runningTimer && this.sizesTimer) {
|
||||||
clearInterval(this.runningTimer);
|
clearInterval(this.runningTimer);
|
||||||
this.runningTimer = null;
|
this.runningTimer = null;
|
||||||
clearInterval(this.sizesTimer);
|
clearInterval(this.sizesTimer);
|
||||||
@ -979,9 +985,9 @@ export class DownloadTable extends VirtualTable {
|
|||||||
case DownloadState.RUNNING:
|
case DownloadState.RUNNING:
|
||||||
this.running.add(item);
|
this.running.add(item);
|
||||||
if (!this.runningTimer) {
|
if (!this.runningTimer) {
|
||||||
this.runningTimer = setInterval(
|
this.runningTimer = window.setInterval(
|
||||||
this.updateRunning.bind(this), RUNNING_TIMEOUT);
|
this.updateRunning.bind(this), RUNNING_TIMEOUT);
|
||||||
this.sizesTimer = setInterval(
|
this.sizesTimer = window.setInterval(
|
||||||
this.updateSizes.bind(this), SIZES_TIMEOUT);
|
this.updateSizes.bind(this), SIZES_TIMEOUT);
|
||||||
this.updateRunning();
|
this.updateRunning();
|
||||||
this.updateSizes();
|
this.updateSizes();
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { _, localize } from "../../lib/i18n";
|
import { _ } from "../../lib/i18n";
|
||||||
import { formatSpeed } from "../../lib/formatters";
|
import { formatSpeed } from "../../lib/formatters";
|
||||||
import { DownloadState } from "./state";
|
import { DownloadState } from "./state";
|
||||||
import { Rect } from "../../uikit/lib/rect";
|
import { Rect } from "../../uikit/lib/rect";
|
||||||
@ -155,7 +155,7 @@ export class Tooltip {
|
|||||||
if (!el) {
|
if (!el) {
|
||||||
throw new Error("invalid template");
|
throw new Error("invalid template");
|
||||||
}
|
}
|
||||||
this.elem = localize(el.cloneNode(true) as HTMLElement);
|
this.elem = el.cloneNode(true) as HTMLElement;
|
||||||
this.adjust(pos);
|
this.adjust(pos);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
@ -184,12 +184,13 @@ export class Tooltip {
|
|||||||
this.from.textContent = item.usable;
|
this.from.textContent = item.usable;
|
||||||
this.size.textContent = item.fmtSize;
|
this.size.textContent = item.fmtSize;
|
||||||
this.date.textContent = new Date(item.startDate).toLocaleString();
|
this.date.textContent = new Date(item.startDate).toLocaleString();
|
||||||
|
this.eta.textContent = item.fmtETA;
|
||||||
|
|
||||||
const running = item.state === DownloadState.RUNNING;
|
const running = item.state === DownloadState.RUNNING;
|
||||||
const hidden = this.eta.classList.contains("hidden");
|
const hidden = this.speedbox.classList.contains("hidden");
|
||||||
|
|
||||||
if (!running && !hidden) {
|
if (!running && !hidden) {
|
||||||
this.eta.classList.add("hidden");
|
this.eta.style.fontWeight = "bold";
|
||||||
this.etalabel.classList.add("hidden");
|
this.etalabel.classList.add("hidden");
|
||||||
this.speedbox.classList.add("hidden");
|
this.speedbox.classList.add("hidden");
|
||||||
this.progressbar.classList.add("hidden");
|
this.progressbar.classList.add("hidden");
|
||||||
@ -199,14 +200,13 @@ export class Tooltip {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
this.eta.classList.remove("hidden");
|
this.eta.style.fontWeight = "auto";
|
||||||
this.etalabel.classList.remove("hidden");
|
this.etalabel.classList.remove("hidden");
|
||||||
this.speedbox.classList.remove("hidden");
|
this.speedbox.classList.remove("hidden");
|
||||||
this.progressbar.classList.remove("hidden");
|
this.progressbar.classList.remove("hidden");
|
||||||
this.adjust(null);
|
this.adjust(null);
|
||||||
}
|
}
|
||||||
this.progress.style.width = `${item.percent * 100}%`;
|
this.progress.style.width = `${item.percent * 100}%`;
|
||||||
this.eta.textContent = item.fmtETA;
|
|
||||||
this.current.textContent = formatSpeed(item.stats.current);
|
this.current.textContent = formatSpeed(item.stats.current);
|
||||||
this.average.textContent = formatSpeed(item.stats.avg);
|
this.average.textContent = formatSpeed(item.stats.avg);
|
||||||
this.drawSpeeds();
|
this.drawSpeeds();
|
||||||
|
100
windows/popup.html
Normal 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
@ -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);
|
||||||
|
});
|
@ -45,6 +45,7 @@
|
|||||||
<legend data-i18n="pref.ui">UI</legend>
|
<legend data-i18n="pref.ui">UI</legend>
|
||||||
<label><input type="checkbox" id="pref-global-turbo"> <span data-i18n="pref-global-turbo">Global turbo</span></label>
|
<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-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;">
|
<div style="margin-top: 1em;">
|
||||||
<button id="reset-confirmations" data-i18n="reset-confirmations"></button>
|
<button id="reset-confirmations" data-i18n="reset-confirmations"></button>
|
||||||
<button id="reset-layout" data-i18n="reset-layouts"></button>
|
<button id="reset-layout" data-i18n="reset-layouts"></button>
|
||||||
@ -64,11 +65,20 @@
|
|||||||
<label><input type="checkbox" id="pref-remove-missing-on-init"> <span data-i18n="pref-remove-missing-on-init"></span></label>
|
<label><input type="checkbox" id="pref-remove-missing-on-init"> <span data-i18n="pref-remove-missing-on-init"></span></label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="pref-conflict-action">
|
<fieldset id="pref-conflict-action">
|
||||||
<legend>When a file exists</legend>
|
<legend data-i18="prefs.conflict">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="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="uniquify"> <span>Rename</span></label>
|
||||||
<!--<label><input type="radio" name="pref-conflict-action" value="prompt"> <span>Prompt</span></label>-->
|
<!--<label><input type="radio" name="pref-conflict-action" value="prompt"> <span>Prompt</span></label>-->
|
||||||
</fieldset>
|
</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>
|
</article>
|
||||||
|
|
||||||
<article id="tab-filters" class="tab">
|
<article id="tab-filters" class="tab">
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
import { _, localize } from "../lib/i18n";
|
import { _, localize, saveCustomLocale } from "../lib/i18n";
|
||||||
import { Prefs, PrefWatcher } from "../lib/prefs";
|
import { Prefs, PrefWatcher } from "../lib/prefs";
|
||||||
import { hostToDomain } from "../lib/util";
|
import { hostToDomain } from "../lib/util";
|
||||||
import { filters } from "../lib/filters";
|
import { filters } from "../lib/filters";
|
||||||
import {Limits} from "../lib/manager/limits";
|
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 { TYPE_LINK, TYPE_MEDIA } from "../lib/constants";
|
||||||
import { iconForPath, visible } from "../lib/windowutils";
|
import { iconForPath, visible } from "../lib/windowutils";
|
||||||
import { VirtualTable } from "../uikit/lib/table";
|
import { VirtualTable } from "../uikit/lib/table";
|
||||||
import { Icons } from "./icons";
|
import { Icons } from "./icons";
|
||||||
|
import { $ } from "./winutil";
|
||||||
|
import { runtime } from "../lib/browser";
|
||||||
|
|
||||||
const ICON_BASE_SIZE = 16;
|
const ICON_BASE_SIZE = 16;
|
||||||
|
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
class UIPref<T> extends PrefWatcher {
|
class UIPref<T extends HTMLElement> extends PrefWatcher {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
pref: string;
|
pref: string;
|
||||||
@ -101,20 +103,21 @@ class OptionPref extends UIPref<HTMLElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CreateFilterDialog extends ModalDialog {
|
class CreateFilterDialog extends ModalDialog {
|
||||||
label: any;
|
label: HTMLInputElement;
|
||||||
|
|
||||||
expr: any;
|
expr: HTMLInputElement;
|
||||||
|
|
||||||
link: any;
|
link: HTMLInputElement;
|
||||||
|
|
||||||
media: any;
|
media: HTMLInputElement;
|
||||||
|
|
||||||
get content() {
|
getContent() {
|
||||||
const rv = localize($("#create-filter-template").content.cloneNode(true));
|
const rv = $<HTMLTemplateElement>("#create-filter-template").
|
||||||
this.label = rv.querySelector("#filter-create-label");
|
content.cloneNode(true) as DocumentFragment;
|
||||||
this.expr = rv.querySelector("#filter-create-expr");
|
this.label = $("#filter-create-label", rv);
|
||||||
this.link = rv.querySelector("#filter-create-type-link");
|
this.expr = $("#filter-create-expr", rv);
|
||||||
this.media = rv.querySelector("#filter-create-type-media");
|
this.link = $("#filter-create-type-link", rv);
|
||||||
|
this.media = $("#filter-create-type-media", rv);
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +140,7 @@ class CreateFilterDialog extends ModalDialog {
|
|||||||
this.label.focus();
|
this.label.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
done(b: any) {
|
done(b: ModalButton) {
|
||||||
if (!b || !b.default) {
|
if (!b || !b.default) {
|
||||||
return super.done(b);
|
return super.done(b);
|
||||||
}
|
}
|
||||||
@ -209,7 +212,7 @@ class FiltersUI extends VirtualTable {
|
|||||||
ignoreNext: boolean;
|
ignoreNext: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("#filters");
|
super("#filters", null);
|
||||||
this.filters = [];
|
this.filters = [];
|
||||||
this.icons = new Icons($("#icons"));
|
this.icons = new Icons($("#icons"));
|
||||||
const filter: any = null;
|
const filter: any = null;
|
||||||
@ -402,7 +405,7 @@ class LimitsUI extends VirtualTable {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("#limits");
|
super("#limits", null);
|
||||||
this.limits = [];
|
this.limits = [];
|
||||||
Limits.on("changed", () => {
|
Limits.on("changed", () => {
|
||||||
this.limits = Array.from(Limits);
|
this.limits = Array.from(Limits);
|
||||||
@ -548,6 +551,7 @@ addEventListener("DOMContentLoaded", () => {
|
|||||||
new BoolPref("pref-global-turbo", "global-turbo");
|
new BoolPref("pref-global-turbo", "global-turbo");
|
||||||
new BoolPref("pref-queue-notification", "queue-notification");
|
new BoolPref("pref-queue-notification", "queue-notification");
|
||||||
new BoolPref("pref-finish-notification", "finish-notification");
|
new BoolPref("pref-finish-notification", "finish-notification");
|
||||||
|
new BoolPref("pref-hide-context", "hide-context");
|
||||||
new BoolPref("pref-tooltip", "tooltip");
|
new BoolPref("pref-tooltip", "tooltip");
|
||||||
new BoolPref("pref-open-manager-on-queue", "open-manager-on-queue");
|
new BoolPref("pref-open-manager-on-queue", "open-manager-on-queue");
|
||||||
new BoolPref("pref-text-links", "text-links");
|
new BoolPref("pref-text-links", "text-links");
|
||||||
@ -590,4 +594,40 @@ addEventListener("DOMContentLoaded", () => {
|
|||||||
new IntPref("pref-concurrent-downloads", "concurrent");
|
new IntPref("pref-concurrent-downloads", "concurrent");
|
||||||
|
|
||||||
visible("#limits").then(() => new LimitsUI());
|
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()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,10 +15,17 @@ import { Icons } from "./icons";
|
|||||||
import { sort, naturalCaseCompare } from "../lib/sorting";
|
import { sort, naturalCaseCompare } from "../lib/sorting";
|
||||||
import { hookButton } from "../lib/manager/renamer";
|
import { hookButton } from "../lib/manager/renamer";
|
||||||
import { CellTypes } from "../uikit/lib/constants";
|
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 PORT: RawPort = runtime.connect(null, { name: "select" });
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
const TREE_CONFIG_VERSION = 1;
|
const TREE_CONFIG_VERSION = 1;
|
||||||
|
|
||||||
@ -37,16 +44,27 @@ let Mask: Dropdown;
|
|||||||
let FastFilter: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleaErrors() {
|
||||||
|
const not = $("#notification");
|
||||||
|
not.textContent = "";
|
||||||
|
not.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function matched(item: BaseMatchedItem) {
|
||||||
return item && item.matched && item.matched !== "unmanual";
|
return item && item.matched && item.matched !== "unmanual";
|
||||||
}
|
}
|
||||||
|
|
||||||
class PausedModalDialog extends ModalDialog {
|
class PausedModalDialog extends ModalDialog {
|
||||||
get content() {
|
getContent() {
|
||||||
const content = $("#paused-template").content.cloneNode(true);
|
const tmpl = $<HTMLTemplateElement>("#paused-template");
|
||||||
localize(content);
|
const content = tmpl.content.cloneNode(true) as DocumentFragment;
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,38 +124,42 @@ class CheckClasser extends Map<string, string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeyFn = (item: BaseMatchedItem) => any;
|
||||||
|
|
||||||
class SelectionTable extends VirtualTable {
|
class SelectionTable extends VirtualTable {
|
||||||
checkClasser: CheckClasser;
|
checkClasser: CheckClasser;
|
||||||
|
|
||||||
icons: Icons;
|
icons: Icons;
|
||||||
|
|
||||||
links: any[];
|
links: BaseMatchedItem[];
|
||||||
|
|
||||||
media: any[];
|
media: BaseMatchedItem[];
|
||||||
|
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
items: any[];
|
items: BaseMatchedItem[];
|
||||||
|
|
||||||
status: any;
|
status: HTMLElement;
|
||||||
|
|
||||||
linksTab: any;
|
linksTab: HTMLElement;
|
||||||
|
|
||||||
mediaTab: any;
|
mediaTab: HTMLElement;
|
||||||
|
|
||||||
linksFilters: any;
|
linksFilters: HTMLElement;
|
||||||
|
|
||||||
mediaFilters: any;
|
mediaFilters: HTMLElement;
|
||||||
|
|
||||||
contextMenu: ContextMenu;
|
contextMenu: ContextMenu;
|
||||||
|
|
||||||
sortcol: any;
|
sortcol: number | null;
|
||||||
|
|
||||||
sortasc: boolean;
|
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) {
|
if (type === "links" && !links.length) {
|
||||||
type = "media";
|
type = "media";
|
||||||
}
|
}
|
||||||
@ -147,7 +169,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
super("#items", treeConfig, TREE_CONFIG_VERSION);
|
super("#items", treeConfig, TREE_CONFIG_VERSION);
|
||||||
|
|
||||||
this.checkClasser = new CheckClasser(NUM_FILTER_CLASSES);
|
this.checkClasser = new CheckClasser(NUM_FILTER_CLASSES);
|
||||||
this.icons = new Icons($("#icons"));
|
this.icons = new Icons($("#icons") as HTMLStyleElement);
|
||||||
this.links = links;
|
this.links = links;
|
||||||
this.media = media;
|
this.media = media;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@ -174,13 +196,13 @@ class SelectionTable extends VirtualTable {
|
|||||||
this.linksFilters = $("#linksFilters");
|
this.linksFilters = $("#linksFilters");
|
||||||
this.mediaFilters = $("#mediaFilters");
|
this.mediaFilters = $("#mediaFilters");
|
||||||
|
|
||||||
localize($("#table-context").content);
|
localize(($("#table-context") as HTMLTemplateElement).content);
|
||||||
this.contextMenu = new ContextMenu("#table-context");
|
this.contextMenu = new ContextMenu("#table-context");
|
||||||
Keys.adoptContext(this.contextMenu);
|
Keys.adoptContext(this.contextMenu);
|
||||||
|
|
||||||
this.sortcol = null;
|
this.sortcol = null;
|
||||||
this.sortasc = true;
|
this.sortasc = true;
|
||||||
this.keyfns = new Map([
|
this.keyfns = new Map<string, KeyFn>([
|
||||||
["colDownload", item => item.usable],
|
["colDownload", item => item.usable],
|
||||||
["colTitle", item => [item.title, item.usable]],
|
["colTitle", item => [item.title, item.usable]],
|
||||||
["colDescription", item => [item.description, item.usable]],
|
["colDescription", item => [item.description, item.usable]],
|
||||||
@ -263,7 +285,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
oldmask = "";
|
oldmask = "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
oldmask = m;
|
oldmask = m || oldmask;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Keys.suppressed = true;
|
Keys.suppressed = true;
|
||||||
@ -367,7 +389,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDeltaTo(delta: any[], items: any[]) {
|
applyDeltaTo(delta: ItemDelta[], items: BaseMatchedItem[]) {
|
||||||
const active = items === this.items;
|
const active = items === this.items;
|
||||||
for (const d of delta) {
|
for (const d of delta) {
|
||||||
const {idx = -1, matched = null} = d;
|
const {idx = -1, matched = null} = d;
|
||||||
@ -420,11 +442,12 @@ class SelectionTable extends VirtualTable {
|
|||||||
else {
|
else {
|
||||||
this.status.textContent = _("numitems.label", [selected]);
|
this.status.textContent = _("numitems.label", [selected]);
|
||||||
}
|
}
|
||||||
|
cleaErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowClasses(rowid: number) {
|
getRowClasses(rowid: number) {
|
||||||
const item = this.items[rowid];
|
const item = this.items[rowid];
|
||||||
if (!item || !matched(item)) {
|
if (!item || !matched(item) || !item.matched) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ["filtered", this.checkClasser.get(item.matched)];
|
return ["filtered", this.checkClasser.get(item.matched)];
|
||||||
@ -459,7 +482,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getText(prop: string, idx: number) {
|
getText(prop: string, idx: number) {
|
||||||
const item = this.items[idx];
|
const item: any = this.items[idx];
|
||||||
if (!item || !(prop in item) || !item[prop]) {
|
if (!item || !(prop in item) || !item[prop]) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -498,7 +521,7 @@ class SelectionTable extends VirtualTable {
|
|||||||
|
|
||||||
getCellCheck(rowid: number, colid: number) {
|
getCellCheck(rowid: number, colid: number) {
|
||||||
if (colid === COL_CHECK) {
|
if (colid === COL_CHECK) {
|
||||||
return matched(this.items[rowid]);
|
return !!matched(this.items[rowid]);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -541,13 +564,15 @@ async function download(paused = false) {
|
|||||||
}
|
}
|
||||||
PORT.postMessage({
|
PORT.postMessage({
|
||||||
msg: "queue",
|
msg: "queue",
|
||||||
type: Table.type,
|
|
||||||
items,
|
items,
|
||||||
|
options: {
|
||||||
|
type: Table.type,
|
||||||
paused,
|
paused,
|
||||||
mask,
|
mask,
|
||||||
maskOnce: $("#maskOnceCheck").checked,
|
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
||||||
fast: FastFilter.value,
|
fast: FastFilter.value,
|
||||||
fastOnce: $("#fastOnceCheck").checked,
|
fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
@ -559,17 +584,17 @@ async function download(paused = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Filter {
|
class Filter {
|
||||||
active: any;
|
active: boolean;
|
||||||
|
|
||||||
container: any;
|
|
||||||
|
|
||||||
elem: HTMLLabelElement;
|
|
||||||
|
|
||||||
label: any;
|
|
||||||
|
|
||||||
checkElem: HTMLInputElement;
|
checkElem: HTMLInputElement;
|
||||||
|
|
||||||
id: any;
|
container: HTMLElement;
|
||||||
|
|
||||||
|
elem: HTMLLabelElement;
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
label: string;
|
||||||
|
|
||||||
constructor(container: HTMLElement, raw: any, active = false) {
|
constructor(container: HTMLElement, raw: any, active = false) {
|
||||||
Object.assign(this, raw);
|
Object.assign(this, raw);
|
||||||
@ -620,6 +645,7 @@ function cancel() {
|
|||||||
async function init() {
|
async function init() {
|
||||||
await Promise.all([MASK.init(), FASTFILTER.init()]);
|
await Promise.all([MASK.init(), FASTFILTER.init()]);
|
||||||
Mask = new Dropdown("#mask", MASK.values);
|
Mask = new Dropdown("#mask", MASK.values);
|
||||||
|
Mask.on("changed", cleaErrors);
|
||||||
FastFilter = new Dropdown("#fast", FASTFILTER.values);
|
FastFilter = new Dropdown("#fast", FASTFILTER.values);
|
||||||
FastFilter.on("changed", () => {
|
FastFilter.on("changed", () => {
|
||||||
PORT.postMessage({
|
PORT.postMessage({
|
||||||
@ -661,7 +687,7 @@ addEventListener("DOMContentLoaded", function dom() {
|
|||||||
$("#fastDisableOthers").addEventListener("change", () => {
|
$("#fastDisableOthers").addEventListener("change", () => {
|
||||||
PORT.postMessage({
|
PORT.postMessage({
|
||||||
msg: "onlyfast",
|
msg: "onlyfast",
|
||||||
fast: $("#fastDisableOthers").checked
|
fast: $<HTMLInputElement>("#fastDisableOthers").checked
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
import ModalDialog from "../uikit/lib/modal";
|
import ModalDialog from "../uikit/lib/modal";
|
||||||
import { _, localize } from "../lib/i18n";
|
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 { MASK } from "../lib/recentlist";
|
||||||
import { BatchGenerator } from "../lib/batches";
|
import { BatchGenerator } from "../lib/batches";
|
||||||
import { WindowState } from "./windowstate";
|
import { WindowState } from "./windowstate";
|
||||||
@ -12,11 +13,11 @@ import { Dropdown } from "./dropdown";
|
|||||||
import { Keys } from "./keys";
|
import { Keys } from "./keys";
|
||||||
import { hookButton } from "../lib/manager/renamer";
|
import { hookButton } from "../lib/manager/renamer";
|
||||||
import { runtime } from "../lib/browser";
|
import { runtime } from "../lib/browser";
|
||||||
|
import { $ } from "./winutil";
|
||||||
|
|
||||||
const PORT = runtime.connect(null, { name: "single" });
|
const PORT = runtime.connect(null, { name: "single" });
|
||||||
const $ = document.querySelector.bind(document);
|
|
||||||
|
|
||||||
let ITEM: any;
|
let ITEM: BaseItem;
|
||||||
let Mask: Dropdown;
|
let Mask: Dropdown;
|
||||||
|
|
||||||
class BatchModalDialog extends ModalDialog {
|
class BatchModalDialog extends ModalDialog {
|
||||||
@ -27,12 +28,11 @@ class BatchModalDialog extends ModalDialog {
|
|||||||
this.gen = gen;
|
this.gen = gen;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content() {
|
getContent() {
|
||||||
const content = $("#batch-template").content.cloneNode(true);
|
const tmpl = $("#batch-template") as HTMLTemplateElement;
|
||||||
localize(content);
|
const content = tmpl.content.cloneNode(true) as DocumentFragment;
|
||||||
const $$ = content.querySelector.bind(content);
|
$(".batch-items", content).textContent = this.gen.length.toLocaleString();
|
||||||
$$(".batch-items").textContent = this.gen.length.toLocaleString();
|
$(".batch-preview", content).textContent = this.gen.preview;
|
||||||
$$(".batch-preview").textContent = this.gen.preview;
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class BatchModalDialog extends ModalDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setItem(item: any) {
|
function setItem(item: BaseItem) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -73,11 +73,11 @@ function setItem(item: any) {
|
|||||||
usableReferrer = "",
|
usableReferrer = "",
|
||||||
mask = ""
|
mask = ""
|
||||||
} = item;
|
} = item;
|
||||||
$("#URL").value = usable;
|
$<HTMLInputElement>("#URL").value = usable;
|
||||||
$("#filename").value = fileName;
|
$<HTMLInputElement>("#filename").value = fileName;
|
||||||
$("#title").value = title;
|
$<HTMLInputElement>("#title").value = title;
|
||||||
$("#description").value = description;
|
$<HTMLInputElement>("#description").value = description;
|
||||||
$("#referrer").value = usableReferrer;
|
$<HTMLInputElement>("#referrer").value = usableReferrer;
|
||||||
if (mask) {
|
if (mask) {
|
||||||
Mask.value = mask;
|
Mask.value = mask;
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ function displayError(err: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadInternal(paused: boolean) {
|
async function downloadInternal(paused: boolean) {
|
||||||
let usable = $("#URL").value.trim();
|
let usable = $<HTMLInputElement>("#URL").value.trim();
|
||||||
let url;
|
let url;
|
||||||
try {
|
try {
|
||||||
url = new URL(usable).toString();
|
url = new URL(usable).toString();
|
||||||
@ -98,7 +98,7 @@ async function downloadInternal(paused: boolean) {
|
|||||||
catch (ex) {
|
catch (ex) {
|
||||||
try {
|
try {
|
||||||
url = new URL(`https://${usable}`).toString();
|
url = new URL(`https://${usable}`).toString();
|
||||||
$("#URL").value = usable = `https://${usable}`;
|
$<HTMLInputElement>("#URL").value = usable = `https://${usable}`;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return displayError("error.invalidURL");
|
return displayError("error.invalidURL");
|
||||||
@ -107,7 +107,7 @@ async function downloadInternal(paused: boolean) {
|
|||||||
|
|
||||||
const gen = new BatchGenerator(usable);
|
const gen = new BatchGenerator(usable);
|
||||||
|
|
||||||
const usableReferrer = $("#referrer").value.trim();
|
const usableReferrer = $<HTMLInputElement>("#referrer").value.trim();
|
||||||
let referrer;
|
let referrer;
|
||||||
try {
|
try {
|
||||||
referrer = usableReferrer ? new URL(usableReferrer).toString() : "";
|
referrer = usableReferrer ? new URL(usableReferrer).toString() : "";
|
||||||
@ -116,9 +116,9 @@ async function downloadInternal(paused: boolean) {
|
|||||||
return displayError("error.invalidReferrer");
|
return displayError("error.invalidReferrer");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = $("#filename").value.trim();
|
const fileName = $<HTMLInputElement>("#filename").value.trim();
|
||||||
const title = $("#title").value.trim();
|
const title = $<HTMLInputElement>("#title").value.trim();
|
||||||
const description = $("#description").value.trim();
|
const description = $<HTMLInputElement>("#description").value.trim();
|
||||||
const mask = Mask.value.trim();
|
const mask = Mask.value.trim();
|
||||||
if (!mask) {
|
if (!mask) {
|
||||||
return displayError("error.invalidMask");
|
return displayError("error.invalidMask");
|
||||||
@ -180,10 +180,12 @@ async function downloadInternal(paused: boolean) {
|
|||||||
|
|
||||||
PORT.postMessage({
|
PORT.postMessage({
|
||||||
msg: "queue",
|
msg: "queue",
|
||||||
paused,
|
|
||||||
items,
|
items,
|
||||||
|
options: {
|
||||||
|
paused,
|
||||||
mask,
|
mask,
|
||||||
maskOnce: $("#maskOnceCheck").checked,
|
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -198,15 +200,35 @@ function cancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
await localize(document.documentElement);
|
||||||
await Promise.all([MASK.init()]);
|
await Promise.all([MASK.init()]);
|
||||||
Mask = new Dropdown("#mask", MASK.values);
|
Mask = new Dropdown("#mask", MASK.values);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener("DOMContentLoaded", function dom() {
|
addEventListener("DOMContentLoaded", async function dom() {
|
||||||
removeEventListener("DOMContentLoaded", 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));
|
$("#btnDownload").addEventListener("click", () => download(false));
|
||||||
$("#btnPaused").addEventListener("click", () => download(true));
|
$("#btnPaused").addEventListener("click", () => download(true));
|
||||||
$("#btnCancel").addEventListener(
|
$("#btnCancel").addEventListener(
|
||||||
@ -225,27 +247,10 @@ addEventListener("DOMContentLoaded", function dom() {
|
|||||||
return true;
|
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"));
|
hookButton($("#maskButton"));
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("load", function() {
|
addEventListener("load", function () {
|
||||||
$("#URL").focus();
|
$("#URL").focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -259,7 +264,7 @@ addEventListener("contextmenu", event => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("beforeunload", function() {
|
addEventListener("beforeunload", function () {
|
||||||
PORT.disconnect();
|
PORT.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { RawPort } from "../lib/browser";
|
||||||
|
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
export class WindowState {
|
export class WindowState {
|
||||||
private readonly port: any;
|
private readonly port: RawPort;
|
||||||
|
|
||||||
constructor(port: any) {
|
constructor(port: RawPort) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.update = this.update.bind(this);
|
this.update = this.update.bind(this);
|
||||||
addEventListener("resize", this.update);
|
addEventListener("resize", this.update);
|
||||||
|
10
windows/winutil.ts
Normal 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;
|
||||||
|
}
|