downthemall/lib/background.ts
2019-09-04 14:15:05 +02:00

580 lines
15 KiB
TypeScript

"use strict";
// License: MIT
import { ALLOWED_SCHEMES, TRANSFERABLE_PROPERTIES } from "./constants";
import { API } from "./api";
import { Finisher, makeUniqueItems } from "./item";
import { Prefs } from "./prefs";
import { _, locale } from "./i18n";
import { openPrefs, openManager } from "./windowutils";
import { filters } from "./filters";
import { getManager } from "./manager/man";
import {
browserAction as action,
menus as _menus, contextMenus as _cmenus,
tabs,
webNavigation as nav,
// eslint-disable-next-line no-unused-vars
Tab,
// eslint-disable-next-line no-unused-vars
MenuClickInfo,
CHROME,
} from "./browser";
import { Bus } from "./bus";
import { filterInSitu } from "./util";
const menus = typeof (_menus) !== "undefined" && _menus || _cmenus;
const GATHER = "/bundles/content-gather.js";
const CHROME_CONTEXTS = Object.freeze(new Set([
"all",
"audio",
"browser_action",
"editable",
"frame",
"image",
"launcher",
"link",
"page",
"page_action",
"selection",
"video",
]));
async function runContentJob(tab: Tab, file: string, msg: any) {
try {
const res = await tabs.executeScript(tab.id, {
file,
allFrames: true,
runAt: "document_start"
});
if (!msg) {
return res;
}
const promises = [];
const results: any[] = [];
for (const frame of await nav.getAllFrames({ tabId: tab.id })) {
promises.push(tabs.sendMessage(tab.id, msg, {
frameId: frame.frameId}
).then(function(res: any) {
results.push(res);
}).catch(console.error));
}
await Promise.all(promises);
return results;
}
catch (ex) {
console.error("Failed to execute content script", file,
ex.message || ex.toString(), ex);
return [];
}
}
type SelectionOptions = {
selectionOnly: boolean;
allTabs: boolean;
turbo: boolean;
tab: Tab;
};
class Handler {
async processResults(turbo = false, results: any[]) {
const links = this.makeUnique(results, "links");
const media = this.makeUnique(results, "media");
await API[turbo ? "turbo" : "regular"](links, media);
}
makeUnique(results: any[], what: string) {
return makeUniqueItems(
results.filter(e => e[what]).map(e => {
const finisher = new Finisher(e);
return filterInSitu(e[what].
map((item: any) => finisher.finish(item)), e => !!e);
}));
}
async performSelection(options: SelectionOptions) {
try {
const toptions: any = {
currentWindow: true,
discarded: false,
};
if (!CHROME) {
toptions.hidden = false;
}
const selectedTabs = options.allTabs ?
await tabs.query(toptions) as any[] :
[options.tab];
const textLinks = await Prefs.get("text-links", true);
const goptions = {
type: "DTA:gather",
selectionOnly: options.selectionOnly,
textLinks,
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
};
const results = await Promise.all(selectedTabs.
map((tab: any) => runContentJob(tab, GATHER, goptions)));
await this.processResults(options.turbo, results.flat());
}
catch (ex) {
console.error(ex.toString(), ex.stack, ex);
}
}
}
locale.then(() => {
new class Action extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
action.onClicked.addListener(this.onClicked);
}
async onClicked(tab: {id: number}) {
if (!tab.id) {
return;
}
try {
await this.processResults(
true,
await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly: false,
textLinks: await Prefs.get("text-links", true),
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
}));
}
catch (ex) {
console.error(ex);
}
}
}();
const menuHandler = new class Menus extends Handler {
constructor() {
super();
this.onClicked = this.onClicked.bind(this);
const alls = new Map<string, string[]>();
const mcreate = (options: any) => {
if (CHROME) {
delete options.icons;
options.contexts = options.contexts.
filter((e: string) => CHROME_CONTEXTS.has(e));
if (!options.contexts.length) {
return;
}
}
if (options.contexts.includes("all")) {
alls.set(options.id, options.contexts);
}
menus.create(options);
};
mcreate({
id: "DTARegularLink",
contexts: ["link"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.link"),
});
mcreate({
id: "DTATurboLink",
contexts: ["link"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.link"),
});
mcreate({
id: "DTARegularImage",
contexts: ["image"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.image"),
});
mcreate({
id: "DTATurboImage",
contexts: ["image"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.image"),
});
mcreate({
id: "DTARegularMedia",
contexts: ["video", "audio"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.media"),
});
mcreate({
id: "DTATurboMedia",
contexts: ["video", "audio"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.media"),
});
mcreate({
id: "DTARegularSelection",
contexts: ["selection"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular.selection"),
});
mcreate({
id: "DTATurboSelection",
contexts: ["selection"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo.selection"),
});
mcreate({
id: "DTARegular",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta.regular"),
});
mcreate({
id: "DTATurbo",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta.turbo"),
});
mcreate({
id: "sep-1",
contexts: ["all", "browser_action", "tools_menu"],
type: "separator"
});
mcreate({
id: "DTARegularAll",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-regular.png",
32: "/style/button-regular@2x.png",
},
title: _("dta-regular-all"),
});
mcreate({
id: "DTATurboAll",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
},
title: _("dta-turbo-all"),
});
const sep2ctx = menus.ACTION_MENU_TOP_LEVEL_LIMIT === 6 ?
["all", "tools_menu"] :
["all", "browser_action", "tools_menu"];
mcreate({
id: "sep-2",
contexts: sep2ctx,
type: "separator"
});
mcreate({
id: "DTAAdd",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/add.svg",
32: "/style/add.svg",
64: "/style/add.svg",
128: "/style/add.svg",
},
title: _("add-download"),
});
mcreate({
id: "sep-3",
contexts: ["all", "browser_action", "tools_menu"],
type: "separator"
});
mcreate({
id: "DTAManager",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/button-manager.png",
32: "/style/button-manager@2x.png",
},
title: _("manager.short"),
});
mcreate({
id: "DTAPrefs",
contexts: ["all", "browser_action", "tools_menu"],
icons: {
16: "/style/settings.svg",
32: "/style/settings.svg",
64: "/style/settings.svg",
128: "/style/settings.svg",
},
title: _("prefs.short"),
});
Object.freeze(alls);
const adjustMenus = (v: boolean) => {
for (const [id, contexts] of alls.entries()) {
const adjusted = v ?
contexts.filter(e => e !== "all") :
contexts;
menus.update(id, {
contexts: adjusted
});
}
};
Prefs.get("hide-context", false).then((v: boolean) => {
// This is the initial load, so no need to adjust when visible already
if (!v) {
return;
}
adjustMenus(v);
});
Prefs.on("hide-context", (prefs, key, value: boolean) => {
adjustMenus(value);
});
menus.onClicked.addListener(this.onClicked);
}
*makeSingleItemList(url: string, results: any[]) {
for (const result of results) {
const finisher = new Finisher(result);
for (const list of [result.links, result.media]) {
for (const e of list) {
if (e.url !== url) {
continue;
}
const finished = finisher.finish(e);
if (!finished) {
continue;
}
yield finished;
}
}
}
}
async findSingleItem(tab: Tab, url: string, turbo = false) {
if (!url) {
return;
}
const results = await runContentJob(
tab, "/bundles/content-gather.js", {
type: "DTA:gather",
selectionOnly: false,
schemes: Array.from(ALLOWED_SCHEMES.values()),
transferable: TRANSFERABLE_PROPERTIES,
});
const found = Array.from(this.makeSingleItemList(url, results));
const unique = makeUniqueItems([found]);
if (!unique.length) {
return;
}
const [item] = unique;
API[turbo ? "singleTurbo" : "singleRegular"](item);
}
onClicked(info: MenuClickInfo, tab: Tab) {
if (!tab.id) {
return;
}
const {menuItemId} = info;
const {[`onClicked${menuItemId}`]: handler}: any = this;
if (!handler) {
console.error("Invalid Handler for", menuItemId);
return;
}
const rv: Promise<void> | void = handler.call(this, info, tab);
if (rv && rv.catch) {
rv.catch(console.error);
}
}
async enumulate(action: string) {
const tab = await tabs.query({
active: true,
currentWindow: true,
});
if (!tab || !tab.length) {
return;
}
this.onClicked({
menuItemId: action
}, tab[0]);
}
async onClickedDTARegular(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: false,
turbo: false,
tab,
});
}
async onClickedDTARegularAll(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: true,
turbo: false,
tab,
});
}
async onClickedDTARegularSelection(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: true,
allTabs: false,
turbo: false,
tab,
});
}
async onClickedDTATurbo(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: false,
turbo: true,
tab,
});
}
async onClickedDTATurboAll(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: false,
allTabs: true,
turbo: true,
tab,
});
}
async onClickedDTATurboSelection(info: MenuClickInfo, tab: Tab) {
return await this.performSelection({
selectionOnly: true,
allTabs: false,
turbo: true,
tab,
});
}
async onClickedDTARegularLink(info: MenuClickInfo, tab: Tab) {
if (!info.linkUrl) {
return;
}
await this.findSingleItem(tab, info.linkUrl, false);
}
async onClickedDTATurboLink(info: MenuClickInfo, tab: Tab) {
if (!info.linkUrl) {
return;
}
await this.findSingleItem(tab, info.linkUrl, true);
}
async onClickedDTARegularImage(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboImage(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, true);
}
async onClickedDTARegularMedia(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, false);
}
async onClickedDTATurboMedia(info: MenuClickInfo, tab: Tab) {
if (!info.srcUrl) {
return;
}
await this.findSingleItem(tab, info.srcUrl, true);
}
onClickedDTAAdd() {
API.singleRegular(null);
}
async onClickedDTAManager() {
await openManager();
}
async onClickedDTAPrefs() {
await openPrefs();
}
}();
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
Bus.on("do-single", () => API.singleRegular(null));
Bus.on("open-manager", () => openManager(true));
Bus.on("open-prefs", () => openPrefs());
function adjustAction(globalTurbo: boolean) {
action.setPopup({
popup: globalTurbo ? "" : "/windows/popup.html"
});
action.setIcon({
path: globalTurbo ? {
16: "/style/button-turbo.png",
32: "/style/button-turbo@2x.png",
} : {
16: "/style/icon16.png",
32: "/style/icon32.png",
48: "/style/icon48.png",
64: "/style/icon64.png",
96: "/style/icon96.png",
128: "/style/icon128.png",
256: "/style/icon256.png"
}
});
}
(async function init() {
await Prefs.set("last-run", new Date());
Prefs.get("global-turbo", false).then(v => adjustAction(v));
Prefs.on("global-turbo", (prefs, key, value) => {
adjustAction(value);
});
await filters();
await getManager();
})().catch(ex => {
console.error("Failed to init components", ex.toString(), ex.stack, ex);
});
});