📜 ⬆️ ⬇️

Firefox Download Panel Improvements

It will focus on the features of the new download panel in Firefox and the Download Panel Tweaker extension, eliminating some of the unwanted features.
In particular, about the most controversial, in my opinion, innovation, due to which completed downloads disappear from the list (although they remain visible in the corresponding section of the “library”) - it just so happened that the improvement of time took most of this correction .
The result looks like this (this is a “compact” version of the settings , “very compact” will save a little more space):
Screenshot version 0.2.0
But how it was originally .
There will also be quite a few examples of code (and where else without details?).

Instead of the preface, or long live the compactness!

For starters, all the items on the default download panel are huge! Firstly, I do not have a touchscreen monitor - thanks, but I can get to the right place anyway. And secondly, there are only three points, not more. That is, the place is spent, but the benefits of something a little.
In general, if the limitation of the apparent number of downloads could be configured with built-in tools, the extensions could not be, because the sizes can be easily configured using userChrome.css or the Stylish extension.
In addition, styles alone cannot (?) Output download speeds - it is in the tooltip, but the pseudo-classes :: before and :: after do not always work in XUL (apparently, these are limitations of anonymous nodes ), so this does not help:
.downloadDetails[tooltiptext]::after { content: attr(tooltiptext) !important; } 


Increasing the number of visible downloads

As a result, the code responsible for the number of downloads in the panel was found quite quickly in the chrome file : //browser/content/downloads/downloads.js :
 const DownloadsView = { ////////////////////////////////////////////////////////////////////////////// //// Functions handling download items in the list /** * Maximum number of items shown by the list at any given time. */ kItemCountLimit: 3, 

But the changes will work only if they were made when the browser was started, that is, before the download panel was initialized.
Therefore, further searches resulted in the resource file : //app/modules/DownloadsCommon.jsm and in the DownloadCommon.getSummary () function:
  /** * Returns a reference to the DownloadsSummaryData singleton - creating one * in the process if one hasn't been instantiated yet. * * @param aWindow * The browser window which owns the download button. * @param aNumToExclude * The number of items on the top of the downloads list to exclude * from the summary. */ getSummary: function DC_getSummary(aWindow, aNumToExclude) { if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { if (this._privateSummary) { return this._privateSummary; } return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); } else { if (this._summary) { return this._summary; } return this._summary = new DownloadsSummaryData(false, aNumToExclude); } }, 

