Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / resources / downloads / item_view.js
blobf6112270a76f513c101be4fd0bace0325940d195
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() {
6   /** @const */ var Item = downloads.Item;
8   /**
9    * Creates and updates the DOM representation for a download.
10    * @constructor
11    */
12   function ItemView() {
13     this.node = $('templates').querySelector('.download').cloneNode(true);
15     this.safe_ = this.queryRequired_('.safe');
16     this.since_ = this.queryRequired_('.since');
17     this.dateContainer = this.queryRequired_('.date-container');
18     this.date_ = this.queryRequired_('.date');
19     this.save_ = this.queryRequired_('.save');
20     this.backgroundProgress_ = this.queryRequired_('.progress.background');
21     this.foregroundProgress_ = /** @type !HTMLCanvasElement */(
22         this.queryRequired_('canvas.progress'));
23     this.safeImg_ = /** @type !HTMLImageElement */(
24         this.queryRequired_('.safe img'));
25     this.fileName_ = this.queryRequired_('span.name');
26     this.fileLink_ = this.queryRequired_('[is="action-link"].name');
27     this.status_ = this.queryRequired_('.status');
28     this.srcUrl_ = this.queryRequired_('.src-url');
29     this.show_ = this.queryRequired_('.show');
30     this.retry_ = this.queryRequired_('.retry');
31     this.pause_ = this.queryRequired_('.pause');
32     this.resume_ = this.queryRequired_('.resume');
33     this.safeRemove_ = this.queryRequired_('.safe .remove');
34     this.cancel_ = this.queryRequired_('.cancel');
35     this.controlledBy_ = this.queryRequired_('.controlled-by');
37     this.dangerous_ = this.queryRequired_('.dangerous');
38     this.dangerImg_ = /** @type {!HTMLImageElement} */(
39         this.queryRequired_('.dangerous img'));
40     this.description_ = this.queryRequired_('.description');
41     this.malwareControls_ = this.queryRequired_('.dangerous .controls');
42     this.restore_ = this.queryRequired_('.restore');
43     this.dangerRemove_ = this.queryRequired_('.dangerous .remove');
44     this.save_ = this.queryRequired_('.save');
45     this.discard_ = this.queryRequired_('.discard');
47     // Event handlers (bound once on creation).
48     this.safe_.ondragstart = this.onSafeDragstart_.bind(this);
49     this.fileLink_.onclick = this.onFileLinkClick_.bind(this);
50     this.show_.onclick = this.onShowClick_.bind(this);
51     this.pause_.onclick = this.onPauseClick_.bind(this);
52     this.resume_.onclick = this.onResumeClick_.bind(this);
53     this.safeRemove_.onclick = this.onSafeRemoveClick_.bind(this);
54     this.cancel_.onclick = this.onCancelClick_.bind(this);
55     this.restore_.onclick = this.onRestoreClick_.bind(this);
56     this.save_.onclick = this.onSaveClick_.bind(this);
57     this.dangerRemove_.onclick = this.onDangerRemoveClick_.bind(this);
58     this.discard_.onclick = this.onDiscardClick_.bind(this);
59   }
61   /** Progress meter constants. */
62   ItemView.Progress = {
63     /** @const {number} */
64     START_ANGLE: -0.5 * Math.PI,
65     /** @const {number} */
66     SIDE: 48,
67   };
69   /** @const {number} */
70   ItemView.Progress.HALF = ItemView.Progress.SIDE / 2;
72   ItemView.computeDownloadProgress = function() {
73     /**
74      * @param {number} a Some float.
75      * @param {number} b Some float.
76      * @param {number=} opt_pct Percent of min(a,b).
77      * @return {boolean} true if a is within opt_pct percent of b.
78      */
79     function floatEq(a, b, opt_pct) {
80       return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0);
81     }
83     if (floatEq(ItemView.Progress.scale, window.devicePixelRatio)) {
84       // Zooming in or out multiple times then typing Ctrl+0 resets the zoom
85       // level directly to 1x, which fires the matchMedia event multiple times.
86       return;
87     }
88     var Progress = ItemView.Progress;
89     Progress.scale = window.devicePixelRatio;
90     Progress.width = Progress.SIDE * Progress.scale;
91     Progress.height = Progress.SIDE * Progress.scale;
92     Progress.radius = Progress.HALF * Progress.scale;
93     Progress.centerX = Progress.HALF * Progress.scale;
94     Progress.centerY = Progress.HALF * Progress.scale;
95   };
96   ItemView.computeDownloadProgress();
98   // Listens for when device-pixel-ratio changes between any zoom level.
99   [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].
100       forEach(function(scale) {
101     var media = '(-webkit-min-device-pixel-ratio:' + scale + ')';
102     window.matchMedia(media).addListener(ItemView.computeDownloadProgress);
103   });
105   /**
106    * @return {!HTMLImageElement} The correct <img> to show when an item is
107    *     progressing in the foreground.
108    */
109   ItemView.getForegroundProgressImage = function() {
110     var x = window.devicePixelRatio >= 2 ? '2x' : '1x';
111     ItemView.foregroundImages_ = ItemView.foregroundImages_ || {};
112     if (!ItemView.foregroundImages_[x]) {
113       ItemView.foregroundImages_[x] = new Image;
114       var IMAGE_URL = 'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32';
115       ItemView.foregroundImages_[x].src = IMAGE_URL + '@' + x;
116     }
117     return ItemView.foregroundImages_[x];
118   };
120   /** @private {Array<{img: HTMLImageElement, url: string}>} */
121   ItemView.iconsToLoad_ = [];
123   /**
124    * Load the provided |url| into |img.src| after appending ?scale=.
125    * @param {!HTMLImageElement} img An <img> to show the loaded image in.
126    * @param {string} url A remote image URL to load.
127    */
128   ItemView.loadScaledIcon = function(img, url) {
129     var scale = '?scale=' + window.devicePixelRatio + 'x';
130     ItemView.iconsToLoad_.push({img: img, url: url + scale});
131     ItemView.loadNextIcon_();
132   };
134   /** @private */
135   ItemView.loadNextIcon_ = function() {
136     if (ItemView.isIconLoading_)
137       return;
139     ItemView.isIconLoading_ = true;
141     while (ItemView.iconsToLoad_.length) {
142       var request = ItemView.iconsToLoad_.shift();
143       var img = request.img;
145       if (img.src == request.url)
146         continue;
148       img.onabort = img.onerror = img.onload = function() {
149         ItemView.isIconLoading_ = false;
150         ItemView.loadNextIcon_();
151       };
153       img.src = request.url;
154       return;
155     }
157     // If we reached here, there's no more work to do.
158     ItemView.isIconLoading_ = false;
159   };
161   ItemView.prototype = {
162     /** @param {!downloads.Data} data */
163     update: function(data) {
164       assert(!this.id_ || data.id == this.id_);
165       this.id_ = data.id;  // This is the only thing saved from |data|.
167       this.node.classList.toggle('otr', data.otr);
169       var dangerText = this.getDangerText_(data);
170       this.dangerous_.hidden = !dangerText;
171       this.safe_.hidden = !!dangerText;
173       if (dangerText) {
174         this.ensureTextIs_(this.description_, dangerText);
176         var dangerousFile = data.danger_type == Item.DangerType.DANGEROUS_FILE;
177         this.description_.classList.toggle('malware', !dangerousFile);
179         var idr = dangerousFile ? 'IDR_WARNING' : 'IDR_SAFEBROWSING_WARNING';
180         ItemView.loadScaledIcon(this.dangerImg_, 'chrome://theme/' + idr);
182         var showMalwareControls =
183             data.danger_type == Item.DangerType.DANGEROUS_CONTENT ||
184             data.danger_type == Item.DangerType.DANGEROUS_HOST ||
185             data.danger_type == Item.DangerType.DANGEROUS_URL ||
186             data.danger_type == Item.DangerType.POTENTIALLY_UNWANTED;
188         this.malwareControls_.hidden = !showMalwareControls;
189         this.discard_.hidden = showMalwareControls;
190         this.save_.hidden = showMalwareControls;
191       } else {
192         var path = encodeURIComponent(data.file_path);
193         ItemView.loadScaledIcon(this.safeImg_, 'chrome://fileicon/' + path);
195         /** @const */ var isInProgress = data.state == Item.States.IN_PROGRESS;
196         this.node.classList.toggle('in-progress', isInProgress);
198         /** @const */ var completelyOnDisk =
199             data.state == Item.States.COMPLETE && !data.file_externally_removed;
201         this.fileLink_.href = data.url;
202         this.ensureTextIs_(this.fileLink_, data.file_name);
203         this.fileLink_.hidden = !completelyOnDisk;
205         /** @const */ var isInterrupted = data.state == Item.States.INTERRUPTED;
206         this.fileName_.classList.toggle('interrupted', isInterrupted);
207         this.ensureTextIs_(this.fileName_, data.file_name);
208         this.fileName_.hidden = completelyOnDisk;
210         this.show_.hidden = !completelyOnDisk;
212         this.retry_.href = data.url;
213         this.retry_.hidden = !data.retry;
215         this.pause_.hidden = !isInProgress;
217         this.resume_.hidden = !data.resume;
219         /** @const */ var isPaused = data.state == Item.States.PAUSED;
220         /** @const */ var showCancel = isPaused || isInProgress;
221         this.cancel_.hidden = !showCancel;
223         this.safeRemove_.hidden = showCancel ||
224             !loadTimeData.getBoolean('allow_deleting_history');
226         /** @const */ var controlledByExtension = data.by_ext_id &&
227                                                   data.by_ext_name;
228         this.controlledBy_.hidden = !controlledByExtension;
229         if (controlledByExtension) {
230           var link = this.controlledBy_.querySelector('a');
231           link.href = 'chrome://extensions#' + data.by_ext_id;
232           link.setAttribute('column-type', 'controlled-by');
233           link.textContent = data.by_ext_name;
234         }
236         this.ensureTextIs_(this.since_, data.since_string);
237         this.ensureTextIs_(this.date_, data.date_string);
238         this.ensureTextIs_(this.srcUrl_, data.url);
239         this.srcUrl_.href = data.url;
240         this.ensureTextIs_(this.status_, this.getStatusText_(data));
242         this.foregroundProgress_.hidden = !isInProgress;
243         this.backgroundProgress_.hidden = !isInProgress;
245         if (isInProgress) {
246           this.foregroundProgress_.width = ItemView.Progress.width;
247           this.foregroundProgress_.height = ItemView.Progress.height;
249           if (!this.progressContext_) {
250             /** @private */
251             this.progressContext_ = /** @type !CanvasRenderingContext2D */(
252                 this.foregroundProgress_.getContext('2d'));
253           }
255           var foregroundImage = ItemView.getForegroundProgressImage();
257           // Draw a pie-slice for the progress.
258           this.progressContext_.globalCompositeOperation = 'copy';
259           this.progressContext_.drawImage(
260               foregroundImage,
261               0, 0,  // sx, sy
262               foregroundImage.width,
263               foregroundImage.height,
264               0, 0,  // x, y
265               ItemView.Progress.width, ItemView.Progress.height);
267           this.progressContext_.globalCompositeOperation = 'destination-in';
268           this.progressContext_.beginPath();
269           this.progressContext_.moveTo(ItemView.Progress.centerX,
270                                        ItemView.Progress.centerY);
272           // Draw an arc CW for both RTL and LTR. http://crbug.com/13215
273           this.progressContext_.arc(
274               ItemView.Progress.centerX,
275               ItemView.Progress.centerY,
276               ItemView.Progress.radius,
277               ItemView.Progress.START_ANGLE,
278               ItemView.Progress.START_ANGLE + Math.PI * 0.02 * data.percent,
279               false);
281           this.progressContext_.lineTo(ItemView.Progress.centerX,
282                                        ItemView.Progress.centerY);
283           this.progressContext_.fill();
284           this.progressContext_.closePath();
285         }
286       }
287     },
289     destroy: function() {
290       if (this.node.parentNode)
291         this.node.parentNode.removeChild(this.node);
292     },
294     /**
295      * @param {string} selector A CSS selector (e.g. '.class-name').
296      * @return {!Element} The element found by querying for |selector|.
297      * @private
298      */
299     queryRequired_: function(selector) {
300       return assert(this.node.querySelector(selector));
301     },
303     /**
304      * Overwrite |el|'s textContent if it differs from |text|.
305      * @param {!Element} el
306      * @param {string} text
307      * @private
308      */
309     ensureTextIs_: function(el, text) {
310       if (el.textContent != text)
311         el.textContent = text;
312     },
314     /**
315      * @param {!downloads.Data} data
316      * @return {string} Text describing the danger of a download. Empty if not
317      *     dangerous.
318      */
319     getDangerText_: function(data) {
320       switch (data.danger_type) {
321         case Item.DangerType.DANGEROUS_FILE:
322           return loadTimeData.getStringF('danger_file_desc', data.file_name);
323         case Item.DangerType.DANGEROUS_URL:
324           return loadTimeData.getString('danger_url_desc');
325         case Item.DangerType.DANGEROUS_CONTENT:  // Fall through.
326         case Item.DangerType.DANGEROUS_HOST:
327           return loadTimeData.getStringF('danger_content_desc', data.file_name);
328         case Item.DangerType.UNCOMMON_CONTENT:
329           return loadTimeData.getStringF('danger_uncommon_desc',
330                                          data.file_name);
331         case Item.DangerType.POTENTIALLY_UNWANTED:
332           return loadTimeData.getStringF('danger_settings_desc',
333                                          data.file_name);
334         default:
335           return '';
336       }
337     },
339     /**
340      * @param {!downloads.Data} data
341      * @return {string} User-visible status update text.
342      * @private
343      */
344     getStatusText_: function(data) {
345       switch (data.state) {
346         case Item.States.IN_PROGRESS:
347         case Item.States.PAUSED:  // Fallthrough.
348           return assert(data.progress_status_text);
349         case Item.States.CANCELLED:
350           return loadTimeData.getString('status_cancelled');
351         case Item.States.DANGEROUS:
352           break;  // Intentionally hit assertNotReached(); at bottom.
353         case Item.States.INTERRUPTED:
354           return assert(data.last_reason_text);
355         case Item.States.COMPLETE:
356           return data.file_externally_removed ?
357               loadTimeData.getString('status_removed') : '';
358       }
359       assertNotReached();
360       return '';
361     },
363     /**
364      * @private
365      * @param {Event} e
366      */
367     onSafeDragstart_: function(e) {
368       e.preventDefault();
369       chrome.send('drag', [this.id_]);
370     },
372     /**
373      * @param {Event} e
374      * @private
375      */
376     onFileLinkClick_: function(e) {
377       e.preventDefault();
378       chrome.send('openFile', [this.id_]);
379     },
381     /** @private */
382     onShowClick_: function() {
383       chrome.send('show', [this.id_]);
384     },
386     /** @private */
387     onPauseClick_: function() {
388       chrome.send('pause', [this.id_]);
389     },
391     /** @private */
392     onResumeClick_: function() {
393       chrome.send('resume', [this.id_]);
394     },
396     /** @private */
397     onSafeRemoveClick_: function() {
398       chrome.send('remove', [this.id_]);
399     },
401     /** @private */
402     onCancelClick_: function() {
403       chrome.send('cancel', [this.id_]);
404     },
406     /** @private */
407     onRestoreClick_: function() {
408       this.onSaveClick_();
409     },
411     /** @private */
412     onSaveClick_: function() {
413       chrome.send('saveDangerous', [this.id_]);
414     },
416     /** @private */
417     onDangerRemoveClick_: function() {
418       this.onDiscardClick_();
419     },
421     /** @private */
422     onDiscardClick_: function() {
423       chrome.send('discardDangerous', [this.id_]);
424     },
425   };
427   return {ItemView: ItemView};