Add a subfolders dropdown

The dropdown will help users switch between folders easier than using the mask.
Also, the dropdown is more discoverable than masked folders.

See #2
This commit is contained in:
Nils Maier 2019-09-14 01:05:56 +02:00
parent 71d98bc603
commit 31cb23923a
13 changed files with 119 additions and 19 deletions

View File

@ -315,6 +315,14 @@
"description": "Error Message; select window", "description": "Error Message; select window",
"message": "No items selected" "message": "No items selected"
}, },
"error_noabsolutepath": {
"description": "Error Message; select/single window",
"message": "Absolute paths for subfolders are not supported by browsers"
},
"error_nodotsinpath": {
"description": "Error Message; select/single window",
"message": "Dots (.) in subfolders are not supported by browsers"
},
"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"
@ -1201,6 +1209,14 @@
"description": "Status bar tooltip; manager network icon", "description": "Status bar tooltip; manager network icon",
"message": "No new Downloads will be started" "message": "No new Downloads will be started"
}, },
"subfolder": {
"description": "label text",
"message": "Subfolder:"
},
"subfolder_placeholder": {
"description": "placeholder text within an input box",
"message": "Place files in this subfolder within your downloads directory"
},
"title": { "title": {
"description": "Column text; Title label (short)", "description": "Column text; Title label (short)",
"message": "Title" "message": "Title"

View File

@ -11,7 +11,7 @@ import { getManager } from "./manager/man";
import { select } from "./select"; import { select } from "./select";
import { single } from "./single"; import { single } from "./single";
import { Notification } from "./notifications"; import { Notification } from "./notifications";
import { MASK, FASTFILTER } from "./recentlist"; import { MASK, FASTFILTER, SUBFOLDER } from "./recentlist";
import { openManager } from "./windowutils"; import { openManager } from "./windowutils";
import { _ } from "./i18n"; import { _ } from "./i18n";
@ -19,6 +19,7 @@ const MAX_BATCH = 10000;
export interface QueueOptions { export interface QueueOptions {
mask?: string; mask?: string;
subfolder?: string;
paused?: boolean; paused?: boolean;
} }
@ -28,8 +29,9 @@ export const API = new class APIImpl {
} }
async queue(items: BaseItem[], options: QueueOptions) { async queue(items: BaseItem[], options: QueueOptions) {
await MASK.init(); await Promise.all([MASK.init(), SUBFOLDER.init()]);
const {mask = MASK.current} = options; const {mask = MASK.current} = options;
const {subfolder = SUBFOLDER.current} = options;
const {paused = false} = options; const {paused = false} = options;
const defaults: any = { const defaults: any = {
@ -46,6 +48,7 @@ export const API = new class APIImpl {
private: false, private: false,
postData: null, postData: null,
mask, mask,
subfolder,
date: Date.now(), date: Date.now(),
paused paused
}; };
@ -117,6 +120,10 @@ export const API = new class APIImpl {
await FASTFILTER.init(); await FASTFILTER.init();
await FASTFILTER.push(options.fast); await FASTFILTER.push(options.fast);
} }
if (typeof options.subfolder === "string" && !options.subfolderOnce) {
await SUBFOLDER.init();
await SUBFOLDER.push(options.subfolder);
}
if (typeof options.type === "string") { if (typeof options.type === "string") {
await Prefs.set("last-type", options.type); await Prefs.set("last-type", options.type);
} }

View File

@ -15,6 +15,7 @@ export interface BaseItem {
batch?: number; batch?: number;
idx: number; idx: number;
mask?: string; mask?: string;
subfolder?: string;
startDate?: number; startDate?: number;
private?: boolean; private?: boolean;
postData?: string; postData?: string;
@ -27,6 +28,7 @@ const OPTIONPROPS = Object.freeze([
"fileName", "fileName",
"batch", "idx", "batch", "idx",
"mask", "mask",
"subfolder",
"startDate", "startDate",
"private", "private",
"postData", "postData",

View File

@ -5,6 +5,8 @@
import { parsePath, URLd } from "../util"; import { parsePath, URLd } from "../util";
import { QUEUED, RUNNING, PAUSED } from "./state"; import { QUEUED, RUNNING, PAUSED } from "./state";
import Renamer from "./renamer"; import Renamer from "./renamer";
// eslint-disable-next-line no-unused-vars
import { BaseItem } from "../item";
const SAVEDPROPS = [ const SAVEDPROPS = [
"state", "state",
@ -14,6 +16,7 @@ const SAVEDPROPS = [
"usableReferrer", "usableReferrer",
"fileName", "fileName",
"mask", "mask",
"subfolder",
"date", "date",
// batches // batches
"batch", "batch",
@ -105,11 +108,13 @@ export class BaseDownload {
public mask: string; public mask: string;
public subfolder: string;
public prerolled: boolean; public prerolled: boolean;
public retries: number; public retries: number;
constructor(options: any) { constructor(options: BaseItem) {
Object.assign(this, DEFAULTS); Object.assign(this, DEFAULTS);
this.assign(options); this.assign(options);
if (this.state === RUNNING) { if (this.state === RUNNING) {
@ -120,12 +125,13 @@ export class BaseDownload {
this.retries = 0; this.retries = 0;
} }
assign(options: any) { assign(options: BaseItem) {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this; const self: any = this;
const other: any = options;
for (const prop of SAVEDPROPS) { for (const prop of SAVEDPROPS) {
if (prop in options) { if (prop in options) {
self[prop] = options[prop]; self[prop] = other[prop];
} }
} }
this.uURL = new URL(this.url) as URLd; this.uURL = new URL(this.url) as URLd;

View File

@ -193,24 +193,25 @@ export default class Renamer {
} }
toString() { toString() {
const {mask} = this.d; const {mask, subfolder} = this.d;
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this; const self: any = this;
// XXX flat const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) { console.log(mask, subfolder, baseMask);
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
let prop = type.substr(1, type.length - 2); let prop = type.substr(1, type.length - 2);
const flat = prop.startsWith("flat"); const flat = prop.startsWith("flat");
if (flat) { if (flat) {
prop = prop.substr(4); prop = prop.substr(4);
} }
prop = `p_${prop}`; prop = `p_${prop}`;
const rv = (prop in self) ? let rv = (prop in self) ?
(self[prop] || "").trim() : (self[prop] || "").trim() :
type; type;
if (flat) { if (flat) {
return rv.replace(/[/\\]+/g, "-"); rv = rv.replace(/[/\\]+/g, "-");
} }
return rv; return rv.replace(/\/{2,}/g, "/");
})); }));
} }
} }

View File

@ -116,3 +116,9 @@ export const FASTFILTER = new RecentList("fastfilter", [
"*.z??, *.css, *.html" "*.z??, *.css, *.html"
]); ]);
FASTFILTER.init().catch(console.error); FASTFILTER.init().catch(console.error);
export const SUBFOLDER = new RecentList("subfolder", [
"",
"/downthemall",
]);
SUBFOLDER.init().catch(console.error);

View File

@ -357,3 +357,20 @@ export function mapFilterInSitu<TRes, T>(
export function randint(min: number, max: number) { export function randint(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min; return Math.floor(Math.random() * (max - min)) + min;
} }
export function validateSubFolder(folder: string) {
if (!folder) {
return;
}
folder = folder.replace(/[/\\]+/g, "/");
if (folder.startsWith("/")) {
throw new Error("error.noabsolutepath");
}
if (/^[a-z]:\//i.test(folder)) {
throw new Error("error.noabsolutepath");
}
if (/^\.+\/|\/\.+\/|\/\.+$/g.test(folder)) {
throw new Error("error.nodotsinpath");
}
}

View File

@ -63,6 +63,7 @@ p.example {
align-items: center; align-items: center;
} }
#options > #subfolderOptions,
#options > #maskOptions { #options > #maskOptions {
display: grid; display: grid;
grid-template-columns: 2fr auto auto; grid-template-columns: 2fr auto auto;

View File

@ -22,6 +22,9 @@ export class Dropdown extends EventEmitter {
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.classList.add("dropdown"); this.container.classList.add("dropdown");
if (input.id) {
this.container.id = `${input.id}-dropdown`;
}
input = input.parentElement.replaceChild(this.container, input); input = input.parentElement.replaceChild(this.container, input);
this.input = input as HTMLInputElement; this.input = input as HTMLInputElement;

View File

@ -59,6 +59,15 @@
<input type="checkbox" id="fastOnceCheck"> <input type="checkbox" id="fastOnceCheck">
<span data-i18n="useonlyonce">Use only once</span> <span data-i18n="useonlyonce">Use only once</span>
</label> </label>
<h2 data-i18n="subfolder"></h2>
<input id="subfolder" data-i18n="placeholder=subfolder.placeholder">
<span></span>
<label>
<input type="checkbox" id="subfolderOnceCheck">
<span data-i18n="useonlyonce">Use only once</span>
</label>
<h2 data-i18n="mask">Mask</h2> <h2 data-i18n="mask">Mask</h2>
<input id="mask"> <input id="mask">
<button id="maskButton" class="icon-tags"></button> <button id="maskButton" class="icon-tags"></button>

View File

@ -7,7 +7,7 @@ import { ContextMenu } from "./contextmenu";
import { iconForPath } from "../lib/windowutils"; import { iconForPath } from "../lib/windowutils";
import { _, localize } from "../lib/i18n"; import { _, localize } from "../lib/i18n";
import { Prefs } from "../lib/prefs"; import { Prefs } from "../lib/prefs";
import { MASK, FASTFILTER } from "../lib/recentlist"; import { MASK, FASTFILTER, SUBFOLDER } from "../lib/recentlist";
import { WindowState } from "./windowstate"; import { WindowState } from "./windowstate";
import { Dropdown } from "./dropdown"; import { Dropdown } from "./dropdown";
import { Keys } from "./keys"; import { Keys } from "./keys";
@ -24,6 +24,7 @@ import { BaseItem } from "../lib/item";
import { ItemDelta } from "../lib/select"; import { ItemDelta } from "../lib/select";
// 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 { validateSubFolder as validateSubfolder } from "../lib/util";
const PORT: RawPort = runtime.connect(null, { name: "select" }); const PORT: RawPort = runtime.connect(null, { name: "select" });
@ -42,6 +43,7 @@ const NUM_FILTER_CLASSES = 8;
let Table: SelectionTable; let Table: SelectionTable;
let Mask: Dropdown; let Mask: Dropdown;
let FastFilter: Dropdown; let FastFilter: Dropdown;
let Subfolder: Dropdown;
type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]}; type DELTAS = {deltaLinks: ItemDelta[]; deltaMedia: ItemDelta[]};
@ -51,7 +53,7 @@ interface BaseMatchedItem extends BaseItem {
rowid: number; rowid: number;
} }
function cleaErrors() { function clearErrors() {
const not = $("#notification"); const not = $("#notification");
not.textContent = ""; not.textContent = "";
not.style.display = "none"; not.style.display = "none";
@ -572,7 +574,7 @@ class SelectionTable extends VirtualTable {
else { else {
this.status.textContent = _("numitems.label", [selected]); this.status.textContent = _("numitems.label", [selected]);
} }
cleaErrors(); clearErrors();
} }
getRowClasses(rowid: number) { getRowClasses(rowid: number) {
@ -673,6 +675,9 @@ async function download(paused = false) {
if (!mask) { if (!mask) {
throw new Error("error.invalidMask"); throw new Error("error.invalidMask");
} }
const subfolder = Subfolder.value;
validateSubfolder(subfolder);
const items = Table.items.checkedIndexes; const items = Table.items.checkedIndexes;
if (!items.length) { if (!items.length) {
throw new Error("error.noItemsSelected"); throw new Error("error.noItemsSelected");
@ -706,6 +711,8 @@ async function download(paused = false) {
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked, maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
fast: FastFilter.value, fast: FastFilter.value,
fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked, fastOnce: $<HTMLInputElement>("#fastOnceCheck").checked,
subfolder,
subfolderOnce: $<HTMLInputElement>("#subfolderOnceCheck").checked,
} }
}); });
} }
@ -781,9 +788,9 @@ function cancel() {
} }
async function init() { async function init() {
await Promise.all([MASK.init(), FASTFILTER.init()]); await Promise.all([MASK.init(), FASTFILTER.init(), SUBFOLDER.init()]);
Mask = new Dropdown("#mask", MASK.values); Mask = new Dropdown("#mask", MASK.values);
Mask.on("changed", cleaErrors); Mask.on("changed", clearErrors);
FastFilter = new Dropdown("#fast", FASTFILTER.values); FastFilter = new Dropdown("#fast", FASTFILTER.values);
FastFilter.on("changed", () => { FastFilter.on("changed", () => {
PORT.postMessage({ PORT.postMessage({
@ -791,6 +798,8 @@ async function init() {
fastFilter: FastFilter.value fastFilter: FastFilter.value
}); });
}); });
Subfolder = new Dropdown("#subfolder", SUBFOLDER.values);
Subfolder.on("changed", clearErrors);
} }
const LOADED = new Promise(resolve => { const LOADED = new Promise(resolve => {

View File

@ -47,6 +47,15 @@
<span data-i18n="useonlyonce">Use only once</span> <span data-i18n="useonlyonce">Use only once</span>
</label> </label>
</div> </div>
<h3 data-i18n="subfolder"></h3>
<div id="subfolderOptions">
<input type="text" id="subfolder" data-i18n="placeholder=subfolder.placeholder">
<span></span>
<label id="subfolderOnce">
<input type="checkbox" id="subfolderOnceCheck">
<span data-i18n="useonlyonce">Use only once</span>
</label>
</div>
</section> </section>
<section id="notification"></section> <section id="notification"></section>
<section id="buttons"> <section id="buttons">

View File

@ -6,7 +6,7 @@ import ModalDialog from "../uikit/lib/modal";
import { _, localize } from "../lib/i18n"; import { _, localize } from "../lib/i18n";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { Item, BaseItem } from "../lib/item"; import { Item, BaseItem } from "../lib/item";
import { MASK } from "../lib/recentlist"; import { MASK, SUBFOLDER } from "../lib/recentlist";
import { BatchGenerator } from "../lib/batches"; import { BatchGenerator } from "../lib/batches";
import { WindowState } from "./windowstate"; import { WindowState } from "./windowstate";
import { Dropdown } from "./dropdown"; import { Dropdown } from "./dropdown";
@ -14,11 +14,13 @@ import { Keys } from "./keys";
import { hookButton } from "../lib/manager/renamer"; import { hookButton } from "../lib/manager/renamer";
import { runtime } from "../lib/browser"; import { runtime } from "../lib/browser";
import { $ } from "./winutil"; import { $ } from "./winutil";
import { validateSubFolder } from "../lib/util";
const PORT = runtime.connect(null, { name: "single" }); const PORT = runtime.connect(null, { name: "single" });
let ITEM: BaseItem; let ITEM: BaseItem;
let Mask: Dropdown; let Mask: Dropdown;
let Subfolder: Dropdown;
class BatchModalDialog extends ModalDialog { class BatchModalDialog extends ModalDialog {
private readonly gen: BatchGenerator; private readonly gen: BatchGenerator;
@ -71,7 +73,8 @@ function setItem(item: BaseItem) {
title = "", title = "",
description = "", description = "",
usableReferrer = "", usableReferrer = "",
mask = "" mask = "",
subfolder = "",
} = item; } = item;
$<HTMLInputElement>("#URL").value = usable; $<HTMLInputElement>("#URL").value = usable;
$<HTMLInputElement>("#filename").value = fileName; $<HTMLInputElement>("#filename").value = fileName;
@ -81,6 +84,9 @@ function setItem(item: BaseItem) {
if (mask) { if (mask) {
Mask.value = mask; Mask.value = mask;
} }
if (subfolder) {
Subfolder.value = subfolder;
}
} }
function displayError(err: string) { function displayError(err: string) {
@ -124,6 +130,9 @@ async function downloadInternal(paused: boolean) {
return displayError("error.invalidMask"); return displayError("error.invalidMask");
} }
const subfolder = Subfolder.value.trim();
validateSubFolder(subfolder);
const items = []; const items = [];
if (!ITEM) { if (!ITEM) {
@ -136,6 +145,7 @@ async function downloadInternal(paused: boolean) {
title, title,
description, description,
mask, mask,
subfolder
}); });
} }
else { else {
@ -143,6 +153,7 @@ async function downloadInternal(paused: boolean) {
ITEM.title = title; ITEM.title = title;
ITEM.description = description; ITEM.description = description;
ITEM.mask = mask; ITEM.mask = mask;
ITEM.subfolder = subfolder;
if (usableReferrer !== ITEM.usableReferrer) { if (usableReferrer !== ITEM.usableReferrer) {
ITEM.referrer = referrer; ITEM.referrer = referrer;
ITEM.usableReferrer = usableReferrer; ITEM.usableReferrer = usableReferrer;
@ -185,6 +196,8 @@ async function downloadInternal(paused: boolean) {
paused, paused,
mask, mask,
maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked, maskOnce: $<HTMLInputElement>("#maskOnceCheck").checked,
subfolder,
subfolderOnce: $<HTMLInputElement>("#subfolderOnceCheck").checked,
} }
}); });
return null; return null;
@ -201,8 +214,9 @@ function cancel() {
async function init() { async function init() {
await localize(document.documentElement); await localize(document.documentElement);
await Promise.all([MASK.init()]); await Promise.all([MASK.init(), SUBFOLDER.init()]);
Mask = new Dropdown("#mask", MASK.values); Mask = new Dropdown("#mask", MASK.values);
Subfolder = new Dropdown("#subfolder", SUBFOLDER.values);
} }
addEventListener("DOMContentLoaded", async function dom() { addEventListener("DOMContentLoaded", async function dom() {