downthemall/lib/select.ts
2019-08-26 19:50:06 +02:00

231 lines
5.9 KiB
TypeScript

"use strict";
// License: MIT
// eslint-disable-next-line no-unused-vars
import { Bus, Port } from "./bus";
import { Prefs } from "./prefs";
import { Promised, timeout } from "./util";
import { donate, openPrefs, openUrls } from "./windowutils";
// eslint-disable-next-line no-unused-vars
import { filters, FAST, Filter } from "./filters";
import { WindowStateTracker } from "./windowstatetracker";
import { windows } from "./browser";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "./item";
interface BaseMatchedItem extends BaseItem {
matched?: string | null;
prevMatched?: string | null;
}
export interface ItemDelta {
idx: number;
matched?: string | null;
}
function computeSelection(
filters: Filter[],
items: BaseMatchedItem[],
onlyFast: boolean): ItemDelta[] {
let ws = items.map((item, idx: number) => {
item.idx = idx;
const {matched = null} = item;
item.prevMatched = matched;
item.matched = null;
return item;
});
for (const filter of filters) {
ws = ws.filter(item => {
if (filter.matchItem(item)) {
if (filter.id === FAST) {
item.matched = "fast";
}
else if (!onlyFast && typeof filter.id === "string") {
item.matched = filter.id;
}
else {
item.matched = null;
}
}
return !item.matched;
});
}
return items.filter(item => item.prevMatched !== item.matched). map(item => {
return {
idx: item.idx,
matched: item.matched
};
});
}
function *computeActiveFiltersGen(
filters: Filter[], activeOverrides: Map<string, boolean>) {
for (const filter of filters) {
if (typeof filter.id !== "string") {
continue;
}
const override = activeOverrides.get(filter.id);
if (typeof override === "boolean") {
if (override) {
yield filter;
}
continue;
}
if (filter.active) {
yield filter;
}
}
}
function computeActiveFilters(
filters: Filter[], activeOverrides: Map<string, boolean>) {
return Array.from(computeActiveFiltersGen(filters, activeOverrides));
}
function filtersToDescs(filters: Filter[]) {
return filters.map(f => f.descriptor);
}
export async function select(links: BaseItem[], media: BaseItem[]) {
const fm = await filters();
const tracker = new WindowStateTracker("select", {
minWidth: 700,
minHeight: 500,
});
await tracker.init();
const windowOptions = tracker.getOptions({
url: "/windows/select.html",
type: "popup",
});
const window = await windows.create(windowOptions);
try {
const port = await Promise.race<Port>([
new Promise<Port>(resolve => Bus.oncePort("select", resolve)),
timeout<Port>(5 * 1000)]);
if (!port.isSelf) {
throw Error("Invalid sender connected");
}
tracker.track(window.id, port);
const overrides = new Map();
let fast: Filter | null = null;
let onlyFast: false;
try {
fast = await fm.getFastFilter();
}
catch (ex) {
// ignored
}
const sendFilters = function(delta = false) {
const {linkFilters, mediaFilters} = fm;
const alink = computeActiveFilters(linkFilters, overrides);
const amedia = computeActiveFilters(mediaFilters, overrides);
const sactiveFilters = new Set<any>();
[alink, amedia].forEach(
a => a.forEach(filter => sactiveFilters.add(filter.id)));
const activeFilters = Array.from(sactiveFilters);
const linkFilterDescs = filtersToDescs(linkFilters);
const mediaFilterDescs = filtersToDescs(mediaFilters);
port.post("filters", {linkFilterDescs, mediaFilterDescs, activeFilters});
if (fast) {
alink.unshift(fast);
amedia.unshift(fast);
}
const deltaLinks = computeSelection(alink, links, onlyFast);
const deltaMedia = computeSelection(amedia, media, onlyFast);
if (delta) {
port.post("item-delta", {deltaLinks, deltaMedia});
}
};
const done = new Promised();
port.on("disconnect", () => {
done.reject(new Error("Prematurely disconnected"));
});
port.on("cancel", () => {
done.reject(new Error("User canceled"));
});
port.on("queue", (msg: any) => {
done.resolve(msg);
});
port.on("filter-changed", (spec: any) => {
overrides.set(spec.id, spec.value);
sendFilters(true);
});
port.on("fast-filter", ({fastFilter}) => {
if (fastFilter) {
try {
fast = fm.getFastFilterFor(fastFilter);
}
catch (ex) {
console.error(ex);
fast = null;
}
}
else {
fast = null;
}
sendFilters(true);
});
port.on("onlyfast", ({fast}) => {
onlyFast = fast;
sendFilters(true);
});
port.on("donate", () => {
donate();
});
port.on("prefs", () => {
openPrefs();
});
port.on("openUrls", ({urls}) => {
openUrls(urls);
});
try {
fm.on("changed", () => sendFilters(true));
sendFilters(false);
const type = await Prefs.get("last-type", "links");
port.post("items", {type, links, media});
const {items, options} = await done;
const selectedIndexes = new Set<number>(items);
const selectedList = (options.type === "links" ? links : media);
const selectedItems = selectedList.filter(
(item: BaseItem, idx: number) => selectedIndexes.has(idx));
for (const [filter, override] of overrides) {
const f = fm.get(filter);
if (f) {
f.active = override;
}
}
await fm.save();
return {items: selectedItems, options};
}
finally {
fm.off("changed", sendFilters);
}
}
finally {
try {
await tracker.finalize();
}
catch (ex) {
// window might be gone; ignored
}
try {
await windows.remove(window.id);
}
catch (ex) {
// window might be gone; ignored
}
}
}