663 lines
16 KiB
TypeScript
663 lines
16 KiB
TypeScript
"use strict";
|
|
// License: MIT
|
|
|
|
import {
|
|
ALL_LANGS,
|
|
_,
|
|
getCurrentLanguage,
|
|
localize,
|
|
saveCustomLocale,
|
|
} from "../lib/i18n";
|
|
import { Prefs, PrefWatcher } from "../lib/prefs";
|
|
import { hostToDomain } from "../lib/util";
|
|
import { filters } from "../lib/filters";
|
|
import {Limits} from "../lib/manager/limits";
|
|
// eslint-disable-next-line no-unused-vars
|
|
import ModalDialog, { ModalButton } from "../uikit/lib/modal";
|
|
import { TYPE_LINK, TYPE_MEDIA } from "../lib/constants";
|
|
import { iconForPath, visible } from "../lib/windowutils";
|
|
import { VirtualTable } from "../uikit/lib/table";
|
|
import { Icons } from "./icons";
|
|
import { $ } from "./winutil";
|
|
import { runtime, storage } from "../lib/browser";
|
|
|
|
const ICON_BASE_SIZE = 16;
|
|
|
|
|
|
class UIPref<T extends HTMLElement> extends PrefWatcher {
|
|
id: string;
|
|
|
|
pref: string;
|
|
|
|
elem: T;
|
|
|
|
constructor(id: string, pref: string) {
|
|
super(pref);
|
|
this.id = id;
|
|
this.pref = pref;
|
|
this.elem = $(`#${id}`);
|
|
if (!this.elem) {
|
|
throw new Error(`Invalid id: ${id}`);
|
|
}
|
|
}
|
|
|
|
async save(value: any) {
|
|
await Prefs.set(this.pref, value);
|
|
}
|
|
}
|
|
|
|
class BoolPref extends UIPref<HTMLInputElement> {
|
|
constructor(id: string, pref: string) {
|
|
super(id, pref);
|
|
this.elem.addEventListener("change", this.change.bind(this));
|
|
}
|
|
|
|
change() {
|
|
this.save(!!this.elem.checked);
|
|
}
|
|
|
|
changed(prefs: any, key: string, value: any) {
|
|
this.elem.checked = !!value;
|
|
return super.changed(prefs, key, value);
|
|
}
|
|
}
|
|
|
|
class IntPref extends UIPref<HTMLInputElement> {
|
|
constructor(id: string, pref: string) {
|
|
super(id, pref);
|
|
this.elem.addEventListener("change", this.change.bind(this));
|
|
}
|
|
|
|
change() {
|
|
if (!this.elem.checkValidity()) {
|
|
return;
|
|
}
|
|
this.save(this.elem.value);
|
|
}
|
|
|
|
changed(prefs: any, key: string, value: any) {
|
|
this.elem.value = value;
|
|
return super.changed(prefs, key, value);
|
|
}
|
|
}
|
|
|
|
class OptionPref extends UIPref<HTMLElement> {
|
|
options: HTMLInputElement[];
|
|
|
|
constructor(id: string, pref: string) {
|
|
super(id, pref);
|
|
this.options = Array.from(this.elem.querySelectorAll<HTMLInputElement>(`*[name='${id}']`));
|
|
this.options.forEach(o => {
|
|
o.addEventListener("change", () => this.change());
|
|
});
|
|
}
|
|
|
|
change() {
|
|
const opt = this.options.find(e => e.checked);
|
|
if (opt && opt.value) {
|
|
this.save(opt.value);
|
|
}
|
|
}
|
|
|
|
changed(prefs: any, key: string, value: any) {
|
|
const opt = this.options.find(e => e.value === value);
|
|
if (opt) {
|
|
opt.checked = true;
|
|
}
|
|
return super.changed(prefs, key, value);
|
|
}
|
|
}
|
|
|
|
class CreateFilterDialog extends ModalDialog {
|
|
label: HTMLInputElement;
|
|
|
|
expr: HTMLInputElement;
|
|
|
|
link: HTMLInputElement;
|
|
|
|
media: HTMLInputElement;
|
|
|
|
getContent() {
|
|
const rv = $<HTMLTemplateElement>("#create-filter-template").
|
|
content.cloneNode(true) as DocumentFragment;
|
|
this.label = $("#filter-create-label", rv);
|
|
this.expr = $("#filter-create-expr", rv);
|
|
this.link = $("#filter-create-type-link", rv);
|
|
this.media = $("#filter-create-type-media", rv);
|
|
return rv;
|
|
}
|
|
|
|
get buttons() {
|
|
return [
|
|
{
|
|
title: _("create-filter"),
|
|
value: "ok",
|
|
default: true
|
|
},
|
|
{
|
|
title: _("cancel"),
|
|
value: "cancel",
|
|
dismiss: true
|
|
}
|
|
];
|
|
}
|
|
|
|
shown() {
|
|
this.label.focus();
|
|
}
|
|
|
|
done(b: ModalButton) {
|
|
if (!b || !b.default) {
|
|
return super.done(b);
|
|
}
|
|
const label = this.label.value.trim();
|
|
const expr = this.expr.value.trim();
|
|
let type = 0;
|
|
if (this.link.checked) {
|
|
type |= TYPE_LINK;
|
|
}
|
|
if (this.media.checked) {
|
|
type |= TYPE_MEDIA;
|
|
}
|
|
|
|
let valid = true;
|
|
if (!label) {
|
|
valid = false;
|
|
this.label.setCustomValidity(_("cannot-be-empty"));
|
|
}
|
|
else {
|
|
this.label.setCustomValidity("");
|
|
}
|
|
if (!expr) {
|
|
valid = false;
|
|
this.expr.setCustomValidity(_("cannot-be-empty"));
|
|
}
|
|
else {
|
|
this.expr.setCustomValidity("");
|
|
}
|
|
if (!type) {
|
|
valid = false;
|
|
this.link.setCustomValidity(_("filter-at-least-one"));
|
|
this.media.setCustomValidity(_("filter-at-least-one"));
|
|
}
|
|
else {
|
|
this.link.setCustomValidity("");
|
|
this.media.setCustomValidity("");
|
|
}
|
|
if (!valid) {
|
|
return undefined;
|
|
}
|
|
|
|
filters().then(async filters => {
|
|
await filters.create(label, expr, type);
|
|
}).catch(console.error);
|
|
|
|
|
|
return super.done(b);
|
|
}
|
|
|
|
async show() {
|
|
await super.show();
|
|
}
|
|
}
|
|
|
|
class FiltersUI extends VirtualTable {
|
|
filters: any[];
|
|
|
|
icons: Icons;
|
|
|
|
edit: {
|
|
label: HTMLInputElement;
|
|
expr: HTMLInputElement;
|
|
link: HTMLInputElement;
|
|
media: HTMLInputElement;
|
|
filter: any;
|
|
row: number;
|
|
};
|
|
|
|
ignoreNext: boolean;
|
|
|
|
constructor() {
|
|
super("#filters", null);
|
|
this.filters = [];
|
|
this.icons = new Icons($("#icons"));
|
|
const filter: any = null;
|
|
this.edit = {
|
|
label: $("#filter-edit-label"),
|
|
expr: $("#filter-edit-expr"),
|
|
link: $("#filter-edit-type-link"),
|
|
media: $("#filter-edit-type-media"),
|
|
filter,
|
|
row: filter,
|
|
};
|
|
this.edit.label.addEventListener("input", () => {
|
|
if (!this.edit.filter) {
|
|
return;
|
|
}
|
|
if (!this.edit.label.checkValidity() ||
|
|
this.edit.label.value.length <= 0) {
|
|
return;
|
|
}
|
|
this.edit.filter.label = this.edit.label.value;
|
|
this.ignoreNext = true;
|
|
this.saveFilter(this.edit.filter, this.edit.row);
|
|
}, true);
|
|
this.edit.expr.addEventListener("input", () => {
|
|
if (!this.edit.filter) {
|
|
return;
|
|
}
|
|
if (!this.edit.expr.checkValidity() || this.edit.expr.value.length <= 0) {
|
|
return;
|
|
}
|
|
this.edit.filter.expr = this.edit.expr.value;
|
|
this.ignoreNext = true;
|
|
this.saveFilter(this.edit.filter, this.edit.row);
|
|
}, true);
|
|
const updateTypes = () => {
|
|
if (!this.edit.filter) {
|
|
return;
|
|
}
|
|
const link = this.edit.link.checked ? TYPE_LINK : 0;
|
|
const media = this.edit.media.checked ? TYPE_MEDIA : 0;
|
|
const type = link | media;
|
|
if (!type) {
|
|
return;
|
|
}
|
|
this.edit.filter.type = type;
|
|
this.ignoreNext = true;
|
|
this.saveFilter(this.edit.filter, this.edit.row);
|
|
};
|
|
this.edit.link.addEventListener("change", updateTypes);
|
|
this.edit.media.addEventListener("change", updateTypes);
|
|
this.on("selection-changed", () => {
|
|
this.edit.filter = null;
|
|
if (this.selection.empty) {
|
|
this.resetEdits();
|
|
return;
|
|
}
|
|
this.edit.row = this.selection.first;
|
|
const f = this.edit.filter = this.filters[this.edit.row];
|
|
if (!this.edit.filter) {
|
|
this.resetEdits();
|
|
return;
|
|
}
|
|
$("#filter-edit").classList.remove("hidden");
|
|
this.edit.label.value = f.label;
|
|
this.edit.expr.value = f.expr;
|
|
this.edit.link.checked = !!(f.type & TYPE_LINK);
|
|
this.edit.media.checked = !!(f.type & TYPE_MEDIA);
|
|
if (this.edit.filter.custom) {
|
|
$("#filter-delete").classList.remove("hidden");
|
|
$("#filter-reset").classList.add("hidden");
|
|
}
|
|
else {
|
|
$("#filter-delete").classList.add("hidden");
|
|
$("#filter-reset").classList.remove("hidden");
|
|
}
|
|
});
|
|
$("#filter-delete").addEventListener("click", () => {
|
|
if (!this.edit.filter) {
|
|
return;
|
|
}
|
|
this.edit.filter.delete().
|
|
then(this.reload.bind(this)).
|
|
catch(console.error);
|
|
});
|
|
$("#filter-reset").addEventListener("click", () => {
|
|
if (!this.edit.filter) {
|
|
return;
|
|
}
|
|
this.edit.filter.reset().
|
|
then(this.reload.bind(this)).
|
|
catch(console.error);
|
|
});
|
|
this.reload().catch(console.error);
|
|
|
|
$("#filter-create-button").addEventListener("click", () => {
|
|
new CreateFilterDialog().show().catch(console.error);
|
|
});
|
|
|
|
filters().then(filters => {
|
|
filters.on("changed", () => {
|
|
this.reload().catch(console.error);
|
|
});
|
|
});
|
|
}
|
|
|
|
async reload() {
|
|
if (this.ignoreNext) {
|
|
return;
|
|
}
|
|
this.ignoreNext = false;
|
|
|
|
this.resetEdits();
|
|
this.filters = (await filters()).all;
|
|
this.init();
|
|
this.invalidate();
|
|
}
|
|
|
|
resetEdits() {
|
|
this.edit.label.value = "";
|
|
this.edit.expr.value = "";
|
|
this.edit.link.checked = false;
|
|
this.edit.media.checked = false;
|
|
$("#filter-delete").classList.add("hidden");
|
|
$("#filter-reset").classList.add("hidden");
|
|
$("#filter-edit").classList.add("hidden");
|
|
}
|
|
|
|
async saveFilter(filter: any, row: any) {
|
|
try {
|
|
this.invalidateRow(row);
|
|
await filter.save();
|
|
}
|
|
catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
|
|
get rowCount() {
|
|
return this.filters.length;
|
|
}
|
|
|
|
getCellIcon(rowid: number, colid: number) {
|
|
if (!colid) {
|
|
const f = this.filters[rowid];
|
|
if (!f) {
|
|
return null;
|
|
}
|
|
const icon = iconForPath(`file${f.icon ? `.${f.icon}` : ""}`, ICON_BASE_SIZE);
|
|
return this.icons.get(icon);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getCellText(rowid: number, colid: number) {
|
|
const f = this.filters[rowid];
|
|
if (!f) {
|
|
return null;
|
|
}
|
|
switch (colid) {
|
|
case 0:
|
|
return f.label;
|
|
|
|
case 1:
|
|
return f.expr;
|
|
|
|
case 2:
|
|
return [TYPE_LINK, TYPE_MEDIA].
|
|
map(t => f.type & t ? _(`filter-type-${t === TYPE_LINK ? "link" : "media"}`) : 0).
|
|
filter(e => e).
|
|
join(", ");
|
|
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
}
|
|
|
|
class LimitsUI extends VirtualTable {
|
|
limits: any[];
|
|
|
|
edit: {
|
|
limit: any;
|
|
domain: HTMLInputElement;
|
|
conlimited: HTMLInputElement;
|
|
conunlimited: HTMLInputElement;
|
|
conlimit: HTMLInputElement;
|
|
save: HTMLButtonElement;
|
|
delete: HTMLButtonElement;
|
|
row: number;
|
|
};
|
|
|
|
constructor() {
|
|
super("#limits", null);
|
|
this.limits = [];
|
|
Limits.on("changed", () => {
|
|
this.limits = Array.from(Limits);
|
|
this.invalidate();
|
|
this.resetEdits();
|
|
});
|
|
Limits.load().then(() => {
|
|
this.limits = Array.from(Limits);
|
|
this.invalidate();
|
|
});
|
|
|
|
this.edit = {
|
|
limit: null,
|
|
domain: $("#limit-edit-domain"),
|
|
conlimited: $("#limit-edit-concurrent-limited"),
|
|
conunlimited: $("#limit-edit-concurrent-unlimited"),
|
|
conlimit: $("#limit-edit-concurrent-limit"),
|
|
save: $("#limit-save"),
|
|
delete: $("#limit-delete"),
|
|
row: -1,
|
|
};
|
|
|
|
this.on("selection-changed", () => {
|
|
this.edit.limit = null;
|
|
if (this.selection.empty) {
|
|
this.resetEdits();
|
|
return;
|
|
}
|
|
this.edit.row = this.selection.first;
|
|
const l = this.edit.limit = this.limits[this.edit.row];
|
|
if (!l) {
|
|
this.resetEdits();
|
|
return;
|
|
}
|
|
$("#limit-edit").classList.remove("hidden");
|
|
this.edit.domain.value = l.domain;
|
|
this.edit.domain.setAttribute("readonly", "readonly");
|
|
if (l.concurrent <= 0) {
|
|
this.edit.conunlimited.checked = true;
|
|
this.edit.conlimit.value = "3";
|
|
}
|
|
else {
|
|
this.edit.conlimited.checked = true;
|
|
this.edit.conlimit.value = l.concurrent;
|
|
}
|
|
|
|
if (l.domain === "*") {
|
|
this.edit.delete.classList.add("hidden");
|
|
}
|
|
else {
|
|
this.edit.delete.classList.remove("hidden");
|
|
}
|
|
});
|
|
|
|
$("#limit-create").addEventListener("click", () => {
|
|
this.selection.clear();
|
|
this.resetEdits();
|
|
this.edit.delete.classList.add("hidden");
|
|
$("#limit-edit").classList.remove("hidden");
|
|
this.edit.domain.focus();
|
|
});
|
|
|
|
this.edit.save.addEventListener("click", () => {
|
|
let domain;
|
|
try {
|
|
if (this.edit.domain.value !== "*") {
|
|
domain = hostToDomain(this.edit.domain.value);
|
|
}
|
|
else {
|
|
domain = "*";
|
|
}
|
|
if (!domain) {
|
|
this.edit.domain.setCustomValidity(
|
|
_("invalid-domain-pref"));
|
|
return;
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.error(ex.message, ex.stack, ex);
|
|
this.edit.domain.setCustomValidity(
|
|
_("invalid-domain-pref"));
|
|
this.edit.domain.setCustomValidity(ex.message || ex.toString());
|
|
return;
|
|
}
|
|
if (this.edit.conlimited.checked && !this.edit.conlimit.checkValidity()) {
|
|
return;
|
|
}
|
|
const concurrent = this.edit.conunlimited.checked ?
|
|
-1 :
|
|
parseInt(this.edit.conlimit.value, 10);
|
|
Limits.saveEntry(domain, {
|
|
domain,
|
|
concurrent
|
|
});
|
|
});
|
|
|
|
this.edit.delete.addEventListener("click", () => {
|
|
if (!this.edit.limit) {
|
|
return;
|
|
}
|
|
Limits.delete(this.edit.limit.domain);
|
|
});
|
|
}
|
|
|
|
resetEdits() {
|
|
this.edit.limit = null;
|
|
this.edit.domain.removeAttribute("readonly");
|
|
this.edit.domain.value = "";
|
|
this.edit.domain.setCustomValidity("");
|
|
this.edit.conunlimited.checked = true;
|
|
this.edit.conlimit.value = "3";
|
|
this.edit.delete.classList.add("hidden");
|
|
$("#limit-edit").classList.add("hidden");
|
|
}
|
|
|
|
get rowCount() {
|
|
return this.limits.length;
|
|
}
|
|
|
|
getCellText(rowid: number, colid: number) {
|
|
const f = this.limits[rowid];
|
|
if (!f) {
|
|
return null;
|
|
}
|
|
switch (colid) {
|
|
case 0:
|
|
return f.domain;
|
|
|
|
case 1:
|
|
return f.concurrent <= 0 ? _("unlimited") : f.concurrent;
|
|
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
addEventListener("DOMContentLoaded", async () => {
|
|
await localize(document.documentElement);
|
|
|
|
// General
|
|
new BoolPref("pref-global-turbo", "global-turbo");
|
|
new BoolPref("pref-queue-notification", "queue-notification");
|
|
new BoolPref("pref-finish-notification", "finish-notification");
|
|
new BoolPref("pref-hide-context", "hide-context");
|
|
new BoolPref("pref-tooltip", "tooltip");
|
|
new BoolPref("pref-open-manager-on-queue", "open-manager-on-queue");
|
|
new BoolPref("pref-text-links", "text-links");
|
|
new BoolPref("pref-add-paused", "add-paused");
|
|
new BoolPref("pref-show-urls", "show-urls");
|
|
new BoolPref("pref-remove-missing-on-init", "remove-missing-on-init");
|
|
new OptionPref("pref-conflict-action", "conflict-action");
|
|
|
|
$("#reset-confirmations").addEventListener("click", async () => {
|
|
for (const k of Prefs) {
|
|
if (!k.startsWith("confirmations.")) {
|
|
continue;
|
|
}
|
|
await Prefs.reset(k);
|
|
}
|
|
await ModalDialog.inform(
|
|
_("information.title"), _("reset-confirmations.done"), _("ok"));
|
|
});
|
|
$("#reset-layout").addEventListener("click", async () => {
|
|
for (const k of Prefs) {
|
|
if (!k.startsWith("tree-config-")) {
|
|
continue;
|
|
}
|
|
await Prefs.reset(k);
|
|
}
|
|
for (const k of Prefs) {
|
|
if (!k.startsWith("window-state-")) {
|
|
continue;
|
|
}
|
|
await Prefs.reset(k);
|
|
}
|
|
await ModalDialog.inform(
|
|
_("information.title"), _("reset-layouts.done"), _("ok"));
|
|
});
|
|
|
|
|
|
const langs = $<HTMLSelectElement>("#languages");
|
|
const currentLang = getCurrentLanguage();
|
|
for (const [code, lang] of ALL_LANGS.entries()) {
|
|
const langEl = document.createElement("option");
|
|
langEl.textContent = lang;
|
|
langEl.value = code;
|
|
if (code === currentLang) {
|
|
langEl.selected = true;
|
|
}
|
|
langs.appendChild(langEl);
|
|
}
|
|
langs.addEventListener("change", async () => {
|
|
await storage.sync.set({language: langs.value});
|
|
if (langs.value === currentLang) {
|
|
return;
|
|
}
|
|
// eslint-disable-next-line max-len
|
|
if (confirm("Changing the selected translation requires restarting the extension.\nDo you want to restart the extension now?")) {
|
|
runtime.reload();
|
|
}
|
|
});
|
|
|
|
// Filters
|
|
visible("#filters").then(() => new FiltersUI());
|
|
|
|
// Network
|
|
new IntPref("pref-concurrent-downloads", "concurrent");
|
|
|
|
visible("#limits").then(() => new LimitsUI());
|
|
|
|
const customLocale = $<HTMLInputElement>("#customLocale");
|
|
$<HTMLInputElement>("#loadCustomLocale").addEventListener("click", () => {
|
|
customLocale.click();
|
|
});
|
|
$<HTMLInputElement>("#clearCustomLocale").addEventListener("click", () => {
|
|
saveCustomLocale(undefined);
|
|
runtime.reload();
|
|
});
|
|
customLocale.addEventListener("change", async () => {
|
|
if (!customLocale.files || !customLocale.files.length) {
|
|
return;
|
|
}
|
|
const [file] = customLocale.files;
|
|
if (!file || file.size > (5 << 20)) {
|
|
return;
|
|
}
|
|
try {
|
|
const text = await new Promise<string>((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
resolve(reader.result as string);
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsText(file);
|
|
});
|
|
saveCustomLocale(text);
|
|
if (confirm("Imported your file.\nWant to relaod the extension now?")) {
|
|
runtime.reload();
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.error(ex);
|
|
alert(`Could not load your translation file:\n${ex.toString()}`);
|
|
}
|
|
});
|
|
});
|