Bug 808277 - Show the progress of downloads that are not visible in the Downloads Panel in a summary. r=mak.

This commit is contained in:
Mike Conley 2012-11-16 16:19:45 -05:00
parent 81724f8edd
commit 25eb07c6c1
10 changed files with 797 additions and 128 deletions

View File

@ -24,8 +24,16 @@
<xul:image class="downloadTypeIcon blockedIcon"/>
<xul:vbox pack="center"
flex="1">
<!-- We're letting localizers put a min-width in here primarily
because of the downloads summary at the bottom of the list of
download items. An element in the summary has the same min-width
on a description, and we don't want the panel to change size if the
summary isn't being displayed, so we ensure that items share the
same minimum width.
-->
<xul:description class="downloadTarget"
crop="center"
style="min-width: &downloadsSummary.minWidth;"
xbl:inherits="value=target,tooltiptext=target"/>
<xul:progressmeter anonid="progressmeter"
class="downloadProgress"

View File

@ -89,3 +89,8 @@ richlistitem[type="download"]:not([selected]) button {
{
visibility: hidden;
}
#downloadsSummary:not([inprogress="true"]) #downloadsSummaryProgress,
#downloadsSummary:not([inprogress="true"]) #downloadsSummaryDetails {
display: none;
}

View File

@ -540,11 +540,10 @@ const DownloadsView = {
DownloadsPanel.panel.removeAttribute("hasdownloads");
}
let s = DownloadsCommon.strings;
this.downloadsHistory.label = (hiddenCount > 0)
? s.showMoreDownloads(hiddenCount)
: s.showAllDownloads;
this.downloadsHistory.accessKey = s.showDownloadsAccessKey;
// If we've got some hidden downloads, we should show the summary just
// below the list.
this.downloadsHistory.collapsed = hiddenCount > 0;
DownloadsSummary.visible = this.downloadsHistory.collapsed;
},
/**
@ -1409,3 +1408,155 @@ DownloadsViewItemController.prototype = {
protocolSvc.loadUrl(makeFileURI(aFile));
}
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadsSummary
/**
* Manages the summary at the bottom of the downloads panel list if the number
* of items in the list exceeds the panels limit.
*/
const DownloadsSummary = {
/**
* Sets the collapsed state of the summary, and automatically subscribes or
* unsubscribes from the DownloadsCommon DownloadsSummaryData singleton.
*
* @param aVisible
* True if the summary should be shown.
*/
set visible(aVisible)
{
if (aVisible == this._visible || !this._summaryNode) {
return;
}
if (aVisible) {
DownloadsCommon.getSummary(DownloadsView.kItemCountLimit)
.addView(this);
} else {
DownloadsCommon.getSummary(DownloadsView.kItemCountLimit)
.removeView(this);
}
this._summaryNode.collapsed = !aVisible;
return this._visible = aVisible;
},
_visible: false,
/**
* Sets whether or not we show the progress bar.
*
* @param aShowingProgress
* True if we should show the progress bar.
*/
set showingProgress(aShowingProgress)
{
if (aShowingProgress) {
this._summaryNode.setAttribute("inprogress", "true");
} else {
this._summaryNode.removeAttribute("inprogress");
}
},
/**
* Sets the amount of progress that is visible in the progress bar.
*
* @param aValue
* A value between 0 and 100 to represent the progress of the
* summarized downloads.
*/
set percentComplete(aValue)
{
if (this._progressNode) {
this._progressNode.setAttribute("value", aValue);
}
return aValue;
},
/**
* Sets the description for the download summary.
*
* @param aValue
* A string representing the description of the summarized
* downloads.
*/
set description(aValue)
{
if (this._descriptionNode) {
this._descriptionNode.setAttribute("value", aValue);
this._descriptionNode.setAttribute("tooltiptext", aValue);
}
return aValue;
},
/**
* Sets the details for the download summary, such as the time remaining,
* the amount of bytes transferred, etc.
*
* @param aValue
* A string representing the details of the summarized
* downloads.
*/
set details(aValue)
{
if (this._detailsNode) {
this._detailsNode.setAttribute("value", aValue);
this._detailsNode.setAttribute("tooltiptext", aValue);
}
return aValue;
},
/**
* Element corresponding to the root of the downloads summary.
*/
get _summaryNode()
{
let node = document.getElementById("downloadsSummary");
if (!node) {
return null;
}
delete this._summaryNode;
return this._summaryNode = node;
},
/**
* Element corresponding to the progress bar in the downloads summary.
*/
get _progressNode()
{
let node = document.getElementById("downloadsSummaryProgress");
if (!node) {
return null;
}
delete this._progressNode;
return this._progressNode = node;
},
/**
* Element corresponding to the main description of the downloads
* summary.
*/
get _descriptionNode()
{
let node = document.getElementById("downloadsSummaryDescription");
if (!node) {
return null;
}
delete this._descriptionNode;
return this._descriptionNode = node;
},
/**
* Element corresponding to the secondary description of the downloads
* summary.
*/
get _detailsNode()
{
let node = document.getElementById("downloadsSummaryDetails");
if (!node) {
return null;
}
delete this._detailsNode;
return this._detailsNode = node;
}
}

