Nils Maier 4d953c373f Basic import/export
Closes #64
2019-09-15 12:28:31 +02:00

1338 lines
34 KiB
TypeScript

"use strict";
// License: MIT
import { VirtualTable } from "../../uikit/lib/table";
import {
ContextMenu,
MenuItem,
// eslint-disable-next-line no-unused-vars
SubMenuItem
} from "../contextmenu";
import { iconForPath } from "../../lib/windowutils";
import { formatSpeed, formatSize, formatTimeDelta } from "../../lib/formatters";
import { filters } from "../../lib/filters";
import { _ } from "../../lib/i18n";
import { EventEmitter } from "../../lib/events";
import { Prefs, PrefWatcher } from "../../lib/prefs";
// eslint-disable-next-line no-unused-vars
import { debounce, URLd } from "../../lib/util";
import { Keys } from "../keys";
import { Broadcaster } from "../broadcaster";
import { Icons } from "../icons";
import { Buttons } from "./buttons";
import {
TextFilter, UrlMenuFilter, StateMenuFilter, SizeMenuFilter,
// eslint-disable-next-line no-unused-vars
MenuFilter
} from "./itemfilters";
import { FilteredCollection } from "./itemfilters";
import { RemovalModalDialog, DeleteFilesDialog } from "./removaldlg";
import { Stats } from "./stats";
import PORT from "./port";
import { DownloadState, StateTexts, StateClasses, StateIcons } from "./state";
import { Tooltip } from "./tooltip";
import "../../lib/util";
import { CellTypes } from "../../uikit/lib/constants";
import { downloads, CHROME } from "../../lib/browser";
import { $ } from "../winutil";
// eslint-disable-next-line no-unused-vars
import { TableConfig } from "../../uikit/lib/config";
import { IconCache } from "../../lib/iconcache";
import * as imex from "../../lib/imex";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../../lib/item";
import { API } from "../../lib/api";
const TREE_CONFIG_VERSION = 2;
const RUNNING_TIMEOUT = 1000;
const SIZES_TIMEOUT = 150;
const COL_URL = 0;
const COL_DOMAIN = 1;
const COL_PROGRESS = 2;
const COL_PER = 3;
const COL_SIZE = 4;
const COL_ETA = 5;
const COL_SPEED = 6;
const COL_MASK = 7;
const COL_SEGS = 8;
const HIDPI = window.matchMedia &&
window.matchMedia("(min-resolution: 2dppx)").matches;
const ICON_BASE_SIZE = 16;
const ICON_REAL_SIZE = !CHROME && HIDPI ? ICON_BASE_SIZE * 2 : ICON_BASE_SIZE;
// eslint-disable-next-line no-magic-numbers
const LARGE_ICON_BASE_SIZE = CHROME ? 32 : 64;
// eslint-disable-next-line no-magic-numbers
const MAX_ICON_BASE_SIZE = CHROME ? 32 : 127;
const LARGE_ICON_REAL_SIZE = HIDPI ? MAX_ICON_BASE_SIZE : LARGE_ICON_BASE_SIZE;
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 rv = new Intl.NumberFormat(undefined, {
style: "decimal",
useGrouping: true,
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
return rv.format.bind(rv);
})();
class ShowUrlsWatcher extends PrefWatcher {
private readonly table: DownloadTable;
constructor(table: DownloadTable) {
super("show-urls", false);
this.table = table;
}
changed(prefs: any, name: string, value: any) {
const rv = super.changed(prefs, name, value);
this.table.invalidate();
return rv;
}
}
export class DownloadItem extends EventEmitter {
public readonly stats: Stats;
public readonly owner: DownloadTable;
private lastWritten: number;
public state: number;
public totalSize: number;
public written: number;
public domain: string;
public usable: string;
public description: string;
public error: string;
public currentName: string;
public ext?: string;
public position: number;
public filteredPosition: number;
public manId: number;
public url: string;
public uURL: URLd;
public destFull: string;
public startDate: number;
public sessionId: number;
public batch: number;
public mask: string;
private iconField?: string;
private largeIconField?: string;
public opening: boolean;
public retries: number;
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
super();
Object.assign(this, raw);
this.updateURL();
this.stats = stats || new Stats();
this.owner = owner;
this.owner.updatedState(this, undefined, this.state);
this.lastWritten = 0;
}
get icon() {
if (this.iconField) {
return this.iconField;
}
this.iconField = this.owner.icons.get(
iconForPath(this.currentName, ICON_BASE_SIZE));
if (this.ext) {
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
if (icon) {
this.iconField = this.owner.icons.get(icon);
if (typeof this.filteredPosition !== undefined) {
this.owner.invalidateCell(this.filteredPosition, COL_URL);
}
}
});
}
return this.iconField || "";
}
get largeIcon() {
if (this.largeIconField) {
return this.largeIconField;
}
this.largeIconField = this.owner.icons.get(
iconForPath(this.currentName, LARGE_ICON_BASE_SIZE));
if (this.ext) {
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
if (icon) {
this.largeIconField = this.owner.icons.get(icon);
}
this.emit("largeIcon");
});
}
return this.largeIconField || "";
}
get eta() {
const {avg} = this.stats;
if (!this.totalSize || !avg) {
return TEXT_SIZE_UNKNOWM;
}
const remain = this.totalSize - this.written;
return formatTimeDelta(remain / avg);
}
get isFiltered() {
return typeof this.filteredPosition !== "undefined";
}
get percent() {
if (this.state === DownloadState.DONE) {
return 1;
}
if (!this.totalSize) {
return 0;
}
return this.written / this.totalSize;
}
get fmtName() {
if (this.owner.showUrls.value) {
return this.usable;
}
return this.currentName;
}
get fmtSize() {
if (this.state & (DownloadState.RUNNING | DownloadState.PAUSED)) {
if (!this.written) {
return TEXT_SIZE_UNKNOWM;
}
if (!this.totalSize) {
return formatSize(this.written);
}
return _("size-progress",
formatSize(this.written), formatSize(this.totalSize));
}
if (!this.totalSize) {
return TEXT_SIZE_UNKNOWM;
}
return formatSize(this.totalSize);
}
get fmtPercent() {
return `${(this.percent * 100).toFixed(0)}%`;
}
get fmtETA() {
if (this.state === DownloadState.RUNNING) {
return this.eta;
}
if (this.state === DownloadState.RETRYING) {
if (this.error) {
return _("retrying_error", _(this.error) || this.error);
}
return _("retrying");
}
if (this.error) {
return _(this.error) || this.error;
}
return REAL_STATE_TEXTS.get(this.state) || "";
}
get fmtSpeed() {
return this.state === DownloadState.RUNNING ?
formatSpeed(this.stats.avg) :
"";
}
get fmtDomain() {
return this.domain;
}
updateDownload(raw: any) {
if (("position" in raw) && raw.position !== this.position) {
console.warn("position mismatch", raw.position, this.position);
PORT.post("all");
return;
}
if (("ext" in raw) && raw.ext !== this.ext) {
this.clearIcons();
}
delete raw.position;
delete raw.owner;
const oldState = this.state;
Object.assign(this, raw);
if (raw.url) {
this.updateURL();
}
if (this.state !== oldState) {
this.stats.clear();
this.owner.updatedState(this, oldState, this.state);
}
this.owner.updatedDownload(this);
this.emit("update");
}
async queryState() {
const [state] = await downloads.search({id: this.manId});
return state;
}
adoptSize(state: any) {
const {
bytesReceived,
totalBytes,
fileSize
} = state;
this.written = Math.max(0, bytesReceived);
this.totalSize = Math.max(0, fileSize >= 0 ? fileSize : totalBytes);
}
async updateSizes() {
if (!this.manId) {
return;
}
const state = await this.queryState();
if (!this.manId) {
return;
}
this.adoptSize(state);
if (this.isFiltered) {
this.owner.invalidateCell(this.filteredPosition, COL_PROGRESS);
this.owner.invalidateCell(this.filteredPosition, COL_PER);
this.owner.invalidateCell(this.filteredPosition, COL_SIZE);
}
}
async updateStats() {
if (this.state !== DownloadState.RUNNING) {
return -1;
}
let v = 0;
try {
if (this.manId) {
const state = await this.queryState();
if (!this.manId) {
return -1;
}
this.adoptSize(state);
if (!this.lastWritten) {
this.lastWritten = Math.max(0, this.written);
return -1;
}
v = Math.max(0, this.written - this.lastWritten);
this.lastWritten = Math.max(0, this.written);
}
}
catch (ex) {
console.error("failed to stat", ex);
}
this.stats.add(v);
if (this.isFiltered) {
this.owner.invalidateRow(this.filteredPosition);
}
this.emit("stats");
return this.stats.avg;
}
updateURL() {
this.uURL = new URL(this.url) as URLd;
this.domain = this.uURL.domain;
this.emit("url");
}
clearIcons() {
this.iconField = undefined;
this.largeIconField = undefined;
}
clearFontIcons() {
if (this.iconField && this.iconField.startsWith("icon-")) {
this.iconField = undefined;
}
if (this.largeIconField && this.largeIconField.startsWith("icon-")) {
this.largeIconField = undefined;
}
}
}
export class DownloadTable extends VirtualTable {
private finished: number;
private readonly resumeAction: Broadcaster;
private readonly pauseAction: Broadcaster;
private readonly cancelAction: Broadcaster;
private readonly running: Set<DownloadItem>;
public readonly showUrls: ShowUrlsWatcher;
private runningTimer: number | null;
private sizesTimer: number | null;
private readonly globalStats: Stats;
private readonly downloads: FilteredCollection;
private readonly sids: Map<number, DownloadItem>;
public readonly icons: Icons;
private readonly contextMenu: ContextMenu;
private readonly forceAction: Broadcaster;
private readonly openFileAction: Broadcaster;
private readonly openDirectoryAction: Broadcaster;
private readonly deleteFilesAction: Broadcaster;
private readonly moveTopAction: Broadcaster;
private readonly moveUpAction: Broadcaster;
private readonly moveDownAction: Broadcaster;
private readonly moveBottomAction: Broadcaster;
private readonly disableSet: Set<Broadcaster>;
private tooltip: Tooltip | null;
constructor(treeConfig: TableConfig | null) {
super("#items", treeConfig, TREE_CONFIG_VERSION);
TEXT_SIZE_UNKNOWM = _("size-unknown");
this.finished = 0;
this.running = new Set();
this.runningTimer = null;
this.globalStats = new Stats();
this.showUrls = new ShowUrlsWatcher(this);
this.updateCounts = debounce(this.updateCounts.bind(this), 100);
this.onIconCached = debounce(this.onIconCached.bind(this), 1000);
this.downloads = new FilteredCollection(this);
this.downloads.on("changed", () => this.updateCounts());
this.downloads.on("added", () => this.updateCounts());
this.downloads.on("sorted", () => {
PORT.post("sorted", {sids: this.downloads.items.map(i => i.sessionId)});
});
this.updateCounts();
new TextFilter(this.downloads);
const menufilters = new Map<string, MenuFilter>([
["colURL", new UrlMenuFilter(this.downloads)],
["colETA", new StateMenuFilter(this.downloads, REAL_STATE_TEXTS)],
["colSize", new SizeMenuFilter(this.downloads)],
]);
this.on("column-clicked", (id, evt, col) => {
const mf = menufilters.get(id);
const {left, bottom} = col.elem.getBoundingClientRect();
if (!mf) {
return undefined;
}
mf.show({clientX: left, clientY: bottom});
return true;
});
const filterforColumn = new Map(Array.from(
menufilters.entries()).map(([col, f]) => [f.id, col]));
this.downloads.on("filter-active", filter => {
const name = filterforColumn.get(filter);
if (!name) {
return;
}
const col = this.getColumnByName(name);
if (!col) {
return;
}
col.iconElem.classList.add("icon-filter");
});
this.downloads.on("filter-inactive", filter => {
const name = filterforColumn.get(filter);
if (!name) {
return;
}
const col = this.getColumnByName(name);
if (!col) {
return;
}
col.iconElem.classList.remove("icon-filter");
});
IconCache.on("cached", this.onIconCached.bind(this));
this.sids = new Map<number, DownloadItem>();
this.icons = new Icons($("#icons"));
const ctx = this.contextMenu = new ContextMenu("#table-context");
Keys.adoptContext(ctx);
Keys.adoptButtons($("#toolbar"));
this.on("config-changed", () => {
Prefs.set("tree-config-manager", JSON.stringify(this));
});
Keys.on("ACCEL-KeyA", (event: Event) => {
const target = event.target as HTMLElement;
if (target.localName === "input") {
return false;
}
this.selectAll();
return true;
});
Keys.on("ACCEL-KeyI", () => {
this.selectToggle();
return true;
});
Keys.on("Delete", (event: Event) => {
const target = event.target as HTMLElement;
if (target.localName === "input") {
return false;
}
this.removeDownloads();
return true;
});
Keys.on("ALT-Delete", (event: Event) => {
const target = event.target as HTMLElement;
if (target.localName === "input") {
return false;
}
this.removeMissingDownloads();
return true;
});
Keys.on("SHIFT-Delete", (event: Event) => {
const target = event.target as HTMLElement;
if (target.localName === "input") {
return false;
}
this.removeCompleteDownloads(false);
return true;
});
ctx.on("ctx-remove-all", () => this.removeAllDownloads());
ctx.on("ctx-remove-complete-all",
() => this.removeCompleteDownloads(false));
ctx.on("ctx-remove-complete-selected",
() => this.removeCompleteDownloads(true));
ctx.on("ctx-remove-domain",
() => this.removeDomainDownloads(false));
ctx.on("ctx-remove-complete-domain",
() => this.removeDomainDownloads(true));
ctx.on("ctx-remove-failed", () => this.removeFailedDownloads());
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
ctx.on("ctx-remove-batch", () => this.removeBatchDownloads());
ctx.on("ctx-import", () => this.importDownloads());
ctx.on("ctx-export-text", () => this.exportDownloads(imex.textExporter));
ctx.on("ctx-export-aria2", () => this.exportDownloads(imex.aria2Exporter));
ctx.on("ctx-export-metalink",
() => this.exportDownloads(imex.metalinkExporter));
ctx.on("dismissed", () => this.table.focus());
this.on("contextmenu", (tree, event) => {
if (!this.selection.empty) {
this.showContextMenu(event);
}
return true;
});
ctx.on("clicked", e => this.handleFilterRemove(e));
const toolbar = new Buttons("#toolbar");
toolbar.on("btn-add", () => PORT.post("showSingle"));
this.resumeAction = new Broadcaster("btn-resume", "ctx-resume");
this.resumeAction.onaction = this.resumeDownloads.bind(this, false);
this.forceAction = new Broadcaster("ctx-force-download");
this.forceAction.onaction = this.resumeDownloads.bind(this, true);
this.pauseAction = new Broadcaster("btn-pause", "ctx-pause");
this.pauseAction.onaction = this.pauseDownloads.bind(this);
this.cancelAction = new Broadcaster("btn-cancel", "ctx-cancel");
this.cancelAction.onaction = this.cancelDownloads.bind(this);
this.openFileAction = new Broadcaster("ctx-open-file");
this.openFileAction.onaction = this.openFile.bind(this);
this.on("dblclick", () => this.openFile());
this.openDirectoryAction = new Broadcaster("ctx-open-directory");
this.openDirectoryAction.onaction = this.openDirectory.bind(this);
this.deleteFilesAction = new Broadcaster("ctx-delete-files");
this.deleteFilesAction.onaction = this.deleteFiles.bind(this);
const moveAction = (method: string) => {
if (this.selection.empty) {
return;
}
const d: any = this.downloads;
d[method](Array.from(this.selection));
};
this.moveTopAction = new Broadcaster("btn-top", "ctx-move-top");
this.moveTopAction.onaction = moveAction.bind(this, "moveTop");
this.moveUpAction = new Broadcaster("btn-up", "ctx-move-up");
this.moveUpAction.onaction = moveAction.bind(this, "moveUp");
this.moveDownAction = new Broadcaster("btn-down", "ctx-move-down");
this.moveDownAction.onaction = moveAction.bind(this, "moveDown");
this.moveBottomAction = new Broadcaster("btn-bottom", "ctx-move-bottom");
this.moveBottomAction.onaction = moveAction.bind(this, "moveBottom");
this.disableSet = new Set([
this.resumeAction,
this.forceAction,
this.pauseAction,
this.cancelAction,
this.moveTopAction,
this.moveUpAction,
this.moveDownAction,
this.moveBottomAction,
this.openFileAction,
this.openDirectoryAction,
this.deleteFilesAction,
]);
this.on(
"selection-changed", debounce(this.selectionChanged.bind(this), 10));
this.selection.clear();
this.tooltip = null;
const tooltipWatcher = new PrefWatcher("tooltip", true);
this.on("hover", info => {
if (!document.hasFocus()) {
return;
}
if (!tooltipWatcher.value) {
return;
}
const item = this.downloads.filtered[info.rowid];
if (!item) {
return;
}
if (this.tooltip) {
this.tooltip.dismiss();
}
this.tooltip = new Tooltip(item, info);
});
this.on("hover-change", info => {
if (!this.tooltip) {
return;
}
this.tooltip.adjust(info);
});
this.on("hover-done", () => this.dismissTooltip());
this.downloads.on("changed", () => this.dismissTooltip());
this.contextMenu.on("showing", () => this.dismissTooltip());
addEventListener("scroll", () => this.dismissTooltip(), {passive: true});
addEventListener("wheel", () => this.dismissTooltip(), {passive: true});
addEventListener("keydown", () => this.dismissTooltip(), {passive: true});
}
get rowCount() {
return this.downloads.filtered.length;
}
updateCounts() {
const {length: total} = this.downloads.items;
const fTotal = prettyNumber(total);
const fFin = prettyNumber(this.finished);
const fDisp = prettyNumber(this.rowCount);
const fRunning = prettyNumber(this.running.size);
$("#statusItems").textContent = _(
"manager-status-items",
fFin,
fTotal,
fDisp,
fRunning);
if (total) {
document.title = `[${fFin}/${fTotal}] - ${_("manager.title")}`;
}
else {
document.title = _("manager.title");
}
}
async updateSizes() {
for (const r of this.running) {
await r.updateSizes();
}
}
async updateRunning() {
let sum = 0;
for (const r of this.running) {
const v = await r.updateStats();
if (v >= 0) {
sum += v;
}
}
this.globalStats.add(sum);
$("#statusSpeed").textContent = formatSpeed(this.globalStats.avg);
}
dismissTooltip() {
if (!this.tooltip) {
return;
}
this.tooltip.dismiss();
this.tooltip = null;
}
async showContextMenu(event: MouseEvent) {
const {contextMenu: ctx} = this;
const filts = await filters();
const prepareMenu = (prefix: string) => {
const rem = (ctx.get(prefix) as SubMenuItem).menu;
prefix += "-filter-";
Array.from(rem).
filter(e => e.startsWith(prefix)).
forEach(e => rem.remove(e));
for (const filt of filts.all) {
if (typeof filt.id !== "string" || filt.id === "deffilter-all") {
continue;
}
const mi = new MenuItem(rem, `${prefix}-${filt.id}`, filt.label, {
icon: this.icons.get(iconForPath(`file.${filt.icon || "bin"}`, ICON_BASE_SIZE))
});
rem.add(mi);
}
};
prepareMenu("ctx-remove-complete");
prepareMenu("ctx-remove");
ctx.show(event);
}
setItems(items: any[]) {
const savedStats = new Map(
Array.from(this.running).map(item => [item.sessionId, item.stats]));
this.running.clear();
this.sids.clear();
this.downloads.set(items.map(item => {
const rv = new DownloadItem(this, item, savedStats.get(item.sessionId));
this.sids.set(rv.sessionId, rv);
return rv;
}));
}
getSelectedItems() {
const {filtered} = this.downloads;
return Array.from(this.selection).map(e => filtered[e]);
}
getSelectedSids(allowedStates: number) {
const {filtered} = this.downloads;
const selected = Array.from(this.selection);
const allowedItems = selected.filter(
i => allowedStates & filtered[i].state);
return allowedItems.map(i => filtered[i].sessionId);
}
selectionChanged() {
this.dismissTooltip();
const {empty} = this.selection;
if (empty) {
for (const d of this.disableSet) {
d.disabled = true;
}
return;
}
for (const d of this.disableSet) {
d.disabled = false;
}
const items = this.getSelectedItems();
const states = items.reduce((p, c) => p |= c.state, 0);
if (!(states & DownloadState.PAUSABLE)) {
this.pauseAction.disabled = true;
}
if (!(states & DownloadState.RESUMABLE)) {
this.resumeAction.disabled = true;
}
if (!(states & DownloadState.FORCABLE)) {
this.forceAction.disabled = true;
}
if (!(states & DownloadState.CANCELABLE)) {
this.cancelAction.disabled = true;
}
if (!(states & DownloadState.DONE)) {
this.deleteFilesAction.disabled = true;
}
const item = this.focusRow >= 0 ?
this.downloads.filtered[this.focusRow] :
null;
const canOpen = item && item.manId && item.state === DownloadState.DONE;
const canOpenDirectory = item && item.manId;
this.openFileAction.disabled = !canOpen;
this.openDirectoryAction.disabled = !canOpenDirectory;
}
resumeDownloads(forced = false) {
const sids = this.getSelectedSids(
forced ? DownloadState.FORCABLE : DownloadState.RESUMABLE);
if (!sids.length) {
return;
}
PORT.post("resume", {sids, forced});
}
pauseDownloads() {
const sids = this.getSelectedSids(DownloadState.PAUSABLE);
if (!sids.length) {
return;
}
PORT.post("pause", {sids});
}
cancelDownloads() {
const sids = this.getSelectedSids(DownloadState.CANCELABLE);
if (!sids.length) {
return;
}
PORT.post("cancel", {sids});
}
async openFile() {
this.dismissTooltip();
const {focusRow} = this;
if (focusRow < 0) {
return;
}
const item = this.downloads.filtered[focusRow];
if (!item || !item.manId || item.state !== DownloadState.DONE) {
return;
}
item.opening = true;
try {
this.invalidateRow(focusRow);
await downloads.open(item.manId);
}
catch (ex) {
console.error(ex, ex.toString(), ex);
PORT.post("missing", {sid: item.sessionId});
}
finally {
setTimeout(() => {
item.opening = false;
this.invalidateRow(focusRow);
}, 500);
}
}
async openDirectory() {
if (this.focusRow < 0) {
return;
}
const item = this.downloads.filtered[this.focusRow];
if (!item || !item.manId) {
return;
}
try {
await downloads.show(item.manId);
}
catch (ex) {
console.error(ex, ex.toString(), ex);
PORT.post("missing", {sid: item.sessionId});
}
}
async deleteFiles() {
const items = [];
for (const rowid of this.selection) {
const item = this.downloads.filtered[rowid];
if (item.state === DownloadState.DONE && item.manId) {
items.push(item);
}
}
if (!items.length) {
return;
}
const sids = items.map(i => i.sessionId);
const paths = items.map(i => i.destFull);
await new DeleteFilesDialog(paths).show();
await Promise.all(items.map(async item => {
try {
if (item.manId && item.state === DownloadState.DONE) {
await downloads.removeFile(item.manId);
}
}
catch {
// ignored
}
}));
this.removeDownloadsInternal(sids);
}
removeDownloadsInternal(sids?: number[]) {
if (!sids) {
sids = [];
for (const rowid of this.selection) {
sids.push(this.downloads.filtered[rowid].sessionId);
}
}
if (!sids.length) {
return;
}
PORT.post("removeSids", {sids});
}
removeDownloadsByState(state: number, selectionOnly = false) {
const branch = selectionOnly ? "filtered" : "items";
const items = this.downloads[branch].filter(item => {
if (selectionOnly && !this.selection.contains(item.filteredPosition)) {
return false;
}
return item.state === state;
}).map(i => i.sessionId);
if (!items.length) {
return;
}
this.removeDownloadsInternal(items);
}
async removeDownloads() {
await new RemovalModalDialog(
_("remove-download.question"), "remove-selected").show();
this.removeDownloadsInternal();
}
async removeAllDownloads() {
await new RemovalModalDialog(
_("remove-all-downloads.question"), "remove-selected-all").show();
this.removeDownloadsInternal(this.downloads.items.map(e => e.sessionId));
}
async removeCompleteDownloads(selected = false) {
await new RemovalModalDialog(
selected ?
_("remove-selected-complete-downloads.question") :
_("remove-complete-downloads.question"),
selected ?
"remove-selected-complete" :
"remove-complete"
).show();
this.removeDownloadsByState(DownloadState.DONE, selected);
}
async removeFailedDownloads() {
await new RemovalModalDialog(
_("remove-failed-downloads.question"),
"remove-failed"
).show();
this.removeDownloadsByState(DownloadState.CANCELED, false);
}
async removePausedDownloads() {
await new RemovalModalDialog(
_("remove-paused-downloads.question"),
"remove-paused"
).show();
this.removeDownloadsByState(DownloadState.PAUSED, false);
}
async removeMissingDownloads() {
await new RemovalModalDialog(
_("remove-missing-downloads.question"),
"remove-missing"
).show();
this.removeDownloadsByState(DownloadState.MISSING, false);
}
async removeDomainDownloads(complete = false) {
if (this.focusRow < 0) {
return;
}
const item = this.downloads.filtered[this.focusRow];
if (!item) {
return;
}
const {domain} = item;
await new RemovalModalDialog(
complete ?
_("remove-domain-complete-downloads.question", domain) :
_("remove-domain-downloads.question", domain),
complete ?
"remove-domain-complete" :
"remove-domain"
).show();
const items = this.downloads.items.filter(item => {
if (complete && item.state !== DownloadState.DONE) {
return false;
}
return item.domain === domain;
}).map(i => i.sessionId);
if (!items.length) {
return;
}
this.removeDownloadsInternal(items);
}
async removeBatchDownloads(complete = false) {
if (this.focusRow < 0) {
return;
}
const item = this.downloads.filtered[this.focusRow];
if (!item) {
return;
}
const {batch} = item;
await new RemovalModalDialog(
complete ?
_("remove-batch-complete-downloads.question", batch) :
_("remove-batch-downloads.question", batch),
complete ?
"remove-batch-complete" :
"remove-batch"
).show();
const items = this.downloads.items.filter(item => {
if (complete && item.state !== DownloadState.DONE) {
return false;
}
return item.batch === batch;
}).map(i => i.sessionId);
if (!items.length) {
return;
}
this.removeDownloadsInternal(items);
}
async handleFilterRemove(event: string) {
const [prefix, id] = event.split("--", 2);
if (!prefix || !id) {
return;
}
let all = false;
let branch;
switch (prefix) {
case "ctx-remove-filter":
all = true;
branch = "remove-filter-downloads";
break;
case "ctx-remove-complete-filter":
all = false;
branch = "remove-complete-filter-downloads";
break;
default:
return;
}
const filter = (await filters()).get(id);
if (!filter || typeof filter.id !== "string") {
return;
}
await new RemovalModalDialog(
_(`${branch}.question`, filter.label), `${branch}-${filter.id}`).show();
const items = this.downloads.items.filter(item => {
if (!all && item.state !== DownloadState.DONE) {
return false;
}
return filter.match(item.usable);
}).map(i => i.sessionId);
if (!items.length) {
return;
}
this.removeDownloadsInternal(items);
}
updateItems(items: any[]) {
const newDownloads = [];
for (const i of items) {
const item = this.sids.get(i.sessionId);
if (!item) {
const rv = new DownloadItem(this, i);
this.sids.set(rv.sessionId, rv);
newDownloads.push(rv);
continue;
}
item.updateDownload(i);
}
if (newDownloads) {
this.downloads.add(newDownloads);
}
}
updatedDownload(item: DownloadItem) {
this.downloads.recalculateItem(item);
if (item.isFiltered) {
this.invalidateRow(item.filteredPosition);
}
}
updatedState(
item: DownloadItem, oldState: number | undefined, newState: number) {
switch (oldState) {
case DownloadState.RUNNING:
this.running.delete(item);
if (!this.running.size && this.runningTimer && this.sizesTimer) {
clearInterval(this.runningTimer);
this.runningTimer = null;
clearInterval(this.sizesTimer);
this.sizesTimer = null;
$("#statusSpeedContainer").classList.add("hidden");
}
break;
case DownloadState.DONE:
this.finished--;
break;
}
switch (newState) {
case DownloadState.RUNNING:
this.running.add(item);
if (!this.runningTimer) {
this.runningTimer = window.setInterval(
this.updateRunning.bind(this), RUNNING_TIMEOUT);
this.sizesTimer = window.setInterval(
this.updateSizes.bind(this), SIZES_TIMEOUT);
this.updateRunning();
this.updateSizes();
$("#statusSpeedContainer").classList.remove("hidden");
}
if (item.manId && item.ext) {
IconCache.set(item.ext, item.manId).catch(console.error);
}
break;
case DownloadState.DONE:
this.finished++;
if (item.manId && item.ext) {
IconCache.set(item.ext, item.manId).catch(console.error);
}
break;
}
this.selectionChanged();
this.updateCounts();
}
removedItems(sids: number[]) {
const ssids = new Set(sids);
const items = this.downloads.items.filter(i => {
if (!ssids.has(i.sessionId)) {
return true;
}
this.running.delete(i);
this.sids.delete(i.sessionId);
if (i.state === DownloadState.DONE) {
this.finished--;
}
return false;
});
this.downloads.set(items);
}
selectAll() {
this.selection.add(0, this.rowCount - 1);
}
selectToggle() {
this.selection.toggle(0, this.rowCount - 1);
}
importDownloads() {
const picker = document.createElement("input");
picker.setAttribute("type", "file");
picker.setAttribute("accept", "text/*,.txt,.lst,.metalink,.meta4");
picker.onchange = () => {
if (!picker.files || !picker.files.length) {
return;
}
const reader = new FileReader();
reader.onload = () => {
if (!reader.result) {
return;
}
const items = imex.importText(reader.result as string);
if (!items || !items.length) {
return;
}
API.regular(items, []);
};
reader.readAsText(picker.files[0], "utf-8");
};
picker.click();
}
exportDownloads(exporter: imex.Exporter) {
const items = this.getSelectedItems();
if (!items.length) {
return;
}
const text = exporter.getText(items as unknown as BaseItem[]);
const enc = new TextEncoder();
const data = enc.encode(text);
const url = URL.createObjectURL(new Blob([data], {type: "text/plain"}));
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", exporter.fileName);
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
getRowClasses(rowid: number) {
const item = this.downloads.filtered[rowid];
if (!item) {
return null;
}
if (item.opening) {
return ["opening"];
}
const cls = StateClasses.get(item.state);
if (cls && item.opening) {
return [cls, "opening"];
}
if (item.opening) {
return ["opening"];
}
return cls && [cls] || null;
}
getCellIcon(rowid: number, colid: number) {
if (!this.downloads.filtered.length) {
return null;
}
const item = this.downloads.filtered[rowid];
if (colid === COL_URL) {
return item.icon;
}
if (colid === COL_PROGRESS) {
return StateIcons.get(item.state) || null;
}
return null;
}
getCellType(rowid: number, colid: number) {
if (colid === COL_PROGRESS) {
return CellTypes.TYPE_PROGRESS;
}
return CellTypes.TYPE_TEXT;
}
getCellText(rowid: number, colid: number) {
const item = this.downloads.filtered[rowid];
if (!item) {
return "";
}
switch (colid) {
case COL_URL:
return item.fmtName;
case COL_DOMAIN:
return item.fmtDomain;
case COL_PER:
return item.fmtPercent;
case COL_SIZE:
return item.fmtSize;
case COL_ETA:
return item.fmtETA;
case COL_SPEED:
return item.fmtSpeed;
case COL_SEGS:
return ""; // item.fmtSegments;
case COL_MASK:
return item.mask;
}
return "";
}
getCellProgress(rowid: number) {
const item = this.downloads.filtered[rowid];
if (!item) {
return -1;
}
switch (item.state) {
case DownloadState.QUEUED:
return item.percent;
case DownloadState.RUNNING:
return item.percent || -1;
case DownloadState.PAUSED:
return item.percent || -1;
case DownloadState.FINISHING:
return 1;
case DownloadState.DONE:
return 1;
case DownloadState.CANCELED:
return 1;
default:
return -1;
}
}
onIconCached() {
this.downloads.invalidateIcons();
}
}