26 Commits

Author SHA1 Message Date
e4b0629dee Version 4.0.9 2019-09-05 09:18:08 +02:00
5c2700ca36 Tooltip improvements 2019-09-05 09:03:50 +02:00
639a582804 Add small animation when opening files 2019-09-05 08:55:11 +02:00
2d1f185fcd Disable shelf in chrome
We cannot just disable it for our downloads (reliably)
so disable it completely while we're running.
2019-09-05 07:40:17 +02:00
38735ed0ae Make progress bar a little round 2019-09-05 07:39:19 +02:00
216bc590da Do not forget about 405 - Method not allowed 2019-09-04 21:34:52 +02:00
1c10d8005a Improve PREROLL based on user feedback 2019-09-04 21:27:03 +02:00
1fcfbe5360 Adjust default widths a bit 2019-09-04 21:15:32 +02:00
8d3dda1cec Bold buttons 2019-09-04 14:56:37 +02:00
be18f667d9 Trigger the saveQueue before reload 2019-09-04 14:48:35 +02:00
027b2c4fb1 Saving after preroll may have cause dupes 2019-09-04 14:48:35 +02:00
4ed92878be Update of zh_CN locale (#56) 2019-09-04 14:20:13 +02:00
a6930f309e Update ru locale 2019-09-04 14:15:06 +02:00
fdcdae0412 Use promises in content scripts 2019-09-04 14:15:06 +02:00
2c18ddaaa8 Increase shelf timeout 2019-09-04 14:15:06 +02:00
994e7ad0a6 Do not wait on downloads to finish prerollling 2019-09-04 14:15:06 +02:00
95536b36be Do not attempt to restart bg complete 2019-09-04 14:15:06 +02:00
9c159d5d24 Do not wait for the scheduler on startup 2019-09-04 14:15:06 +02:00
42ccfd5dc5 Do not abuse serverName to store browserName 2019-09-04 14:15:05 +02:00
dabf7f8a28 Prerolling and mime detection for some downloads
Only attempt this for a limited subset of downloads for now.
Related #45
2019-09-04 14:15:05 +02:00
9cac48f439 Make all tabs work again 2019-09-04 14:15:05 +02:00
ef6bc840d8 Do not trace 2019-09-04 14:15:00 +02:00
1c38ec1357 Add zh_TW locale (#62) 2019-09-04 12:03:50 +02:00
a4436bd6c8 Force Start should apply to Queued downloads
Closes #57
2019-09-03 18:18:07 +02:00
5a4b8143b2 Remove some any-types 2019-09-03 18:18:07 +02:00
00a5712427 Update of lt locale (#55) 2019-09-03 15:08:24 +02:00
36 changed files with 2923 additions and 654 deletions

View File

@ -77,3 +77,7 @@ Licensed under the Mozilla Public License 2.0.
The list itself is licensed under the Mozilla Public License 2.0. The list itself is licensed under the Mozilla Public License 2.0.
The javascript library accessing it is licensed under the MIT license. The javascript library accessing it is licensed under the MIT license.
## whatwg-mimetype
Licensed under MIT

View File

@ -13,5 +13,6 @@
"pl": "Polski (PL) [pl]", "pl": "Polski (PL) [pl]",
"pt": "Português (Brasil) [pt]", "pt": "Português (Brasil) [pt]",
"ru": "Русский [ru]", "ru": "Русский [ru]",
"zh_CN": "简体中文 [zh_CN]" "zh_CN": "简体中文 [zh_CN]",
"zh_TW": "正體中文 [zh_TW]"
} }

View File

@ -684,7 +684,7 @@
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_hide_context": { "pref_hide_context": {
"message": "Nerodyti bendrųjų kontekstinio meniu elementų", "message": "Kontekstiniame meniu nerodyti bendrųjų elementų",
"description": "Preferences/General" "description": "Preferences/General"
}, },
"pref_manager_tooltip": { "pref_manager_tooltip": {

View File

@ -12,7 +12,7 @@
"description": "Action: Add paused" "description": "Action: Add paused"
}, },
"add_download": { "add_download": {
"message": "добавить закачку", "message": "Добавить закачку",
"description": "Action for adding a download" "description": "Action for adding a download"
}, },
"add_new": { "add_new": {
@ -244,7 +244,7 @@
"description": "OneClick! action; Menu text" "description": "OneClick! action; Menu text"
}, },
"dta_turbo_all": { "dta_turbo_all": {
"message": "ОднимКликом! - Все вкладки", "message": "OneClick! - Все вкладки",
"description": "Menu text" "description": "Menu text"
}, },
"dta_turbo_image": { "dta_turbo_image": {

File diff suppressed because it is too large Load Diff

1170
_locales/zh_TW/messages.json Normal file

File diff suppressed because it is too large Load Diff

396
data/mime.json Normal file
View File

@ -0,0 +1,396 @@
{
"e": {
"3gpp": "3gp",
"asx": "asf",
"gz": "gzip",
"heic": "heif",
"html": [
"htm",
"shtml",
"php"
],
"jar": [
"war",
"ear"
],
"jpg": [
"jpeg",
"jpe",
"jfif"
],
"js": "jsx",
"mid": [
"midi",
"kar"
],
"mkv": [
"mk3d",
"mks"
],
"mov": [
"qt",
"moov"
],
"mpg": [
"mpe",
"mpeg"
],
"pem": [
"crt",
"der"
],
"pl": "pm",
"prc": "pdb",
"ps": [
"eps",
"ai"
],
"svg": "svgz",
"tcl": "tk",
"tif": "tiff"
},
"m": {
"application/7z": "7z",
"application/7z-compressed": "7z",
"application/ai": "ps",
"application/atom": "atom",
"application/atom+xml": "atom",
"application/bz2": "bz2",
"application/bzip2": "bz2",
"application/cco": "cco",
"application/cocoa": "cco",
"application/compressed": "gz",
"application/crt": "pem",
"application/der": "pem",
"application/doc": "doc",
"application/ear": "jar",
"application/eot": "eot",
"application/eps": "ps",
"application/gz": "gz",
"application/gzip": "gz",
"application/hqx": "hqx",
"application/jar": "jar",
"application/jardiff": "jardiff",
"application/java-archive": "jar",
"application/java-archive-diff": "jardiff",
"application/java-jnlp-file": "jnlp",
"application/javascript": "js",
"application/jnlp": "jnlp",
"application/js": "js",
"application/json": "json",
"application/jsx": "js",
"application/kml": "kml",
"application/kmz": "kmz",
"application/m3u8": "m3u8",
"application/mac-binhex40": "hqx",
"application/makeself": "run",
"application/msword": "doc",
"application/odg": "odg",
"application/odp": "odp",
"application/ods": "ods",
"application/odt": "odt",
"application/pdb": "prc",
"application/pdf": "pdf",
"application/pem": "pem",
"application/perl": "pl",
"application/pilot": "prc",
"application/pl": "pl",
"application/pm": "pl",
"application/postscript": "ps",
"application/ppt": "ppt",
"application/prc": "prc",
"application/ps": "ps",
"application/rar": "rar",
"application/rar-compressed": "rar",
"application/redhat-package-manager": "rpm",
"application/rpm": "rpm",
"application/rss": "rss",
"application/rss+xml": "rss",
"application/rtf": "rtf",
"application/run": "run",
"application/sea": "sea",
"application/shockwave-flash": "swf",
"application/sit": "sit",
"application/stuffit": "sit",
"application/swf": "swf",
"application/tar": "tar",
"application/tcl": "tcl",
"application/tk": "tcl",
"application/vnd.apple.mpegurl": "m3u8",
"application/vnd.google-earth.kml+xml": "kml",
"application/vnd.google-earth.kmz": "kmz",
"application/vnd.ms-excel": "xls",
"application/vnd.ms-fontobject": "eot",
"application/vnd.ms-powerpoint": "ppt",
"application/vnd.oasis.opendocument.graphics": "odg",
"application/vnd.oasis.opendocument.presentation": "odp",
"application/vnd.oasis.opendocument.spreadsheet": "ods",
"application/vnd.oasis.opendocument.text": "odt",
"application/vnd.wap.wmlc": "wmlc",
"application/war": "jar",
"application/wmlc": "wmlc",
"application/x-7z": "7z",
"application/x-7z-compressed": "7z",
"application/x-ai": "ps",
"application/x-atom": "atom",
"application/x-atom+xml": "atom",
"application/x-bz2": "bz2",
"application/x-bzip2": "bz2",
"application/x-cco": "cco",
"application/x-cocoa": "cco",
"application/x-compressed": "gz",
"application/x-crt": "pem",
"application/x-der": "pem",
"application/x-doc": "doc",
"application/x-ear": "jar",
"application/x-eot": "eot",
"application/x-eps": "ps",
"application/x-gz": "gz",
"application/x-gzip": "gz",
"application/x-hqx": "hqx",
"application/x-jar": "jar",
"application/x-jardiff": "jardiff",
"application/x-java-archive": "jar",
"application/x-java-archive-diff": "jardiff",
"application/x-java-jnlp-file": "jnlp",
"application/x-javascript": "js",
"application/x-jnlp": "jnlp",
"application/x-js": "js",
"application/x-json": "json",
"application/x-jsx": "js",
"application/x-kml": "kml",
"application/x-kmz": "kmz",
"application/x-m3u8": "m3u8",
"application/x-mac-binhex40": "hqx",
"application/x-makeself": "run",
"application/x-msword": "doc",
"application/x-odg": "odg",
"application/x-odp": "odp",
"application/x-ods": "ods",
"application/x-odt": "odt",
"application/x-pdb": "prc",
"application/x-pdf": "pdf",
"application/x-pem": "pem",
"application/x-perl": "pl",
"application/x-pilot": "prc",
"application/x-pl": "pl",
"application/x-pm": "pl",
"application/x-postscript": "ps",
"application/x-ppt": "ppt",
"application/x-prc": "prc",
"application/x-ps": "ps",
"application/x-rar": "rar",
"application/x-rar-compressed": "rar",
"application/x-redhat-package-manager": "rpm",
"application/x-rpm": "rpm",
"application/x-rss": "rss",
"application/x-rss+xml": "rss",
"application/x-rtf": "rtf",
"application/x-run": "run",
"application/x-sea": "sea",
"application/x-shockwave-flash": "swf",
"application/x-sit": "sit",
"application/x-stuffit": "sit",
"application/x-swf": "swf",
"application/x-tar": "tar",
"application/x-tcl": "tcl",
"application/x-tk": "tcl",
"application/x-vnd.apple.mpegurl": "m3u8",
"application/x-vnd.google-earth.kml+xml": "kml",
"application/x-vnd.google-earth.kmz": "kmz",
"application/x-vnd.ms-excel": "xls",
"application/x-vnd.ms-fontobject": "eot",
"application/x-vnd.ms-powerpoint": "ppt",
"application/x-vnd.oasis.opendocument.graphics": "odg",
"application/x-vnd.oasis.opendocument.presentation": "odp",
"application/x-vnd.oasis.opendocument.spreadsheet": "ods",
"application/x-vnd.oasis.opendocument.text": "odt",
"application/x-vnd.wap.wmlc": "wmlc",
"application/x-war": "jar",
"application/x-wmlc": "wmlc",
"application/x-x509-ca-cert": "pem",
"application/x-xhtml": "xhtml",
"application/x-xhtml+xml": "xhtml",
"application/x-xls": "xls",
"application/x-xpi": "xpi",
"application/x-xpinstall": "xpi",
"application/x-xspf": "xspf",
"application/x-xspf+xml": "xspf",
"application/x-xz": "xz",
"application/x-zip": "zip",
"application/x509-ca-cert": "pem",
"application/xhtml": "xhtml",
"application/xhtml+xml": "xhtml",
"application/xls": "xls",
"application/xpi": "xpi",
"application/xpinstall": "xpi",
"application/xspf": "xspf",
"application/xspf+xml": "xspf",
"application/xz": "xz",
"application/zip": "zip",
"audio/kar": "mid",
"audio/m4a": "m4a",
"audio/matroska": "mka",
"audio/mid": "mid",
"audio/midi": "mid",
"audio/mka": "mka",
"audio/mp3": "mp3",
"audio/mpeg": "mp3",
"audio/ogg": "ogg",
"audio/ra": "ra",
"audio/realaudio": "ra",
"audio/x-kar": "mid",
"audio/x-m4a": "m4a",
"audio/x-matroska": "mka",
"audio/x-mid": "mid",
"audio/x-midi": "mid",
"audio/x-mka": "mka",
"audio/x-mp3": "mp3",
"audio/x-mpeg": "mp3",
"audio/x-ogg": "ogg",
"audio/x-ra": "ra",
"audio/x-realaudio": "ra",
"font/woff": "woff",
"font/woff2": "woff2",
"font/x-woff": "woff",
"font/x-woff2": "woff2",
"image/bmp": "bmp",
"image/gif": "gif",
"image/heic": "heic",
"image/heif": "heic",
"image/heif-sequence": "heic",
"image/ico": "ico",
"image/icon": "ico",
"image/jfif": "jpg",
"image/jng": "jng",
"image/jpe": "jpg",
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/ms-bmp": "bmp",
"image/png": "png",
"image/svg": "svg",
"image/svg+xml": "svg",
"image/svgz": "svg",
"image/tif": "tif",
"image/tiff": "tif",
"image/vnd.wap.wbmp": "wbmp",
"image/wbmp": "wbmp",
"image/webp": "webp",
"image/x-bmp": "bmp",
"image/x-gif": "gif",
"image/x-heic": "heic",
"image/x-heif": "heic",
"image/x-heif-sequence": "heic",
"image/x-ico": "ico",
"image/x-icon": "ico",
"image/x-jfif": "jpg",
"image/x-jng": "jng",
"image/x-jpe": "jpg",
"image/x-jpeg": "jpg",
"image/x-jpg": "jpg",
"image/x-ms-bmp": "bmp",
"image/x-png": "png",
"image/x-svg": "svg",
"image/x-svg+xml": "svg",
"image/x-svgz": "svg",
"image/x-tif": "tif",
"image/x-tiff": "tif",
"image/x-vnd.wap.wbmp": "wbmp",
"image/x-wbmp": "wbmp",
"image/x-webp": "webp",
"text/component": "htc",
"text/css": "css",
"text/htc": "htc",
"text/htm": "html",
"text/html": "html",
"text/jad": "jad",
"text/javascript": "js",
"text/js": "js",
"text/jsx": "js",
"text/mathml": "mml",
"text/mml": "mml",
"text/php": "html",
"text/plain": "txt",
"text/shtml": "html",
"text/txt": "txt",
"text/vnd.sun.j2me.app-descriptor": "jad",
"text/vnd.wap.wml": "wml",
"text/wml": "wml",
"text/x-component": "htc",
"text/x-css": "css",
"text/x-htc": "htc",
"text/x-htm": "html",
"text/x-html": "html",
"text/x-jad": "jad",
"text/x-javascript": "js",
"text/x-js": "js",
"text/x-jsx": "js",
"text/x-mathml": "mml",
"text/x-mml": "mml",
"text/x-php": "html",
"text/x-plain": "txt",
"text/x-shtml": "html",
"text/x-txt": "txt",
"text/x-vnd.sun.j2me.app-descriptor": "jad",
"text/x-vnd.wap.wml": "wml",
"text/x-wml": "wml",
"text/x-xml": "xml",
"text/xml": "xml",
"video/3gp": "3gpp",
"video/3gpp": "3gpp",
"video/asf": "asx",
"video/asx": "asx",
"video/avi": "avi",
"video/flv": "flv",
"video/m4v": "m4v",
"video/matroska": "mkv",
"video/mk3d": "mkv",
"video/mks": "mkv",
"video/mkv": "mkv",
"video/mng": "mng",
"video/moov": "mov",
"video/mov": "mov",
"video/mp2t": "ts",
"video/mp4": "mp4",
"video/mpe": "mpg",
"video/mpeg": "mpg",
"video/mpg": "mpg",
"video/ms-asf": "asx",
"video/ms-wmv": "wmv",
"video/msvideo": "avi",
"video/opus": "opus",
"video/qt": "mov",
"video/quicktime": "mov",
"video/ts": "ts",
"video/webm": "webm",
"video/wmv": "wmv",
"video/x-3gp": "3gpp",
"video/x-3gpp": "3gpp",
"video/x-asf": "asx",
"video/x-asx": "asx",
"video/x-avi": "avi",
"video/x-flv": "flv",
"video/x-m4v": "m4v",
"video/x-matroska": "mkv",
"video/x-mk3d": "mkv",
"video/x-mks": "mkv",
"video/x-mkv": "mkv",
"video/x-mng": "mng",
"video/x-moov": "mov",
"video/x-mov": "mov",
"video/x-mp2t": "ts",
"video/x-mp4": "mp4",
"video/x-mpe": "mpg",
"video/x-mpeg": "mpg",
"video/x-mpg": "mpg",
"video/x-ms-asf": "asx",
"video/x-ms-wmv": "wmv",
"video/x-msvideo": "avi",
"video/x-opus": "opus",
"video/x-qt": "mov",
"video/x-quicktime": "mov",
"video/x-ts": "ts",
"video/x-webm": "webm",
"video/x-wmv": "wmv"
}
}

View File

@ -103,7 +103,7 @@ class Handler {
discarded: false, discarded: false,
}; };
if (!CHROME) { if (!CHROME) {
toptions.hidden = true; toptions.hidden = false;
} }
const selectedTabs = options.allTabs ? const selectedTabs = options.allTabs ?
await tabs.query(toptions) as any[] : await tabs.query(toptions) as any[] :

View File

@ -49,6 +49,7 @@ export const {runtime} = polyfill;
export const {storage} = polyfill; export const {storage} = polyfill;
export const {tabs} = polyfill; export const {tabs} = polyfill;
export const {webNavigation} = polyfill; export const {webNavigation} = polyfill;
export const {webRequest} = polyfill;
export const {windows} = polyfill; export const {windows} = polyfill;
export const CHROME = navigator.appVersion.includes("Chrome/"); export const CHROME = navigator.appVersion.includes("Chrome/");

View File

@ -9,7 +9,7 @@ import { EventEmitter } from "./events";
import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants"; import { TYPE_LINK, TYPE_MEDIA, TYPE_ALL } from "./constants";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { Overlayable } from "./objectoverlay"; import { Overlayable } from "./objectoverlay";
import * as DEFAULT_FILTERS from "../data/filters.json"; import DEFAULT_FILTERS from "../data/filters.json";
import { FASTFILTER } from "./recentlist"; import { FASTFILTER } from "./recentlist";
import { _, locale } from "./i18n"; import { _, locale } from "./i18n";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars

View File

@ -2,7 +2,7 @@
// License: MIT // License: MIT
import {memoize} from "./memoize"; import {memoize} from "./memoize";
import * as langs from "../_locales/all.json"; import langs from "../_locales/all.json";
import { sorted, naturalCaseCompare } from "./sorting"; import { sorted, naturalCaseCompare } from "./sorting";

View File

@ -27,6 +27,9 @@ const SAVEDPROPS = [
"written", "written",
// server stuff // server stuff
"serverName", "serverName",
"browserName",
"mime",
"prerolled",
// other options // other options
"private", "private",
// db // db
@ -39,10 +42,13 @@ const DEFAULTS = {
state: QUEUED, state: QUEUED,
error: "", error: "",
serverName: "", serverName: "",
browserName: "",
fileName: "", fileName: "",
totalSize: 0, totalSize: 0,
written: 0, written: 0,
manId: 0, manId: 0,
mime: "",
prerolled: false
}; };
let sessionId = 0; let sessionId = 0;
@ -59,14 +65,26 @@ export class BaseDownload {
public url: string; public url: string;
public usable: string;
public uReferrer: URLd; public uReferrer: URLd;
public referrer: string; public referrer: string;
public usableReferrer: string;
public startDate: Date; public startDate: Date;
public fileName: string; public fileName: string;
public description?: string;
public title?: string;
public batch: number;
public idx: number;
public error: string; public error: string;
public postData: any; public postData: any;
@ -79,8 +97,13 @@ export class BaseDownload {
public serverName: string; public serverName: string;
public browserName: string;
public mime: string;
public mask: string; public mask: string;
public prerolled: boolean;
constructor(options: any) { constructor(options: any) {
Object.assign(this, DEFAULTS); Object.assign(this, DEFAULTS);
@ -115,6 +138,10 @@ export class BaseDownload {
return this.serverName || this.fileName || this.urlName || "index.html"; return this.serverName || this.fileName || this.urlName || "index.html";
} }
get currentName() {
return this.browserName || this.dest.name || this.finalName;
}
get urlName() { get urlName() {
const path = parsePath(this.uURL); const path = parsePath(this.uURL);
if (path.name) { if (path.name) {
@ -152,6 +179,7 @@ export class BaseDownload {
rv.destName = dest.name; rv.destName = dest.name;
rv.destPath = dest.path; rv.destPath = dest.path;
rv.destFull = dest.full; rv.destFull = dest.full;
rv.currentName = this.browserName || rv.destName || rv.finalName;
rv.error = this.error; rv.error = this.error;
rv.ext = this.renamer.p_ext; rv.ext = this.renamer.p_ext;
return rv; return rv;

View File

@ -1,25 +1,70 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import MimeType from "whatwg-mimetype";
import { CHROME, downloads, webRequest } from "../browser";
import { Prefs } from "../prefs"; import { Prefs } from "../prefs";
import { parsePath, filterInSitu } from "../util";
import {
QUEUED, RUNNING, CANCELED, PAUSED, MISSING, DONE,
FORCABLE, PAUSABLE, CANCELABLE,
} from "./state";
import { BaseDownload } from "./basedownload";
import { PromiseSerializer } from "../pserializer"; import { PromiseSerializer } from "../pserializer";
import { filterInSitu, parsePath, sanitizePath } from "../util";
import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { Manager } from "./man"; import { Manager } from "./man";
import { downloads, CHROME } from "../browser"; import Renamer from "./renamer";
import { debounce } from "../../uikit/lib/util"; import {
CANCELABLE,
CANCELED,
DONE,
FORCABLE,
MISSING,
PAUSABLE,
PAUSED,
QUEUED,
RUNNING
} from "./state";
const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i;
const PREROLL_HOSTS = /4cdn|chan/;
const PREROLL_TIMEOUT = 10000;
const PREROLL_NOPE = new Set<string>();
const setShelfEnabled = downloads.setShelfEnabled || function() { function parseDisposition(disp: MimeType) {
// ignored if (!disp) {
}; return "";
}
let encoding = (disp.parameters.get("charset") || "utf-8").trim();
let file = (disp.parameters.get("filename") || "").trim().replace(/^(["'])(.*)\1$/, "$2");
if (!file) {
const encoded = disp.parameters.get("filename*");
if (!encoded) {
return "";
}
const pieces = encoded.split("'", 3);
if (pieces.length !== 3) {
return "";
}
encoding = pieces[0].trim() || encoding;
file = (pieces[3] || "").trim().replace(/^(["'])(.*)\1$/, "$2");
}
file = file.trim();
if (!file) {
return "";
}
const reenableShelf = debounce(() => setShelfEnabled(true), 1000, true); try {
// And now for the tricky part...
// First unescape the string, to get the raw bytes
// not utf-8-interpreted bytes
// Then convert the string into an uint8[]
// Then decode
return new TextDecoder(encoding).decode(
new Uint8Array(unescape(file).split("").map(e => e.charCodeAt(0)))
);
}
catch (ex) {
console.error("Cannot decode", encoding, file, ex);
}
return "";
}
type Header = {name: string; value: string}; type Header = {name: string; value: string};
interface Options { interface Options {
@ -53,6 +98,7 @@ export class Download extends BaseDownload {
} }
markDirty() { markDirty() {
this.renamer = new Renamer(this);
this.manager.setDirty(this); this.manager.setDirty(this);
} }
@ -80,6 +126,11 @@ export class Download extends BaseDownload {
this.updateStateFromBrowser(); this.updateStateFromBrowser();
return; return;
} }
if (state[0].state === "complete") {
this.changeState(DONE);
this.updateStateFromBrowser();
return;
}
if (!state[0].canResume) { if (!state[0].canResume) {
throw new Error("Cannot resume"); throw new Error("Cannot resume");
} }
@ -97,9 +148,22 @@ export class Download extends BaseDownload {
if (this.state !== QUEUED) { if (this.state !== QUEUED) {
throw new Error("invalid state"); throw new Error("invalid state");
} }
console.trace("starting", this.toString(), this.toMsg()); console.log("starting", this.toString(), this.toMsg());
this.changeState(RUNNING); this.changeState(RUNNING);
// Do NOT await
this.reallyStart();
}
private async reallyStart() {
try { try {
if (!this.prerolled) {
await this.maybePreroll();
if (this.state !== RUNNING) {
// Aborted by preroll
return;
}
}
const options: Options = { const options: Options = {
conflictAction: await Prefs.get("conflict-action"), conflictAction: await Prefs.get("conflict-action"),
filename: this.dest.full, filename: this.dest.full,
@ -124,24 +188,18 @@ export class Download extends BaseDownload {
this.manager.removeManId(this.manId); this.manager.removeManId(this.manId);
} }
setShelfEnabled(false);
try { try {
try { this.manager.addManId(
this.manager.addManId( this.manId = await downloads.download(options), this);
this.manId = await downloads.download(options), this);
}
catch (ex) {
if (!this.referrer) {
throw ex;
}
// Re-attempt without referrer
filterInSitu(options.headers, h => h.name !== "Referer");
this.manager.addManId(
this.manId = await downloads.download(options), this);
}
} }
finally { catch (ex) {
reenableShelf(); if (!this.referrer) {
throw ex;
}
// Re-attempt without referrer
filterInSitu(options.headers, h => h.name !== "Referer");
this.manager.addManId(
this.manId = await downloads.download(options), this);
} }
this.markDirty(); this.markDirty();
} }
@ -152,6 +210,146 @@ export class Download extends BaseDownload {
} }
} }
private get shouldPreroll() {
const {pathname, search, host} = this.uURL;
if (PREROLL_NOPE.has(host)) {
return false;
}
if (!this.renamer.p_ext) {
return true;
}
if (search.length) {
return true;
}
if (this.uURL.pathname.endsWith("/")) {
return true;
}
if (PREROLL_HEURISTICS.test(pathname)) {
return true;
}
if (PREROLL_HOSTS.test(host)) {
return true;
}
return false;
}
private async maybePreroll() {
try {
if (this.prerolled) {
// Check again, just in case, async and all
return;
}
if (!this.shouldPreroll) {
return;
}
await (CHROME ? this.prerollChrome() : this.prerollFirefox());
}
catch (ex) {
console.error("Failed to preroll", this, ex.toString(), ex.stack, ex);
}
finally {
if (this.state === RUNNING) {
this.prerolled = true;
this.markDirty();
}
}
}
private async prerollFirefox() {
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(this.uURL.toString(), {
method: "HEAD",
mode: "same-origin",
signal,
});
controller.abort();
const {headers} = res;
this.prerollFinialize(headers, res);
}
async prerollChrome() {
let rid = "";
const rurl = this.uURL.toString();
let listener: any;
const wr = new Promise<any[]>(resolve => {
listener = (details: any) => {
const {url, requestId, statusCode} = details;
if (rid !== requestId && url !== rurl) {
return;
}
// eslint-disable-next-line no-magic-numbers
if (statusCode >= 300 && statusCode < 400) {
// Redirect, continue tracking;
rid = requestId;
return;
}
resolve(details.responseHeaders);
};
webRequest.onHeadersReceived.addListener(
listener, {urls: ["<all_urls>"]}, ["responseHeaders"]);
});
const p = Promise.race([
wr,
new Promise<any[]>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT))
]);
p.finally(() => {
webRequest.onHeadersReceived.removeListener(listener);
});
const controller = new AbortController();
const {signal} = controller;
const res = await fetch(rurl, {
method: "HEAD",
signal,
});
controller.abort();
const headers = await p;
this.prerollFinialize(
new Headers(headers.map(i => [i.name, i.value])), res);
}
private prerollFinialize(headers: Headers, res: Response) {
const type = MimeType.parse(headers.get("content-type") || "");
const dispHeader = headers.get("content-disposition");
let file = "";
if (dispHeader) {
const disp = new MimeType(`${type && type.toString() || "application/octet-stream"}; ${dispHeader}`);
file = parseDisposition(disp);
// Sanitize
file = sanitizePath(file.replace(/[/\\]+/g, "-"));
}
if (type) {
this.mime = type.essence;
}
this.serverName = file;
this.markDirty();
const {status} = res;
/* eslint-disable no-magic-numbers */
if (status === 404) {
this.cancel();
this.error = "SERVER_BAD_CONTENT";
}
else if (status === 403) {
this.cancel();
this.error = "SERVER_FORBIDDEN";
}
else if (status === 402 || status === 407) {
this.cancel();
this.error = "SERVER_UNAUTHORIZED";
}
else if (status === 400 || status === 405) {
PREROLL_NOPE.add(this.uURL.host);
}
else if (status > 400 && status < 500) {
this.cancel();
this.error = "SERVER_FAILED";
}
/* eslint-enable no-magic-numbers */
}
resume(forced = false) { resume(forced = false) {
if (!(FORCABLE & this.state)) { if (!(FORCABLE & this.state)) {
return; return;
@ -181,9 +379,10 @@ export class Download extends BaseDownload {
} }
reset() { reset() {
this.prerolled = false;
this.manId = 0; this.manId = 0;
this.written = this.totalSize = 0; this.written = this.totalSize = 0;
this.serverName = ""; this.mime = this.serverName = this.browserName = "";
} }
async removeFromBrowser() { async removeFromBrowser() {
@ -260,8 +459,11 @@ export class Download extends BaseDownload {
const state = (await downloads.search({id: this.manId})).pop(); const state = (await downloads.search({id: this.manId})).pop();
const {filename, error} = state; const {filename, error} = state;
const path = parsePath(filename); const path = parsePath(filename);
this.serverName = path.name; this.browserName = path.name;
this.adoptSize(state); this.adoptSize(state);
if (!this.mime && state.mime) {
this.mime = state.mime;
}
this.markDirty(); this.markDirty();
switch (state.state) { switch (state.state) {
case "in_progress": case "in_progress":

View File

@ -25,11 +25,14 @@ const DIRTY_TIMEOUT = 100;
const MISSING_TIMEOUT = 12 * 1000; const MISSING_TIMEOUT = 12 * 1000;
const RELOAD_TIMEOUT = 10 * 1000; const RELOAD_TIMEOUT = 10 * 1000;
const setShelfEnabled = downloads.setShelfEnabled || function() {
// ignored
};
export class Manager extends EventEmitter { export class Manager extends EventEmitter {
private items: Download[]; private items: Download[];
private active: boolean; public active: boolean;
private notifiedFinished: boolean; private notifiedFinished: boolean;
@ -93,7 +96,10 @@ export class Manager extends EventEmitter {
} }
this.items.push(rv); this.items.push(rv);
}); });
await this.resetScheduler();
// Do not wait for the scheduler
this.resetScheduler();
this.emit("inited"); this.emit("inited");
setTimeout(() => this.checkMissing(), MISSING_TIMEOUT); setTimeout(() => this.checkMissing(), MISSING_TIMEOUT);
runtime.onUpdateAvailable.addListener(() => { runtime.onUpdateAvailable.addListener(() => {
@ -148,7 +154,7 @@ export class Manager extends EventEmitter {
} }
const next = await this.scheduler.next(this.running); const next = await this.scheduler.next(this.running);
if (!next) { if (!next) {
this.maybeNotifyFinished(); this.maybeRunFinishActions();
break; break;
} }
if (this.running.has(next) || next.state !== QUEUED) { if (this.running.has(next) || next.state !== QUEUED) {
@ -168,10 +174,31 @@ export class Manager extends EventEmitter {
async startDownload(download: Download) { async startDownload(download: Download) {
// Add to running first, so we don't confuse the scheduler and other parts // Add to running first, so we don't confuse the scheduler and other parts
this.running.add(download); this.running.add(download);
setShelfEnabled(false);
await download.start(); await download.start();
this.notifiedFinished = false; this.notifiedFinished = false;
} }
async maybeRunFinishActions() {
if (this.running.size) {
return;
}
await this.maybeNotifyFinished();
if (this.running.size) {
return;
}
if (this.shouldReload) {
this.saveQueue.trigger();
setTimeout(() => {
if (this.running.size) {
return;
}
runtime.reload();
}, RELOAD_TIMEOUT);
}
setShelfEnabled(true);
}
async maybeNotifyFinished() { async maybeNotifyFinished() {
if (!(await Prefs.get("finish-notification"))) { if (!(await Prefs.get("finish-notification"))) {
return; return;
@ -181,14 +208,6 @@ export class Manager extends EventEmitter {
} }
this.notifiedFinished = true; this.notifiedFinished = true;
new Notification(null, _("queue-finished")); new Notification(null, _("queue-finished"));
if (this.shouldReload) {
setTimeout(() => {
if (this.running.size) {
return;
}
runtime.reload();
}, RELOAD_TIMEOUT);
}
} }
addManId(id: number, download: Download) { addManId(id: number, download: Download) {
@ -236,7 +255,7 @@ export class Manager extends EventEmitter {
this.emit("dirty", items); this.emit("dirty", items);
} }
save(items: Download[]) { private save(items: Download[]) {
DB.saveItems(items.filter(i => !i.removed)). DB.saveItems(items.filter(i => !i.removed)).
catch(console.error); catch(console.error);
} }
@ -361,6 +380,10 @@ export class Manager extends EventEmitter {
} }
this.emit("active", this.active); this.emit("active", this.active);
} }
getMsgItems() {
return this.items.map(e => e.toMsg());
}
} }
let inited: Promise<Manager>; let inited: Promise<Manager>;

View File

@ -5,6 +5,10 @@ import { donate, openPrefs } from "../windowutils";
import { API } from "../api"; import { API } from "../api";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { BaseDownload } from "./basedownload"; import { BaseDownload } from "./basedownload";
// eslint-disable-next-line no-unused-vars
import { Manager } from "./man";
// eslint-disable-next-line no-unused-vars
import { Port } from "../bus";
type SID = {sid: number}; type SID = {sid: number};
type SIDS = { type SIDS = {
@ -13,9 +17,9 @@ type SIDS = {
}; };
export class ManagerPort { export class ManagerPort {
private manager: any; private manager: Manager;
private port: any; private port: Port;
constructor(manager: any, port: any) { constructor(manager: any, port: any) {
this.manager = manager; this.manager = manager;
@ -79,7 +83,6 @@ export class ManagerPort {
} }
sendAll() { sendAll() {
this.port.post( this.port.post("all", this.manager.getMsgItems());
"all", this.manager.items.map((e: BaseDownload) => e.toMsg()));
} }
} }

View File

@ -2,8 +2,12 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import { parsePath, sanitizePath } from "../util";
import { _ } from "../i18n"; import { _ } from "../i18n";
import { MimeDB } from "../mime";
// eslint-disable-next-line no-unused-vars
import { parsePath, PathInfo, sanitizePath } from "../util";
// eslint-disable-next-line no-unused-vars
import { BaseDownload } from "./basedownload";
const REPLACE_EXPR = /\*\w+\*/gi; const REPLACE_EXPR = /\*\w+\*/gi;
@ -22,21 +26,41 @@ const DATE_FORMATTER = new Intl.NumberFormat(undefined, {
}); });
export default class Renamer { export default class Renamer {
private readonly d: any; private readonly d: BaseDownload;
constructor(download: any) { private readonly nameinfo: PathInfo;
constructor(download: BaseDownload) {
this.d = download; this.d = download;
const info = parsePath(this.d.finalName);
this.nameinfo = this.fixupExtension(info);
} }
get nameinfo() { private fixupExtension(info: PathInfo): PathInfo {
return parsePath(this.d.finalName); if (!this.d.mime) {
return info;
}
const mime = MimeDB.getMime(this.d.mime);
if (!mime) {
return info;
}
const {ext} = info;
if (mime.major === "image" || mime.major === "video") {
if (ext && mime.extensions.has(ext.toLowerCase())) {
return info;
}
return new PathInfo(info.base, mime.primary, info.path);
}
if (ext) {
return info;
}
return new PathInfo(info.base, mime.primary, info.path);
} }
get ref() { get ref() {
return this.d.uReferrer; return this.d.uReferrer;
} }
get p_name() { get p_name() {
return this.nameinfo.base; return this.nameinfo.base;
} }
@ -184,7 +208,7 @@ export default class Renamer {
(self[prop] || "").trim() : (self[prop] || "").trim() :
type; type;
if (flat) { if (flat) {
return rv.replace(/\/+/g, "-"); return rv.replace(/[/\\]+/g, "-");
} }
return rv; return rv;
})); }));

55
lib/mime.ts Normal file
View File

@ -0,0 +1,55 @@
"use strict";
// License: MIT
import mime from "../data/mime.json";
export class MimeInfo {
public readonly type: string;
public readonly extensions: Set<string>;
public readonly major: string;
public readonly minor: string;
public readonly primary: string;
constructor(type: string, extensions: string[]) {
this.type = type;
const [major, minor] = type.split("/", 2);
this.major = major;
this.minor = minor;
[this.primary] = extensions;
this.extensions = new Set(extensions);
Object.freeze(this);
}
}
export const MimeDB = new class {
private readonly mimeToExts: Map<string, MimeInfo>;
constructor() {
const exts = new Map<string, string[]>();
for (const [prim, more] of Object.entries(mime.e)) {
let toadd = more;
if (!Array.isArray(toadd)) {
toadd = [toadd];
}
toadd.unshift(prim);
exts.set(prim, toadd);
}
this.mimeToExts = new Map(Array.from(
Object.entries(mime.m),
([mime, prim]) => [mime, new MimeInfo(mime, exts.get(prim) || [prim])]
));
}
getPrimary(mime: string) {
const info = this.mimeToExts.get(mime.trim().toLocaleLowerCase());
return info ? info.primary : "";
}
getMime(mime: string) {
return this.mimeToExts.get(mime.trim().toLocaleLowerCase());
}
}();

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
// License: MIT // License: MIT
import * as DEFAULT_PREFS from "../data/prefs.json"; import DEFAULT_PREFS from "../data/prefs.json";
import { EventEmitter } from "./events"; import { EventEmitter } from "./events";
import {loadOverlay} from "./objectoverlay"; import { loadOverlay } from "./objectoverlay";
import { storage } from "./browser"; import { storage } from "./browser";
const PREFS = Symbol("PREFS"); const PREFS = Symbol("PREFS");

View File

@ -2,8 +2,8 @@
// License: MIT // License: MIT
import * as psl from "psl"; import * as psl from "psl";
import {memoize, identity} from "./memoize"; import { identity, memoize } from "./memoize";
export {debounce} from "../uikit/lib/util"; export { debounce } from "../uikit/lib/util";
export class Promised { export class Promised {
private promise: Promise<any>; private promise: Promise<any>;
@ -96,8 +96,72 @@ export const IS_WIN = typeof navigator !== "undefined" &&
export const sanitizePath = identity( export const sanitizePath = identity(
IS_WIN ? sanitizePathWindows : sanitizePathGeneric); IS_WIN ? sanitizePathWindows : sanitizePathGeneric);
export class PathInfo {
private baseField: string;
private extField: string;
private pathField: string;
private nameField: string;
private fullField: string;
constructor(base: string, ext: string, path: string) {
this.baseField = base;
this.extField = ext;
this.pathField = path;
this.update();
}
get base() {
return this.baseField;
}
set base(nv) {
this.baseField = sanitizePath(nv);
this.update();
}
get ext() {
return this.extField;
}
set ext(nv) {
this.extField = sanitizePath(nv);
this.update();
}
get name() {
return this.nameField;
}
get path() {
return this.pathField;
}
set path(nv) {
this.pathField = sanitizePath(nv);
this.update();
}
get full() {
return this.fullField;
}
private update() {
this.nameField = this.extField ? `${this.baseField}.${this.extField}` : this.baseField;
this.fullField = this.pathField ? `${this.pathField}/${this.nameField}` : this.nameField;
}
clone() {
return new PathInfo(this.baseField, this.extField, this.pathField);
}
}
// XXX cleanup + test // XXX cleanup + test
export const parsePath = memoize(function parsePath(path: string | URL) { export const parsePath = memoize(function parsePath(
path: string | URL): PathInfo {
if (path instanceof URL) { if (path instanceof URL) {
path = decodeURIComponent(path.pathname); path = decodeURIComponent(path.pathname);
} }
@ -127,13 +191,7 @@ export const parsePath = memoize(function parsePath(path: string | URL) {
} }
path = pieces.join("/"); path = pieces.join("/");
return { return new PathInfo(base, ext, path);
path,
name,
base,
ext,
full: path ? `${path}/${name}` : name
};
}); });
export class CoalescedUpdate<T> extends Set<T> { export class CoalescedUpdate<T> extends Set<T> {

View File

@ -3,7 +3,7 @@
import { windows, tabs, runtime } from "../lib/browser"; import { windows, tabs, runtime } from "../lib/browser";
import {getManager} from "./manager/man"; import {getManager} from "./manager/man";
import * as DEFAULT_ICONS from "../data/icons.json"; import DEFAULT_ICONS from "../data/icons.json";
const DONATE_URL = "https://www.downthemall.org/howto/donate/"; const DONATE_URL = "https://www.downthemall.org/howto/donate/";
const MANAGER_URL = "/windows/manager.html"; const MANAGER_URL = "/windows/manager.html";

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "DownThemAll!", "name": "DownThemAll!",
"version": "4.0.8", "version": "4.0.9",
"description": "__MSG_extensionDescription__", "description": "__MSG_extensionDescription__",
"homepage_url": "https://downthemall.org/", "homepage_url": "https://downthemall.org/",
@ -9,7 +9,7 @@
"default_locale": "en", "default_locale": "en",
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: 'self'; default-src 'self'", "content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: http: https: 'self'; default-src 'self'",
"icons": { "icons": {
"16": "style/icon16.png", "16": "style/icon16.png",
@ -31,7 +31,8 @@
"notifications", "notifications",
"storage", "storage",
"tabs", "tabs",
"webNavigation" "webNavigation",
"webRequest"
], ],
"background": { "background": {

View File

@ -33,7 +33,9 @@
}, },
"dependencies": { "dependencies": {
"@types/psl": "^1.1.0", "@types/psl": "^1.1.0",
"@types/whatwg-mimetype": "^2.1.0",
"psl": "^1.3.0", "psl": "^1.3.0",
"webextension-polyfill": "^0.4.0" "webextension-polyfill": "^0.4.0",
"whatwg-mimetype": "^2.3.0"
} }
} }

View File

@ -295,7 +295,7 @@ class Gatherer {
function gather(msg: any, sender: any, callback: Function) { function gather(msg: any, sender: any, callback: Function) {
try { try {
if (!msg || msg.type !== "DTA:gather" || !callback) { if (!msg || msg.type !== "DTA:gather" || !callback) {
return; return Promise.resolve(null);
} }
const gatherer = new Gatherer(msg); const gatherer = new Gatherer(msg);
const result = { const result = {
@ -313,10 +313,11 @@ function gather(msg: any, sender: any, callback: Function) {
), ),
}; };
urlToUsable(result, result.baseURL); urlToUsable(result, result.baseURL);
callback(result); return Promise.resolve(result);
} }
catch (ex) { catch (ex) {
console.error(ex.toString(), ex.stack, ex); console.error(ex.toString(), ex.stack, ex);
return Promise.resolve(null);
} }
} }

View File

@ -18,6 +18,7 @@
--folder-color: rgb(214, 165, 4); --folder-color: rgb(214, 165, 4);
--maskbutton-color: rgb(236, 185, 16); --maskbutton-color: rgb(236, 185, 16);
--missing-color: rgb(0, 82, 204); --missing-color: rgb(0, 82, 204);
--open-color: rgba(236, 185, 16, 0.8);
} }
html[data-platform="mac"] { html[data-platform="mac"] {

View File

@ -108,11 +108,11 @@ body > * {
} }
#colURL { #colURL {
width: 38%; width: 42%;
} }
#colPercent { #colPercent {
width: 3em; width: 4em;
min-width: 3em; min-width: 3em;
} }
@ -121,11 +121,11 @@ body > * {
} }
#colSize { #colSize {
width: 15em; width: 14em;
} }
#colSpeed { #colSpeed {
width: 6em; width: 7em;
} }
#colDomain, #colDomain,
@ -154,6 +154,14 @@ body > * {
height: 26px; height: 26px;
} }
.virtualtable-row.opening {
background: var(--open-color) !important;
}
.virtualtable-progress-container {
border-radius: 2px;
}
.virtualtable-progress-bar { .virtualtable-progress-bar {
height: 14px; height: 14px;
} }
@ -262,6 +270,7 @@ body > * {
} }
.virtualtable-column-6, .virtualtable-column-6,
.virtualtable-column-4,
.virtualtable-column-3 { .virtualtable-column-3 {
text-align: right; text-align: right;
} }
@ -430,6 +439,8 @@ body > * {
justify-items: stretch; justify-items: stretch;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 2px 6px black; box-shadow: 2px 2px 6px black;
-webkit-user-select: none;
user-select: none;
} }
#tooltip-infos { #tooltip-infos {

View File

@ -232,3 +232,7 @@ body > * {
#maskButton { #maskButton {
justify-self: flex-start; justify-self: flex-start;
} }
#btnDownload {
font-weight: bold;
}

View File

@ -81,3 +81,7 @@ h3 {
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
#btnDownload {
font-weight: bold;
}

View File

@ -19,7 +19,7 @@ const OPTS = {
state: DownloadState.QUEUED, state: DownloadState.QUEUED,
batch: 42, batch: 42,
idx: 23, idx: 23,
mask: "*name*.*ext", mask: "*name*.*ext*",
description: "desc / ript.ion .", description: "desc / ript.ion .",
title: " *** TITLE *** ", title: " *** TITLE *** ",
}; };
@ -57,6 +57,49 @@ describe("Renamer", function() {
expect(dest.path).to.equal(""); expect(dest.path).to.equal("");
}); });
it("*name*.*ext* (mime override)", function() {
const {dest} = new BaseDownload(
Object.assign({}, OPTS, {
mask: "*name* *batch*.*ext*",
mime: "image/jpeg"
}));
expect(dest.full).to.equal("filenäme 042.jpg");
expect(dest.name).to.equal("filenäme 042.jpg");
expect(dest.base).to.equal("filenäme 042");
expect(dest.ext).to.equal("jpg");
expect(dest.path).to.equal("");
});
it("*name*.*ext* (mime no override)", function() {
const {dest} = new BaseDownload(
Object.assign({}, OPTS, {
mask: "*name* *batch*.*ext*",
mime: "image/jpeg",
url: "https://www.example.co.uk/filen%C3%A4me.JPe",
usable: "https://www.example.co.uk/filenäme.JPe",
}));
expect(dest.full).to.equal("filenäme 042.JPe");
expect(dest.name).to.equal("filenäme 042.JPe");
expect(dest.base).to.equal("filenäme 042");
expect(dest.ext).to.equal("JPe");
expect(dest.path).to.equal("");
});
it("*name*.*ext* (mime override; missing ext)", function() {
const {dest} = new BaseDownload(
Object.assign({}, OPTS, {
mask: "*name* *batch*.*ext*",
mime: "application/json",
url: "https://www.example.co.uk/filen%C3%A4me",
usable: "https://www.example.co.uk/filenäme",
}));
expect(dest.full).to.equal("filenäme 042.json");
expect(dest.name).to.equal("filenäme 042.json");
expect(dest.base).to.equal("filenäme 042");
expect(dest.ext).to.equal("json");
expect(dest.path).to.equal("");
});
it("*text*", function() { it("*text*", function() {
const dest = makeOne("*text*"); const dest = makeOne("*text*");
expect(dest.full).to.equal("desc/ript.ion"); expect(dest.full).to.equal("desc/ript.ion");

View File

@ -12,6 +12,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true,
"importHelpers": true,
"sourceMap": true "sourceMap": true
} }
} }