View File

@ -105,8 +105,32 @@
oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
ondragstart="DownloadsView.onDownloadDragStart(event);"/>
<hbox id="downloadsSummary"
collapsed="true"
align="center"
orient="horizontal"
onclick="DownloadsPanel.showDownloadsHistory();">
<image class="downloadTypeIcon" />
<vbox>
<description id="downloadsSummaryDescription"
class="downloadTarget"
style="min-width: &downloadsSummary.minWidth;"/>
<progressmeter id="downloadsSummaryProgress"
class="downloadProgress"
min="0"
max="100"
mode="normal" />
<description id="downloadsSummaryDetails"
class="downloadDetails"
style="width: &downloadDetails.width;"
crop="end"/>
</vbox>
</hbox>
<button id="downloadsHistory"
class="plain"
label="&downloadsHistory.label;"
accesskey="&downloadsHistory.accesskey;"
oncommand="DownloadsPanel.showDownloadsHistory();"/>
</panel>
</popupset>

View File

@ -54,6 +54,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
const nsIDM = Ci.nsIDownloadManager;
@ -72,7 +74,7 @@ const kDownloadsStringsRequiringFormatting = {
};
const kDownloadsStringsRequiringPluralForm = {
showMoreDownloads: true
otherDownloads: true
};
XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
@ -184,7 +186,145 @@ this.DownloadsCommon = {
* This does not need to be a lazy getter, since no initialization is required
* at present.
*/
get indicatorData() DownloadsIndicatorData
get indicatorData() DownloadsIndicatorData,
/**
* Returns a reference to the DownloadsSummaryData singleton - creating one
* in the process if one hasn't been instantiated yet.
*
* @param aNumToExclude
* The number of items on the top of the downloads list to exclude
* from the summary.
*/
_summary: null,
getSummary: function DC_getSummary(aNumToExclude)
{
if (this._summary) {
return this._summary;
}
return this._summary = new DownloadsSummaryData(aNumToExclude);
},
/**
* Given an iterable collection of nsIDownload's, generates and returns
* statistics about that collection.
*
* @param aDownloads An iterable collection of nsIDownloads.
*
* @return Object whose properties are the generated statistics. Currently,
* we return the following properties:
*
* numActive : The total number of downloads.
* numPaused : The total number of paused downloads.
* numScanning : The total number of downloads being scanned.
* numDownloading : The total number of downloads being downloaded.
* totalSize : The total size of all downloads once completed.
* totalTransferred: The total amount of transferred data for these
* downloads.
* slowestSpeed : The slowest download rate.
* rawTimeLeft : The estimated time left for the downloads to
* complete.
* percentComplete : The percentage of bytes successfully downloaded.
*/
summarizeDownloads: function DC_summarizeDownloads(aDownloads)
{
let summary = {
numActive: 0,
numPaused: 0,
numScanning: 0,
numDownloading: 0,
totalSize: 0,
totalTransferred: 0,
// slowestSpeed is Infinity so that we can use Math.min to
// find the slowest speed. We'll set this to 0 afterwards if
// it's still at Infinity by the time we're done iterating all
// downloads.
slowestSpeed: Infinity,
rawTimeLeft: -1,
percentComplete: -1
}
// If no download has been loaded, don't use the methods of the Download
// Manager service, so that it is not initialized unnecessarily.
for (let download of aDownloads) {
summary.numActive++;
switch (download.state) {
case nsIDM.DOWNLOAD_PAUSED:
summary.numPaused++;
break;
case nsIDM.DOWNLOAD_SCANNING:
summary.numScanning++;
break;
case nsIDM.DOWNLOAD_DOWNLOADING:
summary.numDownloading++;
if (download.size > 0 && download.speed > 0) {
let sizeLeft = download.size - download.amountTransferred;
summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
sizeLeft / download.speed);
summary.slowestSpeed = Math.min(summary.slowestSpeed,
download.speed);
}
break;
}
// Only add to total values if we actually know the download size.
if (download.size > 0 &&
download.state != nsIDM.DOWNLOAD_CANCELED &&
download.state != nsIDM.DOWNLOAD_FAILED) {
summary.totalSize += download.size;
summary.totalTransferred += download.amountTransferred;
}
}
if (summary.numActive != 0 && summary.totalSize != 0 &&
summary.numActive != summary.numScanning) {
summary.percentComplete = (summary.totalTransferred /
summary.totalSize) * 100;
}
if (summary.slowestSpeed == Infinity) {
summary.slowestSpeed = 0;
}
return summary;
},
/**
* If necessary, smooths the estimated number of seconds remaining for one
* or more downloads to complete.
*
* @param aSeconds
* Current raw estimate on number of seconds left for one or more
* downloads. This is a floating point value to help get sub-second
* accuracy for current and future estimates.
*/
smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds)
{
// We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
// though tailored to a single time estimation for all downloads. We never
// apply sommothing if the new value is less than half the previous value.
let shouldApplySmoothing = aLastSeconds >= 0 &&
aSeconds > aLastSeconds / 2;
if (shouldApplySmoothing) {
// Apply hysteresis to favor downward over upward swings. Trust only 30%
// of the new value if lower, and 10% if higher (exponential smoothing).
let (diff = aSeconds - aLastSeconds) {
aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff;
}
// If the new time is similar, reuse something close to the last time
// left, but subtract a little to provide forward progress.
let diff = aSeconds - aLastSeconds;
let diffPercent = diff / aLastSeconds * 100;
if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
aSeconds = aLastSeconds - (diff < 0 ? .4 : .2);
}
}
// In the last few seconds of downloading, we are always subtracting and
// never adding to the time left. Ensure that we never fall below one
// second left until all downloads are actually finished.
return aLastSeconds = Math.max(aSeconds, 1);
}
};
/**
@ -613,6 +753,7 @@ const DownloadsData = {
dataItem.startTime = Math.round(aDownload.startTime / 1000);
dataItem.currBytes = aDownload.amountTransferred;
dataItem.maxBytes = aDownload.size;
dataItem.download = aDownload;
this._views.forEach(
function (view) view.getViewItem(dataItem).onStateChange()
@ -919,19 +1060,13 @@ DownloadsDataItem.prototype = {
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadsIndicatorData
//// DownloadsViewPrototype
/**
* This object registers itself with DownloadsData as a view, and transforms the
* notifications it receives into overall status data, that is then broadcast to
* the registered download status indicators.
*
* Note that using this object does not automatically start the Download Manager
* service. Consumers will see an empty list of downloads until the service is
* actually started. This is useful to display a neutral progress indicator in
* the main browser window until the autostart timeout elapses.
* A prototype for an object that registers itself with DownloadsData as soon
* as a view is registered with it.
*/
const DownloadsIndicatorData = {
const DownloadsViewPrototype = {
//////////////////////////////////////////////////////////////////////////////
//// Registration of views
@ -946,10 +1081,10 @@ const DownloadsIndicatorData = {
* The specified object is initialized with the currently available status.
*
* @param aView
* DownloadsIndicatorView object to be added. This reference must be
* View object to be added. This reference must be
* passed to removeView before termination.
*/
addView: function DID_addView(aView)
addView: function DVP_addView(aView)
{
// Start receiving events when the first of our views is registered.
if (this._views.length == 0) {
@ -964,11 +1099,12 @@ const DownloadsIndicatorData = {
* Updates the properties of an object previously added using addView.
*
* @param aView
* DownloadsIndicatorView object to be updated.
* View object to be updated.
*/
refreshView: function DID_refreshView(aView)
refreshView: function DVP_refreshView(aView)
{
// Update immediately even if we are still loading data asynchronously.
// Subclasses must provide these two functions!
this._refreshProperties();
this._updateView(aView);
},
@ -977,9 +1113,9 @@ const DownloadsIndicatorData = {
* Removes an object previously added using addView.
*
* @param aView
* DownloadsIndicatorView object to be removed.
* View object to be removed.
*/
removeView: function DID_removeView(aView)
removeView: function DVP_removeView(aView)
{
let index = this._views.indexOf(aView);
if (index != -1) {
@ -989,7 +1125,6 @@ const DownloadsIndicatorData = {
// Stop receiving events when the last of our views is unregistered.
if (this._views.length == 0) {
DownloadsCommon.data.removeView(this);
this._itemCount = 0;
}
},
@ -1004,7 +1139,7 @@ const DownloadsIndicatorData = {
/**
* Called before multiple downloads are about to be loaded.
*/
onDataLoadStarting: function DID_onDataLoadStarting()
onDataLoadStarting: function DVP_onDataLoadStarting()
{
this._loading = true;
},
@ -1012,9 +1147,134 @@ const DownloadsIndicatorData = {
/**
* Called after data loading finished.
*/
onDataLoadCompleted: function DID_onDataLoadCompleted()
onDataLoadCompleted: function DVP_onDataLoadCompleted()
{
this._loading = false;
},
/**
* Called when the downloads database becomes unavailable (for example, we
* entered Private Browsing Mode and the database backend changed).
* References to existing data should be discarded.
*
* @note Subclasses should override this.
*/
onDataInvalidated: function DVP_onDataInvalidated()
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Called when a new download data item is available, either during the
* asynchronous data load or when a new download is started.
*
* @param aDataItem
* DownloadsDataItem object that was just added.
* @param aNewest
* When true, indicates that this item is the most recent and should be
* added in the topmost position. This happens when a new download is
* started. When false, indicates that the item is the least recent
* with regard to the items that have been already added. The latter
* generally happens during the asynchronous data load.
*
* @note Subclasses should override this.
*/
onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest)
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Called when a data item is removed, ensures that the widget associated with
* the view item is removed from the user interface.
*
* @param aDataItem
* DownloadsDataItem object that is being removed.
*
* @note Subclasses should override this.
*/
onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem)
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Returns the view item associated with the provided data item for this view.
*
* @param aDataItem
* DownloadsDataItem object for which the view item is requested.
*
* @return Object that can be used to notify item status events.
*
* @note Subclasses should override this.
*/
getViewItem: function DID_getViewItem(aDataItem)
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Private function used to refresh the internal properties being sent to
* each registered view.
*
* @note Subclasses should override this.
*/
_refreshProperties: function DID_refreshProperties()
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* Private function used to refresh an individual view.
*
* @note Subclasses should override this.
*/
_updateView: function DID_updateView()
{
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadsIndicatorData
/**
* This object registers itself with DownloadsData as a view, and transforms the
* notifications it receives into overall status data, that is then broadcast to
* the registered download status indicators.
*
* Note that using this object does not automatically start the Download Manager
* service. Consumers will see an empty list of downloads until the service is
* actually started. This is useful to display a neutral progress indicator in
* the main browser window until the autostart timeout elapses.
*/
const DownloadsIndicatorData = {
__proto__: DownloadsViewPrototype,
/**
* Removes an object previously added using addView.
*
* @param aView
* DownloadsIndicatorView object to be removed.
*/
removeView: function DID_removeView(aView)
{
DownloadsViewPrototype.removeView.call(this, aView);
if (this._views.length == 0) {
this._itemCount = 0;
}
},
//////////////////////////////////////////////////////////////////////////////
//// Callback functions from DownloadsData
/**
* Called after data loading finished.
*/
onDataLoadCompleted: function DID_onDataLoadCompleted()
{
DownloadsViewPrototype.onDataLoadCompleted.call(this);
this._updateViews();
},
@ -1179,40 +1439,21 @@ const DownloadsIndicatorData = {
_lastTimeLeft: -1,
/**
* Update the estimated time until all in-progress downloads will finish.
*
* @param aSeconds
* Current raw estimate on number of seconds left for all downloads.
* This is a floating point value to help get sub-second accuracy for
* current and future estimates.
* A generator function for the downloads that this summary is currently
* interested in. This generator is passed off to summarizeDownloads in order
* to generate statistics about the downloads we care about - in this case,
* it's all active downloads.
*/
_updateTimeLeft: function DID_updateTimeLeft(aSeconds)
_activeDownloads: function DID_activeDownloads()
{
// We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
// though tailored to a single time estimation for all downloads. We never
// apply sommothing if the new value is less than half the previous value.
let shouldApplySmoothing = this._lastTimeLeft >= 0 &&
aSeconds > this._lastTimeLeft / 2;
if (shouldApplySmoothing) {
// Apply hysteresis to favor downward over upward swings. Trust only 30%
// of the new value if lower, and 10% if higher (exponential smoothing).
let (diff = aSeconds - this._lastTimeLeft) {
aSeconds = this._lastTimeLeft + (diff < 0 ? .3 : .1) * diff;
}
// If the new time is similar, reuse something close to the last time
// left, but subtract a little to provide forward progress.
let diff = aSeconds - this._lastTimeLeft;
let diffPercent = diff / this._lastTimeLeft * 100;
if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
aSeconds = this._lastTimeLeft - (diff < 0 ? .4 : .2);
// If no download has been loaded, don't use the methods of the Download
// Manager service, so that it is not initialized unnecessarily.
if (this._itemCount > 0) {
let downloads = Services.downloads.activeDownloads;
while (downloads.hasMoreElements()) {
yield downloads.getNext().QueryInterface(Ci.nsIDownload);
}
}
// In the last few seconds of downloading, we are always subtracting and
// never adding to the time left. Ensure that we never fall below one
// second left until all downloads are actually finished.
this._lastTimeLeft = Math.max(aSeconds, 1);
},
/**
@ -1220,69 +1461,236 @@ const DownloadsIndicatorData = {
*/
_refreshProperties: function DID_refreshProperties()
{
let numActive = 0;
let numPaused = 0;
let numScanning = 0;
let totalSize = 0;
let totalTransferred = 0;
let rawTimeLeft = -1;
// If no download has been loaded, don't use the methods of the Download
// Manager service, so that it is not initialized unnecessarily.
if (this._itemCount > 0) {
let downloads = Services.downloads.activeDownloads;
while (downloads.hasMoreElements()) {
let download = downloads.getNext().QueryInterface(Ci.nsIDownload);
numActive++;
switch (download.state) {
case nsIDM.DOWNLOAD_PAUSED:
numPaused++;
break;
case nsIDM.DOWNLOAD_SCANNING:
numScanning++;
break;
case nsIDM.DOWNLOAD_DOWNLOADING:
if (download.size > 0 && download.speed > 0) {
let sizeLeft = download.size - download.amountTransferred;
rawTimeLeft = Math.max(rawTimeLeft, sizeLeft / download.speed);
}
break;
}
// Only add to total values if we actually know the download size.
if (download.size > 0) {
totalSize += download.size;
totalTransferred += download.amountTransferred;
}
}
}
let summary =
DownloadsCommon.summarizeDownloads(this._activeDownloads());
// Determine if the indicator should be shown or get attention.
this._hasDownloads = (this._itemCount > 0);
if (numActive == 0 || totalSize == 0 || numActive == numScanning) {
// Don't display the current progress.
this._percentComplete = -1;
} else {
// Display the current progress.
this._percentComplete = (totalTransferred / totalSize) * 100;
}
// If all downloads are paused, show the progress indicator as paused.
this._paused = numActive > 0 && numActive == numPaused;
this._paused = summary.numActive > 0 &&
summary.numActive == summary.numPaused;
this._percentComplete = summary.percentComplete;
// Display the estimated time left, if present.
if (rawTimeLeft == -1) {
if (summary.rawTimeLeft == -1) {
// There are no downloads with a known time left.
this._lastRawTimeLeft = -1;
this._lastTimeLeft = -1;
this._counter = "";
} else {
// Compute the new time left only if state actually changed.
if (this._lastRawTimeLeft != rawTimeLeft) {
this._lastRawTimeLeft = rawTimeLeft;
this._updateTimeLeft(rawTimeLeft);
if (this._lastRawTimeLeft != summary.rawTimeLeft) {
this._lastRawTimeLeft = summary.rawTimeLeft;
this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
this._lastTimeLeft);
}
this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft);
}
}
}
////////////////////////////////////////////////////////////////////////////////
//// DownloadsSummaryData
/**
* 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 aNumToExclude
* The number of items to exclude from the summary, starting from the
* top of the list.
*/
function DownloadsSummaryData(aNumToExclude) {
this._numToExclude = aNumToExclude;
// Since we can have multiple instances of DownloadsSummaryData, we
// override these values from the prototype so that each instance can be
// completely separated from one another.
this._views = [];
this._loading = false;
this._dataItems = [];
// Floating point value indicating the last number of seconds estimated until
// the longest download will finish. We need to store this value so that we
// don't continuously apply smoothing if the actual download state has not
// changed. This is set to -1 if the previous value is unknown.
this._lastRawTimeLeft = -1;
// Last number of seconds estimated until all in-progress downloads with a
// known size and speed will finish. This value is stored to allow smoothing
// in case of small variations. This is set to -1 if the previous value is
// unknown.
this._lastTimeLeft = -1;
// The following properties are updated by _refreshProperties and are then
// propagated to the views.
this._showingProgress = false;
this._details = "";
this._description = "";
this._numActive = 0;
this._percentComplete = -1;
}
DownloadsSummaryData.prototype = {
__proto__: DownloadsViewPrototype,
/**
* Removes an object previously added using addView.
*
* @param aView
* DownloadsSummary view to be removed.
*/
removeView: function DSD_removeView(aView)
{
DownloadsViewPrototype.removeView.call(this, aView);
if (this._views.length == 0) {
// Clear out our collection of DownloadsDataItems. If we ever have
// another view registered with us, this will get re-populated.
this._dataItems = [];
}
},
//////////////////////////////////////////////////////////////////////////////
//// Callback functions from DownloadsData - see the documentation in
//// DownloadsViewPrototype for more information on what these functions
//// are used for.
onDataLoadCompleted: function DSD_onDataLoadCompleted()
{
DownloadsViewPrototype.onDataLoadCompleted.call(this);
this._updateViews();
},
onDataInvalidated: function DSD_onDataInvalidated()
{
this._dataItems = [];
},
onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
{
if (aNewest) {
this._dataItems.unshift(aDataItem);
} else {
this._dataItems.push(aDataItem);
}
this._updateViews();
},
onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
{
let itemIndex = this._dataItems.indexOf(aDataItem);
this._dataItems.splice(itemIndex, 1);
this._updateViews();
},
getViewItem: function DSD_getViewItem(aDataItem)
{
let self = this;
return Object.freeze({
onStateChange: function DIVI_onStateChange()
{
// Since the state of a download changed, reset the estimated time left.
self._lastRawTimeLeft = -1;
self._lastTimeLeft = -1;
self._updateViews();
},
onProgressChange: function DIVI_onProgressChange()
{
self._updateViews();
}
});
},
//////////////////////////////////////////////////////////////////////////////
//// Propagation of properties to our views
/**
* Computes aggregate values and propagates the changes to our views.
*/
_updateViews: function DSD_updateViews()
{
// Do not update the status indicators during batch loads of download items.
if (this._loading) {
return;
}
this._refreshProperties();
this._views.forEach(this._updateView, this);
},
/**
* Updates the specified view with the current aggregate values.
*
* @param aView
* DownloadsIndicatorView object to be updated.
*/
_updateView: function DSD_updateView(aView)
{
aView.showingProgress = this._showingProgress;
aView.percentComplete = this._percentComplete;
aView.description = this._description;
aView.details = this._details;
},
//////////////////////////////////////////////////////////////////////////////
//// Property updating based on current download status
/**
* A generator function for the downloads that this summary is currently
* interested in. This generator is passed off to summarizeDownloads in order
* to generate statistics about the downloads we care about - in this case,
* it's the downloads in this._dataItems after the first few to exclude,
* which was set when constructing this DownloadsSummaryData instance.
*/
_downloadsForSummary: function DSD_downloadsForSummary()
{
if (this._dataItems.length > 0) {
for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
yield this._dataItems[i].download;
}
}
},
/**
* Computes aggregate values based on the current state of downloads.
*/
_refreshProperties: function DSD_refreshProperties()
{
// Pre-load summary with default values.
let summary =
DownloadsCommon.summarizeDownloads(this._downloadsForSummary());
this._description = DownloadsCommon.strings
.otherDownloads(summary.numActive);
this._percentComplete = summary.percentComplete;
// If all downloads are paused, show the progress indicator as paused.
this._showingProgress = summary.numDownloading > 0 ||
summary.numPaused > 0;
// Display the estimated time left, if present.
if (summary.rawTimeLeft == -1) {
// There are no downloads with a known time left.
this._lastRawTimeLeft = -1;
this._lastTimeLeft = -1;
this._details = "";
} else {
// Compute the new time left only if state actually changed.
if (this._lastRawTimeLeft != summary.rawTimeLeft) {
this._lastRawTimeLeft = summary.rawTimeLeft;
this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
this._lastTimeLeft);
}
[this._details] = DownloadUtils.getDownloadStatusNoRate(
summary.totalTransferred, summary.totalSize, summary.slowestSpeed,
this._lastTimeLeft);
}
}
}

View File

@ -25,6 +25,21 @@
-->
<!ENTITY downloadDetails.width "50ch">
<!-- LOCALIZATION NOTE (downloadsSummary.minWidth):
Minimum width for the main description of the downloads summary,
which is displayed at the bottom of the Downloads Panel if the
number of downloads exceeds the limit that the panel can display.
A good rule of thumb here is to look at the otherDownloads string
in downloads.properties, and make a reasonable estimate of its
maximum length. For English, this seems like a reasonable limit:
+999 other current downloads
that's 28 characters, so we set the minimum width to 28ch.
-->
<!ENTITY downloadsSummary.minWidth "28ch">
<!ENTITY cmd.pause.label "Pause">
<!ENTITY cmd.pause.accesskey "P">
<!ENTITY cmd.resume.label "Resume">
@ -50,3 +65,10 @@
<!ENTITY cmd.clearList.label "Clear List">
<!ENTITY cmd.clearList.accesskey "a">
<!-- LOCALIZATION NOTE (downloadsHistory.label, downloadsHistory.accesskey):
This string is shown at the bottom of the Downloads Panel when all the
downloads fit in the available space, or when there are no downloads in
the panel at all.
-->
<!ENTITY downloadsHistory.label "Show All Downloads">
<!ENTITY downloadsHistory.accesskey "S">

View File

@ -67,20 +67,13 @@ shortTimeLeftDays=%1$Sd
statusSeparator=%1$S \u2014 %2$S
statusSeparatorBeforeNumber=%1$S \u2014 %2$S
# LOCALIZATION NOTE (showMoreDownloads):
# This string is shown in the Downloads Panel when there are more active
# downloads than can fit in the available space. The phrase should be read as
# "Show N more of my recent downloads". Use a semi-colon list of plural forms.
# See: http://developer.mozilla.org/en/Localization_and_Plurals
showMoreDownloads=Show 1 More Recent Download;Show %1$S More Recent Downloads
# LOCALIZATION NOTE (showAllDownloads):
# This string is shown in place of showMoreDownloads when all the downloads fit
# in the available space, or when there are no downloads in the panel at all.
showAllDownloads=Show All Downloads
# LOCALIZATION NOTE (showDownloadsAccessKey):
# This access key applies to both showMoreDownloads and showAllDownloads.
showDownloadsAccessKey=S
fileExecutableSecurityWarning="%S" is an executable file. Executable files may contain viruses or other malicious code that could harm your computer. Use caution when opening this file. Are you sure you want to launch "%S"?
fileExecutableSecurityWarningTitle=Open Executable File?
fileExecutableSecurityWarningDontAsk=Don't ask me this again
# LOCALIZATION NOTE (otherDownloads):
# This is displayed in an item at the bottom of the Downloads Panel when
# there are more downloads than can fit in the list in the panel. Use a
# semi-colon list of plural forms.
# See: http://developer.mozilla.org/en/Localization_and_Plurals
otherDownloads=+%1$S other current download; +%1$S other current downloads

View File

@ -24,6 +24,7 @@
cursor: pointer;
}
#downloadsSummary,
#downloadsPanel[hasdownloads] > #downloadsHistory {
border-top: 1px solid ThreeDShadow;
background-image: -moz-linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px);
@ -37,17 +38,36 @@
outline: 1px -moz-dialogtext dotted;
}
/*** List items ***/
/*** Downloads Summary and List items ***/
#downloadsSummary,
richlistitem[type="download"] {
height: 6em;
-moz-padding-end: 0;
color: inherit;
}
#downloadsSummary {
padding: 8px 38px 8px 12px;
cursor: pointer;
}
#downloadsSummary > .downloadTypeIcon {
height: 32px;
width: 32px;
list-style-image: url("chrome://mozapps/skin/downloads/downloadIcon.png");
}
#downloadsSummaryDescription {
color: -moz-nativehyperlinktext;
}
richlistitem[type="download"] {
margin: 0;
border-top: 1px solid hsla(0,0%,100%,.2);
border-bottom: 1px solid hsla(0,0%,0%,.15);
background: transparent;
padding: 8px;
-moz-padding-end: 0;
color: inherit;
}
richlistitem[type="download"]:first-child {

View File

@ -31,6 +31,7 @@
border-top-right-radius: 6px;
}
#downloadsSummary,
#downloadsPanel[hasdownloads] > #downloadsHistory {
background: #e5e5e5;
border-top: 1px solid hsla(0,0%,0%,.1);
@ -53,17 +54,34 @@
border-bottom-right-radius: 6px;
}
/*** List items ***/
/*** Downloads Summary and List items ***/
#downloadsSummary,
richlistitem[type="download"] {
height: 7em;
-moz-padding-end: 0;
color: inherit;
}
#downloadsSummary {
padding: 8px 38px 8px 12px;
cursor: pointer;
}
#downloadsSummary > .downloadTypeIcon {
list-style-image: url("chrome://mozapps/skin/downloads/downloadIcon.png");
}
#downloadsSummaryDescription {
color: -moz-nativehyperlinktext;
}
richlistitem[type="download"] {
margin: 0;
border-top: 1px solid hsla(0,0%,100%,.07);
border-bottom: 1px solid hsla(0,0%,0%,.2);
background: transparent;
padding: 8px;
-moz-padding-end: 0;
color: inherit;
}
richlistitem[type="download"]:first-child {

View File

@ -25,23 +25,43 @@
}
@media (-moz-windows-default-theme) {
#downloadsSummary,
#downloadsPanel[hasdownloads] > #downloadsHistory {
background-color: hsla(216,45%,88%,.98);
box-shadow: 0px 1px 2px rgb(204,214,234) inset;
box-shadow: 0px 1px 2px rgb(204,214,234) inset;
}
}
/*** List items ***/
/*** Downloads Summary and List items ***/
#downloadsSummary,
richlistitem[type="download"] {
height: 7em;
-moz-padding-end: 0;
color: inherit;
}
#downloadsSummary {
padding: 8px 38px 8px 12px;
cursor: pointer;
}
#downloadsSummary > .downloadTypeIcon {
height: 24px;
width: 24px;
list-style-image: url("chrome://mozapps/skin/downloads/downloadIcon.png");
}
#downloadsSummaryDescription {
color: -moz-nativehyperlinktext;
}
richlistitem[type="download"] {
margin: 0;
border-top: 1px solid hsla(0,0%,100%,.3);
border-bottom: 1px solid hsla(220,18%,51%,.25);
background: transparent;
padding: 8px;
-moz-padding-end: 0;
color: inherit;
}
richlistitem[type="download"]:first-child {