Implement own locale loader
Why? * Because i18n sucks multi-browser, especially on Chrome. * What's more, this will allow overlaying "incomplete" locales with missing strings over the base locale and get a semi-translated one, which probably is better than nothing. * Additionally we kinda need that implementation anyway for node-based tests. * Also, while not currently implemented, it could allow (hot) reloading of locales, or loading external ones, which would help translators, or providing an option to the user to choose a locale. * And finally, not calling i18n will avoid the "context switch" into browserland. Some implementation details: * Before code can use a locale, it has to be loaded. Sadly sync loading is not really supported. So `await locale` or `await localize`. * Background force reloads locales right now, and caches them in localStorage. Windows will look into localStorage for that cache. * The locale loader will not verify locales other than some rudimentary checks. It is assumed that shipped locales where verified before check-in.
This commit is contained in:
parent
2bfb3d5363
commit
8235af22db
@ -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";
|
||||||
@ -106,409 +106,411 @@ class Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new class Action extends Handler {
|
locale.then(() => {
|
||||||
constructor() {
|
new class Action extends Handler {
|
||||||
super();
|
constructor() {
|
||||||
this.onClicked = this.onClicked.bind(this);
|
super();
|
||||||
action.onClicked.addListener(this.onClicked);
|
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 {
|
async onClicked(tab: {id: number}) {
|
||||||
constructor() {
|
if (!tab.id) {
|
||||||
super();
|
|
||||||
this.onClicked = this.onClicked.bind(this);
|
|
||||||
const alls = new Map<string, string[]>();
|
|
||||||
const mcreate = (options: any) => {
|
|
||||||
if (options.contexts.includes("all")) {
|
|
||||||
alls.set(options.id, options.contexts);
|
|
||||||
}
|
|
||||||
return menus.create(options);
|
|
||||||
};
|
|
||||||
mcreate({
|
|
||||||
id: "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: "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: "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;
|
return;
|
||||||
}
|
}
|
||||||
adjustMenus(v);
|
try {
|
||||||
});
|
await this.processResults(
|
||||||
Prefs.on("hide-context", (prefs, key, value: boolean) => {
|
true,
|
||||||
adjustMenus(value);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
menus.onClicked.addListener(this.onClicked);
|
const menuHandler = new class Menus extends Handler {
|
||||||
}
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.onClicked = this.onClicked.bind(this);
|
||||||
|
const alls = new Map<string, string[]>();
|
||||||
|
const mcreate = (options: any) => {
|
||||||
|
if (options.contexts.includes("all")) {
|
||||||
|
alls.set(options.id, options.contexts);
|
||||||
|
}
|
||||||
|
return menus.create(options);
|
||||||
|
};
|
||||||
|
mcreate({
|
||||||
|
id: "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: "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: "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);
|
||||||
|
|
||||||
*makeSingleItemList(url: string, results: any[]) {
|
const adjustMenus = (v: boolean) => {
|
||||||
for (const result of results) {
|
for (const [id, contexts] of alls.entries()) {
|
||||||
const finisher = new Finisher(result);
|
const adjusted = v ?
|
||||||
for (const list of [result.links, result.media]) {
|
contexts.filter(e => e !== "all") :
|
||||||
for (const e of list) {
|
contexts;
|
||||||
if (e.url !== url) {
|
menus.update(id, {
|
||||||
continue;
|
contexts: adjusted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Prefs.get("hide-context", false).then((v: boolean) => {
|
||||||
|
// This is the initial load, so no need to adjust when visible already
|
||||||
|
if (!v) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
adjustMenus(v);
|
||||||
|
});
|
||||||
|
Prefs.on("hide-context", (prefs, key, value: boolean) => {
|
||||||
|
adjustMenus(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
menus.onClicked.addListener(this.onClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
*makeSingleItemList(url: string, results: any[]) {
|
||||||
|
for (const result of results) {
|
||||||
|
const finisher = new Finisher(result);
|
||||||
|
for (const list of [result.links, result.media]) {
|
||||||
|
for (const e of list) {
|
||||||
|
if (e.url !== url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const finished = finisher.finish(e);
|
||||||
|
if (!finished) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
yield finished;
|
||||||
}
|
}
|
||||||
const finished = finisher.finish(e);
|
|
||||||
if (!finished) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
yield finished;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async findSingleItem(tab: any, url: string, turbo = false) {
|
async findSingleItem(tab: any, url: string, turbo = false) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const results = await runContentJob(
|
||||||
|
tab, "/bundles/content-gather.js", {
|
||||||
|
type: "DTA:gather",
|
||||||
|
selectionOnly: false,
|
||||||
|
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
||||||
|
transferable: TRANSFERABLE_PROPERTIES,
|
||||||
|
});
|
||||||
|
const found = Array.from(this.makeSingleItemList(url, results));
|
||||||
|
const unique = makeUniqueItems([found]);
|
||||||
|
if (!unique.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [item] = unique;
|
||||||
|
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
||||||
}
|
}
|
||||||
const results = await runContentJob(
|
|
||||||
tab, "/bundles/content-gather.js", {
|
onClicked(info: any, tab: any) {
|
||||||
type: "DTA:gather",
|
if (!tab.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {menuItemId} = info;
|
||||||
|
const {[`onClicked${menuItemId}`]: handler}: any = this;
|
||||||
|
if (!handler) {
|
||||||
|
console.error("Invalid Handler for", menuItemId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.call(this, info, tab).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enumulate(action: string) {
|
||||||
|
const tab = await tabs.query({active: true});
|
||||||
|
if (!tab || !tab.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onClicked({
|
||||||
|
menuItemId: action
|
||||||
|
}, tab[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTARegular(info: any, tab: any) {
|
||||||
|
return await this.performSelection({
|
||||||
selectionOnly: false,
|
selectionOnly: false,
|
||||||
schemes: Array.from(ALLOWED_SCHEMES.values()),
|
allTabs: false,
|
||||||
transferable: TRANSFERABLE_PROPERTIES,
|
turbo: false,
|
||||||
|
tab,
|
||||||
});
|
});
|
||||||
const found = Array.from(this.makeSingleItemList(url, results));
|
|
||||||
const unique = makeUniqueItems([found]);
|
|
||||||
if (!unique.length) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const [item] = unique;
|
|
||||||
API[turbo ? "singleTurbo" : "singleRegular"](item);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked(info: any, tab: any) {
|
async onClickedDTARegularAll(info: any, tab: any) {
|
||||||
if (!tab.id) {
|
return await this.performSelection({
|
||||||
return;
|
selectionOnly: false,
|
||||||
|
allTabs: true,
|
||||||
|
turbo: false,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const {menuItemId} = info;
|
|
||||||
const {[`onClicked${menuItemId}`]: handler}: any = this;
|
async onClickedDTARegularSelection(info: any, tab: any) {
|
||||||
if (!handler) {
|
return await this.performSelection({
|
||||||
console.error("Invalid Handler for", menuItemId);
|
selectionOnly: true,
|
||||||
return;
|
allTabs: false,
|
||||||
|
turbo: false,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
handler.call(this, info, tab).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async enumulate(action: string) {
|
async onClickedDTATurbo(info: any, tab: any) {
|
||||||
const tab = await tabs.query({active: true});
|
return await this.performSelection({
|
||||||
if (!tab || !tab.length) {
|
selectionOnly: false,
|
||||||
return;
|
allTabs: false,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.onClicked({
|
|
||||||
menuItemId: action
|
|
||||||
}, tab[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTARegular(info: any, tab: any) {
|
async onClickedDTATurboAll(info: any, tab: any) {
|
||||||
return await this.performSelection({
|
return await this.performSelection({
|
||||||
selectionOnly: false,
|
selectionOnly: false,
|
||||||
allTabs: false,
|
allTabs: true,
|
||||||
turbo: false,
|
turbo: true,
|
||||||
tab,
|
tab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTATurboSelection(info: any, tab: any) {
|
||||||
|
return await this.performSelection({
|
||||||
|
selectionOnly: true,
|
||||||
|
allTabs: false,
|
||||||
|
turbo: true,
|
||||||
|
tab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTARegularLink(info: any, tab: any) {
|
||||||
|
return await this.findSingleItem(tab, info.linkUrl, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTATurboLink(info: any, tab: any) {
|
||||||
|
return await this.findSingleItem(tab, info.linkUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTARegularImage(info: any, tab: any) {
|
||||||
|
return await this.findSingleItem(tab, info.srcUrl, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTATurboImage(info: any, tab: any) {
|
||||||
|
return await this.findSingleItem(tab, info.srcUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTARegularMedia(info: any, tab: any) {
|
||||||
|
return await this.findSingleItem(tab, info.srcUrl, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTATurboMedia(info: any, tab: any) {
|
||||||
|
return await this.findSingleItem(tab, info.srcUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickedDTAAdd() {
|
||||||
|
API.singleRegular(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTAManager() {
|
||||||
|
await openManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickedDTAPrefs() {
|
||||||
|
await openPrefs();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
|
||||||
|
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
|
||||||
|
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
|
||||||
|
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
|
||||||
|
Bus.on("do-single", () => API.singleRegular(null));
|
||||||
|
Bus.on("open-manager", () => openManager(true));
|
||||||
|
Bus.on("open-prefs", () => openPrefs());
|
||||||
|
|
||||||
|
function adjustAction(globalTurbo: boolean) {
|
||||||
|
action.setPopup({
|
||||||
|
popup: globalTurbo ? "" : null
|
||||||
|
});
|
||||||
|
action.setIcon({
|
||||||
|
path: globalTurbo ? {
|
||||||
|
16: "/style/button-turbo.png",
|
||||||
|
32: "/style/button-turbo@2x.png",
|
||||||
|
} : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickedDTARegularAll(info: any, tab: any) {
|
(async function init() {
|
||||||
return await this.performSelection({
|
await Prefs.set("last-run", new Date());
|
||||||
selectionOnly: false,
|
Prefs.get("global-turbo", false).then(v => adjustAction(v));
|
||||||
allTabs: true,
|
Prefs.on("global-turbo", (prefs, key, value) => {
|
||||||
turbo: false,
|
adjustAction(value);
|
||||||
tab,
|
|
||||||
});
|
});
|
||||||
}
|
await filters();
|
||||||
|
await getManager();
|
||||||
async onClickedDTARegularSelection(info: any, tab: any) {
|
})().catch(ex => {
|
||||||
return await this.performSelection({
|
console.error("Failed to init components", ex.toString(), ex.stack, ex);
|
||||||
selectionOnly: true,
|
|
||||||
allTabs: false,
|
|
||||||
turbo: false,
|
|
||||||
tab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTATurbo(info: any, tab: any) {
|
|
||||||
return await this.performSelection({
|
|
||||||
selectionOnly: false,
|
|
||||||
allTabs: false,
|
|
||||||
turbo: true,
|
|
||||||
tab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTATurboAll(info: any, tab: any) {
|
|
||||||
return await this.performSelection({
|
|
||||||
selectionOnly: false,
|
|
||||||
allTabs: true,
|
|
||||||
turbo: true,
|
|
||||||
tab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTATurboSelection(info: any, tab: any) {
|
|
||||||
return await this.performSelection({
|
|
||||||
selectionOnly: true,
|
|
||||||
allTabs: false,
|
|
||||||
turbo: true,
|
|
||||||
tab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTARegularLink(info: any, tab: any) {
|
|
||||||
return await this.findSingleItem(tab, info.linkUrl, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTATurboLink(info: any, tab: any) {
|
|
||||||
return await this.findSingleItem(tab, info.linkUrl, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTARegularImage(info: any, tab: any) {
|
|
||||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTATurboImage(info: any, tab: any) {
|
|
||||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTARegularMedia(info: any, tab: any) {
|
|
||||||
return await this.findSingleItem(tab, info.srcUrl, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTATurboMedia(info: any, tab: any) {
|
|
||||||
return await this.findSingleItem(tab, info.srcUrl, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickedDTAAdd() {
|
|
||||||
API.singleRegular(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTAManager() {
|
|
||||||
await openManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickedDTAPrefs() {
|
|
||||||
await openPrefs();
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
Bus.on("do-regular", () => menuHandler.enumulate("DTARegular"));
|
|
||||||
Bus.on("do-regular-all", () => menuHandler.enumulate("DTARegularAll"));
|
|
||||||
Bus.on("do-turbo", () => menuHandler.enumulate("DTATurbo"));
|
|
||||||
Bus.on("do-turbo-all", () => menuHandler.enumulate("DTATurboAll"));
|
|
||||||
Bus.on("do-single", () => API.singleRegular(null));
|
|
||||||
Bus.on("open-manager", () => openManager(true));
|
|
||||||
Bus.on("open-prefs", () => openPrefs());
|
|
||||||
|
|
||||||
function adjustAction(globalTurbo: boolean) {
|
|
||||||
action.setPopup({
|
|
||||||
popup: globalTurbo ? "" : null
|
|
||||||
});
|
});
|
||||||
action.setIcon({
|
|
||||||
path: globalTurbo ? {
|
|
||||||
16: "/style/button-turbo.png",
|
|
||||||
32: "/style/button-turbo@2x.png",
|
|
||||||
} : null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(async 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);
|
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
// 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;
|
|
||||||
export const {extension} = polyfill;
|
export const {extension} = polyfill;
|
||||||
export const {notifications} = polyfill;
|
export const {notifications} = polyfill;
|
||||||
export const {browserAction} = polyfill;
|
export const {browserAction} = polyfill;
|
||||||
|
@ -4,13 +4,14 @@
|
|||||||
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 { 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 { FASTFILTER } from "./recentlist";
|
||||||
|
import { _, locale } from "./i18n";
|
||||||
|
|
||||||
const REG_ESCAPE = /[{}()[\]\\^$.]/g;
|
const REG_ESCAPE = /[{}()[\]\\^$.]/g;
|
||||||
const REG_FNMATCH = /[*?]/;
|
const REG_FNMATCH = /[*?]/;
|
||||||
@ -205,7 +206,7 @@ export class Filter {
|
|||||||
this._label = this.raw.label;
|
this._label = this.raw.label;
|
||||||
if (this.id !== FAST && this.id.startsWith("deffilter-") &&
|
if (this.id !== FAST && this.id.startsWith("deffilter-") &&
|
||||||
!this.raw.isOverridden("label")) {
|
!this.raw.isOverridden("label")) {
|
||||||
this._label = i18n.getMessage(this.id) || this._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);
|
||||||
@ -475,6 +476,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) {
|
||||||
|
220
lib/i18n.ts
220
lib/i18n.ts
@ -3,57 +3,170 @@
|
|||||||
|
|
||||||
import {memoize} from "./memoize";
|
import {memoize} from "./memoize";
|
||||||
|
|
||||||
|
|
||||||
declare let browser: any;
|
declare let browser: any;
|
||||||
declare let chrome: any;
|
declare let chrome: any;
|
||||||
|
|
||||||
function load() {
|
interface JSONEntry {
|
||||||
try {
|
message: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
placeholders: any;
|
||||||
if (typeof browser !== "undefined" && browser.i18n) {
|
}
|
||||||
return browser.i18n;
|
|
||||||
|
class Entry {
|
||||||
|
private message: string;
|
||||||
|
|
||||||
|
constructor(entry: JSONEntry) {
|
||||||
|
if (!entry.message.includes("$")) {
|
||||||
|
throw new Error("Not entry-able");
|
||||||
}
|
}
|
||||||
if (typeof chrome !== "undefined" && chrome.i18n) {
|
let hit = false;
|
||||||
return chrome.i18n;
|
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");
|
||||||
}
|
}
|
||||||
throw new Error("not in a webext");
|
|
||||||
}
|
}
|
||||||
catch (ex) {
|
|
||||||
|
localize(args: any[]) {
|
||||||
|
return this.message.replace(/\$\d+\$/g, (r: string) => {
|
||||||
|
const idx = parseInt(r.substr(1, r.length - 2), 10) - 1;
|
||||||
|
return args[idx] || "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Localization {
|
||||||
|
private strings: Map<string, Entry | string>;
|
||||||
|
|
||||||
|
constructor(baseLanguage: any, ...overlayLanguages: any) {
|
||||||
|
this.strings = new Map();
|
||||||
|
const mapLanguage = (lang: any) => {
|
||||||
|
for (const [id, entry] of Object.entries<JSONEntry>(lang)) {
|
||||||
|
if (!entry.message) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.strings.set(id, new Entry(entry));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
this.strings.set(id, entry.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mapLanguage(baseLanguage);
|
||||||
|
overlayLanguages.forEach(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_KEY = "_cached_locales";
|
||||||
|
|
||||||
|
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 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, 10);
|
|
||||||
|
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
|
||||||
@ -62,21 +175,21 @@ const memoGetMessage = memoize(i18n.getMessage, 10 * 1000, 10);
|
|||||||
* @returns {string} Localized message
|
* @returns {string} Localized message
|
||||||
*/
|
*/
|
||||||
export 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)
|
}
|
||||||
*/
|
|
||||||
export function localize<T extends HTMLElement | DocumentFragment>(elem: T): T {
|
|
||||||
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) {
|
||||||
@ -109,3 +222,14 @@ export function localize<T extends HTMLElement | DocumentFragment>(elem: T): T {
|
|||||||
}
|
}
|
||||||
return elem as T;
|
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);
|
||||||
|
}
|
||||||
|
@ -23,7 +23,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 +35,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,7 +87,7 @@ export default class ModalDialog {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content(): DocumentFragment | HTMLElement {
|
getContent(): Promise<DocumentFragment | HTMLElement> {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +160,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);
|
||||||
|
@ -20,37 +20,6 @@ const LOADED = new Promise(resolve => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
LOADED.then(async () => {
|
|
||||||
const nag = await Prefs.get("nagging", 0);
|
|
||||||
const nagnext = await Prefs.get("nagging-next", 6);
|
|
||||||
const next = Math.ceil(Math.log2(Math.max(1, nag)));
|
|
||||||
const el = $("#nagging");
|
|
||||||
const remove = () => {
|
|
||||||
el.parentElement.removeChild(el);
|
|
||||||
};
|
|
||||||
if (next <= nagnext) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
$("#nagging-donate").addEventListener("click", () => {
|
|
||||||
PORT.post("donate");
|
|
||||||
Prefs.set("nagging-next", next);
|
|
||||||
remove();
|
|
||||||
});
|
|
||||||
$("#nagging-later").addEventListener("click", () => {
|
|
||||||
Prefs.set("nagging-next", next);
|
|
||||||
remove();
|
|
||||||
});
|
|
||||||
$("#nagging-never").addEventListener("click", () => {
|
|
||||||
Prefs.set("nagging-next", Number.MAX_SAFE_INTEGER);
|
|
||||||
remove();
|
|
||||||
});
|
|
||||||
$("#nagging-message").textContent = _(
|
|
||||||
"nagging-message", nag.toLocaleString());
|
|
||||||
$("#nagging").classList.remove("hidden");
|
|
||||||
}, 2 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
addEventListener("DOMContentLoaded", function dom() {
|
addEventListener("DOMContentLoaded", function dom() {
|
||||||
removeEventListener("DOMContentLoaded", dom);
|
removeEventListener("DOMContentLoaded", dom);
|
||||||
|
|
||||||
@ -70,10 +39,40 @@ addEventListener("DOMContentLoaded", function dom() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const tabled = new Promised();
|
const tabled = new Promised();
|
||||||
const loaded = Promise.all([LOADED, platformed]);
|
const localized = localize(document.documentElement);
|
||||||
const fullyloaded = Promise.all([LOADED, platformed, tabled]);
|
const loaded = Promise.all([LOADED, platformed, localized]);
|
||||||
|
const fullyloaded = Promise.all([LOADED, platformed, tabled, localized]);
|
||||||
|
fullyloaded.then(async () => {
|
||||||
|
const nag = await Prefs.get("nagging", 0);
|
||||||
|
const nagnext = await Prefs.get("nagging-next", 6);
|
||||||
|
const next = Math.ceil(Math.log2(Math.max(1, nag)));
|
||||||
|
const el = $("#nagging");
|
||||||
|
const remove = () => {
|
||||||
|
el.parentElement.removeChild(el);
|
||||||
|
};
|
||||||
|
if (next <= nagnext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
$("#nagging-donate").addEventListener("click", () => {
|
||||||
|
PORT.post("donate");
|
||||||
|
Prefs.set("nagging-next", next);
|
||||||
|
remove();
|
||||||
|
});
|
||||||
|
$("#nagging-later").addEventListener("click", () => {
|
||||||
|
Prefs.set("nagging-next", next);
|
||||||
|
remove();
|
||||||
|
});
|
||||||
|
$("#nagging-never").addEventListener("click", () => {
|
||||||
|
Prefs.set("nagging-next", Number.MAX_SAFE_INTEGER);
|
||||||
|
remove();
|
||||||
|
});
|
||||||
|
$("#nagging-message").textContent = _(
|
||||||
|
"nagging-message", nag.toLocaleString());
|
||||||
|
$("#nagging").classList.remove("hidden");
|
||||||
|
}, 2 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
localize(document.documentElement);
|
|
||||||
$("#donate").addEventListener("click", () => {
|
$("#donate").addEventListener("click", () => {
|
||||||
PORT.post("donate");
|
PORT.post("donate");
|
||||||
});
|
});
|
||||||
@ -110,7 +109,8 @@ addEventListener("DOMContentLoaded", function dom() {
|
|||||||
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"));
|
||||||
|
@ -20,7 +20,6 @@ import {DownloadItem, DownloadTable} from "./table";
|
|||||||
import {formatSize} from "../../lib/formatters";
|
import {formatSize} from "../../lib/formatters";
|
||||||
import {_} from "../../lib/i18n";
|
import {_} from "../../lib/i18n";
|
||||||
import {$} from "../winutil";
|
import {$} from "../winutil";
|
||||||
import {StateTexts} from "./state";
|
|
||||||
|
|
||||||
const TIMEOUT_SEARCH = 750;
|
const TIMEOUT_SEARCH = 750;
|
||||||
|
|
||||||
@ -260,7 +259,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,
|
||||||
|
@ -21,10 +21,10 @@ export default class RemovalModalDialog extends ModalDialog {
|
|||||||
this.check = null;
|
this.check = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content() {
|
async getContent() {
|
||||||
const content = $<HTMLTemplateElement>("#removal-template").
|
const content = $<HTMLTemplateElement>("#removal-template").
|
||||||
content.cloneNode(true) as DocumentFragment;
|
content.cloneNode(true) as DocumentFragment;
|
||||||
localize(content);
|
await localize(content);
|
||||||
this.check = content.querySelector(".removal-remember");
|
this.check = content.querySelector(".removal-remember");
|
||||||
$(".removal-text", content).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
|
||||||
@ -52,7 +52,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>());
|
||||||
|
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, {
|
||||||
@ -190,7 +194,7 @@ export class DownloadItem extends EventEmitter {
|
|||||||
if (this.error) {
|
if (this.error) {
|
||||||
return _(this.error) || this.error;
|
return _(this.error) || this.error;
|
||||||
}
|
}
|
||||||
return StateTexts.get(this.state);
|
return REAL_STATE_TEXTS.get(this.state) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
get fmtSpeed() {
|
get fmtSpeed() {
|
||||||
@ -342,6 +346,8 @@ export class DownloadTable extends VirtualTable {
|
|||||||
constructor(treeConfig: any) {
|
constructor(treeConfig: any) {
|
||||||
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;
|
||||||
@ -362,7 +368,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) => {
|
||||||
@ -402,7 +408,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($<HTMLTemplateElement>("#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"));
|
||||||
|
@ -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
|
||||||
|
@ -109,15 +109,14 @@ class CreateFilterDialog extends ModalDialog {
|
|||||||
|
|
||||||
media: HTMLInputElement;
|
media: HTMLInputElement;
|
||||||
|
|
||||||
get content() {
|
getContent() {
|
||||||
const tmpl = $<HTMLTemplateElement>("#create-filter-template").
|
const rv = $<HTMLTemplateElement>("#create-filter-template").
|
||||||
content.cloneNode(true) as DocumentFragment;
|
content.cloneNode(true) as DocumentFragment;
|
||||||
const rv = localize(tmpl);
|
|
||||||
this.label = $("#filter-create-label", rv);
|
this.label = $("#filter-create-label", rv);
|
||||||
this.expr = $("#filter-create-expr", rv);
|
this.expr = $("#filter-create-expr", rv);
|
||||||
this.link = $("#filter-create-type-link", rv);
|
this.link = $("#filter-create-type-link", rv);
|
||||||
this.media = $("#filter-create-type-media", rv);
|
this.media = $("#filter-create-type-media", rv);
|
||||||
return rv;
|
return Promise.resolve(rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
get buttons() {
|
get buttons() {
|
||||||
|
@ -198,15 +198,15 @@ 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);
|
await init();
|
||||||
|
|
||||||
localize(document.documentElement);
|
|
||||||
$("#btnDownload").addEventListener("click", () => download(false));
|
$("#btnDownload").addEventListener("click", () => download(false));
|
||||||
$("#btnPaused").addEventListener("click", () => download(true));
|
$("#btnPaused").addEventListener("click", () => download(true));
|
||||||
$("#btnCancel").addEventListener(
|
$("#btnCancel").addEventListener(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user