23
util/additional.types Normal file
View File

@ -0,0 +1,23 @@
types {
application/x-x509-ca-cert pem crt der;
application/javascript js jsx;
audio/x-matroska mka;
image/bmp bmp;
image/heic heic heif;
image/heic heic heif;
image/heif-sequence heic heif;
image/heif-sequence heic heif;
image/jpeg jpg jpeg jpe jfif;
image/webp webp;
text/html html htm shtml php;
text/javascript js jsx;
video/mpeg mpg mpe mpeg mpg;
video/opus opus;
video/x-matroska mkv mk3d mks;
video/quicktime mov qt moov;
application/x-compressed gz;
application/x-gzip gz gzip;
application/x-bzip2 bz2;
application/x-tar tar;
application/x-xz xz;
}

View File

@ -26,7 +26,7 @@ UNCOMPRESSABLE = set((".png", ".jpg", ".zip", ".woff2"))
LICENSED = set((".css", ".html", ".js", "*.ts")) LICENSED = set((".css", ".html", ".js", "*.ts"))
IGNORED = set((".DS_Store", "Thumbs.db")) IGNORED = set((".DS_Store", "Thumbs.db"))
PERM_IGNORED_FX = set(("downloads.shelf",)) PERM_IGNORED_FX = set(("downloads.shelf", "webRequest"))
PERM_IGNORED_CHROME = set(("menus",)) PERM_IGNORED_CHROME = set(("menus",))
SCRIPTS = [ SCRIPTS = [

98
util/mime.types Normal file
View File

@ -0,0 +1,98 @@
https://github.com/nginx/nginx/raw/master/conf/mime.types
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

76
util/seed_mime.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
import json
import re
import sys
from collections import OrderedDict
def unique(seq):
return list(OrderedDict([i, None] for i in seq if i))
def generate(major, minor, exts):
exts = exts[:]
yield f"{major}/{minor}", exts
if (minor.startswith("x-")):
yield f"{major}/{minor[2:]}", exts
else:
yield f"{major}/x-{minor}", exts
for ext in exts:
yield f"{major}/{ext}", exts
yield f"{major}/x-{ext}", exts
def make(text, final):
lines = "".join([
line.strip()
for line in re.search(r"\{(.*)\}", text, re.S).group(1).split("\n")
if line.strip() and not line.strip().startswith("#")
]).split(";")
additional = []
for line in lines:
if not line:
continue
m = re.match(r"([a-z1-9]+)/([^\s]+)\s+(.+?)$", line)
if not m:
continue
[major, minor, exts] = m.groups()
exts = unique(e.lower().strip() for e in exts.split(" ") if e.strip())
mime = f"{major}/{minor}"
if mime == "application/octet-stream":
continue
if mime in final:
final[mime] += exts
continue
final[mime] = exts
additional += (major, minor, exts),
for [major, minor, exts] in additional:
for [mime, exts] in generate(major, minor, exts):
if mime in final:
continue
final[mime] = exts
final = OrderedDict()
for file in sys.argv[1:]:
with open(file, "r") as fp:
make(fp.read(), final)
multi = dict()
for [mime, exts] in list(final.items()):
exts = unique(exts)
prim = exts[0]
final[mime] = prim
if len(exts) == 1:
continue
exts = exts[1:]
if len(exts) == 1:
multi[prim] = exts[0]
else:
multi[prim] = exts
final = OrderedDict(sorted(final.items()))
multi = OrderedDict(sorted(multi.items()))
print(json.dumps(dict(e=multi, m=final), indent=2))
print("generated", len(final), "mimes", "with", len(multi), "multis", file=sys.stderr)

View File

@ -94,8 +94,10 @@ export class TextFilter extends ItemFilter {
} }
allow(item: DownloadItem) { allow(item: DownloadItem) {
return this.expr.test( const {expr} = this;
[item.usable, item.description, item.finalName].join(" ")); return expr.test(item.currentName) ||
expr.test(item.usable) ||
expr.test(item.description);
} }
} }

