Home | History | Annotate | Download | only in download_manager
      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 
      5 function pointInElement(p, elem) {
      6   return ((p.x >= elem.offsetLeft) &&
      7           (p.x <= (elem.offsetLeft + elem.offsetWidth)) &&
      8           (p.y >= elem.offsetTop) &&
      9           (p.y <= (elem.offsetTop + elem.offsetHeight)));
     10 };
     11 
     12 function setLastOpened() {
     13   localStorage.popupLastOpened = (new Date()).getTime();
     14   chrome.runtime.sendMessage('poll');
     15 };
     16 
     17 function loadI18nMessages() {
     18   function setProperty(selector, prop, msg) {
     19     document.querySelector(selector)[prop] = chrome.i18n.getMessage(msg);
     20   }
     21 
     22   setProperty('title', 'innerText', 'tabTitle');
     23   setProperty('#q', 'placeholder', 'searchPlaceholder');
     24   setProperty('#clear-all', 'title', 'clearAllTitle');
     25   setProperty('#open-folder', 'title', 'openDownloadsFolderTitle');
     26   setProperty('#empty', 'innerText', 'zeroItems');
     27   setProperty('#searching', 'innerText', 'searching');
     28   setProperty('#search-zero', 'innerText', 'zeroSearchResults');
     29   setProperty('#management-permission-info', 'innerText',
     30               'managementPermissionInfo');
     31   setProperty('#grant-management-permission', 'innerText',
     32               'grantManagementPermission');
     33   setProperty('#older', 'innerText', 'showOlderDownloads');
     34   setProperty('#loading-older', 'innerText', 'loadingOlderDownloads');
     35   setProperty('.pause', 'title', 'pauseTitle');
     36   setProperty('.resume', 'title', 'resumeTitle');
     37   setProperty('.cancel', 'title', 'cancelTitle');
     38   setProperty('.show-folder', 'title', 'showInFolderTitle');
     39   setProperty('.erase', 'title', 'eraseTitle');
     40   setProperty('.url', 'title', 'retryTitle');
     41   setProperty('.referrer', 'title', 'referrerTitle');
     42   setProperty('.open-filename', 'title', 'openTitle');
     43   setProperty('#bad-chrome-version', 'innerText', 'badChromeVersion');
     44   setProperty('.remove-file', 'title', 'removeFileTitle');
     45 
     46   document.querySelector('.progress').style.minWidth =
     47     getTextWidth(formatBytes(1024 * 1024 * 1023.9) + '/' +
     48                  formatBytes(1024 * 1024 * 1023.9)) + 'px';
     49 
     50   // This only covers {timeLeft,openWhenComplete}{Finishing,Days}. If
     51   // ...Hours/Minutes/Seconds could be longer for any locale, then this should
     52   // test them.
     53   var max_time_left_width = 0;
     54   for (var i = 0; i < 4; ++i) {
     55     max_time_left_width = Math.max(max_time_left_width, getTextWidth(
     56         formatTimeLeft(0 == (i % 2),
     57                        (i < 2) ? 0 : ((100 * 24) + 23) * 60 * 60 * 1000)));
     58   }
     59   document.querySelector('body div.item span.time-left').style.minWidth =
     60      max_time_left_width + 'px';
     61 };
     62 
     63 function getTextWidth(s) {
     64   var probe = document.getElementById('text-width-probe');
     65   probe.innerText = s;
     66   return probe.offsetWidth;
     67 };
     68 
     69 function formatDateTime(date) {
     70   var now = new Date();
     71   var zpad_mins = ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
     72   if (date.getYear() != now.getYear()) {
     73     return '' + (1900 + date.getYear());
     74   } else if ((date.getMonth() != now.getMonth()) ||
     75              (date.getDate() != now.getDate())) {
     76     return date.getDate() + ' ' + chrome.i18n.getMessage(
     77       'month' + date.getMonth() + 'abbr');
     78   } else if (date.getHours() == 12) {
     79     return '12' + zpad_mins + 'pm';
     80   } else if (date.getHours() > 12) {
     81     return (date.getHours() - 12) + zpad_mins + 'pm';
     82   }
     83   return date.getHours() + zpad_mins + 'am';
     84 }
     85 
     86 function formatBytes(n) {
     87   if (n < 1024) {
     88     return n + 'B';
     89   }
     90   var prefixes = 'KMGTPEZY';
     91   var mul = 1024;
     92   for (var i = 0; i < prefixes.length; ++i) {
     93     if (n < (1024 * mul)) {
     94       return (parseInt(n / mul) + '.' + parseInt(10 * ((n / mul) % 1)) +
     95               prefixes[i] + 'B');
     96     }
     97     mul *= 1024;
     98   }
     99   return '!!!';
    100 }
    101 
    102 function formatTimeLeft(openWhenComplete, ms) {
    103   var prefix = openWhenComplete ? 'openWhenComplete' : 'timeLeft';
    104   if (ms < 1000) {
    105     return chrome.i18n.getMessage(prefix + 'Finishing');
    106   }
    107   var days = parseInt(ms / (24 * 60 * 60 * 1000));
    108   var hours = parseInt(ms / (60 * 60 * 1000)) % 24;
    109   if (days) {
    110     return chrome.i18n.getMessage(prefix + 'Days', [days, hours]);
    111   }
    112   var minutes = parseInt(ms / (60 * 1000)) % 60;
    113   if (hours) {
    114     return chrome.i18n.getMessage(prefix + 'Hours', [hours, minutes]);
    115   }
    116   var seconds = parseInt(ms / 1000) % 60;
    117   if (minutes) {
    118     return chrome.i18n.getMessage(prefix + 'Minutes', [minutes, seconds]);
    119   }
    120   return chrome.i18n.getMessage(prefix + 'Seconds', [seconds]);
    121 }
    122 
    123 function ratchetWidth(w) {
    124   var current = parseInt(document.body.style.minWidth) || 0;
    125   document.body.style.minWidth = Math.max(w, current) + 'px';
    126 }
    127 
    128 function ratchetHeight(h) {
    129   var current = parseInt(document.body.style.minHeight) || 0;
    130   document.body.style.minHeight = Math.max(h, current) + 'px';
    131 }
    132 
    133 function binarySearch(array, target, cmp) {
    134   var low = 0, high = array.length - 1, i, comparison;
    135   while (low <= high) {
    136     i = (low + high) >> 1;
    137     comparison = cmp(target, array[i]);
    138     if (comparison < 0) {
    139       low = i + 1;
    140     } else if (comparison > 0) {
    141       high = i - 1;
    142     } else {
    143       return i;
    144     }
    145   }
    146   return i;
    147 };
    148 
    149 function arrayFrom(seq) {
    150   return Array.prototype.slice.apply(seq);
    151 };
    152 
    153 function DownloadItem(data) {
    154   var item = this;
    155   for (var prop in data) {
    156     item[prop] = data[prop];
    157   }
    158   item.startTime = new Date(item.startTime);
    159   if (item.canResume == undefined) {
    160     DownloadItem.canResumeHack = true;
    161   }
    162 
    163   item.div = document.querySelector('body>div.item').cloneNode(true);
    164   item.div.id = 'item' + item.id;
    165   item.div.item = item;
    166 
    167   var items_div = document.getElementById('items');
    168   if ((items_div.childNodes.length == 0) ||
    169       (item.startTime.getTime() < items_div.childNodes[
    170        items_div.childNodes.length - 1].item.startTime.getTime())) {
    171     items_div.appendChild(item.div);
    172   } else if (item.startTime.getTime() >
    173              items_div.childNodes[0].item.startTime.getTime()) {
    174     items_div.insertBefore(item.div, items_div.childNodes[0]);
    175   } else {
    176     var adjacent_div = items_div.childNodes[
    177       binarySearch(arrayFrom(items_div.childNodes),
    178                    item.startTime.getTime(),
    179                    function(target, other) {
    180           return target - other.item.startTime.getTime();
    181     })];
    182     var adjacent_item = adjacent_div.item;
    183     if (adjacent_item.startTime.getTime() < item.startTime.getTime()) {
    184       items_div.insertBefore(item.div, adjacent_div);
    185     } else {
    186       items_div.insertBefore(item.div, adjacent_div.nextSibling);
    187     }
    188   }
    189 
    190   item.getElement('referrer').onclick = function() {
    191     chrome.tabs.create({url: item.referrer});
    192     return false;
    193   };
    194   item.getElement('by-ext').onclick = function() {
    195     chrome.tabs.create({url: 'chrome://extensions#' + item.byExtensionId});
    196     return false;
    197   }
    198   item.getElement('open-filename').onclick = function() {
    199     item.open();
    200     return false;
    201   };
    202   item.getElement('open-filename').ondragstart = function() {
    203     item.drag();
    204     return false;
    205   };
    206   item.getElement('pause').onclick = function() {
    207     item.pause();
    208     return false;
    209   };
    210   item.getElement('cancel').onclick = function() {
    211     item.cancel();
    212     return false;
    213   };
    214   item.getElement('resume').onclick = function() {
    215     item.resume();
    216     return false;
    217   };
    218   item.getElement('show-folder').onclick = function() {
    219     item.show();
    220     return false;
    221   };
    222   item.getElement('remove-file').onclick = function() {
    223     item.removeFile();
    224     return false;
    225   };
    226   item.getElement('erase').onclick = function() {
    227     item.erase();
    228     return false;
    229   };
    230 
    231   item.more_mousemove = function(evt) {
    232     var mouse = {x:evt.x, y:evt.y+document.body.scrollTop};
    233     if (item.getElement('more') &&
    234         (pointInElement(mouse, item.div) ||
    235          pointInElement(mouse, item.getElement('more')))) {
    236       return;
    237     }
    238     if (item.getElement('more')) {
    239       item.getElement('more').hidden = true;
    240     }
    241     window.removeEventListener('mousemove', item.more_mousemove);
    242   };
    243   [item.div, item.getElement('more')].concat(
    244       item.getElement('more').children).forEach(function(elem) {
    245     elem.onmouseover = function() {
    246       arrayFrom(items_div.children).forEach(function(other) {
    247         if (other.item != item) {
    248           other.item.getElement('more').hidden = true;
    249         }
    250       });
    251       item.getElement('more').hidden = false;
    252       item.getElement('more').style.top =
    253         (item.div.offsetTop + item.div.offsetHeight) + 'px';
    254       item.getElement('more').style.left = item.div.offsetLeft + 'px';
    255       if (window.innerHeight < (parseInt(item.getElement('more').style.top) +
    256                                 item.getElement('more').offsetHeight)) {
    257         item.getElement('more').style.top = (
    258           item.div.offsetTop - item.getElement('more').offsetHeight) + 'px';
    259       }
    260       window.addEventListener('mousemove', item.more_mousemove);
    261     };
    262   });
    263 
    264   if (item.referrer) {
    265     item.getElement('referrer').href = item.referrer;
    266   } else {
    267     item.getElement('referrer').hidden = true;
    268   }
    269   item.getElement('url').href = item.url;
    270   item.getElement('url').innerText = item.url;
    271   item.render();
    272 }
    273 DownloadItem.canResumeHack = false;
    274 
    275 DownloadItem.prototype.getElement = function(name) {
    276   return document.querySelector('#item' + this.id + ' .' + name);
    277 };
    278 
    279 DownloadItem.prototype.render = function() {
    280   var item = this;
    281   var now = new Date();
    282   var in_progress = (item.state == 'in_progress')
    283   var openable = (item.state != 'interrupted') && item.exists && !item.deleted;
    284 
    285   item.startTime = new Date(item.startTime);
    286   if (DownloadItem.canResumeHack) {
    287     item.canResume = in_progress && item.paused;
    288   }
    289   if (item.filename) {
    290     item.basename = item.filename.substring(Math.max(
    291       item.filename.lastIndexOf('\\'),
    292       item.filename.lastIndexOf('/')) + 1);
    293   }
    294   if (item.estimatedEndTime) {
    295     item.estimatedEndTime = new Date(item.estimatedEndTime);
    296   }
    297   if (item.endTime) {
    298     item.endTime = new Date(item.endTime);
    299   }
    300 
    301   if (item.filename && !item.icon_url) {
    302     chrome.downloads.getFileIcon(
    303       item.id,
    304       {'size': 32},
    305       function(icon_url) {
    306         item.getElement('icon').hidden = !icon_url;
    307         if (icon_url) {
    308           item.icon_url = icon_url;
    309           item.getElement('icon').src = icon_url;
    310         }
    311     });
    312   }
    313 
    314   item.getElement('removed').style.display = openable ? 'none' : 'inline';
    315   item.getElement('open-filename').style.display = (
    316     openable ? 'inline' : 'none');
    317   item.getElement('in-progress').hidden = !in_progress;
    318   item.getElement('pause').style.display = (
    319     !in_progress || item.paused) ? 'none' : 'inline-block';
    320   item.getElement('resume').style.display = (
    321     !in_progress || !item.canResume) ? 'none' : 'inline-block';
    322   item.getElement('cancel').style.display = (
    323     !in_progress ? 'none' : 'inline-block');
    324   item.getElement('remove-file').hidden = (
    325     (item.state != 'complete') ||
    326     !item.exists ||
    327     item.deleted ||
    328     !chrome.downloads.removeFile);
    329   item.getElement('erase').hidden = in_progress;
    330 
    331   var could_progress = in_progress || item.canResume;
    332   item.getElement('progress').style.display = (
    333     could_progress ? 'inline-block' : 'none');
    334   item.getElement('meter').hidden = !could_progress || !item.totalBytes;
    335 
    336   item.getElement('removed').innerText = item.basename;
    337   item.getElement('open-filename').innerText = item.basename;
    338 
    339   function setByExtension(show) {
    340     if (show) {
    341       item.getElement('by-ext').title = item.byExtensionName;
    342       item.getElement('by-ext').href =
    343         'chrome://extensions#' + item.byExtensionId;
    344       item.getElement('by-ext img').src =
    345         'chrome://extension-icon/' + item.byExtensionId + '/48/1';
    346     } else {
    347       item.getElement('by-ext').hidden = true;
    348     }
    349   }
    350   if (item.byExtensionId && item.byExtensionName) {
    351     chrome.permissions.contains({permissions: ['management']},
    352                                 function(result) {
    353       if (result) {
    354         setByExtension(true);
    355       } else {
    356         setByExtension(false);
    357         if (!localStorage.managementPermissionDenied) {
    358           document.getElementById('request-management-permission').hidden =
    359             false;
    360           document.getElementById('grant-management-permission').onclick =
    361               function() {
    362             chrome.permissions.request({permissions: ['management']},
    363                                       function(granted) {
    364               setByExtension(granted);
    365               if (!granted) {
    366                 localStorage.managementPermissionDenied = true;
    367               }
    368             });
    369             return false;
    370           };
    371         }
    372       }
    373     });
    374   } else {
    375     setByExtension(false);
    376   }
    377 
    378   if (!item.getElement('error').hidden) {
    379     if (item.error) {
    380       // TODO(benjhayden) When https://codereview.chromium.org/16924017/ is
    381       // released, set minimum_chrome_version and remove the error_N messages.
    382       item.getElement('error').innerText = chrome.i18n.getMessage(
    383           'error_' + item.error);
    384       if (!item.getElement('error').innerText) {
    385         item.getElement('error').innerText = item.error;
    386       }
    387     } else if (!openable) {
    388       item.getElement('error').innerText = chrome.i18n.getMessage(
    389           'errorRemoved');
    390     }
    391   }
    392 
    393   item.getElement('complete-size').innerText = formatBytes(
    394     item.bytesReceived);
    395   if (item.totalBytes && (item.state != 'complete')) {
    396     item.getElement('progress').innerText = (
    397       item.getElement('complete-size').innerText + '/' +
    398       formatBytes(item.totalBytes));
    399     item.getElement('meter').children[0].style.width = parseInt(
    400         100 * item.bytesReceived / item.totalBytes) + '%';
    401   }
    402 
    403   if (in_progress) {
    404     if (item.estimatedEndTime && !item.paused) {
    405       var openWhenComplete = false;
    406       try {
    407         openWhenComplete = JSON.parse(localStorage.openWhenComplete).indexOf(
    408             item.id) >= 0;
    409       } catch (e) {
    410       }
    411       item.getElement('time-left').innerText = formatTimeLeft(
    412           openWhenComplete, item.estimatedEndTime.getTime() - now.getTime());
    413     } else {
    414       item.getElement('time-left').innerText = String.fromCharCode(160);
    415     }
    416   }
    417 
    418   if (item.startTime) {
    419     item.getElement('start-time').innerText = formatDateTime(
    420         item.startTime);
    421   }
    422 
    423   ratchetWidth(item.getElement('icon').offsetWidth +
    424                item.getElement('file-url').offsetWidth +
    425                item.getElement('cancel').offsetWidth +
    426                item.getElement('pause').offsetWidth +
    427                item.getElement('resume').offsetWidth);
    428   ratchetWidth(item.getElement('more').offsetWidth);
    429 
    430   this.maybeAccept();
    431 };
    432 
    433 DownloadItem.prototype.onChanged = function(delta) {
    434   for (var key in delta) {
    435     if (key != 'id') {
    436       this[key] = delta[key].current;
    437     }
    438   }
    439   this.render();
    440   if (delta.state) {
    441     setLastOpened();
    442   }
    443   if ((this.state == 'in_progress') && !this.paused) {
    444     DownloadManager.startPollingProgress();
    445   }
    446 };
    447 
    448 DownloadItem.prototype.onErased = function() {
    449   window.removeEventListener('mousemove', this.more_mousemove);
    450   document.getElementById('items').removeChild(this.div);
    451 };
    452 
    453 DownloadItem.prototype.drag = function() {
    454   chrome.downloads.drag(this.id);
    455 };
    456 
    457 DownloadItem.prototype.show = function() {
    458   chrome.downloads.show(this.id);
    459 };
    460 
    461 DownloadItem.prototype.open = function() {
    462   if (this.state == 'complete') {
    463     chrome.downloads.open(this.id);
    464     return;
    465   }
    466   chrome.runtime.sendMessage({openWhenComplete:this.id});
    467 };
    468 
    469 DownloadItem.prototype.removeFile = function() {
    470   chrome.downloads.removeFile(this.id);
    471   this.deleted = true;
    472   this.render();
    473 };
    474 
    475 DownloadItem.prototype.erase = function() {
    476   chrome.downloads.erase({id: this.id});
    477 };
    478 
    479 DownloadItem.prototype.pause = function() {
    480   chrome.downloads.pause(this.id);
    481 };
    482 
    483 DownloadItem.prototype.resume = function() {
    484   chrome.downloads.resume(this.id);
    485 };
    486 
    487 DownloadItem.prototype.cancel = function() {
    488   chrome.downloads.cancel(this.id);
    489 };
    490 
    491 DownloadItem.prototype.maybeAccept = function() {
    492   // This function is safe to call at any time for any item, and it will always
    493   // do the right thing, which is to display the danger prompt only if the item
    494   // is in_progress and dangerous, and if the prompt is not already displayed.
    495   if ((this.state != 'in_progress') ||
    496       (this.danger == 'safe') ||
    497       (this.danger == 'accepted') ||
    498       DownloadItem.prototype.maybeAccept.accepting_danger) {
    499     return;
    500   }
    501   ratchetWidth(400);
    502   ratchetHeight(200);
    503   DownloadItem.prototype.maybeAccept.accepting_danger = true;
    504   // On Mac, window.onload is run while the popup is animating in, before it is
    505   // considered "visible". Prompts will not be displayed over an invisible
    506   // window, so the popup will become stuck. Just wait a little bit for the
    507   // window to finish animating in. http://crbug.com/280107
    508   // This has been fixed, so this setTimeout can be removed when the fix has
    509   // been released to stable, and minimum_chrome_version can be set.
    510   var id = this.id;
    511   setTimeout(function() {
    512     chrome.downloads.acceptDanger(id, function() {
    513       DownloadItem.prototype.maybeAccept.accepting_danger = false;
    514       arrayFrom(document.getElementById('items').childNodes).forEach(
    515         function(item_div) { item_div.item.maybeAccept(); });
    516     });
    517   }, 500);
    518 };
    519 DownloadItem.prototype.maybeAccept.accepting_danger = false;
    520 
    521 var DownloadManager = {};
    522 
    523 DownloadManager.showingOlder = false;
    524 
    525 DownloadManager.getItem = function(id) {
    526   var item_div = document.getElementById('item' + id);
    527   return item_div ? item_div.item : null;
    528 };
    529 
    530 DownloadManager.getOrCreate = function(data) {
    531   var item = DownloadManager.getItem(data.id);
    532   return item ? item : new DownloadItem(data);
    533 };
    534 
    535 DownloadManager.forEachItem = function(cb) {
    536   // Calls cb(item, index) in the order that they are displayed, i.e. in order
    537   // of decreasing startTime.
    538   arrayFrom(document.getElementById('items').childNodes).forEach(
    539     function(item_div, index) { cb(item_div.item, index); });
    540 };
    541 
    542 DownloadManager.startPollingProgress = function() {
    543   if (DownloadManager.startPollingProgress.tid < 0) {
    544     DownloadManager.startPollingProgress.tid = setTimeout(
    545       DownloadManager.startPollingProgress.pollProgress,
    546       DownloadManager.startPollingProgress.MS);
    547   }
    548 }
    549 DownloadManager.startPollingProgress.MS = 200;
    550 DownloadManager.startPollingProgress.tid = -1;
    551 DownloadManager.startPollingProgress.pollProgress = function() {
    552   DownloadManager.startPollingProgress.tid = -1;
    553   chrome.downloads.search({state: 'in_progress', paused: false},
    554       function(results) {
    555     if (!results.length)
    556       return;
    557     results.forEach(function(result) {
    558       var item = DownloadManager.getOrCreate(result);
    559       for (var prop in result) {
    560         item[prop] = result[prop];
    561       }
    562       item.render();
    563       if ((item.state == 'in_progress') && !item.paused) {
    564         DownloadManager.startPollingProgress();
    565       }
    566     });
    567   });
    568 };
    569 
    570 DownloadManager.showNew = function() {
    571   var any_items = (document.getElementById('items').childNodes.length > 0);
    572   document.getElementById('empty').style.display =
    573     any_items ? 'none' : 'inline-block';
    574   document.getElementById('head').style.borderBottomWidth =
    575     (any_items ? 1 : 0) + 'px';
    576   document.getElementById('clear-all').hidden = !any_items;
    577 
    578   var query_search = document.getElementById('q');
    579   query_search.hidden = !any_items;
    580 
    581   if (!any_items) {
    582     return;
    583   }
    584   var old_ms = (new Date()).getTime() - kOldMs;
    585   var any_hidden = false;
    586   var any_showing = false;
    587   // First show up to kShowNewMax items newer than kOldMs. If there aren't any
    588   // items newer than kOldMs, then show up to kShowNewMax items of any age. If
    589   // there are any hidden items, show the Show Older button.
    590   DownloadManager.forEachItem(function(item, index) {
    591     item.div.hidden = !DownloadManager.showingOlder && (
    592       (item.startTime.getTime() < old_ms) || (index >= kShowNewMax));
    593     any_hidden = any_hidden || item.div.hidden;
    594     any_showing = any_showing || !item.div.hidden;
    595   });
    596   if (!any_showing) {
    597     any_hidden = false;
    598     DownloadManager.forEachItem(function(item, index) {
    599       item.div.hidden = !DownloadManager.showingOlder && (index >= kShowNewMax);
    600       any_hidden = any_hidden || item.div.hidden;
    601       any_showing = any_showing || !item.div.hidden;
    602     });
    603   }
    604   document.getElementById('older').hidden = !any_hidden;
    605 
    606   query_search.focus();
    607 };
    608 
    609 DownloadManager.showOlder = function() {
    610   DownloadManager.showingOlder = true;
    611   var loading_older_span = document.getElementById('loading-older');
    612   document.getElementById('older').hidden = true;
    613   loading_older_span.hidden = false;
    614   chrome.downloads.search({}, function(results) {
    615     results.forEach(function(result) {
    616       var item = DownloadManager.getOrCreate(result);
    617       item.div.hidden = false;
    618     });
    619     loading_older_span.hidden = true;
    620   });
    621 };
    622 
    623 DownloadManager.onSearch = function() {
    624   // split string by space, but ignore space in quotes
    625   // http://stackoverflow.com/questions/16261635
    626   var query = document.getElementById('q').value.match(/(?:[^\s"]+|"[^"]*")+/g);
    627   if (!query) {
    628     DownloadManager.showNew();
    629     document.getElementById('search-zero').hidden = true;
    630   } else {
    631     query = query.map(function(term) {
    632       // strip quotes
    633       return (term.match(/\s/) &&
    634               term[0].match(/["']/) &&
    635               term[term.length - 1] == term[0]) ?
    636         term.substr(1, term.length - 2) : term;
    637     });
    638     var searching = document.getElementById('searching');
    639     searching.hidden = false;
    640     chrome.downloads.search({query: query}, function(results) {
    641       document.getElementById('older').hidden = true;
    642       DownloadManager.forEachItem(function(item) {
    643         item.div.hidden = true;
    644       });
    645       results.forEach(function(result) {
    646         DownloadManager.getOrCreate(result).div.hidden = false;
    647       });
    648       searching.hidden = true;
    649       document.getElementById('search-zero').hidden = (results.length != 0);
    650     });
    651   }
    652 };
    653 
    654 DownloadManager.clearAll = function() {
    655   DownloadManager.forEachItem(function(item) {
    656     if (!item.div.hidden) {
    657       item.erase();
    658       // The onErased handler should circle back around to loadItems.
    659     }
    660   });
    661 };
    662 
    663 var kShowNewMax = 50;
    664 var kOldMs = 1000 * 60 * 60 * 24 * 7;
    665 
    666 // These settings can be tuned by modifying localStorage in dev-tools.
    667 if ('kShowNewMax' in localStorage) {
    668   kShowNewMax = parseInt(localStorage.kShowNewMax);
    669 }
    670 if ('kOldMs' in localStorage) {
    671   kOldMs = parseInt(localStorage.kOldMs);
    672 }
    673 
    674 DownloadManager.loadItems = function() {
    675   // Request up to kShowNewMax + 1, but only display kShowNewMax; the +1 is a
    676   // probe to see if there are any older downloads.
    677   // TODO(benjhayden) When https://codereview.chromium.org/16924017/ is
    678   // released, set minimum_chrome_version and remove this try/catch.
    679   try {
    680     chrome.downloads.search({
    681         orderBy: ['-startTime'],
    682         limit: kShowNewMax + 1},
    683       function(results) {
    684         DownloadManager.loadItems.items = results;
    685         DownloadManager.loadItems.onLoaded();
    686     });
    687   } catch (exc) {
    688     chrome.downloads.search({
    689         orderBy: '-startTime',
    690         limit: kShowNewMax + 1},
    691       function(results) {
    692         DownloadManager.loadItems.items = results;
    693         DownloadManager.loadItems.onLoaded();
    694     });
    695   }
    696 };
    697 DownloadManager.loadItems.items = [];
    698 DownloadManager.loadItems.window_loaded = false;
    699 
    700 DownloadManager.loadItems.onLoaded = function() {
    701   if (!DownloadManager.loadItems.window_loaded) {
    702     return;
    703   }
    704   DownloadManager.loadItems.items.forEach(function(item) {
    705     DownloadManager.getOrCreate(item);
    706   });
    707   DownloadManager.loadItems.items = [];
    708   DownloadManager.showNew();
    709 };
    710 
    711 DownloadManager.loadItems.onWindowLoaded = function() {
    712   DownloadManager.loadItems.window_loaded = true;
    713   DownloadManager.loadItems.onLoaded();
    714 };
    715 
    716 // If this extension is installed on a stable-channel chrome, where the
    717 // downloads API is not available, do not use the downloads API, and link to the
    718 // beta channel.
    719 if (chrome.downloads) {
    720   // Start searching ASAP, don't wait for onload.
    721   DownloadManager.loadItems();
    722 
    723   chrome.downloads.onCreated.addListener(function(item) {
    724     DownloadManager.getOrCreate(item);
    725     DownloadManager.showNew();
    726     DownloadManager.startPollingProgress();
    727   });
    728 
    729   chrome.downloads.onChanged.addListener(function(delta) {
    730     var item = DownloadManager.getItem(delta.id);
    731     if (item) {
    732       item.onChanged(delta);
    733     }
    734   });
    735 
    736   chrome.downloads.onErased.addListener(function(id) {
    737     var item = DownloadManager.getItem(id);
    738     if (!item) {
    739       return;
    740     }
    741     item.onErased();
    742     DownloadManager.loadItems();
    743   });
    744 
    745   window.onload = function() {
    746     ratchetWidth(
    747       document.getElementById('q-outer').offsetWidth +
    748       document.getElementById('clear-all').offsetWidth +
    749       document.getElementById('open-folder').offsetWidth);
    750     setLastOpened();
    751     loadI18nMessages();
    752     DownloadManager.loadItems.onWindowLoaded();
    753     document.getElementById('older').onclick = function() {
    754       DownloadManager.showOlder();
    755       return false;
    756     };
    757     document.getElementById('q').onsearch = function() {
    758       DownloadManager.onSearch();
    759     };
    760     document.getElementById('clear-all').onclick = function() {
    761       DownloadManager.clearAll();
    762       return false;
    763     };
    764     if (chrome.downloads.showDefaultFolder) {
    765       document.getElementById('open-folder').onclick = function() {
    766         chrome.downloads.showDefaultFolder();
    767         return false;
    768       };
    769     } else {
    770       document.getElementById('open-folder').hidden = true;
    771     }
    772   };
    773 } else {
    774   // The downloads API is not available.
    775   // TODO(benjhayden) Remove this when minimum_chrome_version is set.
    776   window.onload = function() {
    777     loadI18nMessages();
    778     var bad_version = document.getElementById('bad-chrome-version');
    779     bad_version.hidden = false;
    780     bad_version.onclick = function() {
    781       chrome.tabs.create({url: bad_version.href});
    782       return false;
    783     };
    784     document.getElementById('empty').style.display = 'none';
    785     document.getElementById('q').style.display = 'none';
    786     document.getElementById('open-folder').style.display = 'none';
    787     document.getElementById('clear-all').style.display = 'none';
    788   };
    789 }
    790