1// Copyright (c) 2013 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.
4
5if (chrome.downloads.setShelfEnabled)
6  chrome.downloads.setShelfEnabled(false);
7
8var colors = {
9  progressColor: '#0d0',
10  arrow: '#555',
11  danger: 'red',
12  complete: 'green',
13  paused: 'grey',
14  background: 'white',
15};
16
17function drawLine(ctx, x1, y1, x2, y2) {
18  ctx.beginPath();
19  ctx.moveTo(x1, y1);
20  ctx.lineTo(x2, y2);
21  ctx.stroke();
22}
23
24Math.TAU = 2 * Math.PI;  // http://tauday.com/tau-manifesto
25
26function drawProgressArc(ctx, startAngle, endAngle) {
27  var center = ctx.canvas.width/2;
28  ctx.lineWidth = Math.round(ctx.canvas.width*0.1);
29  ctx.beginPath();
30  ctx.moveTo(center, center);
31  ctx.arc(center, center, center * 0.9, startAngle, endAngle, false);
32  ctx.fill();
33  ctx.stroke();
34}
35
36function drawUnknownProgressSpinner(ctx) {
37  var center = ctx.canvas.width/2;
38  const segments = 16;
39  var segArc = Math.TAU / segments;
40  for (var seg = 0; seg < segments; ++seg) {
41    ctx.fillStyle = ctx.strokeStyle = (
42      ((seg % 2) == 0) ? colors.progressColor : colors.background);
43    drawProgressArc(ctx, (seg-4)*segArc, (seg-3)*segArc);
44  }
45}
46
47function drawProgressSpinner(ctx, stage) {
48  ctx.fillStyle = ctx.strokeStyle = colors.progressColor;
49  var clocktop = -Math.TAU/4;
50  drawProgressArc(ctx, clocktop, clocktop + (stage * Math.TAU));
51}
52
53function drawArrow(ctx) {
54  ctx.beginPath();
55  ctx.lineWidth = Math.round(ctx.canvas.width*0.1);
56  ctx.lineJoin = 'round';
57  ctx.strokeStyle = ctx.fillStyle = colors.arrow;
58  var center = ctx.canvas.width/2;
59  var minw2 = center*0.2;
60  var maxw2 = center*0.60;
61  var height2 = maxw2;
62  ctx.moveTo(center-minw2, center-height2);
63  ctx.lineTo(center+minw2, center-height2);
64  ctx.lineTo(center+minw2, center);
65  ctx.lineTo(center+maxw2, center);
66  ctx.lineTo(center, center+height2);
67  ctx.lineTo(center-maxw2, center);
68  ctx.lineTo(center-minw2, center);
69  ctx.lineTo(center-minw2, center-height2);
70  ctx.lineTo(center+minw2, center-height2);
71  ctx.stroke();
72  ctx.fill();
73}
74
75function drawDangerBadge(ctx) {
76  var s = ctx.canvas.width/100;
77  ctx.fillStyle = colors.danger;
78  ctx.strokeStyle = colors.background;
79  ctx.lineWidth = Math.round(s*5);
80  var edge = ctx.canvas.width-ctx.lineWidth;
81  ctx.beginPath();
82  ctx.moveTo(s*75, s*55);
83  ctx.lineTo(edge, edge);
84  ctx.lineTo(s*55, edge);
85  ctx.lineTo(s*75, s*55);
86  ctx.lineTo(edge, edge);
87  ctx.fill();
88  ctx.stroke();
89}
90
91function drawPausedBadge(ctx) {
92  var s = ctx.canvas.width/100;
93  ctx.beginPath();
94  ctx.strokeStyle = colors.background;
95  ctx.lineWidth = Math.round(s*5);
96  ctx.rect(s*55, s*55, s*15, s*35);
97  ctx.fillStyle = colors.paused;
98  ctx.fill();
99  ctx.stroke();
100  ctx.rect(s*75, s*55, s*15, s*35);
101  ctx.fill();
102  ctx.stroke();
103}
104
105function drawCompleteBadge(ctx) {
106  var s = ctx.canvas.width/100;
107  ctx.beginPath();
108  ctx.arc(s*75, s*75, s*15, 0, Math.TAU, false);
109  ctx.fillStyle = colors.complete;
110  ctx.fill();
111  ctx.strokeStyle = colors.background;
112  ctx.lineWidth = Math.round(s*5);
113  ctx.stroke();
114}
115
116function drawIcon(side, options) {
117  var canvas = document.createElement('canvas');
118  canvas.width = canvas.height = side;
119  document.body.appendChild(canvas);
120  var ctx = canvas.getContext('2d');
121  if (options.anyInProgress) {
122    if (options.anyMissingTotalBytes) {
123      drawUnknownProgressSpinner(ctx);
124    } else {
125      drawProgressSpinner(ctx, (options.totalBytesReceived /
126                                options.totalTotalBytes));
127    }
128  }
129  drawArrow(ctx);
130  if (options.anyDangerous) {
131    drawDangerBadge(ctx);
132  } else if (options.anyPaused) {
133    drawPausedBadge(ctx);
134  } else if (options.anyRecentlyCompleted) {
135    drawCompleteBadge(ctx);
136  }
137  return canvas;
138}
139
140function maybeOpen(id) {
141  var openWhenComplete = [];
142  try {
143    openWhenComplete = JSON.parse(localStorage.openWhenComplete);
144  } catch (e) {
145    localStorage.openWhenComplete = JSON.stringify(openWhenComplete);
146  }
147  var openNowIndex = openWhenComplete.indexOf(id);
148  if (openNowIndex >= 0) {
149    chrome.downloads.open(id);
150    openWhenComplete.splice(openNowIndex, 1);
151    localStorage.openWhenComplete = JSON.stringify(openWhenComplete);
152  }
153}
154
155function setBrowserActionIcon(options) {
156  var canvas1 = drawIcon(19, options);
157  var canvas2 = drawIcon(38, options);
158  var imageData = {};
159  imageData['' + canvas1.width] = canvas1.getContext('2d').getImageData(
160        0, 0, canvas1.width, canvas1.height);
161  imageData['' + canvas2.width] = canvas2.getContext('2d').getImageData(
162        0, 0, canvas2.width, canvas2.height);
163  chrome.browserAction.setIcon({imageData:imageData});
164  canvas1.parentNode.removeChild(canvas1);
165  canvas2.parentNode.removeChild(canvas2);
166}
167
168function pollProgress() {
169  pollProgress.tid = -1;
170  chrome.downloads.search({}, function(items) {
171    var popupLastOpened = parseInt(localStorage.popupLastOpened);
172    var options = {anyMissingTotalBytes: false,
173                   anyInProgress: false,
174                   anyRecentlyCompleted: false,
175                   anyPaused: false,
176                   anyDangerous: false,
177                   totalBytesReceived: 0,
178                   totalTotalBytes: 0};
179    items.forEach(function(item) {
180      if (item.state == 'in_progress') {
181        options.anyInProgress = true;
182        if (item.totalBytes) {
183          options.totalTotalBytes += item.totalBytes;
184          options.totalBytesReceived += item.bytesReceived;
185        } else {
186          options.anyMissingTotalBytes = true;
187        }
188        var dangerous = ((item.danger != 'safe') &&
189                         (item.danger != 'accepted'));
190        options.anyDangerous = options.anyDangerous || dangerous;
191        options.anyPaused = options.anyPaused || item.paused;
192      } else if ((item.state == 'complete') && item.endTime && !item.error) {
193        options.anyRecentlyCompleted = (
194          options.anyRecentlyCompleted ||
195          ((new Date(item.endTime)).getTime() >= popupLastOpened));
196        maybeOpen(item.id);
197      }
198    });
199
200    var targetIcon = JSON.stringify(options);
201    if (sessionStorage.currentIcon != targetIcon) {
202      setBrowserActionIcon(options);
203      sessionStorage.currentIcon = targetIcon;
204    }
205
206    if (options.anyInProgress &&
207        (pollProgress.tid < 0)) {
208      pollProgress.start();
209    }
210  });
211}
212pollProgress.tid = -1;
213pollProgress.MS = 200;
214
215pollProgress.start = function() {
216  if (pollProgress.tid < 0) {
217    pollProgress.tid = setTimeout(pollProgress, pollProgress.MS);
218  }
219};
220
221function isNumber(n) {
222  return !isNaN(parseFloat(n)) && isFinite(n);
223}
224
225if (!isNumber(localStorage.popupLastOpened)) {
226  localStorage.popupLastOpened = '' + (new Date()).getTime();
227}
228
229chrome.downloads.onCreated.addListener(function(item) {
230  pollProgress();
231});
232
233pollProgress();
234
235function openWhenComplete(downloadId) {
236  var ids = [];
237  try {
238    ids = JSON.parse(localStorage.openWhenComplete);
239  } catch (e) {
240    localStorage.openWhenComplete = JSON.stringify(ids);
241  }
242  pollProgress.start();
243  if (ids.indexOf(downloadId) >= 0) {
244    return;
245  }
246  ids.push(downloadId);
247  localStorage.openWhenComplete = JSON.stringify(ids);
248}
249
250chrome.runtime.onMessage.addListener(function(request) {
251  if (request == 'poll') {
252    pollProgress.start();
253  }
254  if (request == 'icons') {
255    [16, 19, 38, 128].forEach(function(s) {
256      var canvas = drawIcon(s);
257      chrome.downloads.download({
258        url: canvas.toDataURL('image/png', 1.0),
259        filename: 'icon' + s + '.png',
260      });
261      canvas.parentNode.removeChild(canvas);
262    });
263  }
264  if (isNumber(request.openWhenComplete)) {
265    openWhenComplete(request.openWhenComplete);
266  }
267});
268