View File

@ -116,7 +116,7 @@ export class DownloadItem extends EventEmitter {
public error: string; public error: string;
public finalName: string; public currentName: string;
public ext?: string; public ext?: string;
@ -144,6 +144,8 @@ export class DownloadItem extends EventEmitter {
private largeIconField?: string; private largeIconField?: string;
public opening: boolean;
constructor(owner: DownloadTable, raw: any, stats?: Stats) { constructor(owner: DownloadTable, raw: any, stats?: Stats) {
super(); super();
Object.assign(this, raw); Object.assign(this, raw);
@ -159,7 +161,7 @@ export class DownloadItem extends EventEmitter {
return this.iconField; return this.iconField;
} }
this.iconField = this.owner.icons.get( this.iconField = this.owner.icons.get(
iconForPath(this.finalName, ICON_BASE_SIZE)); iconForPath(this.currentName, ICON_BASE_SIZE));
if (this.ext) { if (this.ext) {
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => { IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
if (icon) { if (icon) {
@ -178,7 +180,7 @@ export class DownloadItem extends EventEmitter {
return this.largeIconField; return this.largeIconField;
} }
this.largeIconField = this.owner.icons.get( this.largeIconField = this.owner.icons.get(
iconForPath(this.finalName, LARGE_ICON_BASE_SIZE)); iconForPath(this.currentName, LARGE_ICON_BASE_SIZE));
if (this.ext) { if (this.ext) {
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => { IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
if (icon) { if (icon) {
@ -217,7 +219,7 @@ export class DownloadItem extends EventEmitter {
if (this.owner.showUrls.value) { if (this.owner.showUrls.value) {
return this.usable; return this.usable;
} }
return this.finalName; return this.currentName;
} }
get fmtSize() { get fmtSize() {
@ -607,8 +609,12 @@ export class DownloadTable extends VirtualTable {
this.selection.clear(); this.selection.clear();
this.tooltip = null; this.tooltip = null;
this.on("hover", async info => { const tooltipWatcher = new PrefWatcher("tooltip", true);
if (!(await Prefs.get("tooltip"))) { this.on("hover", info => {
if (!document.hasFocus()) {
return;
}
if (!tooltipWatcher.value) {
return; return;
} }
const item = this.downloads.filtered[info.rowid]; const item = this.downloads.filtered[info.rowid];
@ -777,7 +783,8 @@ export class DownloadTable extends VirtualTable {
} }
resumeDownloads(forced = false) { resumeDownloads(forced = false) {
const sids = this.getSelectedSids(DownloadState.RESUMABLE); const sids = this.getSelectedSids(
forced ? DownloadState.FORCABLE : DownloadState.RESUMABLE);
if (!sids.length) { if (!sids.length) {
return; return;
} }
@ -801,20 +808,30 @@ export class DownloadTable extends VirtualTable {
} }
async openFile() { async openFile() {
if (this.focusRow < 0) { this.dismissTooltip();
const {focusRow} = this;
if (focusRow < 0) {
return; return;
} }
const item = this.downloads.filtered[this.focusRow]; const item = this.downloads.filtered[focusRow];
if (!item || !item.manId || item.state !== DownloadState.DONE) { if (!item || !item.manId || item.state !== DownloadState.DONE) {
return; return;
} }
item.opening = true;
try { try {
this.invalidateRow(focusRow);
await downloads.open(item.manId); await downloads.open(item.manId);
} }
catch (ex) { catch (ex) {
console.error(ex, ex.toString(), ex); console.error(ex, ex.toString(), ex);
PORT.post("missing", {sid: item.sessionId}); PORT.post("missing", {sid: item.sessionId});
} }
finally {
setTimeout(() => {
item.opening = false;
this.invalidateRow(focusRow);
}, 500);
}
} }
async openDirectory() { async openDirectory() {
@ -1111,7 +1128,16 @@ export class DownloadTable extends VirtualTable {
if (!item) { if (!item) {
return null; return null;
} }
if (item.opening) {
return ["opening"];
}
const cls = StateClasses.get(item.state); const cls = StateClasses.get(item.state);
if (cls && item.opening) {
return [cls, "opening"];
}
if (item.opening) {
return ["opening"];
}
return cls && [cls] || null; return cls && [cls] || null;
} }

View File

@ -46,6 +46,11 @@
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.0.tgz#390c5df1613b166ce3c3eb9fda4d93dc3eeec7b5" resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.0.tgz#390c5df1613b166ce3c3eb9fda4d93dc3eeec7b5"
integrity sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ== integrity sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==
"@types/whatwg-mimetype@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f981bbdf1813a75820a6ec3a7fdfa0d452552cc7"
integrity sha512-bJ/bZ+pA69lm+Ll8JJRoAD9saH7unIMfxPQQpl7bxa00qNqvUXSyk3xvoRMea1uCpAOxweI7CzjWx48ysX6yug==
"@typescript-eslint/eslint-plugin@^2.0.0": "@typescript-eslint/eslint-plugin@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz#609a5d7b00ce21a6f94d7ef282eba9da57ca1e42" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz#609a5d7b00ce21a6f94d7ef282eba9da57ca1e42"
@ -3718,6 +3723,11 @@ webpack@^4.39.3:
watchpack "^1.6.0" watchpack "^1.6.0"
webpack-sources "^1.4.1" webpack-sources "^1.4.1"
whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
which-module@^2.0.0: which-module@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"