810 lines
19 KiB
TypeScript
810 lines
19 KiB
TypeScript
"use strict";
|
|
// License: MIT
|
|
|
|
import { VirtualTable } from "../uikit/lib/table";
|
|
import ModalDialog from "../uikit/lib/modal";
|
|
import { ContextMenu } from "./contextmenu";
|
|
import { iconForPath } from "../lib/windowutils";
|
|
import { _, localize } from "../lib/i18n";
|
|
import { Prefs } from "../lib/prefs";
|
|
import { MASK, FASTFILTER } from "../lib/recentlist";
|
|
import { WindowState } from "./windowstate";
|
|
import { Dropdown } from "./dropdown";
|
|
import { Keys } from "./keys";
|
|
import { Icons } from "./icons";
|
|
import { sort, naturalCaseCompare } from "../lib/sorting";
|
|
import { hookButton } from "../lib/manager/renamer";
|
|
import { CellTypes } from "../uikit/lib/constants";
|
|
// eslint-disable-next-line no-unused-vars
|
|
import { runtime, RawPort } from "../lib/browser";
|
|
import { $ } from "./winutil";
|
|
// eslint-disable-next-line no-unused-vars
|
|
import { BaseItem } from "../lib/item";
|
|
// eslint-disable-next-line no-unused-vars
|
|
import { ItemDelta } from "../lib/select";
|
|
// eslint-disable-next-line no-unused-vars
|
|
import { TableConfig } from "../uikit/lib/config";
|
|
|
|
const PORT: RawPort = runtime.connect(null, { name: "select" });
|
|
|
|
const TREE_CONFIG_VERSION = 1;
|
|
|
|
const COL_CHECK = 0;
|
|
const COL_DOWNLOAD = 1;
|
|
const COL_TITLE = 2;
|
|
const COL_DESC = 3;
|
|
const COL_MASK = 4;
|
|
const COL_REFERRER = 5;
|
|
|
|
const ICON_BASE_SIZE = 16;
|
|
const NUM_FILTER_CLASSES = 8;
|
|
|
|
let Table: SelectionTable;
|
|
let Mask: Dropdown;
|
|
let FastFilter: Dropdown;
|
|
|
|
|
|
type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]};
|
|
|
|
interface BaseMatchedItem extends BaseItem {
|
|
matched?: string | null;
|
|
rowid: number;
|
|
}
|
|
|
|
function cleaErrors() {
|
|
const not = $("#notification");
|
|
not.textContent = "";
|
|
not.style.display = "none";
|
|
}
|
|
|
|
|
|
function matched(item: BaseMatchedItem) {
|
|
return item && item.matched && item.matched !== "unmanual";
|
|
}
|
|
|
|
class PausedModalDialog extends ModalDialog {
|
|
getContent() {
|
|
const tmpl = $<HTMLTemplateElement>("#paused-template");
|
|
const content = tmpl.content.cloneNode(true) as DocumentFragment;
|
|
return content;
|
|
}
|
|
|
|
shown() {
|
|
this.focusDefault();
|
|
}
|
|
|
|
get buttons() {
|
|
return [
|
|
{
|
|
title: _("remember"),
|
|
value: "ok",
|
|
default: true,
|
|
dismiss: false
|
|
},
|
|
{
|
|
title: _("add-paused-once"),
|
|
value: "once",
|
|
default: false,
|
|
dismiss: false
|
|
},
|
|
{
|
|
title: _("cancel"),
|
|
value: "cancel",
|
|
default: false,
|
|
dismiss: true
|
|
}
|
|
];
|
|
}
|
|
}
|
|
|
|
class CheckClasser extends Map<string, string> {
|
|
gen: IterableIterator<string>;
|
|
|
|
constructor(numClasses: number) {
|
|
super();
|
|
this.gen = (function *() {
|
|
for (;;) {
|
|
for (let c = 0; c < numClasses; ++c) {
|
|
yield `filter-${c + 1}`;
|
|
}
|
|
}
|
|
})();
|
|
this.set("manual", "filter-manual");
|
|
this.set("fast", "filter-fast");
|
|
}
|
|
|
|
"get"(key: string) {
|
|
let result = super.get(key);
|
|
if (typeof result !== "string") {
|
|
result = this.gen.next().value;
|
|
super.set(key, result);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
type KeyFn = (item: BaseMatchedItem) => any;
|
|
|
|
class ItemCollection {
|
|
private items: BaseMatchedItem[];
|
|
|
|
private indexes: Map<number, BaseMatchedItem>;
|
|
|
|
constructor(items: BaseMatchedItem[]) {
|
|
this.items = items;
|
|
this.assignRows();
|
|
this.indexes = new Map(items.map(i => [i.idx, i]));
|
|
}
|
|
|
|
assignRows() {
|
|
this.items.forEach((item, idx) => item.rowid = idx);
|
|
}
|
|
|
|
get length() {
|
|
return this.items.length;
|
|
}
|
|
|
|
get checked() {
|
|
const rv: number[] = [];
|
|
this.items.forEach(function (item, idx) {
|
|
if (item.matched && item.matched !== "unmanual") {
|
|
rv.push(idx);
|
|
}
|
|
});
|
|
return rv;
|
|
}
|
|
|
|
get checkedIndexes() {
|
|
const rv: number[] = [];
|
|
this.items.forEach(function (item) {
|
|
if (item.matched && item.matched !== "unmanual") {
|
|
rv.push(item.idx);
|
|
}
|
|
});
|
|
return rv;
|
|
}
|
|
|
|
|
|
at(idx: number) {
|
|
return this.items[idx];
|
|
}
|
|
|
|
byIndex(idx: number) {
|
|
return this.indexes.get(idx);
|
|
}
|
|
|
|
sort(keyFn: KeyFn) {
|
|
sort(this.items, keyFn, naturalCaseCompare);
|
|
this.assignRows();
|
|
}
|
|
|
|
reverse() {
|
|
this.items.reverse();
|
|
this.assignRows();
|
|
}
|
|
|
|
filter(fn: (item: BaseMatchedItem, idx: number) => boolean) {
|
|
return this.items.filter(fn);
|
|
}
|
|
}
|
|
|
|
class SelectionTable extends VirtualTable {
|
|
checkClasser: CheckClasser;
|
|
|
|
icons: Icons;
|
|
|
|
links: ItemCollection;
|
|
|
|
media: ItemCollection;
|
|
|
|
items: ItemCollection;
|
|
|
|
type: string;
|
|
|
|
status: HTMLElement;
|
|
|
|
linksTab: HTMLElement;
|
|
|
|
mediaTab: HTMLElement;
|
|
|
|
linksFilters: HTMLElement;
|
|
|
|
mediaFilters: HTMLElement;
|
|
|
|
contextMenu: ContextMenu;
|
|
|
|
sortcol: number | null;
|
|
|
|
sortasc: boolean;
|
|
|
|
keyfns: Map<string, KeyFn>;
|
|
|
|
constructor(
|
|
treeConfig: TableConfig | null, type: string,
|
|
links: BaseMatchedItem[], media: BaseMatchedItem[]) {
|
|
if (type === "links" && !links.length) {
|
|
type = "media";
|
|
}
|
|
else if (type === "media" && !media.length) {
|
|
type = "links";
|
|
}
|
|
super("#items", treeConfig, TREE_CONFIG_VERSION);
|
|
|
|
this.checkClasser = new CheckClasser(NUM_FILTER_CLASSES);
|
|
this.icons = new Icons($("#icons") as HTMLStyleElement);
|
|
this.links = new ItemCollection(links);
|
|
this.media = new ItemCollection(media);
|
|
this.type = type;
|
|
this.items = type === "links" ? this.links : this.media;
|
|
|
|
this.status = $("#statusItems");
|
|
this.linksTab = $("#linksTab");
|
|
if (!links.length) {
|
|
this.linksTab.classList.add("disabled");
|
|
}
|
|
else {
|
|
this.linksTab.addEventListener(
|
|
"click", this.switchTab.bind(this, "links"));
|
|
}
|
|
|
|
this.mediaTab = $("#mediaTab");
|
|
if (!media.length) {
|
|
this.mediaTab.classList.add("disabled");
|
|
}
|
|
else {
|
|
this.mediaTab.addEventListener(
|
|
"click", this.switchTab.bind(this, "media"));
|
|
}
|
|
this.linksFilters = $("#linksFilters");
|
|
this.mediaFilters = $("#mediaFilters");
|
|
|
|
localize(($("#table-context") as HTMLTemplateElement).content);
|
|
this.contextMenu = new ContextMenu("#table-context");
|
|
Keys.adoptContext(this.contextMenu);
|
|
|
|
this.sortcol = null;
|
|
this.sortasc = true;
|
|
this.keyfns = new Map<string, KeyFn>([
|
|
["colDownload", item => item.usable],
|
|
["colTitle", item => [item.title, item.usable]],
|
|
["colDescription", item => [item.description, item.usable]],
|
|
["colMask", item => [item.mask, item.usable]],
|
|
]);
|
|
|
|
this.on("config-changed", () => {
|
|
Prefs.set("tree-config-select", JSON.stringify(this));
|
|
});
|
|
this.on("column-clicked", colid => {
|
|
const keyfn = this.keyfns.get(colid);
|
|
if (!keyfn) {
|
|
return false;
|
|
}
|
|
this.links.sort(keyfn);
|
|
this.media.sort(keyfn);
|
|
const elem = document.querySelector<HTMLElement>(`#${colid}`);
|
|
const oldelem = (this.sortcol && document.querySelector<HTMLElement>(`#${this.sortcol}`));
|
|
if (this.sortcol === colid && this.sortasc) {
|
|
this.links.reverse();
|
|
this.media.reverse();
|
|
this.sortasc = false;
|
|
}
|
|
else {
|
|
this.sortcol = colid;
|
|
this.sortasc = true;
|
|
}
|
|
if (oldelem) {
|
|
oldelem.dataset.sortdir = "";
|
|
}
|
|
if (elem) {
|
|
elem.dataset.sortdir = this.sortasc ? "asc" : "desc";
|
|
}
|
|
this.invalidate();
|
|
return true;
|
|
});
|
|
this.on(" -keypress", () => this.checkSelection());
|
|
this.contextMenu.on("ctx-check-selected", () => {
|
|
this.checkSelection("manual");
|
|
});
|
|
this.contextMenu.on("ctx-uncheck-selected", () => {
|
|
this.checkSelection("unmanual");
|
|
});
|
|
this.contextMenu.on("ctx-toggle-selected", () => {
|
|
this.checkSelection("toggle");
|
|
});
|
|
|
|
Keys.on("ACCEL-KeyA", (event: KeyboardEvent) => {
|
|
const target = event.target as HTMLElement;
|
|
if (target.localName === "input") {
|
|
return false;
|
|
}
|
|
this.selectAll();
|
|
return true;
|
|
});
|
|
|
|
Keys.on("ACCEL-KeyF", () => {
|
|
this.selectChecked();
|
|
return true;
|
|
});
|
|
|
|
Keys.on("ACCEL-KeyI", () => {
|
|
this.selectToggle();
|
|
return true;
|
|
});
|
|
|
|
Keys.on("ACCEL-KeyO", () => {
|
|
this.openSelection();
|
|
return true;
|
|
});
|
|
|
|
this.contextMenu.on("ctx-mask", async() => {
|
|
if (this.selection.empty) {
|
|
return;
|
|
}
|
|
let oldmask = "";
|
|
for (const r of this.selection) {
|
|
const m = this.items.at(r).mask;
|
|
if (oldmask && m !== oldmask) {
|
|
oldmask = "";
|
|
break;
|
|
}
|
|
oldmask = m || oldmask;
|
|
}
|
|
try {
|
|
Keys.suppressed = true;
|
|
const newmask = await ModalDialog.prompt(
|
|
"Renaming mask", "Set new renaming mask", oldmask);
|
|
for (const r of this.selection) {
|
|
this.items.at(r).mask = newmask;
|
|
this.invalidateRow(r);
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.warn("mask dismissed", ex);
|
|
}
|
|
finally {
|
|
Keys.suppressed = false;
|
|
}
|
|
});
|
|
|
|
this.contextMenu.on("dismissed", () => this.table.focus());
|
|
|
|
this.on("contextmenu", (tree, event) => {
|
|
if (!this.selection.empty) {
|
|
this.contextMenu.show(event);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
this.init();
|
|
this.switchTab(type);
|
|
}
|
|
|
|
get rowCount() {
|
|
return this.items.length;
|
|
}
|
|
|
|
checkSelection(state?: string) {
|
|
if (this.selection.empty) {
|
|
return false;
|
|
}
|
|
for (const rowid of this.selection) {
|
|
const item = this.items.at(rowid);
|
|
if (!state) {
|
|
state = matched(item) ? "unmanual" : "manual";
|
|
}
|
|
let ns;
|
|
switch (state) {
|
|
case "toggle":
|
|
ns = matched(item) ? "unmanual" : "manual";
|
|
break;
|
|
default:
|
|
ns = state;
|
|
}
|
|
item.matched = ns;
|
|
this.invalidateRow(rowid);
|
|
}
|
|
this.updateStatus();
|
|
return true;
|
|
}
|
|
|
|
selectAll() {
|
|
this.selection.add(0, this.rowCount - 1);
|
|
}
|
|
|
|
selectChecked() {
|
|
this.selection.clear();
|
|
let min = null;
|
|
for (const ci of this.items.checked) {
|
|
this.selection.add(ci);
|
|
min = min === null ? ci : Math.min(min, ci);
|
|
}
|
|
if (min !== null) {
|
|
this.scrollIntoView(min);
|
|
}
|
|
}
|
|
|
|
selectToggle() {
|
|
this.selection.toggle(0, this.rowCount - 1);
|
|
}
|
|
|
|
openSelection() {
|
|
const items = this.items.filter((i, idx) => this.selection.contains(idx));
|
|
if (!items.length) {
|
|
if (this.focusRow < 0) {
|
|
return;
|
|
}
|
|
items.push(this.items.at(this.focusRow));
|
|
}
|
|
PORT.postMessage({
|
|
msg: "openUrls",
|
|
urls: items.map(e => e.url)
|
|
});
|
|
}
|
|
|
|
applyDeltaTo(delta: ItemDelta[], items: ItemCollection) {
|
|
const active = items === this.items;
|
|
for (const d of delta) {
|
|
const {idx = -1, matched = null} = d;
|
|
if (idx < 0) {
|
|
continue;
|
|
}
|
|
const item = items.byIndex(idx);
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
if (item.matched === matched) {
|
|
continue;
|
|
}
|
|
if (matched !== "fast" &&
|
|
(item.matched === "manual" || item.matched === "unmanual")) {
|
|
// Skip manually selected items
|
|
continue;
|
|
}
|
|
item.matched = matched;
|
|
if (active) {
|
|
this.invalidateRow(item.rowid);
|
|
}
|
|
}
|
|
}
|
|
|
|
applyDeltas({deltaLinks = [], deltaMedia = []}: DELTAS) {
|
|
this.applyDeltaTo(deltaLinks, this.links);
|
|
this.applyDeltaTo(deltaMedia, this.media);
|
|
this.updateStatus();
|
|
}
|
|
|
|
switchTab(type: string) {
|
|
this.type = type;
|
|
const isLinks = type === "links";
|
|
this.linksTab.classList[isLinks ? "add" : "remove"]("active");
|
|
this.mediaTab.classList[!isLinks ? "add" : "remove"]("active");
|
|
this.linksFilters.classList[isLinks ? "add" : "remove"]("active");
|
|
this.mediaFilters.classList[!isLinks ? "add" : "remove"]("active");
|
|
this.items = (this as any)[type];
|
|
this.selection.clear();
|
|
this.invalidate();
|
|
this.updateStatus();
|
|
}
|
|
|
|
updateStatus() {
|
|
const selected = this.items.checked.length;
|
|
if (!selected) {
|
|
this.status.textContent = _("noitems.label");
|
|
}
|
|
else {
|
|
this.status.textContent = _("numitems.label", [selected]);
|
|
}
|
|
cleaErrors();
|
|
}
|
|
|
|
getRowClasses(rowid: number) {
|
|
const item = this.items.at(rowid);
|
|
if (!item || !matched(item) || !item.matched) {
|
|
return null;
|
|
}
|
|
return ["filtered", this.checkClasser.get(item.matched)];
|
|
}
|
|
|
|
getCellIcon(rowid: number, colid: number) {
|
|
const item = this.items.at(rowid);
|
|
if (item && colid === COL_DOWNLOAD) {
|
|
return this.icons.get(iconForPath(item.url, ICON_BASE_SIZE));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getCellType(rowid: number, colid: number) {
|
|
switch (colid) {
|
|
case COL_CHECK:
|
|
return CellTypes.TYPE_CHECK;
|
|
default:
|
|
return CellTypes.TYPE_TEXT;
|
|
}
|
|
}
|
|
|
|
getDownloadText(idx: number) {
|
|
const item = this.items.at(idx);
|
|
if (!item) {
|
|
return "";
|
|
}
|
|
if (item.fileName) {
|
|
return `${item.usable} (${item.fileName})`;
|
|
}
|
|
return item.usable;
|
|
}
|
|
|
|
getText(prop: string, idx: number) {
|
|
const item: any = this.items.at(idx);
|
|
if (!item || !(prop in item) || !item[prop]) {
|
|
return "";
|
|
}
|
|
return item[prop];
|
|
}
|
|
|
|
getMaskText(idx: number) {
|
|
const item = this.items.at(idx);
|
|
if (item) {
|
|
return item.mask;
|
|
}
|
|
return _("mask.default");
|
|
}
|
|
|
|
getCellText(rowid: number, colid: number) {
|
|
switch (colid) {
|
|
case COL_DOWNLOAD:
|
|
return this.getDownloadText(rowid);
|
|
|
|
case COL_TITLE:
|
|
return this.getText("title", rowid);
|
|
|
|
case COL_DESC:
|
|
return this.getText("description", rowid);
|
|
|
|
case COL_REFERRER:
|
|
return this.getText("usableReferrer", rowid);
|
|
|
|
case COL_MASK:
|
|
return this.getMaskText(rowid);
|
|
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
getCellCheck(rowid: number, colid: number) {
|
|
if (colid === COL_CHECK) {
|
|
return !!matched(this.items.at(rowid));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
setCellCheck(rowid: number, colid: number, value: boolean) {
|
|
this.items.at(rowid).matched = value ? "manual" : "unmanual";
|
|
this.invalidateRow(rowid);
|
|
this.updateStatus();
|
|
}
|
|
}
|
|
|
|
async function download(paused = false) {
|
|
try {
|
|
const mask = Mask.value;
|
|
if (!mask) {
|
|
throw new Error("error.invalidMask");
|
|
}
|
|
const items = Table.items.checkedIndexes;
|
|
if (!items.length) {
|
|
throw new Error("error.noItemsSelected");
|
|
}
|
|
if (paused && !(await Prefs.get("add-paused"))) {
|
|
try {
|
|
Keys.suppressed = true;
|
|
const remember = await new PausedModalDialog().show();
|
|
if (remember === "ok") {
|
|
await Prefs.set("add-paused", true);
|
|
await Prefs.save();
|
|
}
|
|
}
|
|
catch (ex) {
|
|
return;
|
|
}
|
|
finally {
|
|
Keys.suppressed = false;
|
|
}
|
|
}
|
|
if (!paused) {
|
|
await Prefs.set("add-paused", false);
|
|
}
|
|
PORT.postMessage({
|
|
msg: "queue",
|
|
items,
|
|
options: {
|
|
type: Table.type,
|
|
paused,
|
|
mask,
|
|
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
|
|
fast: FastFilter.value,
|
|
fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
|
|
}
|
|
});
|
|
}
|
|
catch (ex) {
|
|
const not = $("#notification");
|
|
const msg = _(ex.message || ex);
|
|
not.textContent = msg || ex.message || ex;
|
|
not.style.display = "block";
|
|
}
|
|
}
|
|
|
|
class Filter {
|
|
active: boolean;
|
|
|
|
checkElem: HTMLInputElement;
|
|
|
|
container: HTMLElement;
|
|
|
|
elem: HTMLLabelElement;
|
|
|
|
id: string;
|
|
|
|
label: string;
|
|
|
|
constructor(container: HTMLElement, raw: any, active = false) {
|
|
Object.assign(this, raw);
|
|
this.active = active;
|
|
this.container = container;
|
|
|
|
this.elem = document.createElement("label");
|
|
this.elem.classList.add("filter");
|
|
this.elem.setAttribute("title", this.elem.textContent = this.label);
|
|
this.checkElem = document.createElement("input");
|
|
this.checkElem.setAttribute("type", "checkbox");
|
|
this.checkElem.checked = active;
|
|
this.elem.insertBefore(this.checkElem, this.elem.firstChild);
|
|
this.container.appendChild(this.elem);
|
|
this.checkElem.addEventListener("change", this.changed.bind(this));
|
|
}
|
|
|
|
changed() {
|
|
PORT.postMessage({
|
|
msg: "filter-changed",
|
|
id: this.id,
|
|
value: this.checkElem.checked
|
|
});
|
|
}
|
|
}
|
|
|
|
function setFiltersInternal(
|
|
desc: string, filters: any[], active: Set<string>) {
|
|
const container = $(desc);
|
|
container.textContent = "";
|
|
for (let filter of filters) {
|
|
filter = new Filter(container, filter, active.has(filter.id));
|
|
}
|
|
}
|
|
|
|
function setFilters(filters: any) {
|
|
const {
|
|
linkFilterDescs = [],
|
|
mediaFilterDescs = [],
|
|
activeFilters = []
|
|
} = filters;
|
|
const active: Set<string> = new Set(activeFilters);
|
|
setFiltersInternal("#linksFilters", linkFilterDescs, active);
|
|
setFiltersInternal("#mediaFilters", mediaFilterDescs, active);
|
|
}
|
|
|
|
function cancel() {
|
|
PORT.postMessage("cancel");
|
|
return true;
|
|
}
|
|
|
|
async function init() {
|
|
await Promise.all([MASK.init(), FASTFILTER.init()]);
|
|
Mask = new Dropdown("#mask", MASK.values);
|
|
Mask.on("changed", cleaErrors);
|
|
FastFilter = new Dropdown("#fast", FASTFILTER.values);
|
|
FastFilter.on("changed", () => {
|
|
PORT.postMessage({
|
|
msg: "fast-filter",
|
|
fastFilter: FastFilter.value
|
|
});
|
|
});
|
|
}
|
|
|
|
const LOADED = new Promise(resolve => {
|
|
addEventListener("load", function dom() {
|
|
removeEventListener("load", dom);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
|
|
addEventListener("DOMContentLoaded", function dom() {
|
|
removeEventListener("DOMContentLoaded", dom);
|
|
|
|
init().catch(console.error);
|
|
|
|
localize(document.documentElement);
|
|
|
|
$("#donate").addEventListener("click", () => {
|
|
PORT.postMessage({
|
|
msg: "donate",
|
|
});
|
|
});
|
|
$("#statusPrefs").addEventListener("click", () => {
|
|
PORT.postMessage({
|
|
msg: "prefs",
|
|
});
|
|
});
|
|
$("#btnDownload").addEventListener("click", () => download(false));
|
|
$("#btnPaused").addEventListener("click", () => download(true));
|
|
$("#btnCancel").addEventListener(
|
|
"click", cancel);
|
|
$("#fastDisableOthers").addEventListener("change", () => {
|
|
PORT.postMessage({
|
|
msg: "onlyfast",
|
|
fast: $<HTMLInputElement>("#fastDisableOthers").checked
|
|
});
|
|
});
|
|
|
|
Keys.on("Enter", "Return", () => {
|
|
download(false);
|
|
return true;
|
|
});
|
|
Keys.on("ACCEL-Enter", "ACCEL-Return", () => {
|
|
download(true);
|
|
return true;
|
|
});
|
|
|
|
PORT.onMessage.addListener(async (msg: any) => {
|
|
try {
|
|
await LOADED;
|
|
switch (msg.msg) {
|
|
case "items": {
|
|
const {type = "links", links = [], media = []} = msg.data;
|
|
const treeConfig = JSON.parse(
|
|
await Prefs.get("tree-config-select", "{}"));
|
|
requestAnimationFrame(() => {
|
|
Table = new SelectionTable(treeConfig, type, links, media);
|
|
});
|
|
return;
|
|
}
|
|
|
|
case "filters":
|
|
setFilters(msg.data);
|
|
return;
|
|
|
|
case "item-delta":
|
|
requestAnimationFrame(() => {
|
|
Table.applyDeltas(msg.data);
|
|
});
|
|
return;
|
|
|
|
default:
|
|
throw Error("Unhandled message");
|
|
}
|
|
}
|
|
catch (ex) {
|
|
console.error("Failed to process message", msg, ex);
|
|
}
|
|
});
|
|
|
|
Keys.on("Escape", cancel);
|
|
|
|
hookButton($("#maskButton"));
|
|
});
|
|
|
|
addEventListener("contextmenu", event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return false;
|
|
});
|
|
|
|
|
|
addEventListener("beforeunload", function() {
|
|
PORT.disconnect();
|
|
});
|
|
|
|
new WindowState(PORT);
|
|
|