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
;
9 * Creates and updates the DOM representation for a download.
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);
61 /** Progress meter constants. */
63 /** @const {number} */
64 START_ANGLE
: -0.5 * Math
.PI
,
65 /** @const {number} */
69 /** @const {number} */
70 ItemView
.Progress
.HALF
= ItemView
.Progress
.SIDE
/ 2;
72 ItemView
.computeDownloadProgress = function() {
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.
79 function floatEq(a
, b
, opt_pct
) {
80 return Math
.abs(a
- b
) < (Math
.min(a
, b
) * (opt_pct
|| 1.0) / 100.0);
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.
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
;
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
);
106 * @return {!HTMLImageElement} The correct <img> to show when an item is
107 * progressing in the foreground.
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
;
117 return ItemView
.foregroundImages_
[x
];
120 /** @private {Array<{img: HTMLImageElement, url: string}>} */
121 ItemView
.iconsToLoad_
= [];
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.
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_();
135 ItemView
.loadNextIcon_ = function() {
136 if (ItemView
.isIconLoading_
)
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
)
148 img
.onabort
= img
.onerror
= img
.onload = function() {
149 ItemView
.isIconLoading_
= false;
150 ItemView
.loadNextIcon_();
153 img
.src
= request
.url
;
157 // If we reached here, there's no more work to do.
158 ItemView
.isIconLoading_
= false;
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
;
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
;
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
&&
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
;
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
;
246 this.foregroundProgress_
.width
= ItemView
.Progress
.width
;
247 this.foregroundProgress_
.height
= ItemView
.Progress
.height
;
249 if (!this.progressContext_
) {
251 this.progressContext_
= /** @type !CanvasRenderingContext2D */(
252 this.foregroundProgress_
.getContext('2d'));
255 var foregroundImage
= ItemView
.getForegroundProgressImage();
257 // Draw a pie-slice for the progress.
258 this.progressContext_
.globalCompositeOperation
= 'copy';
259 this.progressContext_
.drawImage(
262 foregroundImage
.width
,
263 foregroundImage
.height
,
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
,
281 this.progressContext_
.lineTo(ItemView
.Progress
.centerX
,
282 ItemView
.Progress
.centerY
);
283 this.progressContext_
.fill();
284 this.progressContext_
.closePath();
289 destroy: function() {
290 if (this.node
.parentNode
)
291 this.node
.parentNode
.removeChild(this.node
);
295 * @param {string} selector A CSS selector (e.g. '.class-name').
296 * @return {!Element} The element found by querying for |selector|.
299 queryRequired_: function(selector
) {
300 return assert(this.node
.querySelector(selector
));
304 * Overwrite |el|'s textContent if it differs from |text|.
305 * @param {!Element} el
306 * @param {string} text
309 ensureTextIs_: function(el
, text
) {
310 if (el
.textContent
!= text
)
311 el
.textContent
= text
;
315 * @param {!downloads.Data} data
316 * @return {string} Text describing the danger of a download. Empty if not
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',
331 case Item
.DangerType
.POTENTIALLY_UNWANTED
:
332 return loadTimeData
.getStringF('danger_settings_desc',
340 * @param {!downloads.Data} data
341 * @return {string} User-visible status update text.
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') : '';
367 onSafeDragstart_: function(e
) {
369 chrome
.send('drag', [this.id_
]);
376 onFileLinkClick_: function(e
) {
378 chrome
.send('openFile', [this.id_
]);
382 onShowClick_: function() {
383 chrome
.send('show', [this.id_
]);
387 onPauseClick_: function() {
388 chrome
.send('pause', [this.id_
]);
392 onResumeClick_: function() {
393 chrome
.send('resume', [this.id_
]);
397 onSafeRemoveClick_: function() {
398 chrome
.send('remove', [this.id_
]);
402 onCancelClick_: function() {
403 chrome
.send('cancel', [this.id_
]);
407 onRestoreClick_: function() {
412 onSaveClick_: function() {
413 chrome
.send('saveDangerous', [this.id_
]);
417 onDangerRemoveClick_: function() {
418 this.onDiscardClick_();
422 onDiscardClick_: function() {
423 chrome
.send('discardDangerous', [this.id_
]);
427 return {ItemView
: ItemView
};