And what is actually happening in the DownloadsSummaryData constructor:
 /** * DownloadsSummaryData is a view for DownloadsData that produces a summary * of all downloads after a certain exclusion point aNumToExclude. For example, * if there were 5 downloads in progress, and a DownloadsSummaryData was * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData * would produce a summary of the last 2 downloads. * * @param aIsPrivate * True if the browser window which owns the download button is a private * window. * @param aNumToExclude * The number of items to exclude from the summary, starting from the * top of the list. */ function DownloadsSummaryData(aIsPrivate, aNumToExclude) { this._numToExclude = aNumToExclude; 

Everything is simple - just do it like this:
 var itemCountLimit = 5; //        3  5 if(DownloadsCommon._privateSummary) DownloadsCommon._privateSummary._numToExclude = itemCountLimit; if(DownloadsCommon._summary) DownloadsCommon._summary._numToExclude = itemCountLimit; 

That is, the limit is adjusted for already created regular and private copies of DownloadsSummaryData .
The most difficult part is that the list of downloads in itself, in accordance with the new settings, is not overlooked.
But here I had a head start: in the process of developing the Private Tab extension (by which, by the way, there was also an article ), the exact same question arose, because it was necessary to change the list of downloads from normal to private when switching tabs (and there was not a lot of experiments of varying degrees of success).
But without a trick, as usual, it still wasn’t enough: Firefox 28 removed the function for cleaning the downloads panel, quite (before it was to switch between the old and the new download engine). So I had to write an analogue - good, it's simple.
The result can be viewed at the link already provided above or in the extension code .
And in the expansion there is a feature: if you just do
 DownloadsView._viewItems = {}; DownloadsView._dataItems = []; 

, there will be a memory leak, because the objects will be created in the scope of the extension, so that the commonly used constructor functions came in handy:
 DownloadsView._viewItems = new window.Object(); DownloadsView._dataItems = new window.Array(); 

In this case, the window is the window in which DownloadsView is located (that is, DownloadsView === window.DownloadsView ).

Save downloads on exit

This turned out to be the most difficult, but not so much due to the fact that the internal implementation of downloads has changed in Firefox 26, but because of many related problems. Many of these difficulties are reflected in the corresponding issue , but better everything in order.
')

Older versions and cleanup downloads

In Firefox 25 and older versions, downloads were forcibly cleared ( resource: //app/components/DownloadsStartup.js ):
  case "browser-lastwindow-close-granted": // When using the panel interface, downloads that are already completed // should be removed when the last full browser window is closed. This // event is invoked only if the application is not shutting down yet. // If the Download Manager service is not initialized, we don't want to // initialize it just to clean up completed downloads, because they can // be present only in case there was a browser crash or restart. if (this._downloadsServiceInitialized && !DownloadsCommon.useToolkitUI) { Services.downloads.cleanUp(); } break; ... case "quit-application": ... // When using the panel interface, downloads that are already completed // should be removed when quitting the application. if (!DownloadsCommon.useToolkitUI && aData != "restart") { this._cleanupOnShutdown = true; } break; case "profile-change-teardown": // If we need to clean up, we must do it synchronously after all the // "quit-application" listeners are invoked, so that the Download // Manager service has a chance to pause or cancel in-progress downloads // before we remove completed downloads from the list. Note that, since // "quit-application" was invoked, we've already exited Private Browsing // Mode, thus we are always working on the disk database. if (this._cleanupOnShutdown) { Services.downloads.cleanUp(); } 

As a result, when the browser was closed, the Services.downloads.cleanUp () function was called.
(By the way, there are already traces of a new engine in the same place - check for the value of browser.download.useJSTransfer setting.)
I had to redefine Services. Downloads entirely, because this
 Components.classes["@mozilla.org/download-manager;1"] .getService(Components.interfaces.nsIDownloadManager); 

(see resource: //gre/modules/Services.jsm ), and the properties of services cannot be changed:
 Object.defineProperty(Services.downloads, "cleanUp", { value: function() {}, enumerable: true, configurable: true, writable: true }); // Exception: can't redefine non-configurable property 'cleanUp' 

If simplistic, the result is the following:
 var downloads = Services.downloads; var downloadsWrapper = { __proto__: downloads, cleanUp: function() { ... } }; this.setProperty(Services, "downloads", downloadsWrapper); 

That is, our fake object contains its own implementation of the function-property cleanUp and inherits everything else from the present Services.downloads .
Well, from the fake Services.downloads.cleanUp () function, you can check the call stack, and if this is the very DownloadStartup.js , then do nothing. This is not very reliable, but it is easily restored when the extension is disabled. You can even complicate the task and add checks in case any other extension makes a similar wrapper.

New versions, selective saving of downloads and many hacks

Then, in Firefox 26, the new download engine was enabled by default and temporary downloads were transferred (namely, they are displayed in the download panel) to the downloads.json file in the profile. In addition, instead of cleaning, filtering was done while saving:
resource: //gre/modules/DownloadStore.jsm
 this.DownloadStore.prototype = { ... /** * This function is called with a Download object as its first argument, and * should return true if the item should be saved. */ onsaveitem: () => true, ... /** * Saves persistent downloads from the list to the file. * * If an error occurs, the previous file is not deleted. * * @return {Promise} * @resolves When the operation finished successfully. * @rejects JavaScript exception. */ save: function DS_save() { return Task.spawn(function task_DS_save() { let downloads = yield this.list.getAll(); ... for (let download of downloads) { try { if (!this.onsaveitem(download)) { continue; } 

Then in resource: //gre/modules/DownloadIntegration.jsm, the onsaveitem method is overridden:
 this.DownloadIntegration = { ... initializePublicDownloadList: function(aList) { return Task.spawn(function task_DI_initializePublicDownloadList() { ... this._store.onsaveitem = this.shouldPersistDownload.bind(this); ... /** * Determines if a Download object from the list of persistent downloads * should be saved into a file, so that it can be restored across sessions. * * This function allows filtering out downloads that the host application is * not interested in persisting across sessions, for example downloads that * finished successfully. * * @param aDownload * The Download object to be inspected. This is originally taken from * the global DownloadList object for downloads that were not started * from a private browsing window. The item may have been removed * from the list since the save operation started, though in this case * the save operation will be repeated later. * * @return True to save the download, false otherwise. */ shouldPersistDownload: function (aDownload) { // In the default implementation, we save all the downloads currently in // progress, as well as stopped downloads for which we retained partially // downloaded data. Stopped downloads for which we don't need to track the // presence of a ".part" file are only retained in the browser history. // On b2g, we keep a few days of history. //@line 319 "c:\builds\moz2_slave\m-cen-w32-ntly-000000000000000\build\toolkit\components\jsdownloads\src\DownloadIntegration.jsm" return aDownload.hasPartialData || !aDownload.stopped; //@line 321 "c:\builds\moz2_slave\m-cen-w32-ntly-000000000000000\build\toolkit\components\jsdownloads\src\DownloadIntegration.jsm" }, 

Thus, there is a DownloadStore.prototype.onsaveitem () function that always allows saving and is overwritten for each specific implementation of new DownloadStore () .
(Looking ahead, I’ll add that, unfortunately, not all comments are equally useful and true.)
And in the source code DownloadIntegration.jsm there is an interesting conditional comment:
  shouldPersistDownload: function (aDownload) { // In the default implementation, we save all the downloads currently in // progress, as well as stopped downloads for which we retained partially // downloaded data. Stopped downloads for which we don't need to track the // presence of a ".part" file are only retained in the browser history. // On b2g, we keep a few days of history. #ifdef MOZ_B2G let maxTime = Date.now() - Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000; return (aDownload.startTime > maxTime) || aDownload.hasPartialData || !aDownload.stopped; #else return aDownload.hasPartialData || !aDownload.stopped; #endif }, 

However, if you replace the DownloadIntegration.shouldPersistDownload () function (and do not forget about DownloadIntegration._store.onsaveitem () in case the downloads have already been initialized), by analogy with the code from this conditional comment, a whole bunch of unpleasant surprises will pop up - like, and documented, it only works correctly in its original form, when completed downloads are not saved.

Firstly , after restarting, all completed downloads will be shown with a zero size and browser start time (although everything is correctly stored in downloads.json ).
Invalid date caused by code from resource: //app/modules/DownloadsCommon.jsm :
 /** * Represents a single item in the list of downloads. * * The endTime property is initialized to the current date and time. * * @param aDownload * The Download object with the current state. */ function DownloadsDataItem(aDownload) { this._download = aDownload; ... this.endTime = Date.now(); this.updateFromDownload(); } 

Then this time does not change when you call DownloadsDataItem.prototype.updateFromJSDownload () (Firefox 26-27) and DownloadsDataItem.prototype.updateFromDownload () (Firefox 28+).
Fortunately, you can make a wrapper around this function and make the necessary edits for each call .

Secondly , the code from the conditional comment about MOZ_B2G does not actually work: completed downloads will never be deleted. And other suitable conditional comments about MOZ_B2G could not be found (apparently, it doesn’t work there either), although I didn’t really try - it’s easy to fix everything there .
Then from the same place and thirdly : a large list of completed downloads cannot be loaded - a stack overflow occurs (“ too much recursion ” error ). And the problem can be obtained already on the list of 35 downloads.
Apparently, the used implementation of promises ( promises ) does not know how to work correctly with actually synchronous calls.
For example, if in DownloadStore.prototype.load () ( resource: //gre/modules/DownloadStore.jsm ) a little tweak and replace in
  /** * Loads persistent downloads from the file to the list. * * @return {Promise} * @resolves When the operation finished successfully. * @rejects JavaScript exception. */ load: function DS_load() { return Task.spawn(function task_DS_load() { let bytes; try { bytes = yield OS.File.read(this.path); } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { // If the file does not exist, there are no downloads to load. return; } let storeData = JSON.parse(gTextDecoder.decode(bytes)); // Create live downloads based on the static snapshot. for (let downloadData of storeData.list) { try { let download = yield Downloads.createDownload(downloadData); 

last line on
  let {Download} = Components.utils.import("resource://gre/modules/DownloadCore.jsm", {}); let download = Download.fromSerializable(downloadData); 

, the stack overflow will not go anywhere, but it will happen with a slightly larger number of saved downloads.
Fourth , somewhere else there is optimization (?) , So deleting completed downloads through the interface may not start saving data in downloads.json (by default, they are not saved), so after rebooting everything will remain as it was.
Fortunately, you can use a simple hack: add saving current downloads when the browser is shut down.
Fifth , when you start the browser, if there is something in the downloads pane (even if it is a paused or completed download) there will be a notification about the start of the download.
But we already have a wrapper for DownloadsDataItem.prototype.updateFromDownload () , so this is easy to fix .
Well, the downloads.json reading code, unfortunately, had to be rewritten . This is why my opinion about promises (promises) has not improved at all - any technology should be applied wisely and only where it is really needed (and not shoved anywhere, just because it is modern and fashionable).
There is also a strange problem with the download list: if the dates are almost the same, the sorting will be inverted (the new ones will be from the bottom, not the top), but if you open the downloads panel when the browser starts up, the order will be correct ( but only in this window).
In addition, problems with a large list of downloads are not limited to reading ... Although even with reading there is another problem: after each addition of a new download, an update of the interface is called, synchronously. When restoring a saved list, an addition is also added, followed by notifying all concerned. I had to make a couple more corrections here .
Here, the built-in profiler (Shift + F5) provided invaluable help. Without it, it would be practically impossible to figure out the reasons for hanging up - the logic that is painfully complicated there (and the call stack terrifies).
Well, besides reading the full list, the list cleaning function works as a minimum, so there too can fall out into the stack overflow if the list is big enough. This is not fixed yet. But, in principle, this is not critical: if something should not be saved, there is always a private mode, and the piece deletion (and the added point of cleaning in general for all downloads) works.
What is the most interesting, with incomplete loads of problems is much less - their recovery for some reason will not fall into recursion, even if there are quite a lot of them (I checked for 150 pieces). Apparently, this is due to the OS.File API for working with files separated into a separate stream (and the downloads of the files put on pause just check for the presence of the file), because of it, vague errors like
Error: Win error 2 during operation open (Can not find the file specified.
)
Source file: resource: //gre/modules/commonjs/sdk/core/promise.js
Line: 133
(This is if you delete the file of the paused download)
In addition, after the corrections made, the automatic download began to work normally, but if you try to open the downloads panel immediately after launching the browser, it will work out some other code and, if there are a lot of downloads, it may hang.

PS The new version of the extension is still being tested, "position in the queue: 4 out of 17". Initially, I planned to wait (and the text, in terms of editing - and it was written more than a week ago - this is only good), but there is a limit to everything.
PPS I tried to write so as not to impose my opinion on the quality of the new code in Firefox to the reader, I hope I succeeded, and let everyone make his own conclusions. However, I repeat, in the original form, no particular problems arise.

Source: https://habr.com/ru/post/215175/


All Articles