1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-20 15:28:00 +01:00

Add an experimental prototype download manager replacement. Fix some bugs.

--HG--
extra : rebase_source : aea703414d4bd601bfdea779c5878a30d5b3d782
This commit is contained in:
Kris Maglione
2011-01-16 22:38:44 -05:00
parent b52e78b92e
commit ed696fe5c4
16 changed files with 444 additions and 77 deletions

View File

@@ -133,7 +133,7 @@ var Browser = Module("browser", {
},
commands: function () {
commands.add(["downl[oads]", "dl"],
commands.add(["old-downl[oads]", "old-dl"],
"Show progress of current downloads",
function () {
dactyl.open("chrome://mozapps/content/downloads/downloads.xul",

View File

@@ -786,6 +786,10 @@ var Buffer = Module("buffer", {
var persist = services.Persist();
persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
| persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
persist.progressListener = new window.DownloadListener(window,
services.Transfer(uri, services.io.newFileURI(file), "",
null, null, null, persist));
persist.saveURI(uri, null, null, null, null, file);
}, {
autocomplete: true,

View File

@@ -265,10 +265,12 @@ var CommandWidgets = Class("CommandWidgets", {
while (elem.contentDocument.documentURI != elem.getAttribute("src") ||
["viewable", "complete"].indexOf(elem.contentDocument.readyState) < 0)
util.threadYield();
return elem;
res = res || (processor || util.identity).call(self, elem);
return res;
}
});
return Class.replaceProperty(this, name, (processor || util.identity).call(this, this[name]))
let res, self = this;
return Class.replaceProperty(this, name, this[name])
},
get completionList() this._whenReady("completionList", "dactyl-completions"),
@@ -748,8 +750,15 @@ var CommandLine = Module("commandline", {
XML.ignoreWhitespace = false;
XML.prettyPrinting = false;
let style = typeof str === "string" ? "pre" : "nowrap";
this._lastMowOutput = <div class="ex-command-output" style={"white-space: " + style} highlight={highlightGroup}>{str}</div>;
let output = util.xmlToDom(this._lastMowOutput, doc);
if (callable(str)) {
this._lastMowOutput = null;
var output = util.xmlToDom(<div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}/>, doc);
output.appendChild(str(doc));
}
else {
this._lastMowOutput = <div class="ex-command-output" style={"white-space: " + style} highlight={highlightGroup}>{str}</div>;
var output = util.xmlToDom(this._lastMowOutput, doc);
}
// FIXME: need to make sure an open MOW is closed when commands
// that don't generate output are executed
@@ -824,7 +833,7 @@ var CommandLine = Module("commandline", {
let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
let action = this._echoLine;
if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE))
if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || !isString(str)) && !(flags & this.FORCE_SINGLELINE))
action = this._echoMultiline;
if (single)
@@ -1073,63 +1082,69 @@ var CommandLine = Module("commandline", {
// FIXME: if 'more' is set and the MOW is not scrollable we should still
// allow a down motion after an up rather than closing
onMultilineOutputEvent: function onMultilineOutputEvent(event) {
const KILL = false, PASS = true;
try {
const KILL = false, PASS = true;
let win = this.widgets.multilineOutput.contentWindow;
let elem = win.document.documentElement;
let win = this.widgets.multilineOutput.contentWindow;
let elem = win.document.documentElement;
let key = events.toString(event);
let key = events.toString(event);
function openLink(where) {
event.preventDefault();
dactyl.open(event.target.href, where);
}
// TODO: Wouldn't multiple handlers be cleaner? --djk
if (event.type == "click" && event.target instanceof HTMLAnchorElement) {
let command = event.originalTarget.getAttributeNS(NS.uri, "command");
if (command && dactyl.commands[command]) {
const openLink = function openLink(where) {
event.preventDefault();
return dactyl.withSavedValues(["forceNewTab"], function () {
dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
return dactyl.commands[command](event);
});
dactyl.open(event.target.href, where);
}
switch (key) {
case "<LeftMouse>":
event.preventDefault();
openLink(dactyl.CURRENT_TAB);
return KILL;
case "<MiddleMouse>":
case "<C-LeftMouse>":
case "<C-M-LeftMouse>":
openLink({ where: dactyl.NEW_TAB, background: true });
return KILL;
case "<S-MiddleMouse>":
case "<C-S-LeftMouse>":
case "<C-M-S-LeftMouse>":
openLink({ where: dactyl.NEW_TAB, background: false });
return KILL;
case "<S-LeftMouse>":
openLink(dactyl.NEW_WINDOW);
return KILL;
// TODO: Wouldn't multiple handlers be cleaner? --djk
if (event.type == "click" && (event.target instanceof HTMLAnchorElement ||
event.originalTarget.hasAttributeNS(NS, "command"))) {
let command = event.originalTarget.getAttributeNS(NS, "command");
if (command && dactyl.commands[command]) {
event.preventDefault();
return dactyl.withSavedValues(["forceNewTab"], function () {
dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
return dactyl.commands[command](event);
});
}
switch (key) {
case "<LeftMouse>":
event.preventDefault();
openLink(dactyl.CURRENT_TAB);
return KILL;
case "<MiddleMouse>":
case "<C-LeftMouse>":
case "<C-M-LeftMouse>":
openLink({ where: dactyl.NEW_TAB, background: true });
return KILL;
case "<S-MiddleMouse>":
case "<C-S-LeftMouse>":
case "<C-M-S-LeftMouse>":
openLink({ where: dactyl.NEW_TAB, background: false });
return KILL;
case "<S-LeftMouse>":
openLink(dactyl.NEW_WINDOW);
return KILL;
}
return PASS;
}
return PASS;
if (event instanceof MouseEvent)
return KILL;
const atEnd = function atEnd(dir) !Buffer.isScrollable(elem, dir || 1);
if (!options["more"] || atEnd(1)) {
modes.pop();
events.feedkeys(key);
}
else
commandline.updateMorePrompt(false, true);
}
if (event instanceof MouseEvent)
return KILL;
function atEnd(dir) !Buffer.isScrollable(elem, dir || 1);
if (!options["more"] || atEnd(1)) {
modes.pop();
events.feedkeys(key);
catch (e) {
util.reportError(e);
}
else
commandline.updateMorePrompt(false, true);
return PASS;
},

View File

@@ -53,16 +53,18 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
},
observers: {
"dactyl-cleanup": function () {
"dactyl-cleanup": function dactyl_cleanup() {
let modules = dactyl.modules;
for (let name in values(Object.getOwnPropertyNames(modules).reverse())) {
let mod = Object.getOwnPropertyDescriptor(modules, name).value;
if (mod instanceof Class) {
if ("cleanup" in mod)
mod.cleanup();
this.trapErrors(mod.cleanup, mod);
if ("destroy" in mod)
mod.destroy();
this.trapErrors(mod.destroy, mod);
if ("INIT" in mod && "cleanup" in mod.INIT)
this.trapErrors(mod.cleanup, mod, dactyl, modules, window);
}
}
@@ -360,6 +362,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
},
userEval: function (str, context, fileName, lineNumber) {
if (jsmodules.__proto__ != window)
str = "with (window) { with (modules) { this.eval(" + str.quote() + ") } }";
if (fileName == null)
if (io.sourcing && io.sourcing.file[0] !== "[")
({ file: fileName, line: lineNumber }) = io.sourcing;
@@ -389,9 +394,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
if (!context)
context = _userContext;
if (window.isPrototypeOf(modules))
return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
return Cu.evalInSandbox("with (window) { with (modules) { this.eval(" + str.quote() + ") } }", context, "1.8", fileName, lineNumber);
return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
},
/**
@@ -2178,7 +2181,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
let init = services.environment.get(config.idName + "_INIT");
let rcFile = io.getRCFile("~");
if (dactyl.userEval('typeof document') === "undefined")
if (dactyl.userEval("typeof document", null, "test.js") === "undefined")
jsmodules.__proto__ = XPCSafeJSObjectWrapper(window);
try {

View File

@@ -175,6 +175,9 @@ var Modes = Module("modes", {
}
});
},
cleanup: function () {
modes.reset();
},
_getModeMessage: function () {
// when recording a macro

View File

@@ -70,9 +70,8 @@
<spec>:downl<oa>oads</oa></spec>
<description>
<p>
Show progress of current downloads. Open the standard &dactyl.host;
download dialog in a new tab. Here, downloads can be paused,
resumed, and canceled.
Show progress of current downloads. Here, downloads can
be paused, resumed, and canceled.
</p>
</description>
</item>

View File

@@ -737,9 +737,9 @@ Class.memoize = function memoize(getter)
configurable: true,
enumerable: true,
init: function (key) {
this.get = function replace() (
Class.replaceProperty(this, key, null),
Class.replaceProperty(this, key, getter.call(this, key)))
this.get = function replace() let (obj = this.instance || this) (
Class.replaceProperty(obj, key, null),
Class.replaceProperty(obj, key, getter.call(this, key)))
}
});
@@ -1196,6 +1196,9 @@ update(iter, {
return undefined;
},
sort: function sort(iter, fn, self)
array(this.toArray(iter).sort(fn, self)),
uniq: function uniq(iter) {
let seen = {};
for (let item in iter)

View File

@@ -426,9 +426,16 @@ var ConfigBase = Class("ConfigBase", {
Keyword color: red;
Tag color: blue;
Usage position: relative; padding-right: 2em;
Usage>LineInfo position: absolute; left: 100%; padding: 1ex; margin: -1ex -1em; background: rgba(255, 255, 255, .8); border-radius: 1ex;
Usage:not(:hover)>LineInfo opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden;
Link position: relative; padding-right: 2em;
Link:not(:hover)>LinkInfo opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden;
LinkInfo {
position: absolute;
left: 100%;
padding: 1ex;
margin: -1ex -1em;
background: rgba(255, 255, 255, .8);
border-radius: 1ex;
}
StatusLine;;;FontFixed {
-moz-appearance: none !important;
@@ -490,6 +497,30 @@ var ConfigBase = Class("ConfigBase", {
HintActive;;* background-color: #88FF00 !important; color: black !important;
HintImage;;* opacity: .5 !important;
Button display: inline-block; font-weight: bold; cursor: pointer;
Button:hover text-decoration: underline;
Button[collapsed] visibility: collapse; width: 0;
Button::before content: "["; color: gray; text-decoration: none !important;
Button::after content: "]"; color: gray; text-decoration: none !important;
Button:not([collapsed]) ~ Button:not([collapsed])::before content: "/[";
Downloads display: table; margin: 0; padding: 0;
DownloadHead;;;CompTitle display: table-row;
DownloadHead>*;;;DownloadCell display: table-cell;
Download display: table-row;
DownloadCell display: table-cell; padding: 0 1ex;
DownloadButtons;;;DownloadCell
DownloadPercent;;;DownloadCell
DownloadProgress;;;DownloadCell
DownloadProgressHave
DownloadProgressTotal
DownloadSource;;;DownloadCell,URL
DownloadState;;;DownloadCell
DownloadTime;;;DownloadCell
DownloadTitle;;;DownloadCell,URL
// </css>
]]></>, /&#x0d;/g, "\n")),

View File

@@ -0,0 +1,268 @@
// Copyright (c) 2011 by 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";
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("downloads", {
exports: ["Download", "Downloads", "downloads"],
use: ["io", "services", "template", "util"]
}, this);
Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
let prefix = "DOWNLOAD_";
var states = iter([v, k.slice(prefix.length).toLowerCase()]
for ([k, v] in Iterator(Ci.nsIDownloadManager))
if (k.indexOf(prefix) == 0))
.toObject();
var Download = Class("Download", {
init: function init(id, document) {
let self = XPCSafeJSObjectWrapper(services.downloadManager.getDownload(id));
self.__proto__ = this;
this.instance = this;
this.nodes = {};
util.xmlToDom(
<li highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
<span highlight="DownloadTitle">
<span highlight="Link">
<a key="title" href={self.target.spec}>{self.displayName}</a>
<span highlight="LinkInfo">{self.targetFile.path}</span>
</span>
</span>
<span highlight="DownloadState" key="state"/>
<span highlight="DownloadButtons">
<span highlight="Button" key="pause">Pause</span>
<span highlight="Button" key="remove">Remove</span>
<span highlight="Button" key="resume">Resume</span>
<span highlight="Button" key="retry">Retry</span>
<span highlight="Button" key="cancel">Cancel</span>
<span highlight="Button" key="delete">Delete</span>
</span>
<span highlight="DownloadProgress" key="progress">
<span highlight="DownloadProgressHave" key="progressHave"
/>/<span highlight="DownloadProgressTotal" key="progressTotal"/>
</span>
<span highlight="DownloadPercent" key="percent"/>
<span highlight="DownloadTime" key="time"/>
<a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a>
</li>,
document, this.nodes);
for (let [key, node] in Iterator(this.nodes)) {
node.dactylDownload = self;
if (node.getAttributeNS(NS, "highlight") == "Button") {
node.setAttributeNS(NS, "command", "download.command");
update(node, {
set collapsed(collapsed) {
if (collapsed)
this.setAttribute("collapsed", "true");
else
this.removeAttribute("collapsed");
},
get collapsed() !!this.getAttribute("collapsed")
});
}
}
self.updateStatus();
return self;
},
get status() states[this.state],
inState: function inState(states) states.indexOf(this.status) >= 0,
get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
allowed: Class.memoize(function () let (self = this) ({
get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
get delete() !this.cancel && self.targetFile.exists(),
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"])
})),
command: function command(name) {
util.assert(set.has(this.allowed, name), "Unknown command");
util.assert(this.allowed[name], "Command not allowed");
if (set.has(this.commands, name))
this.commands[name].call(this);
else
services.downloadManager[name + "Download"](this.id);
},
commands: {
delete: function delete() {
this.targetFile.remove(false);
this.updateStatus();
}
},
compare: function compare(other) String.localeCompare(this.displayName, other.displayName),
timeRemaining: Infinity,
updateProgress: function updateProgress() {
let self = this.__proto__;
if (this.amountTransferred === this.size)
this.nodes.time.textContent = "";
else if (this.speed == 0 || this.size == 0)
this.nodes.time.textContent = "Unknown";
else {
let seconds = (this.size - this.amountTransferred) / this.speed;
[, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
if (this.timeRemaining)
this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
else
this.nodes.time.textContent = "~1 second";
}
let total = this.nodes.progressTotal.textContent = this.size ? util.formatBytes(this.size, 1, true) : "Unknown";
let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
},
updateStatus: function updateStatus() {
this.nodes.state.textContent = util.capitalize(this.status);
for (let [command, enabled] in Iterator(this.allowed))
this.nodes[command].collapsed = !enabled;
this.updateProgress();
}
});
var DownloadList = Class("DownloadList",
XPCOM([Ci.nsIDownloadProgressListener,
Ci.nsIObserver,
Ci.nsISupportsWeakReference]), {
init: function init(document, modules) {
this.modules = modules;
this.document = document;
this.nodes = {};
util.xmlToDom(<ul highlight="Downloads" key="list" xmlns={XHTML}>
<li highlight="DownloadHead">
<span>Title</span>
<span>Status</span>
<span></span>
<span>Progress</span>
<span></span>
<span>Time remaining</span>
<span>Source</span>
</li>
</ul>, this.document, this.nodes);
this.downloads = {};
for (let row in iter(services.downloadManager.DBConnection
.createStatement("SELECT id FROM moz_downloads")))
this.addDownload(row.id);
util.addObserver(this);
services.downloadManager.addListener(this);
},
cleanup: function cleanup() {
this.observe.unregister();
services.downloadManager.removeListener(this);
},
addDownload: function addDownload(id) {
if (!(id in this.downloads)) {
this.downloads[id] = Download(id, this.document);
let index = values(this.downloads).sort(function (a, b) a.compare(b))
.indexOf(this.downloads[id]);
this.nodes.list.insertBefore(this.downloads[id].nodes.row,
this.nodes.list.childNodes[index + 1]);
}
},
removeDownload: function removeDownload(id) {
if (id in this.downloads) {
this.nodes.list.removeChild(this.downloads[id].nodes.row);
delete this.downloads[id];
}
},
leave: function leave(stack) {
if (stack.pop)
this.cleanup();
},
observers: {
"download-manager-remove-download": function (id) {
if (id == null)
id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowed.remove)];
else
id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
Array.concat(id).map(this.closure.removeDownload);
}
},
onDownloadStateChange: function (state, download) {
try {
if (download.id in this.downloads)
this.downloads[download.id].updateStatus();
else {
this.addDownload(download.id);
this.modules.commandline.updateOutputHeight(true);
this.nodes.list.scrollIntoView(false);
}
}
catch (e) {
util.reportError(e);
}
},
onProgressChange: function (webProgress, request,
curProgress, maxProgress,
curTotalProgress, maxTotalProgress,
download) {
try {
if (download.id in this.downloads)
this.downloads[download.id].updateProgress();
}
catch (e) {
util.reportError(e);
}
}
});
var Downloads = Module("downloads", {
}, {
}, {
commands: function (dactyl, modules, window) {
const { commands } = modules;
commands.add(["downl[oads]", "dl"],
"Display the downloads list",
function (args) {
modules.commandline.echo(function (doc) {
let downloads = DownloadList(doc, modules);
// Temporary and dangerous hack:
modules.modes.getStack(0).params = downloads;
return downloads.nodes.list;
});
});
},
dactyl: function (dactyl, modules, window) {
dactyl.commands["download.command"] = function (event) {
let elem = event.originalTarget;
elem.dactylDownload.command(elem.getAttribute("key"));
}
}
});
endModule();
// 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:

View File

@@ -92,7 +92,8 @@ var Highlights = Module("Highlight", {
keys: function keys() Object.keys(this.highlight).sort(),
__iterator__: function () values(this.highlight),
__iterator__: function () values(this.highlight).sort(function (a, b) String.localeCompare(a.class, b.class))
.iterValues(),
_create: function (agent, args) {
let obj = Highlight.apply(Highlight, args);
@@ -318,7 +319,7 @@ var Highlights = Module("Highlight", {
if (!modify)
modules.commandline.commandOutput(
template.tabular(["Key", "Sample", "Link", "CSS"],
["padding: 0 1em 0 0; vertical-align: top",
["padding: 0 1em 0 0; vertical-align: top; max-width: 16em; overflow: hidden;",
"text-align: center"],
([h.class,
<span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>,

View File

@@ -149,6 +149,7 @@ var Overlay = Module("Overlay", {
["base",
"completion",
"config",
"downloads",
"javascript",
"overlay",
"prefs",

View File

@@ -79,6 +79,7 @@ var Services = Module("Services", {
[Ci.nsIChannel, Ci.nsIInputStreamChannel, Ci.nsIRequest], "setURI");
this.addClass("String", "@mozilla.org/supports-string;1", Ci.nsISupportsString, "data");
this.addClass("StringStream", "@mozilla.org/io/string-input-stream;1", Ci.nsIStringInputStream, "data");
this.addClass("Transfer", "@mozilla.org/transfer;1", Ci.nsITransfer, "init");
this.addClass("Timer", "@mozilla.org/timer;1", Ci.nsITimer, "initWithCallback");
this.addClass("StreamCopier", "@mozilla.org/network/async-stream-copier;1",Ci.nsIAsyncStreamCopier, "init");
this.addClass("Xmlhttp", "@mozilla.org/xmlextras/xmlhttprequest;1", Ci.nsIXMLHttpRequest);

View File

@@ -259,9 +259,15 @@ var Storage = Module("Storage", {
}
}, {
}, {
init: function (dactyl, modules) {
init: function init(dactyl, modules) {
init.superapply(this, arguments);
storage.infoPath = File(modules.IO.runtimePath.replace(/,.*/, ""))
.child("info").child(dactyl.profileName);
},
cleanup: function (dactyl, modules, window) {
delete window.dactylStorageRefs;
this.removeDeadObservers();
}
});

View File

@@ -331,11 +331,11 @@ var Template = Module("Template", {
this.map(iter, function (item)
<tr>
<td style="padding-right: 2em;">
<span highlight="Usage">{
<span highlight="Usage Link">{
let (name = item.name || item.names[0], frame = item.definedAt)
!frame ? name :
template.helpLink(help(item), name, "Title") +
<span highlight="LineInfo" xmlns:dactyl={NS}>Defined at {sourceLink(frame)}</span>
<span highlight="LinkInfo" xmlns:dactyl={NS}>Defined at {sourceLink(frame)}</span>
}</span>
</td>
<td>{desc(item)}</td>

View File

@@ -135,6 +135,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
throw FailedAssertion(message, 1);
},
/**
* Capitalizes the first character of the given string.
* @param {string} str The string to capitalize
* @returns {string}
*/
capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1),
/**
* Returns a RegExp object that matches characters specified in the range
* expression *list*, or signals an appropriate error if *list* is invalid.
@@ -499,9 +506,10 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
stackLines: function (stack) {
let lines = [];
let match, re = /([^]*?)(@[^@\n]*)(?:\n|$)/g;
let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
while (match = re.exec(stack))
lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + match[2]);
lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
match[2].replace(/.* -> /, ""));
return lines;
},
@@ -653,6 +661,28 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
return strNum[0] + " " + unitVal[unitIndex];
},
/**
* Converts *seconds* into a human readable time string.
*
* @param {number} seconds
* @returns {string}
*/
formatSeconds: function formatSeconds(seconds) {
function div(num, denom) [Math.round(num / denom), Math.round(num % denom)];
let days, hours, minutes;
[minutes, seconds] = div(seconds, 60);
[hours, minutes] = div(minutes, 60);
[days, hours] = div(hours, 24);
if (days)
return days + " days " + hours + " hours"
if (hours)
return hours + "h " + minutes + "m";
if (minutes)
return minutes + ":" + seconds;
return seconds + "s";
},
/**
* Returns the file which backs a given URL, if available.
*

View File

@@ -64,6 +64,8 @@
consistent interactive help facility (improvements include
listing keys for modes other than Normal, filtering the output
and linking to source locations).
- :downloads now opens a download list in the multi-line output
buffer.
- Added :cookies command.
* :extadd now supports remote URLs as well as local files on Firefox 4.
* Added :if/:elseif/:else/:endif conditionals.