parent
da9832552f
commit
4d953c373f
@ -323,6 +323,22 @@
|
|||||||
"description": "Error Message; select/single window",
|
"description": "Error Message; select/single window",
|
||||||
"message": "Dots (.) in subfolders are not supported by browsers"
|
"message": "Dots (.) in subfolders are not supported by browsers"
|
||||||
},
|
},
|
||||||
|
"export": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export To File"
|
||||||
|
},
|
||||||
|
"export_aria2": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export As aria2 List"
|
||||||
|
},
|
||||||
|
"export_metalink": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export As Metalink"
|
||||||
|
},
|
||||||
|
"export_text": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Export As Text"
|
||||||
|
},
|
||||||
"extensionDescription": {
|
"extensionDescription": {
|
||||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one",
|
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one",
|
||||||
"message": "The Mass Downloader for your browser"
|
"message": "The Mass Downloader for your browser"
|
||||||
@ -371,6 +387,10 @@
|
|||||||
"description": "Menu text",
|
"description": "Menu text",
|
||||||
"message": "Force Start"
|
"message": "Force Start"
|
||||||
},
|
},
|
||||||
|
"import": {
|
||||||
|
"description": "menu text",
|
||||||
|
"message": "Import From File"
|
||||||
|
},
|
||||||
"information_title": {
|
"information_title": {
|
||||||
"description": "Used in message boxes",
|
"description": "Used in message boxes",
|
||||||
"message": "Information"
|
"message": "Information"
|
||||||
|
247
lib/imex.ts
Normal file
247
lib/imex.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
"use strict";
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
import { getTextLinks } from "./textlinks";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { BaseItem } from "./item";
|
||||||
|
import { ALLOWED_SCHEMES } from "./constants";
|
||||||
|
|
||||||
|
export const NS_METALINK_RFC5854 = "urn:ietf:params:xml:ns:metalink";
|
||||||
|
export const NS_DTA = "http://www.downthemall.net/properties#";
|
||||||
|
|
||||||
|
function parseNum(
|
||||||
|
file: Element,
|
||||||
|
attr: string,
|
||||||
|
defaultValue: number,
|
||||||
|
ns = NS_METALINK_RFC5854) {
|
||||||
|
const val = file.getAttributeNS(ns, attr);
|
||||||
|
if (!val) {
|
||||||
|
return defaultValue + 1;
|
||||||
|
}
|
||||||
|
const num = parseInt(val, 10);
|
||||||
|
if (isFinite(num)) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
return defaultValue + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importMeta4(data: string) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const document = parser.parseFromString(data, "text/xml");
|
||||||
|
const {documentElement} = document;
|
||||||
|
const items: BaseItem[] = [];
|
||||||
|
let batch = 0;
|
||||||
|
for (const file of documentElement.querySelectorAll("file")) {
|
||||||
|
try {
|
||||||
|
const url = Array.from(file.querySelectorAll("url")).map(u => {
|
||||||
|
try {
|
||||||
|
const {textContent} = u;
|
||||||
|
if (!textContent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const url = new URL(textContent);
|
||||||
|
if (!ALLOWED_SCHEMES.has(url.protocol)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const prio = parseNum(u, "priority", 0);
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
prio
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(u => !!u).reduce((p, c) => {
|
||||||
|
if (!c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!p || p.prio < c.prio) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
if (!url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
batch = parseNum(file, "num", batch, NS_DTA);
|
||||||
|
const idx = parseNum(file, "idx", 0, NS_DTA);
|
||||||
|
const item: BaseItem = {
|
||||||
|
url: url.url.toString(),
|
||||||
|
usable: decodeURIComponent(url.url.toString()),
|
||||||
|
batch,
|
||||||
|
idx
|
||||||
|
};
|
||||||
|
const ref = file.getAttributeNS(NS_DTA, "referrer");
|
||||||
|
if (ref) {
|
||||||
|
item.referrer = ref;
|
||||||
|
item.usableReferrer = decodeURIComponent(ref);
|
||||||
|
}
|
||||||
|
const mask = file.getAttributeNS(NS_DTA, "mask");
|
||||||
|
if (mask) {
|
||||||
|
item.mask = mask;
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.error("Failed to import file", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKV(current: BaseItem, line: string) {
|
||||||
|
const [k, v] = line.split("=", 2);
|
||||||
|
switch (k.toLocaleLowerCase().trim()) {
|
||||||
|
case "referer": {
|
||||||
|
const rurls = getTextLinks(v);
|
||||||
|
if (rurls && rurls.length) {
|
||||||
|
current.referrer = rurls.pop();
|
||||||
|
current.usableReferrer = decodeURIComponent(current.referrer || "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importText(data: string) {
|
||||||
|
if (data.includes(NS_METALINK_RFC5854)) {
|
||||||
|
return importMeta4(data);
|
||||||
|
}
|
||||||
|
const splitter = /(.+)\n|(.+)$/g;
|
||||||
|
const spacer = /^\s+/;
|
||||||
|
let match;
|
||||||
|
let current: BaseItem | undefined = undefined;
|
||||||
|
let idx = 0;
|
||||||
|
const items = [];
|
||||||
|
while ((match = splitter.exec(data)) !== null) {
|
||||||
|
try {
|
||||||
|
const line = match[0].trimRight();
|
||||||
|
if (!line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spacer.test(line)) {
|
||||||
|
if (!current) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parseKV(current, line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const urls = getTextLinks(line);
|
||||||
|
if (!urls || !urls.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current = {
|
||||||
|
url: urls[0],
|
||||||
|
usable: decodeURIComponent(urls[0]),
|
||||||
|
idx: ++idx
|
||||||
|
};
|
||||||
|
items.push(current);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
current = undefined;
|
||||||
|
console.error("Failed to import", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exporter {
|
||||||
|
fileName: string;
|
||||||
|
getText(items: BaseItem[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextExporter {
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fileName = "links.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(items: BaseItem[]) {
|
||||||
|
const lines = [];
|
||||||
|
for (const item of items) {
|
||||||
|
lines.push(item.url);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Aria2Exporter {
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fileName = "links.aria2.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(items: BaseItem[]) {
|
||||||
|
const lines = [];
|
||||||
|
for (const item of items) {
|
||||||
|
lines.push(item.url);
|
||||||
|
if (item.referrer) {
|
||||||
|
lines.push(` referer=${item.referrer}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetalinkExporter {
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fileName = "links.meta4";
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(items: BaseItem[]) {
|
||||||
|
const document = window.document.implementation.
|
||||||
|
createDocument(NS_METALINK_RFC5854, "metalink", null);
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttributeNS(NS_DTA, "generator", "DownThemAll!");
|
||||||
|
root.appendChild(document.createComment(
|
||||||
|
"metalink as exported by DownThemAll!",
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const aitem = item as any;
|
||||||
|
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
|
||||||
|
f.setAttribute("name", aitem.currentName);
|
||||||
|
if (item.batch) {
|
||||||
|
f.setAttributeNS(NS_DTA, "num", item.batch.toString());
|
||||||
|
}
|
||||||
|
if (item.idx) {
|
||||||
|
f.setAttributeNS(NS_DTA, "idx", item.idx.toString());
|
||||||
|
}
|
||||||
|
if (item.referrer) {
|
||||||
|
f.setAttributeNS(NS_DTA, "referrer", item.referrer);
|
||||||
|
}
|
||||||
|
if (item.mask) {
|
||||||
|
f.setAttributeNS(NS_DTA, "mask", item.mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.description) {
|
||||||
|
const n = document.createElementNS(NS_METALINK_RFC5854, "description");
|
||||||
|
n.textContent = item.description;
|
||||||
|
f.appendChild(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
|
||||||
|
u.textContent = item.url;
|
||||||
|
f.appendChild(u);
|
||||||
|
|
||||||
|
if (aitem.totalSize > 0) {
|
||||||
|
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
|
||||||
|
s.textContent = aitem.totalSize.toString();
|
||||||
|
f.appendChild(s);
|
||||||
|
}
|
||||||
|
root.appendChild(f);
|
||||||
|
}
|
||||||
|
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||||
|
xml += root.outerHTML;
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const textExporter = new TextExporter();
|
||||||
|
export const aria2Exporter = new Aria2Exporter();
|
||||||
|
export const metalinkExporter = new MetalinkExporter();
|
@ -28,7 +28,7 @@ function computeSelection(
|
|||||||
items: BaseMatchedItem[],
|
items: BaseMatchedItem[],
|
||||||
onlyFast: boolean): ItemDelta[] {
|
onlyFast: boolean): ItemDelta[] {
|
||||||
let ws = items.map((item, idx: number) => {
|
let ws = items.map((item, idx: number) => {
|
||||||
item.idx = idx;
|
item.idx = item.idx || idx;
|
||||||
const {matched = null} = item;
|
const {matched = null} = item;
|
||||||
item.prevMatched = matched;
|
item.prevMatched = matched;
|
||||||
item.matched = null;
|
item.matched = null;
|
||||||
|
@ -110,6 +110,15 @@
|
|||||||
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
||||||
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
|
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
|
<li id="ctx-import" data-icon="icon-import" data-i18n="import"></li>
|
||||||
|
<li id="ctx-export" data-icon="icon-download" data-i18n="export">
|
||||||
|
<ul class="table-context">
|
||||||
|
<li id="ctx-export-text" data-i18n="export-text"></li>
|
||||||
|
<li id="ctx-export-aria2" data-i18n="export-aria2"></li>
|
||||||
|
<li id="ctx-export-metalink" data-i18n="export-metalink"></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>-</li>
|
||||||
<li id="ctx-move-top" data-key="ALT-Home" data-i18n="move-top" data-icon="icon-top">Top</li>
|
<li id="ctx-move-top" data-key="ALT-Home" data-i18n="move-top" data-icon="icon-top">Top</li>
|
||||||
<li id="ctx-move-up" data-key="ALT-PageUp" data-i18n="move-up" data-icon="icon-up">Up</li>
|
<li id="ctx-move-up" data-key="ALT-PageUp" data-i18n="move-up" data-icon="icon-up">Up</li>
|
||||||
<li id="ctx-move-down" data-key="ALT-PageDown" data-i18n="move-down" data-icon="icon-down">down</li>
|
<li id="ctx-move-down" data-key="ALT-PageDown" data-i18n="move-down" data-icon="icon-down">down</li>
|
||||||
|
@ -38,6 +38,10 @@ import { $ } from "../winutil";
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { TableConfig } from "../../uikit/lib/config";
|
import { TableConfig } from "../../uikit/lib/config";
|
||||||
import { IconCache } from "../../lib/iconcache";
|
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 TREE_CONFIG_VERSION = 2;
|
||||||
const RUNNING_TIMEOUT = 1000;
|
const RUNNING_TIMEOUT = 1000;
|
||||||
@ -556,6 +560,12 @@ export class DownloadTable extends VirtualTable {
|
|||||||
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
|
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
|
||||||
ctx.on("ctx-remove-batch", () => this.removeBatchDownloads());
|
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());
|
ctx.on("dismissed", () => this.table.focus());
|
||||||
|
|
||||||
this.on("contextmenu", (tree, event) => {
|
this.on("contextmenu", (tree, event) => {
|
||||||
@ -1177,6 +1187,49 @@ export class DownloadTable extends VirtualTable {
|
|||||||
this.selection.toggle(0, this.rowCount - 1);
|
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) {
|
getRowClasses(rowid: number) {
|
||||||
const item = this.downloads.filtered[rowid];
|
const item = this.downloads.filtered[rowid];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user