1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('downloads', function() {
7 * Creates and updates the DOM representation for a download.
8 * @param {!downloads.ThrottledIconLoader} iconLoader
11 function ItemView(iconLoader) {
12 /** @private {!downloads.ThrottledIconLoader} */
13 this.iconLoader_ = iconLoader;
15 this.node = $('templates').querySelector('.download').cloneNode(true);
17 this.safe_ = this.queryRequired_('.safe');
18 this.since_ = this.queryRequired_('.since');
19 this.dateContainer = this.queryRequired_('.date-container');
20 this.date_ = this.queryRequired_('.date');
21 this.save_ = this.queryRequired_('.save');
22 this.backgroundProgress_ = this.queryRequired_('.progress.background');
23 this.foregroundProgress_ = /** @type !HTMLCanvasElement */(
24 this.queryRequired_('canvas.progress'));
25 this.safeImg_ = /** @type !HTMLImageElement */(
26 this.queryRequired_('.safe img'));
27 this.fileName_ = this.queryRequired_('span.name');
28 this.fileLink_ = this.queryRequired_('[is="action-link"].name');
29 this.status_ = this.queryRequired_('.status');
30 this.srcUrl_ = this.queryRequired_('.src-url');
31 this.show_ = this.queryRequired_('.show');
32 this.retry_ = this.queryRequired_('.retry');
33 this.pause_ = this.queryRequired_('.pause');
34 this.resume_ = this.queryRequired_('.resume');
35 this.safeRemove_ = this.queryRequired_('.safe .remove');
36 this.cancel_ = this.queryRequired_('.cancel');
37 this.controlledBy_ = this.queryRequired_('.controlled-by');
39 this.dangerous_ = this.queryRequired_('.dangerous');
40 this.dangerImg_ = /** @type {!HTMLImageElement} */(
41 this.queryRequired_('.dangerous img'));
42 this.description_ = this.queryRequired_('.description');
43 this.malwareControls_ = this.queryRequired_('.dangerous .controls');
44 this.restore_ = this.queryRequired_('.restore');
45 this.dangerRemove_ = this.queryRequired_('.dangerous .remove');
46 this.save_ = this.queryRequired_('.save');
47 this.discard_ = this.queryRequired_('.discard');
49 // Event handlers (bound once on creation).
50 this.safe_.ondragstart = this.onSafeDragstart_.bind(this);
51 this.fileLink_.onclick = this.onFileLinkClick_.bind(this);
52 this.show_.onclick = this.onShowClick_.bind(this);
53 this.pause_.onclick = this.onPauseClick_.bind(this);
54 this.resume_.onclick = this.onResumeClick_.bind(this);
55 this.safeRemove_.onclick = this.onSafeRemoveClick_.bind(this);
56 this.cancel_.onclick = this.onCancelClick_.bind(this);
57 this.restore_.onclick = this.onRestoreClick_.bind(this);
58 this.save_.onclick = this.onSaveClick_.bind(this);
59 this.dangerRemove_.onclick = this.onDangerRemoveClick_.bind(this);
60 this.discard_.onclick = this.onDiscardClick_.bind(this);
63 /** Progress meter constants. */
65 /** @const {number} */
66 START_ANGLE: -0.5 * Math.PI,
67 /** @const {number} */
71 /** @const {number} */
72 ItemView.Progress.HALF = ItemView.Progress.SIDE / 2;
74 ItemView.computeDownloadProgress = function() {
76 * @param {number} a Some float.
77 * @param {number} b Some float.
78 * @param {number=} opt_pct Percent of min(a,b).
79 * @return {boolean} true if a is within opt_pct percent of b.
81 function floatEq(a, b, opt_pct) {
82 return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0);
85 if (floatEq(ItemView.Progress.scale, window.devicePixelRatio)) {
86 // Zooming in or out multiple times then typing Ctrl+0 resets the zoom
87 // level directly to 1x, which fires the matchMedia event multiple times.
90 var Progress = ItemView.Progress;
91 Progress.scale = window.devicePixelRatio;
92 Progress.width = Progress.SIDE * Progress.scale;
93 Progress.height = Progress.SIDE * Progress.scale;
94 Progress.radius = Progress.HALF * Progress.scale;
95 Progress.centerX = Progress.HALF * Progress.scale;
96 Progress.centerY = Progress.HALF * Progress.scale;
98 ItemView.computeDownloadProgress();
100 // Listens for when device-pixel-ratio changes between any zoom level.
101 [0.3, 0.4, 0.6, 0.7, 0.8, 0.95, 1.05, 1.2, 1.4, 1.6, 1.9, 2.2, 2.7, 3.5, 4.5].
102 forEach(function(scale) {
103 var media = '(-webkit-min-device-pixel-ratio:' + scale + ')';
104 window.matchMedia(media).addListener(ItemView.computeDownloadProgress);
108 * @return {!HTMLImageElement} The correct <img> to show when an item is
109 * progressing in the foreground.
111 ItemView.getForegroundProgressImage = function() {
112 var x = window.devicePixelRatio >= 2 ? '2x' : '1x';
113 ItemView.foregroundImages_ = ItemView.foregroundImages_ || {};
114 if (!ItemView.foregroundImages_[x]) {
115 ItemView.foregroundImages_[x] = new Image;
116 var IMAGE_URL = 'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32';
117 ItemView.foregroundImages_[x].src = IMAGE_URL + '@' + x;
119 return ItemView.foregroundImages_[x];
122 ItemView.prototype = {
123 /** @param {!downloads.Data} data */
124 update: function(data) {
125 assert(!this.id_ || data.id == this.id_);
126 this.id_ = data.id; // This is the only thing saved from |data|.
128 this.node.classList.toggle('otr', data.otr);
130 var dangerText = this.getDangerText_(data);
131 this.dangerous_.hidden = !dangerText;
132 this.safe_.hidden = !!dangerText;
134 this.ensureTextIs_(this.since_, data.since_string);
135 this.ensureTextIs_(this.date_, data.date_string);
138 this.ensureTextIs_(this.description_, dangerText);
140 var dangerType = data.danger_type;
141 var dangerousFile = dangerType == downloads.DangerType.DANGEROUS_FILE;
142 this.description_.classList.toggle('malware', !dangerousFile);
144 var idr = dangerousFile ? 'IDR_WARNING' : 'IDR_SAFEBROWSING_WARNING';
145 var iconUrl = 'chrome://theme/' + idr;
146 this.iconLoader_.loadScaledIcon(this.dangerImg_, iconUrl);
148 var showMalwareControls =
149 dangerType == downloads.DangerType.DANGEROUS_CONTENT ||
150 dangerType == downloads.DangerType.DANGEROUS_HOST ||
151 dangerType == downloads.DangerType.DANGEROUS_URL ||
152 dangerType == downloads.DangerType.POTENTIALLY_UNWANTED;
154 this.malwareControls_.hidden = !showMalwareControls;
155 this.discard_.hidden = showMalwareControls;
156 this.save_.hidden = showMalwareControls;
158 var iconUrl = 'chrome://fileicon/' + encodeURIComponent(data.file_path);
159 this.iconLoader_.loadScaledIcon(this.safeImg_, iconUrl);
161 /** @const */ var isInProgress =
162 data.state == downloads.States.IN_PROGRESS;
163 this.node.classList.toggle('in-progress', isInProgress);
165 /** @const */ var completelyOnDisk =
166 data.state == downloads.States.COMPLETE &&
167 !data.file_externally_removed;
169 this.fileLink_.href = data.url;
170 this.ensureTextIs_(this.fileLink_, data.file_name);
171 this.fileLink_.hidden = !completelyOnDisk;
173 /** @const */ var isInterrupted =
174 data.state == downloads.States.INTERRUPTED;
175 this.fileName_.classList.toggle('interrupted', isInterrupted);
176 this.ensureTextIs_(this.fileName_, data.file_name);
177 this.fileName_.hidden = completelyOnDisk;
179 this.show_.hidden = !completelyOnDisk;
181 this.retry_.href = data.url;
182 this.retry_.hidden = !data.retry;
184 this.pause_.hidden = !isInProgress;
186 this.resume_.hidden = !data.resume;
188 /** @const */ var isPaused = data.state == downloads.States.PAUSED;
189 /** @const */ var showCancel = isPaused || isInProgress;
190 this.cancel_.hidden = !showCancel;
192 this.safeRemove_.hidden = showCancel ||
193 !loadTimeData.getBoolean('allowDeletingHistory');
195 /** @const */ var controlledByExtension = data.by_ext_id &&
197 this.controlledBy_.hidden = !controlledByExtension;
198 if (controlledByExtension) {
199 var link = this.controlledBy_.querySelector('a');
200 link.href = 'chrome://extensions#' + data.by_ext_id;
201 link.setAttribute('focus-type', 'controlled-by');
202 link.textContent = data.by_ext_name;
205 this.ensureTextIs_(this.srcUrl_, data.url);
206 this.srcUrl_.href = data.url;
207 this.ensureTextIs_(this.status_, this.getStatusText_(data));
209 this.foregroundProgress_.hidden = !isInProgress;
210 this.backgroundProgress_.hidden = !isInProgress;
213 this.foregroundProgress_.width = ItemView.Progress.width;
214 this.foregroundProgress_.height = ItemView.Progress.height;
216 if (!this.progressContext_) {
218 this.progressContext_ = /** @type !CanvasRenderingContext2D */(
219 this.foregroundProgress_.getContext('2d'));
222 var foregroundImage = ItemView.getForegroundProgressImage();
224 // Draw a pie-slice for the progress.
225 this.progressContext_.globalCompositeOperation = 'copy';
226 this.progressContext_.drawImage(
229 foregroundImage.width,
230 foregroundImage.height,
232 ItemView.Progress.width, ItemView.Progress.height);
234 this.progressContext_.globalCompositeOperation = 'destination-in';
235 this.progressContext_.beginPath();
236 this.progressContext_.moveTo(ItemView.Progress.centerX,
237 ItemView.Progress.centerY);
239 // Draw an arc CW for both RTL and LTR. http://crbug.com/13215
240 this.progressContext_.arc(
241 ItemView.Progress.centerX,
242 ItemView.Progress.centerY,
243 ItemView.Progress.radius,
244 ItemView.Progress.START_ANGLE,
245 ItemView.Progress.START_ANGLE + Math.PI * 0.02 * data.percent,
248 this.progressContext_.lineTo(ItemView.Progress.centerX,
249 ItemView.Progress.centerY);
250 this.progressContext_.fill();
251 this.progressContext_.closePath();
256 destroy: function() {
257 if (this.node.parentNode)
258 this.node.parentNode.removeChild(this.node);
262 * @param {string} selector A CSS selector (e.g. '.class-name').
263 * @return {!Element} The element found by querying for |selector|.
266 queryRequired_: function(selector) {
267 return assert(this.node.querySelector(selector));
271 * Overwrite |el|'s textContent if it differs from |text|.
272 * @param {!Element} el
273 * @param {string} text
276 ensureTextIs_: function(el, text) {
277 if (el.textContent != text)
278 el.textContent = text;
282 * @param {!downloads.Data} data
283 * @return {string} Text describing the danger of a download. Empty if not
286 getDangerText_: function(data) {
287 switch (data.danger_type) {
288 case downloads.DangerType.DANGEROUS_FILE:
289 return loadTimeData.getStringF('dangerFileDesc', data.file_name);
290 case downloads.DangerType.DANGEROUS_URL:
291 return loadTimeData.getString('dangerUrlDesc');
292 case downloads.DangerType.DANGEROUS_CONTENT: // Fall through.
293 case downloads.DangerType.DANGEROUS_HOST:
294 return loadTimeData.getStringF('dangerContentDesc', data.file_name);
295 case downloads.DangerType.UNCOMMON_CONTENT:
296 return loadTimeData.getStringF('dangerUncommonDesc', data.file_name);
297 case downloads.DangerType.POTENTIALLY_UNWANTED:
298 return loadTimeData.getStringF('dangerSettingsDesc', data.file_name);
305 * @param {!downloads.Data} data
306 * @return {string} User-visible status update text.
309 getStatusText_: function(data) {
310 switch (data.state) {
311 case downloads.States.IN_PROGRESS:
312 case downloads.States.PAUSED: // Fallthrough.
313 assert(typeof data.progress_status_text == 'string');
314 return data.progress_status_text;
315 case downloads.States.CANCELLED:
316 return loadTimeData.getString('statusCancelled');
317 case downloads.States.DANGEROUS:
318 break; // Intentionally hit assertNotReached(); at bottom.
319 case downloads.States.INTERRUPTED:
320 assert(typeof data.last_reason_text == 'string');
321 return data.last_reason_text;
322 case downloads.States.COMPLETE:
323 return data.file_externally_removed ?
324 loadTimeData.getString('statusRemoved') : '';
334 onSafeDragstart_: function(e) {
336 chrome.send('drag', [this.id_]);
343 onFileLinkClick_: function(e) {
345 chrome.send('openFile', [this.id_]);
349 onShowClick_: function() {
350 chrome.send('show', [this.id_]);
354 onPauseClick_: function() {
355 chrome.send('pause', [this.id_]);
359 onResumeClick_: function() {
360 chrome.send('resume', [this.id_]);
364 onSafeRemoveClick_: function() {
365 chrome.send('remove', [this.id_]);
369 onCancelClick_: function() {
370 chrome.send('cancel', [this.id_]);
374 onRestoreClick_: function() {
379 onSaveClick_: function() {
380 chrome.send('saveDangerous', [this.id_]);
384 onDangerRemoveClick_: function() {
385 this.onDiscardClick_();
389 onDiscardClick_: function() {
390 chrome.send('discardDangerous', [this.id_]);
394 return {ItemView: ItemView};