]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/downloads.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / downloads.jsm
index cbcd9a6e4fcaf4ff3ffccc4ae1bb3e35213c4850..3f4a3501384d6c10baf8b0ab0ccd8cc0892070db 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (c) 2011-2013 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.
@@ -10,20 +10,22 @@ defineModule("downloads", {
 });
 
 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) {
-        this.download = services.downloadManager.getDownload(id);
+    init: function init(download, list) {
+        this.download = download;
         this.list = list;
 
         this.nodes = {
@@ -39,11 +41,9 @@ var Download = Class("Download", {
                             this.targetFile.path]]],
                 ["td", { highlight: "DownloadState", key: "state" }],
                 ["td", { highlight: "DownloadButtons Buttons" },
-                    ["a", { highlight: "Button", href: "javascript:0", key: "pause" }, _("download.action.Pause")],
+                    ["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: "retry" }, _("download.action.Retry")],
-                    ["a", { highlight: "Button", href: "javascript:0", key: "cancel" }, _("download.action.Cancel")],
                     ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("download.action.Delete")]],
                 ["td", { highlight: "DownloadProgress", key: "progress" },
                     ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
@@ -53,8 +53,8 @@ var Download = Class("Download", {
                 ["td", { highlight: "DownloadSpeed", key: "speed" }],
                 ["td", { highlight: "DownloadTime", key: "time" }],
                 ["td", {},
-                    ["a", { highlight: "DownloadSource", key: "source", href: this.source.spec },
-                        this.source.spec]]],
+                    ["a", { highlight: "DownloadSource", key: "source", href: this.source.url },
+                        this.source.url]]],
             this.list.document, this.nodes);
 
         this.nodes.launch.addEventListener("click", (event) => {
@@ -68,38 +68,40 @@ var Download = Class("Download", {
         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() {
             // Behavior mimics that of the builtin Download Manager.
             function action() {
@@ -127,42 +129,52 @@ var Download = Class("Download", {
                     });
             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: (a, b) => a.alive - b.alive,
+        active:   (a, b) => a.active - b.active,
         complete: (a, b) => a.percentComplete - b.percentComplete,
-        date: (a, b) => a.startTime - b.startTime,
+        date:     (a, b) => a.startTime - b.startTime,
         filename: (a, b) => String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
-        size: (a, b) => a.size - b.size,
-        speed: (a, b) => a.speed - b.speed,
-        time: (a, b) => a.timeRemaining - b.timeRemaining,
-        url: (a, b) => String.localeCompare(a.source.spec, b.source.spec)
+        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);
@@ -171,17 +183,20 @@ var Download = Class("Download", {
             }
         }
 
-        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);
@@ -193,14 +208,6 @@ var Download = Class("Download", {
         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,
@@ -213,12 +220,13 @@ var DownloadList = Class("DownloadList",
         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 () {
@@ -258,40 +266,44 @@ var DownloadList = Class("DownloadList",
         this.index = Array.indexOf(this.nodes.list.childNodes,
                                    this.nodes.head);
 
-        let start = Date.now();
-        for (let row in iter(services.downloadManager.DBConnection
-                                     .createStatement("SELECT id FROM moz_downloads"))) {
-            if (Date.now() - start > MAX_LOAD_TIME) {
-                util.dactyl.warn(_("download.givingUpAfter", (Date.now() - start) / 1000));
-                break;
+        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.addDownload(row.id);
-        }
-        this.update();
+            this.update();
 
-        util.addObserver(this);
-        services.downloadManager.addListener(this);
+            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((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 + 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);
         }
     },
 
@@ -301,17 +313,17 @@ var DownloadList = Class("DownloadList",
     },
 
     allowedCommands: Class.Memoize(function () let (self = this) ({
-        get clear() values(self.downloads).some(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((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)
@@ -335,16 +347,19 @@ var DownloadList = Class("DownloadList",
     timeRemaining: Infinity,
 
     updateProgress: function updateProgress() {
-        let downloads = values(this.downloads).toArray();
-        let active    = downloads.filter(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"]))
+        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"]))
@@ -354,68 +369,87 @@ var DownloadList = Class("DownloadList",
             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);
-        }
-    },
-
-    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);
-        }
     }
 });
+["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), {
+var Downloads_ = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
     init: function () {
-        services.downloadManager.addListener(this);
+        Downloads.getList(Downloads.ALL).then(list => {
+            this.list = list;
+            if (!this.dead)
+                this.list.addView(this);
+        });
     },
 
     cleanup: function destroy() {
-        services.downloadManager.removeListener(this);
+        if (this.list)
+            this.list.removeView(this);
+        this.dead = true;
+    },
+
+    onDownloadAdded: function onDownloadAdded(download) {
     },
 
-    onDownloadStateChange: function (state, download) {
-        if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
-            let url   = download.source.spec;
-            let title = download.displayName;
-            let file  = download.targetFile.path;
-            let size  = download.size;
+    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) },
@@ -451,7 +485,7 @@ var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
 
         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;
@@ -489,9 +523,9 @@ var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
                 },
 
                 completer: function (context, extra) {
-                    let seen = Set.has(Set(extra.values.map(val => val.substr(1))));
+                    let seen = RealSet(extra.values.map(val => val.substr(1)));
 
-                    context.completions = iter(this.values).filter(([k, v]) => !seen(k))
+                    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;
@@ -500,9 +534,9 @@ var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
                 has: function () Array.some(arguments, val => this.value.some(v => v.substr(1) == val)),
 
                 validator: function (value) {
-                    let seen = {};
-                    return value.every(val => /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
-                                                                && !Set.add(seen, val.substr(1)))
+                    let seen = RealSet();
+                    return value.every(val => /^[+-]/.test(val) && hasOwnProperty(this.values, val.substr(1))
+                                                                && !seen.add(val.substr(1)))
                         && value.length;
                 }
             });