-// Copyright (c) 2011 by Kris Maglione <maglione.k@gmail.com>
+// Copyright (c) 2011-2014 Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-/* use strict */
+"use strict";
-Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("downloads", {
- exports: ["Download", "Downloads", "downloads"]
-}, this);
+ exports: ["Download", "Downloads", "downloads"],
+ require: ["util"]
+});
+
+lazyRequire("overlay", ["overlay"]);
+lazyRequire("promises", ["Task", "promises"]);
-Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
+lazyRequire("resource://gre/modules/Downloads.jsm", ["Downloads"]);
+lazyRequire("resource://gre/modules/DownloadUtils.jsm", ["DownloadUtils"]);
+
+var MAX_LOAD_TIME = 10 * 1000;
let prefix = "DOWNLOAD_";
var states = iter([v, k.slice(prefix.length).toLowerCase()]
for ([k, v] in Iterator(Ci.nsIDownloadManager))
- if (k.indexOf(prefix) == 0))
+ if (k.startsWith(prefix)))
.toObject();
var Download = Class("Download", {
- init: function init(id, list) {
- let self = this;
- this.download = services.downloadManager.getDownload(id);
+ init: function init(download, list) {
+ this.download = download;
this.list = list;
this.nodes = {
- commandTarget: self
+ commandTarget: this
};
- util.xmlToDom(
- <tr highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
- <td highlight="DownloadTitle">
- <span highlight="Link">
- <a key="launch"
- href={self.target.spec} path={self.targetFile.path}>{self.displayName}</a>
- <span highlight="LinkInfo">{self.targetFile.path}</span>
- </span>
- </td>
- <td highlight="DownloadState" key="state"/>
- <td highlight="DownloadButtons Buttons">
- <a highlight="Button" href="javascript:0" key="pause">{_("download.action.Pause")}</a>
- <a highlight="Button" href="javascript:0" key="remove">{_("download.action.Remove")}</a>
- <a highlight="Button" href="javascript:0" key="resume">{_("download.action.Resume")}</a>
- <a highlight="Button" href="javascript:0" key="retry">{_("download.action.Retry")}</a>
- <a highlight="Button" href="javascript:0" key="cancel">{_("download.action.Cancel")}</a>
- <a highlight="Button" href="javascript:0" key="delete">{_("download.action.Delete")}</a>
- </td>
- <td highlight="DownloadProgress" key="progress">
- <span highlight="DownloadProgressHave" key="progressHave"
- />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
- </td>
- <td highlight="DownloadPercent" key="percent"/>
- <td highlight="DownloadSpeed" key="speed"/>
- <td highlight="DownloadTime" key="time"/>
- <td><a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a></td>
- </tr>,
+ DOM.fromJSON(
+ ["tr", { highlight: "Download", key: "row" },
+ ["td", { highlight: "DownloadTitle" },
+ ["span", { highlight: "Link" },
+ ["a", { key: "launch", href: this.target.spec, path: this.targetFile.path },
+ this.displayName],
+ ["span", { highlight: "LinkInfo" },
+ this.targetFile.path]]],
+ ["td", { highlight: "DownloadState", key: "state" }],
+ ["td", { highlight: "DownloadButtons Buttons" },
+ ["a", { highlight: "Button", href: "javascript:0", key: "stop" }, _("download.action.Stop")],
+ ["a", { highlight: "Button", href: "javascript:0", key: "remove" }, _("download.action.Remove")],
+ ["a", { highlight: "Button", href: "javascript:0", key: "resume" }, _("download.action.Resume")],
+ ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("download.action.Delete")]],
+ ["td", { highlight: "DownloadProgress", key: "progress" },
+ ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
+ "/",
+ ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],,
+ ["td", { highlight: "DownloadPercent", key: "percent" }],
+ ["td", { highlight: "DownloadSpeed", key: "speed" }],
+ ["td", { highlight: "DownloadTime", key: "time" }],
+ ["td", {},
+ ["a", { highlight: "DownloadSource", key: "source", href: this.source.url },
+ this.source.url]]],
this.list.document, this.nodes);
- this.nodes.launch.addEventListener("click", function (event) {
+ this.nodes.launch.addEventListener("click", (event) => {
if (event.button == 0) {
event.preventDefault();
- self.command("launch");
+ this.command("launch");
}
}, false);
- self.updateStatus();
- return self;
+ this.updateStatus();
+ return this;
},
+ get active() !this.stopped,
+
+ get targetFile() File(this.download.target.path),
+
+ get displayName() this.targetFile.leafName,
+
get status() states[this.state],
inState: function inState(states) states.indexOf(this.status) >= 0,
- get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
-
allowedCommands: Class.Memoize(function () let (self = this) ({
- get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
- get delete() !this.cancel && self.targetFile.exists(),
- get launch() self.targetFile.exists() && self.inState(["finished"]),
- get pause() self.inState(["downloading"]),
- get remove() self.inState(["blocked_parental", "blocked_policy",
- "canceled", "dirty", "failed", "finished"]),
- get resume() self.resumable && self.inState(["paused"]),
- get retry() self.inState(["canceled", "failed"])
+ get delete() !self.active && (self.targetFile.exists() || self.hasPartialData),
+ get launch() self.targetFile.exists() && self.succeeded,
+ get stop() self.active,
+ get remove() !self.active,
+ get resume() self.canceled
})),
command: function command(name) {
- util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
+ util.assert(hasOwnProperty(this.allowedCommands, name), _("download.unknownCommand"));
util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
- if (Set.has(this.commands, name))
+ if (hasOwnProperty(this.commands, name))
this.commands[name].call(this);
- else
- services.downloadManager[name + "Download"](this.id);
},
commands: {
- delete: function delete_() {
- this.targetFile.remove(false);
+ delete: promises.task(function delete_() {
+ if (this.hasPartialData)
+ yield this.removePartialData();
+ else if (this.targetFile.exists())
+ this.targetFile.remove(false);
this.updateStatus();
- },
+ }),
launch: function launch() {
- let self = this;
// Behavior mimics that of the builtin Download Manager.
function action() {
try {
if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
- this.MIMEInfo.launchWithFile(file);
+ this.MIMEInfo.launchWithFile(file.file);
else
file.launch();
}
let file = io.File(this.targetFile);
if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
- function (resp) {
+ (resp) => {
if (/^a(lways)$/i.test(resp)) {
prefs.set("browser.download.manager.alertOnEXEOpen", false);
resp = "yes";
}
if (/^y(es)?$/i.test(resp))
- action.call(self);
+ action.call(this);
});
else
action.call(this);
- }
+ },
+ resume: function resume() {
+ this.download.start();
+ },
+ remove: promises.task(function remove() {
+ yield this.list.list.remove(this.download);
+ yield this.download.finalize(true);
+ }),
+ stop: function stop() {
+ this.download.cancel();
+ },
},
_compare: {
- active: function (a, b) a.alive - b.alive,
- complete: function (a, b) a.percentComplete - b.percentComplete,
- date: function (a, b) a.startTime - b.startTime,
- filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
- size: function (a, b) a.size - b.size,
- speed: function (a, b) a.speed - b.speed,
- time: function (a, b) a.timeRemaining - b.timeRemaining,
- url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
+ active: (a, b) => a.active - b.active,
+ complete: (a, b) => a.percentComplete - b.percentComplete,
+ date: (a, b) => a.startTime - b.startTime,
+ filename: (a, b) => String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
+ size: (a, b) => a.totalBytes - b.totalBytes,
+ speed: (a, b) => a.speed - b.speed,
+ time: (a, b) => a.timeRemaining - b.timeRemaining,
+ url: (a, b) => String.localeCompare(a.source.url, b.source.url)
},
compare: function compare(other) values(this.list.sortOrder).map(function (order) {
let val = this._compare[order.substr(1)](this, other);
return (order[0] == "-") ? -val : val;
- }, this).nth(util.identity, 0) || 0,
+ }, this).find(util.identity) || 0,
timeRemaining: Infinity,
updateProgress: function updateProgress() {
let self = this.__proto__;
- if (this.amountTransferred === this.size) {
+ if (!this.active) {
this.nodes.speed.textContent = "";
this.nodes.time.textContent = "";
}
else {
this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
- if (this.speed == 0 || this.size == 0)
+ if (this.speed == 0 || !this.hasProgress)
this.nodes.time.textContent = _("download.unknown");
else {
- let seconds = (this.size - this.amountTransferred) / this.speed;
+ let seconds = (this.totalBytes - this.currentBytes) / this.speed;
[, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
if (this.timeRemaining)
this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
}
}
- let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
- : _("download.unknown");
+ let total = this.nodes.progressTotal.textContent =
+ this.hasProgress && (this.totalBytes || !this.nActive)
+ ? util.formatBytes(this.totalBytes, 1, true)
+ : _("download.unknown");
+
let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
- this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
+ this.nodes.progressHave.textContent = util.formatBytes(this.currentBytes, 1, true).replace(suffix, "");
- this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
+ this.nodes.percent.textContent = this.hasProgress ? this.progress + "%" : "";
},
updateStatus: function updateStatus() {
- this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
+ this.nodes.row[this.active ? "setAttribute" : "removeAttribute"]("active", "true");
this.nodes.row.setAttribute("status", this.status);
this.nodes.state.textContent = util.capitalize(this.status);
this.updateProgress();
}
});
-Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
- if (!(key in Download.prototype))
- Object.defineProperty(Download.prototype, key, {
- get: function get() this.download[key],
- set: function set(val) this.download[key] = val,
- configurable: true
- });
-});
var DownloadList = Class("DownloadList",
XPCOM([Ci.nsIDownloadProgressListener,
this.nodes = {
commandTarget: this
};
- this.downloads = {};
+ this.downloads = Map();
},
cleanup: function cleanup() {
- this.observe.unregister();
- services.downloadManager.removeListener(this);
+ if (this.list)
+ this.list.removeView(this);
+ this.dead = true;
},
message: Class.Memoize(function () {
- util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
- <tr highlight="DownloadHead">
- <span>{_("title.Title")}</span>
- <span>{_("title.Status")}</span>
- <span/>
- <span>{_("title.Progress")}</span>
- <span/>
- <span>{_("title.Speed")}</span>
- <span>{_("title.Time remaining")}</span>
- <span>{_("title.Source")}</span>
- </tr>
- <tr highlight="Download"><span><div style="min-height: 1ex; /* FIXME */"/></span></tr>
- <tr highlight="Download" key="totals" active="true">
- <td><span highlight="Title">{_("title.Totals")}:</span> <span key="total"/></td>
- <td/>
- <td highlight="DownloadButtons">
- <a highlight="Button" href="javascript:0" key="clear">{_("download.action.Clear")}</a>
- </td>
- <td highlight="DownloadProgress" key="progress">
- <span highlight="DownloadProgressHave" key="progressHave"
- />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
- </td>
- <td highlight="DownloadPercent" key="percent"/>
- <td highlight="DownloadSpeed" key="speed"/>
- <td highlight="DownloadTime" key="time"/>
- <td/>
- </tr>
- </table>, this.document, this.nodes);
-
- for (let row in iter(services.downloadManager.DBConnection
- .createStatement("SELECT id FROM moz_downloads")))
- this.addDownload(row.id);
- this.update();
-
- util.addObserver(this);
- services.downloadManager.addListener(this);
+ DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
+ ["tr", { highlight: "DownloadHead", key: "head" },
+ ["span", {}, _("title.Title")],
+ ["span", {}, _("title.Status")],
+ ["span"],
+ ["span", {}, _("title.Progress")],
+ ["span"],
+ ["span", {}, _("title.Speed")],
+ ["span", {}, _("title.Time remaining")],
+ ["span", {}, _("title.Source")]],
+ ["tr", { highlight: "Download" },
+ ["span", {},
+ ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
+ ["tr", { highlight: "Download", key: "totals", active: "true" },
+ ["td", {},
+ ["span", { highlight: "Title" },
+ _("title.Totals") + ":"],
+ " ",
+ ["span", { key: "total" }]],
+ ["td"],
+ ["td", { highlight: "DownloadButtons" },
+ ["a", { highlight: "Button", href: "javascript:0", key: "clear" }, _("download.action.Clear")]],
+ ["td", { highlight: "DownloadProgress", key: "progress" },
+ ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
+ "/",
+ ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
+ ["td", { highlight: "DownloadPercent", key: "percent" }],
+ ["td", { highlight: "DownloadSpeed", key: "speed" }],
+ ["td", { highlight: "DownloadTime", key: "time" }],
+ ["td"]]],
+ this.document, this.nodes);
+
+ this.index = Array.indexOf(this.nodes.list.childNodes,
+ this.nodes.head);
+
+ Task.spawn(function () {
+ this.list = yield Downloads.getList(Downloads.ALL);
+
+ let start = Date.now();
+ for (let download of yield this.list.getAll()) {
+ if (Date.now() - start > MAX_LOAD_TIME) {
+ util.dactyl.warn(_("download.givingUpAfter", (Date.now() - start) / 1000));
+ break;
+ }
+ this.addDownload(download);
+ }
+ this.update();
+
+ if (!this.dead)
+ this.list.addView(this);
+ }.bind(this));
return this.nodes.list;
}),
- addDownload: function addDownload(id) {
- if (!(id in this.downloads)) {
- let download = Download(id, this);
- if (this.filter && download.displayName.indexOf(this.filter) === -1)
+ addDownload: function addDownload(download) {
+ if (!this.downloads.has(download)) {
+ download = Download(download, this);
+ if (this.filter && !download.displayName.contains(this.filter))
return;
- this.downloads[id] = download;
- let index = values(this.downloads).sort(function (a, b) a.compare(b))
- .indexOf(download);
+ this.downloads.set(download.download, download);
+ let index = values(this.downloads).toArray()
+ .sort((a, b) => a.compare(b))
+ .indexOf(download);
this.nodes.list.insertBefore(download.nodes.row,
- this.nodes.list.childNodes[index + 1]);
+ this.nodes.list.childNodes[index + this.index + 1]);
}
},
- removeDownload: function removeDownload(id) {
- if (id in this.downloads) {
- this.nodes.list.removeChild(this.downloads[id].nodes.row);
- delete this.downloads[id];
+ removeDownload: function removeDownload(download) {
+ if (this.downloads.has(download)) {
+ this.nodes.list.removeChild(this.downloads.get(download).nodes.row);
+ delete this.downloads.delete(download);
}
},
},
allowedCommands: Class.Memoize(function () let (self = this) ({
- get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
+ get clear() iter(self.downloads.values()).some(dl => dl.allowedCommands.remove)
})),
commands: {
clear: function () {
- services.downloadManager.cleanUp();
+ this.list.removeFinished();
}
},
sort: function sort() {
- let list = values(this.downloads).sort(function (a, b) a.compare(b));
+ let list = iter(this.downloads.values()).sort((a, b) => a.compare(b));
for (let [i, download] in iter(list))
if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
this.nodes.list.childNodes[i + 1]);
},
- shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
+ shouldSort: function shouldSort() Array.some(arguments, val => this.sortOrder.some(v => v.substr(1) == val)),
update: function update() {
for (let node in values(this.nodes))
timeRemaining: Infinity,
updateProgress: function updateProgress() {
- let downloads = values(this.downloads).toArray();
- let active = downloads.filter(function (d) d.alive);
+ let downloads = iter(this.downloads.values()).toArray();
+ let active = downloads.filter(d => d.active);
let self = Object.create(this);
- for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
- this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
+ for (let prop in values(["currentBytes", "totalBytes", "speed", "timeRemaining"]))
+ this[prop] = active.reduce((acc, dl) => dl[prop] + acc, 0);
+
+ this.hasProgress = active.every(d => d.hasProgress);
+ this.progress = Math.round((this.currentBytes / this.totalBytes) * 100);
+ this.nActive = active.length;
Download.prototype.updateProgress.call(self);
- this.nActive = active.length;
if (active.length)
this.nodes.total.textContent = _("download.nActive", active.length);
else for (let key in values(["total", "percent", "speed", "time"]))
this.sort();
},
- observers: {
- "download-manager-remove-download": function (id) {
- if (id == null)
- id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
- else
- id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
+ onDownloadAdded: function onDownloadAdded(download) {
+ this.addDownload(download);
- Array.concat(id).map(this.closure.removeDownload);
- this.update();
- }
+ this.modules.mow.resize(false);
+ this.nodes.list.scrollIntoView(false);
},
- onDownloadStateChange: function (state, download) {
- try {
- if (download.id in this.downloads)
- this.downloads[download.id].updateStatus();
- else {
- this.addDownload(download.id);
+ onDownloadRemoved: function onDownloadRemoved(download) {
+ this.removeDownload(download);
+ },
+
+ onDownloadChanged: function onDownloadChanged(download) {
+ if (this.downloads.has(download)) {
+ download = this.downloads.get(download)
+
+ download.updateStatus();
+ download.updateProgress();
- this.modules.mow.resize(false);
- this.nodes.list.scrollIntoView(false);
- }
this.update();
if (this.shouldSort("active"))
this.sort();
}
- catch (e) {
- util.reportError(e);
- }
+ }
+});
+["canceled",
+ "contentType",
+ "currentBytes",
+ "error",
+ "hasPartialData",
+ "hasProgress",
+ "launchWhenSucceeded",
+ "launcherPath",
+ "progress",
+ "saver",
+ "source",
+ "speed",
+ "startTime",
+ "stopped",
+ "succeeded",
+ "target",
+ "totalBytes",
+ "tryToKeepPartialData"].forEach(key => {
+ if (!(key in Download.prototype))
+ Object.defineProperty(Download.prototype, key, {
+ get: function get() this.download[key],
+ set: function set(val) this.download[key] = val,
+ configurable: true
+ });
+});
+
+
+var Downloads_ = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
+ init: function () {
+ Downloads.getList(Downloads.ALL).then(list => {
+ this.list = list;
+ if (!this.dead)
+ this.list.addView(this);
+ });
},
- onProgressChange: function (webProgress, request,
- curProgress, maxProgress,
- curTotalProgress, maxTotalProgress,
- download) {
- try {
- if (download.id in this.downloads)
- this.downloads[download.id].updateProgress();
- this.updateProgress();
- }
- catch (e) {
- util.reportError(e);
+ cleanup: function destroy() {
+ if (this.list)
+ this.list.removeView(this);
+ this.dead = true;
+ },
+
+ onDownloadAdded: function onDownloadAdded(download) {
+ },
+
+ onDownloadRemoved: function onDownloadRemoved(download) {
+ },
+
+ onDownloadChanged: function onDownloadChanged(download) {
+ if (download.succeeded) {
+ let target = File(download.target.path);
+
+ let url = download.source.url;
+ let title = target.leafName;
+ let file = target.path;
+ let size = download.totalBytes;
+
+ overlay.modules.forEach(function (modules) {
+ modules.dactyl.echomsg({ domains: [util.getHost(url)], message: _("io.downloadFinished", title, file) },
+ 1, modules.commandline.ACTIVE_WINDOW);
+ modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
+ });
}
}
-});
-
-var Downloads = Module("downloads", {
}, {
}, {
commands: function initCommands(dactyl, modules, window) {
commands.add(["dlc[lear]"],
"Clear completed downloads",
- function (args) { services.downloadManager.cleanUp(); });
+ function (args) { downloads.list.removeFinished(); });
},
options: function initOptions(dactyl, modules, window) {
const { options } = modules;
},
completer: function (context, extra) {
- let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
+ let seen = RealSet(extra.values.map(val => val.substr(1)));
- context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
- .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
- ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
+ context.completions = iter(this.values).filter(([k, v]) => !seen.has(k))
+ .map(([k, v]) => [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
+ ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
.flatten().array;
},
- has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
+ has: function () Array.some(arguments, val => this.value.some(v => v.substr(1) == val)),
validator: function (value) {
- let seen = {};
- return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
- && !Set.add(seen, val.substr(1)),
- this) && value.length;
+ let seen = RealSet();
+ return value.every(val => /^[+-]/.test(val) && hasOwnProperty(this.values, val.substr(1))
+ && !seen.add(val.substr(1)))
+ && value.length;
}
});
}
// catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
-// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: