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:
parent
71d98bc603
commit
31cb23923a
@ -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"
|
||||
|
11
lib/api.ts
11
lib/api.ts
@ -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);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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, "/");
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
17
lib/util.ts
17
lib/util.ts
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ p.example {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#options > #subfolderOptions,
|
||||
#options > #maskOptions {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr auto auto;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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 => {
|
||||
|
@ -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">
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user