"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, runtime, history, sessions, // eslint-disable-next-line no-unused-vars OnInstalled, } 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 { if (tab && tab.incognito && msg) { msg.private = tab.incognito; } 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); } } } function getMajor(version?: string) { if (!version) { return ""; } const match = version.match(/^\d+\.\d+/); if (!match) { return ""; } return match[0]; } runtime.onInstalled.addListener(({reason, previousVersion}: OnInstalled) => { const {version} = runtime.getManifest(); const major = getMajor(version); const prevMajor = getMajor(previousVersion); if (reason === "update" && major !== prevMajor) { tabs.create({ url: `https://about.downthemall.org/changelog/?cur=${major}&prev=${prevMajor}`, }); } else if (reason === "install") { tabs.create({ url: `https://about.downthemall.org/4.0/?cur=${major}`, }); } }); locale.then(() => { const menuHandler = new class Menus extends Handler { constructor() { super(); this.onClicked = this.onClicked.bind(this); const alls = new Map(); 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 = handler.call(this, info, tab); if (rv && rv.catch) { rv.catch(console.error); } } async emulate(action: string) { const tab = await tabs.query({ active: true, currentWindow: true, }); 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(); } }(); new class Action extends Handler { constructor() { super(); this.onClicked = this.onClicked.bind(this); action.onClicked.addListener(this.onClicked); Prefs.get("button-type", false).then(v => this.adjust(v)); Prefs.on("button-type", (prefs, key, value) => { this.adjust(value); }); } adjust(type: string) { action.setPopup({ popup: type !== "popup" ? "" : "/windows/popup.html" }); let icons; switch (type) { case "popup": icons = { 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" }; break; case "dta": icons = { 16: "/style/button-regular.png", 32: "/style/button-regular@2x.png", }; break; case "turbo": icons = { 16: "/style/button-turbo.png", 32: "/style/button-turbo@2x.png", }; break; case "manager": icons = { 16: "/style/button-manager.png", 32: "/style/button-manager@2x.png", }; break; } action.setIcon({path: icons}); } async onClicked() { switch (await Prefs.get("button-type")) { case "popup": break; case "dta": menuHandler.emulate("DTARegular"); break; case "turbo": menuHandler.emulate("DTATurbo"); break; case "manager": menuHandler.emulate("DTAManager"); break; } } }(); Bus.on("do-regular", () => menuHandler.emulate("DTARegular")); Bus.on("do-regular-all", () => menuHandler.emulate("DTARegularAll")); Bus.on("do-turbo", () => menuHandler.emulate("DTATurbo")); Bus.on("do-turbo-all", () => menuHandler.emulate("DTATurboAll")); Bus.on("do-single", () => API.singleRegular(null)); Bus.on("open-manager", () => openManager(true)); Bus.on("open-prefs", () => openPrefs()); (async function init() { const urlBase = runtime.getURL(""); history.onVisited.addListener(({url}: {url: string}) => { if (!url || !url.startsWith(urlBase)) { return; } history.deleteUrl({url}); }); const results: {url?: string}[] = await history.search({text: urlBase}); for (const {url} of results) { if (!url) { continue; } history.deleteUrl({url}); } if (!CHROME) { const sessionRemover = async () => { for (const s of await sessions.getRecentlyClosed()) { if (s.tab) { if (s.tab.url.startsWith(urlBase)) { await sessions.forgetClosedTab(s.tab.windowId, s.tab.sessionId); } continue; } if (!s.window || !s.window.tabs || s.window.tabs.length > 1) { continue; } const [tab] = s.window.tabs; if (tab.url.startsWith(urlBase)) { await sessions.forgetClosedWindow(s.window.sessionId); } } }; sessions.onChanged.addListener(sessionRemover); await sessionRemover(); } await Prefs.set("last-run", new Date()); await filters(); await getManager(); })().catch(ex => { console.error("Failed to init components", ex.toString(), ex.stack, ex); }); });