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",
"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": {
"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"
@ -1201,6 +1209,14 @@
"description": "Status bar tooltip; manager network icon",
"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": {
"description": "Column text; Title label (short)",
"message": "Title"

View File

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

View File

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

View File

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

View File

@ -193,24 +193,25 @@ export default class Renamer {
}
toString() {
const {mask} = this.d;
const {mask, subfolder} = this.d;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this;
// XXX flat
return sanitizePath(mask.replace(REPLACE_EXPR, function(type: string) {
const baseMask = subfolder ? `${subfolder}/${mask}` : mask;
console.log(mask, subfolder, baseMask);
return sanitizePath(baseMask.replace(REPLACE_EXPR, function(type: string) {
let prop = type.substr(1, type.length - 2);
const flat = prop.startsWith("flat");
if (flat) {
prop = prop.substr(4);
}
prop = `p_${prop}`;
const rv = (prop in self) ?
let rv = (prop in self) ?
(self[prop] || "").trim() :
type;
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"
]);
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) {
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;
}
#options > #subfolderOptions,
#options > #maskOptions {
display: grid;
grid-template-columns: 2fr auto auto;

View File

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

View File

@ -59,6 +59,15 @@
<input type="checkbox" id="fastOnceCheck">
<span data-i18n="useonlyonce">Use only once</span>
</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>
<input id="mask">
<button id="maskButton" class="icon-tags"></button>

View File

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

View File

@ -47,6 +47,15 @@
<span data-i18n="useonlyonce">Use only once</span>
</label>
</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 id="notification"></section>
<section id="buttons">

View File

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