blob: 3735dde2d33f178cb53ab7f95b55d38f3d0af661 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Progress center panel.
* @implements {ProgressCenterPanelInterface}
*/
class ProgressCenterPanel {
constructor() {
/**
* Reference to the feedback panel host.
* TODO(crbug.com/947388) Add closure annotation here.
*/
this.feedbackHost_ = document.querySelector('#progress-panel');
/**
* Items that are progressing, or completed.
* Key is item ID.
* @private {!Object<ProgressCenterItem>}
*/
this.items_ = {};
/**
* Callback to be called with the ID of the progress item when the cancel
* button is clicked.
* @type {?function(string)}
*/
this.cancelCallback = null;
/**
* Callback to be called with the ID of the error item when user pressed
* dismiss button of it.
* @type {?function(string)}
*/
this.dismissErrorItemCallback = null;
/**
* Timeout for hiding file operations in progress.
* @type {number}
*/
this.PENDING_TIME_MS_ = 2000;
if (window.IN_TEST) {
this.PENDING_TIME_MS_ = 0;
}
}
/**
* Generate source string for display on the feedback panel.
* @param {!ProgressCenterItem} item Item we're generating a message for.
* @param {Object} info Cached information to use for formatting.
* @return {string} String formatted based on the item state.
*/
generateSourceString_(item, info) {
switch (item.state) {
case 'progressing':
if (item.itemCount === 1) {
if (item.type === ProgressItemType.COPY) {
return strf('COPY_FILE_NAME', info['source']);
} else if (item.type === ProgressItemType.MOVE) {
return strf('MOVE_FILE_NAME', info['source']);
} else {
return item.message;
}
} else {
if (item.type === ProgressItemType.COPY) {
return strf('COPY_ITEMS_REMAINING', info['source']);
} else if (item.type === ProgressItemType.MOVE) {
return strf('MOVE_ITEMS_REMAINING', info['source']);
} else {
return item.message;
}
}
break;
case 'completed':
if (info['count'] > 1) {
return strf('FILE_ITEMS', info['source']);
}
return info['source'] || item.message;
case 'error':
return item.message;
case 'canceled':
return '';
default:
assertNotReached();
break;
}
return '';
}
/**
* Test if we have an empty or all whitespace string.
* @param {string} candidate String we're checking.
* @return {boolean} true if there's content in the candidate.
*/
isNonEmptyString_(candidate) {
if (!candidate || candidate.trim().length === 0) {
return false;
}
return true;
}
/**
* Generate primary text string for display on the feedback panel.
* It is used for TransferDetails mode.
* @param {!ProgressCenterItem} item Item we're generating a message for.
* @param {Object} info Cached information to use for formatting.
* @return {string} String formatted based on the item state.
*/
generatePrimaryString_(item, info) {
const hasDestination = this.isNonEmptyString_(info['destination']);
switch (item.state) {
case 'progressing':
// Source and primary string are the same for missing destination.
if (!hasDestination) {
return this.generateSourceString_(item, info);
}
// fall through
case 'completed':
if (item.itemCount === 1) {
if (item.type === ProgressItemType.COPY) {
if (hasDestination) {
return strf(
'COPY_FILE_NAME_LONG', info['source'], info['destination']);
} else {
return strf('FILE_COPIED', info['source']);
}
} else if (item.type === ProgressItemType.MOVE) {
if (hasDestination) {
return strf(
'MOVE_FILE_NAME_LONG', info['source'], info['destination']);
} else {
return strf('FILE_MOVED', info['source']);
}
} else {
return item.message;
}
} else {
if (item.type === ProgressItemType.COPY) {
if (hasDestination) {
return strf(
'COPY_ITEMS_REMAINING_LONG', info['source'],
info['destination']);
} else {
return strf('FILE_ITEMS_COPIED', info['source']);
}
} else if (item.type === ProgressItemType.MOVE) {
if (hasDestination) {
return strf(
'MOVE_ITEMS_REMAINING_LONG', info['source'],
info['destination']);
} else {
return strf('FILE_ITEMS_MOVED', info['source']);
}
} else {
return item.message;
}
}
break;
case 'error':
return item.message;
case 'canceled':
return '';
default:
assertNotReached();
break;
}
return '';
}
/**
* Generates remaining time message with formatted time.
*
* The time format in hour and minute and the durations more
* than 24 hours also formatted in hour.
*
* As ICU syntax is not implemented in web ui yet (crbug/481718), the i18n
* of time part is handled using Intl methods.
*
* @param {!ProgressCenterItem} item Item we're generating a message for.
* @return {!string} Remaining time message.
*/
generateRemainingTimeMessage(item) {
const seconds = item.remainingTime;
if (seconds == 0 && item.state == 'progressing') {
return str('PENDING_LABEL');
}
// Return empty string for not supported operation (didn't set
// remainingTime) or 0 sec remainingTime in non progressing state.
if (!seconds) {
return '';
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const hourFormatter = new Intl.NumberFormat(
navigator.language, {style: 'unit', unit: 'hour', unitDisplay: 'long'});
const minuteFormatter = new Intl.NumberFormat(
navigator.language,
{style: 'unit', unit: 'minute', unitDisplay: 'short'});
if (hours > 0 && minutes > 0) {
return strf(
'TIME_REMAINING_ESTIMATE_2', hourFormatter.format(hours),
minuteFormatter.format(minutes));
} else if (hours > 0) {
return strf('TIME_REMAINING_ESTIMATE', hourFormatter.format(hours));
} else if (minutes > 0) {
return strf('TIME_REMAINING_ESTIMATE', minuteFormatter.format(minutes));
} else {
// Round up to 1 min for short period of remaining time.
return strf('TIME_REMAINING_ESTIMATE', minuteFormatter.format(1));
}
}
/**
* Process item updates for feedback panels.
* @param {!ProgressCenterItem} item Item being updated.
* @param {?ProgressCenterItem} newItem Item updating with new content.
* @suppress {checkTypes}
* TODO(crbug.com/947388) Remove the suppress, and fix closure compile.
*/
updateFeedbackPanelItem(item, newItem) {
let panelItem = this.feedbackHost_.findPanelItemById(item.id);
if (newItem) {
if (!panelItem) {
panelItem = this.feedbackHost_.createPanelItem(item.id);
// Show the panel only for long running operations.
setTimeout(() => {
this.feedbackHost_.attachPanelItem(panelItem);
}, this.PENDING_TIME_MS_);
if (item.type === 'format') {
panelItem.panelType = panelItem.panelTypeFormatProgress;
} else if (item.type === 'sync') {
panelItem.panelType = panelItem.panelTypeSyncProgress;
} else {
panelItem.panelType = panelItem.panelTypeProgress;
}
panelItem.userData = {
'source': item.sourceMessage,
'destination': item.destinationMessage,
'count': item.itemCount,
};
}
const primaryText = this.generatePrimaryString_(item, panelItem.userData);
panelItem.secondaryText = this.generateRemainingTimeMessage(item);
panelItem.primaryText = primaryText;
panelItem.setAttribute('data-progress-id', item.id);
// On progress panels, make the cancel button aria-label more useful.
const cancelLabel = strf('CANCEL_ACTIVITY_LABEL', primaryText);
panelItem.closeButtonAriaLabel = cancelLabel;
panelItem.signalCallback = (signal) => {
if (signal === 'cancel' && item.cancelCallback) {
item.cancelCallback();
}
if (signal === 'dismiss') {
this.feedbackHost_.removePanelItem(panelItem);
this.dismissErrorItemCallback(item.id);
}
};
panelItem.progress = item.progressRateInPercent.toString();
switch (item.state) {
case 'completed':
// Create a completed panel for copies, moves and formats.
// TODO(crbug.com/947388) decide if we want these for delete, etc.
if (item.type === 'copy' || item.type === 'move' ||
item.type === 'format') {
const donePanelItem = this.feedbackHost_.addPanelItem(item.id);
donePanelItem.id = item.id;
donePanelItem.panelType = donePanelItem.panelTypeDone;
donePanelItem.primaryText = primaryText;
donePanelItem.secondaryText = str('COMPLETE_LABEL');
donePanelItem.signalCallback = (signal) => {
if (signal === 'dismiss') {
this.feedbackHost_.removePanelItem(donePanelItem);
delete this.items_[donePanelItem.id];
}
};
// Delete after 4 seconds, doesn't matter if it's manually deleted
// before the timer fires, as removePanelItem handles that case.
setTimeout(() => {
this.feedbackHost_.removePanelItem(donePanelItem);
delete this.items_[donePanelItem.id];
}, 4000);
}
// Drop through to remove the progress panel.
case 'canceled':
// Remove the feedback panel when complete.
this.feedbackHost_.removePanelItem(panelItem);
break;
case 'error':
panelItem.panelType = panelItem.panelTypeError;
panelItem.primaryText = item.message;
panelItem.secondaryText = '';
// Make sure the panel is attached so it shows immediately.
this.feedbackHost_.attachPanelItem(panelItem);
break;
}
} else if (panelItem) {
this.feedbackHost_.removePanelItem(panelItem);
}
}
/**
* Starts the item update and checks state changes.
* @param {!ProgressCenterItem} item Item containing updated information.
*/
updateItemState_(item) {
// Compares the current state and the new state to check if the update is
// valid or not.
const previousItem = this.items_[item.id];
switch (item.state) {
case ProgressItemState.ERROR:
if (previousItem &&
previousItem.state !== ProgressItemState.PROGRESSING) {
return;
}
this.items_[item.id] = item.clone();
break;
case ProgressItemState.PROGRESSING:
case ProgressItemState.COMPLETED:
if ((!previousItem && item.state === ProgressItemState.COMPLETED) ||
(previousItem &&
previousItem.state !== ProgressItemState.PROGRESSING)) {
return;
}
this.items_[item.id] = item.clone();
break;
case ProgressItemState.CANCELED:
if (!previousItem ||
previousItem.state !== ProgressItemState.PROGRESSING) {
return;
}
delete this.items_[item.id];
}
}
/**
* Updates an item to the progress center panel.
* @param {!ProgressCenterItem} item Item including new contents.
*/
updateItem(item) {
this.updateItemState_(item);
// Update an open view item.
const newItem = this.items_[item.id] || null;
this.updateFeedbackPanelItem(item, newItem);
}
/**
* Called by background page when an error dialog is dismissed.
* @param {string} id Item id.
*/
dismissErrorItem(id) {
delete this.items_[id];
}
}