blob: c9dfd162701ea60683288b8353f140a7b96a3fc5 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function pointInElement(p, elem) {
return ((p.x >= elem.offsetLeft) &&
(p.x <= (elem.offsetLeft + elem.offsetWidth)) &&
(p.y >= elem.offsetTop) &&
(p.y <= (elem.offsetTop + elem.offsetHeight)));
};
function setLastOpened() {
localStorage.popupLastOpened = (new Date()).getTime();
chrome.runtime.sendMessage('poll');
};
function loadI18nMessages() {
function setProperty(selector, prop, msg) {
document.querySelector(selector)[prop] = chrome.i18n.getMessage(msg);
}
setProperty('title', 'innerText', 'tabTitle');
setProperty('#q', 'placeholder', 'searchPlaceholder');
setProperty('#clear-all', 'title', 'clearAllTitle');
setProperty('#open-folder', 'title', 'openDownloadsFolderTitle');
setProperty('#empty', 'innerText', 'zeroItems');
setProperty('#searching', 'innerText', 'searching');
setProperty('#search-zero', 'innerText', 'zeroSearchResults');
setProperty('#management-permission-info', 'innerText',
'managementPermissionInfo');
setProperty('#grant-management-permission', 'innerText',
'grantManagementPermission');
setProperty('#older', 'innerText', 'showOlderDownloads');
setProperty('#loading-older', 'innerText', 'loadingOlderDownloads');
setProperty('.pause', 'title', 'pauseTitle');
setProperty('.resume', 'title', 'resumeTitle');
setProperty('.cancel', 'title', 'cancelTitle');
setProperty('.show-folder', 'title', 'showInFolderTitle');
setProperty('.erase', 'title', 'eraseTitle');
setProperty('.url', 'title', 'retryTitle');
setProperty('.referrer', 'title', 'referrerTitle');
setProperty('.open-filename', 'title', 'openTitle');
setProperty('#bad-chrome-version', 'innerText', 'badChromeVersion');
setProperty('.remove-file', 'title', 'removeFileTitle');
document.querySelector('.progress').style.minWidth =
getTextWidth(formatBytes(1024 * 1024 * 1023.9) + '/' +
formatBytes(1024 * 1024 * 1023.9)) + 'px';
// This only covers {timeLeft,openWhenComplete}{Finishing,Days}. If
// ...Hours/Minutes/Seconds could be longer for any locale, then this should
// test them.
var max_time_left_width = 0;
for (var i = 0; i < 4; ++i) {
max_time_left_width = Math.max(max_time_left_width, getTextWidth(
formatTimeLeft(0 == (i % 2),
(i < 2) ? 0 : ((100 * 24) + 23) * 60 * 60 * 1000)));
}
document.querySelector('body div.item span.time-left').style.minWidth =
max_time_left_width + 'px';
};
function getTextWidth(s) {
var probe = document.getElementById('text-width-probe');
probe.innerText = s;
return probe.offsetWidth;
};
function formatDateTime(date) {
var now = new Date();
var zpad_mins = ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
if (date.getYear() != now.getYear()) {
return '' + (1900 + date.getYear());
} else if ((date.getMonth() != now.getMonth()) ||
(date.getDate() != now.getDate())) {
return date.getDate() + ' ' + chrome.i18n.getMessage(
'month' + date.getMonth() + 'abbr');
} else if (date.getHours() == 12) {
return '12' + zpad_mins + 'pm';
} else if (date.getHours() > 12) {
return (date.getHours() - 12) + zpad_mins + 'pm';
}
return date.getHours() + zpad_mins + 'am';
}
function formatBytes(n) {
if (n < 1024) {
return n + 'B';
}
var prefixes = 'KMGTPEZY';
var mul = 1024;
for (var i = 0; i < prefixes.length; ++i) {
if (n < (1024 * mul)) {
return (parseInt(n / mul) + '.' + parseInt(10 * ((n / mul) % 1)) +
prefixes[i] + 'B');
}
mul *= 1024;
}
return '!!!';
}
function formatTimeLeft(openWhenComplete, ms) {
var prefix = openWhenComplete ? 'openWhenComplete' : 'timeLeft';
if (ms < 1000) {
return chrome.i18n.getMessage(prefix + 'Finishing');
}
var days = parseInt(ms / (24 * 60 * 60 * 1000));
var hours = parseInt(ms / (60 * 60 * 1000)) % 24;
if (days) {
return chrome.i18n.getMessage(prefix + 'Days', [days, hours]);
}
var minutes = parseInt(ms / (60 * 1000)) % 60;
if (hours) {
return chrome.i18n.getMessage(prefix + 'Hours', [hours, minutes]);
}
var seconds = parseInt(ms / 1000) % 60;
if (minutes) {
return chrome.i18n.getMessage(prefix + 'Minutes', [minutes, seconds]);
}
return chrome.i18n.getMessage(prefix + 'Seconds', [seconds]);
}
function ratchetWidth(w) {
var current = parseInt(document.body.style.minWidth) || 0;
document.body.style.minWidth = Math.max(w, current) + 'px';
}
function ratchetHeight(h) {
var current = parseInt(document.body.style.minHeight) || 0;
document.body.style.minHeight = Math.max(h, current) + 'px';
}
function binarySearch(array, target, cmp) {
var low = 0, high = array.length - 1, i, comparison;
while (low <= high) {
i = (low + high) >> 1;
comparison = cmp(target, array[i]);
if (comparison < 0) {
low = i + 1;
} else if (comparison > 0) {
high = i - 1;
} else {
return i;
}
}
return i;
};
function arrayFrom(seq) {
return Array.prototype.slice.apply(seq);
};
function DownloadItem(data) {
var item = this;
for (var prop in data) {
item[prop] = data[prop];
}
item.startTime = new Date(item.startTime);
if (item.canResume == undefined) {
DownloadItem.canResumeHack = true;
}
item.div = document.querySelector('body>div.item').cloneNode(true);
item.div.id = 'item' + item.id;
item.div.item = item;
var items_div = document.getElementById('items');
if ((items_div.childNodes.length == 0) ||
(item.startTime.getTime() < items_div.childNodes[
items_div.childNodes.length - 1].item.startTime.getTime())) {
items_div.appendChild(item.div);
} else if (item.startTime.getTime() >
items_div.childNodes[0].item.startTime.getTime()) {
items_div.insertBefore(item.div, items_div.childNodes[0]);
} else {
var adjacent_div = items_div.childNodes[
binarySearch(arrayFrom(items_div.childNodes),
item.startTime.getTime(),
function(target, other) {
return target - other.item.startTime.getTime();
})];
var adjacent_item = adjacent_div.item;
if (adjacent_item.startTime.getTime() < item.startTime.getTime()) {
items_div.insertBefore(item.div, adjacent_div);
} else {
items_div.insertBefore(item.div, adjacent_div.nextSibling);
}
}
item.getElement('referrer').onclick = function() {
chrome.tabs.create({url: item.referrer});
return false;
};
item.getElement('by-ext').onclick = function() {
chrome.tabs.create({url: 'chrome://extensions#' + item.byExtensionId});
return false;
}
item.getElement('open-filename').onclick = function() {
item.open();
return false;
};
item.getElement('open-filename').ondragstart = function() {
item.drag();
return false;
};
item.getElement('pause').onclick = function() {
item.pause();
return false;
};
item.getElement('cancel').onclick = function() {
item.cancel();
return false;
};
item.getElement('resume').onclick = function() {
item.resume();
return false;
};
item.getElement('show-folder').onclick = function() {
item.show();
return false;
};
item.getElement('remove-file').onclick = function() {
item.removeFile();
return false;
};
item.getElement('erase').onclick = function() {
item.erase();
return false;
};
item.more_mousemove = function(evt) {
var mouse = {x:evt.x, y:evt.y+document.body.scrollTop};
if (item.getElement('more') &&
(pointInElement(mouse, item.div) ||
pointInElement(mouse, item.getElement('more')))) {
return;
}
if (item.getElement('more')) {
item.getElement('more').hidden = true;
}
window.removeEventListener('mousemove', item.more_mousemove);
};
[item.div, item.getElement('more')].concat(
item.getElement('more').children).forEach(function(elem) {
elem.onmouseover = function() {
arrayFrom(items_div.children).forEach(function(other) {
if (other.item != item) {
other.item.getElement('more').hidden = true;
}
});
item.getElement('more').hidden = false;
item.getElement('more').style.top =
(item.div.offsetTop + item.div.offsetHeight) + 'px';
item.getElement('more').style.left = item.div.offsetLeft + 'px';
if (window.innerHeight < (parseInt(item.getElement('more').style.top) +
item.getElement('more').offsetHeight)) {
item.getElement('more').style.top = (
item.div.offsetTop - item.getElement('more').offsetHeight) + 'px';
}
window.addEventListener('mousemove', item.more_mousemove);
};
});
if (item.referrer) {
item.getElement('referrer').href = item.referrer;
} else {
item.getElement('referrer').hidden = true;
}
item.getElement('url').href = item.url;
item.getElement('url').innerText = item.url;
item.render();
}
DownloadItem.canResumeHack = false;
DownloadItem.prototype.getElement = function(name) {
return document.querySelector('#item' + this.id + ' .' + name);
};
DownloadItem.prototype.render = function() {
var item = this;
var now = new Date();
var in_progress = (item.state == 'in_progress')
var openable = (item.state != 'interrupted') && item.exists && !item.deleted;
item.startTime = new Date(item.startTime);
if (DownloadItem.canResumeHack) {
item.canResume = in_progress && item.paused;
}
if (item.filename) {
item.basename = item.filename.substring(Math.max(
item.filename.lastIndexOf('\\'),
item.filename.lastIndexOf('/')) + 1);
}
if (item.estimatedEndTime) {
item.estimatedEndTime = new Date(item.estimatedEndTime);
}
if (item.endTime) {
item.endTime = new Date(item.endTime);
}
if (item.filename && !item.icon_url) {
chrome.downloads.getFileIcon(
item.id,
{'size': 32},
function(icon_url) {
item.getElement('icon').hidden = !icon_url;
if (icon_url) {
item.icon_url = icon_url;
item.getElement('icon').src = icon_url;
}
});
}
item.getElement('removed').style.display = openable ? 'none' : 'inline';
item.getElement('open-filename').style.display = (
openable ? 'inline' : 'none');
item.getElement('in-progress').hidden = !in_progress;
item.getElement('pause').style.display = (
!in_progress || item.paused) ? 'none' : 'inline-block';
item.getElement('resume').style.display = (
!in_progress || !item.canResume) ? 'none' : 'inline-block';
item.getElement('cancel').style.display = (
!in_progress ? 'none' : 'inline-block');
item.getElement('remove-file').hidden = (
(item.state != 'complete') ||
!item.exists ||
item.deleted ||
!chrome.downloads.removeFile);
item.getElement('erase').hidden = in_progress;
var could_progress = in_progress || item.canResume;
item.getElement('progress').style.display = (
could_progress ? 'inline-block' : 'none');
item.getElement('meter').hidden = !could_progress || !item.totalBytes;
item.getElement('removed').innerText = item.basename;
item.getElement('open-filename').innerText = item.basename;
function setByExtension(show) {
if (show) {
item.getElement('by-ext').title = item.byExtensionName;
item.getElement('by-ext').href =
'chrome://extensions#' + item.byExtensionId;
item.getElement('by-ext img').src =
'chrome://extension-icon/' + item.byExtensionId + '/48/1';
} else {
item.getElement('by-ext').hidden = true;
}
}
if (item.byExtensionId && item.byExtensionName) {
chrome.permissions.contains({permissions: ['management']},
function(result) {
if (result) {
setByExtension(true);
} else {
setByExtension(false);
if (!localStorage.managementPermissionDenied) {
document.getElementById('request-management-permission').hidden =
false;
document.getElementById('grant-management-permission').onclick =
function() {
chrome.permissions.request({permissions: ['management']},
function(granted) {
setByExtension(granted);
if (!granted) {
localStorage.managementPermissionDenied = true;
}
});
return false;
};
}
}
});
} else {
setByExtension(false);
}
if (!item.getElement('error').hidden) {
if (item.error) {
// TODO(benjhayden) When https://codereview.chromium.org/16924017/ is
// released, set minimum_chrome_version and remove the error_N messages.
item.getElement('error').innerText = chrome.i18n.getMessage(
'error_' + item.error);
if (!item.getElement('error').innerText) {
item.getElement('error').innerText = item.error;
}
} else if (!openable) {
item.getElement('error').innerText = chrome.i18n.getMessage(
'errorRemoved');
}
}
item.getElement('complete-size').innerText = formatBytes(
item.bytesReceived);
if (item.totalBytes && (item.state != 'complete')) {
item.getElement('progress').innerText = (
item.getElement('complete-size').innerText + '/' +
formatBytes(item.totalBytes));
item.getElement('meter').children[0].style.width = parseInt(
100 * item.bytesReceived / item.totalBytes) + '%';
}
if (in_progress) {
if (item.estimatedEndTime && !item.paused) {
var openWhenComplete = false;
try {
openWhenComplete = JSON.parse(localStorage.openWhenComplete).indexOf(
item.id) >= 0;
} catch (e) {
}
item.getElement('time-left').innerText = formatTimeLeft(
openWhenComplete, item.estimatedEndTime.getTime() - now.getTime());
} else {
item.getElement('time-left').innerText = String.fromCharCode(160);
}
}
if (item.startTime) {
item.getElement('start-time').innerText = formatDateTime(
item.startTime);
}
ratchetWidth(item.getElement('icon').offsetWidth +
item.getElement('file-url').offsetWidth +
item.getElement('cancel').offsetWidth +
item.getElement('pause').offsetWidth +
item.getElement('resume').offsetWidth);
ratchetWidth(item.getElement('more').offsetWidth);
this.maybeAccept();
};
DownloadItem.prototype.onChanged = function(delta) {
for (var key in delta) {
if (key != 'id') {
this[key] = delta[key].current;
}
}
this.render();
if (delta.state) {
setLastOpened();
}
if ((this.state == 'in_progress') && !this.paused) {
DownloadManager.startPollingProgress();
}
};
DownloadItem.prototype.onErased = function() {
window.removeEventListener('mousemove', this.more_mousemove);
document.getElementById('items').removeChild(this.div);
};
DownloadItem.prototype.drag = function() {
chrome.downloads.drag(this.id);
};
DownloadItem.prototype.show = function() {
chrome.downloads.show(this.id);
};
DownloadItem.prototype.open = function() {
if (this.state == 'complete') {
chrome.downloads.open(this.id);
return;
}
chrome.runtime.sendMessage({openWhenComplete:this.id});
};
DownloadItem.prototype.removeFile = function() {
chrome.downloads.removeFile(this.id);
this.deleted = true;
this.render();
};
DownloadItem.prototype.erase = function() {
chrome.downloads.erase({id: this.id});
};
DownloadItem.prototype.pause = function() {
chrome.downloads.pause(this.id);
};
DownloadItem.prototype.resume = function() {
chrome.downloads.resume(this.id);
};
DownloadItem.prototype.cancel = function() {
chrome.downloads.cancel(this.id);
};
DownloadItem.prototype.maybeAccept = function() {
// This function is safe to call at any time for any item, and it will always
// do the right thing, which is to display the danger prompt only if the item
// is in_progress and dangerous, and if the prompt is not already displayed.
if ((this.state != 'in_progress') ||
(this.danger == 'safe') ||
(this.danger == 'accepted') ||
DownloadItem.prototype.maybeAccept.accepting_danger) {
return;
}
ratchetWidth(400);
ratchetHeight(200);
DownloadItem.prototype.maybeAccept.accepting_danger = true;
// On Mac, window.onload is run while the popup is animating in, before it is
// considered "visible". Prompts will not be displayed over an invisible
// window, so the popup will become stuck. Just wait a little bit for the
// window to finish animating in. http://crbug.com/280107
var id = this.id;
setTimeout(function() {
chrome.downloads.acceptDanger(id, function() {
DownloadItem.prototype.maybeAccept.accepting_danger = false;
arrayFrom(document.getElementById('items').childNodes).forEach(
function(item_div) { item_div.item.maybeAccept(); });
});
}, 500);
};
DownloadItem.prototype.maybeAccept.accepting_danger = false;
var DownloadManager = {};
DownloadManager.showingOlder = false;
DownloadManager.getItem = function(id) {
var item_div = document.getElementById('item' + id);
return item_div ? item_div.item : null;
};
DownloadManager.getOrCreate = function(data) {
var item = DownloadManager.getItem(data.id);
return item ? item : new DownloadItem(data);
};
DownloadManager.forEachItem = function(cb) {
// Calls cb(item, index) in the order that they are displayed, i.e. in order
// of decreasing startTime.
arrayFrom(document.getElementById('items').childNodes).forEach(
function(item_div, index) { cb(item_div.item, index); });
};
DownloadManager.startPollingProgress = function() {
if (DownloadManager.startPollingProgress.tid < 0) {
DownloadManager.startPollingProgress.tid = setTimeout(
DownloadManager.startPollingProgress.pollProgress,
DownloadManager.startPollingProgress.MS);
}
}
DownloadManager.startPollingProgress.MS = 200;
DownloadManager.startPollingProgress.tid = -1;
DownloadManager.startPollingProgress.pollProgress = function() {
DownloadManager.startPollingProgress.tid = -1;
chrome.downloads.search({state: 'in_progress', paused: false},
function(results) {
if (!results.length)
return;
results.forEach(function(result) {
var item = DownloadManager.getOrCreate(result);
for (var prop in result) {
item[prop] = result[prop];
}
item.render();
if ((item.state == 'in_progress') && !item.paused) {
DownloadManager.startPollingProgress();
}
});
});
};
DownloadManager.showNew = function() {
var any_items = (document.getElementById('items').childNodes.length > 0);
document.getElementById('empty').style.display =
any_items ? 'none' : 'inline-block';
document.getElementById('head').style.borderBottomWidth =
(any_items ? 1 : 0) + 'px';
document.getElementById('clear-all').hidden = !any_items;
var query_search = document.getElementById('q');
query_search.hidden = !any_items;
if (!any_items) {
return;
}
var old_ms = (new Date()).getTime() - kOldMs;
var any_hidden = false;
var any_showing = false;
// First show up to kShowNewMax items newer than kOldMs. If there aren't any
// items newer than kOldMs, then show up to kShowNewMax items of any age. If
// there are any hidden items, show the Show Older button.
DownloadManager.forEachItem(function(item, index) {
item.div.hidden = !DownloadManager.showingOlder && (
(item.startTime.getTime() < old_ms) || (index >= kShowNewMax));
any_hidden = any_hidden || item.div.hidden;
any_showing = any_showing || !item.div.hidden;
});
if (!any_showing) {
any_hidden = false;
DownloadManager.forEachItem(function(item, index) {
item.div.hidden = !DownloadManager.showingOlder && (index >= kShowNewMax);
any_hidden = any_hidden || item.div.hidden;
any_showing = any_showing || !item.div.hidden;
});
}
document.getElementById('older').hidden = !any_hidden;
query_search.focus();
};
DownloadManager.showOlder = function() {
DownloadManager.showingOlder = true;
var loading_older_span = document.getElementById('loading-older');
document.getElementById('older').hidden = true;
loading_older_span.hidden = false;
chrome.downloads.search({}, function(results) {
results.forEach(function(result) {
var item = DownloadManager.getOrCreate(result);
item.div.hidden = false;
});
loading_older_span.hidden = true;
});
};
DownloadManager.onSearch = function() {
// split string by space, but ignore space in quotes
// http://stackoverflow.com/questions/16261635
var query = document.getElementById('q').value.match(/(?:[^\s"]+|"[^"]*")+/g);
if (!query) {
DownloadManager.showNew();
document.getElementById('search-zero').hidden = true;
} else {
query = query.map(function(term) {
// strip quotes
return (term.match(/\s/) &&
term[0].match(/["']/) &&
term[term.length - 1] == term[0]) ?
term.substr(1, term.length - 2) : term;
});
var searching = document.getElementById('searching');
searching.hidden = false;
chrome.downloads.search({query: query}, function(results) {
document.getElementById('older').hidden = true;
DownloadManager.forEachItem(function(item) {
item.div.hidden = true;
});
results.forEach(function(result) {
DownloadManager.getOrCreate(result).div.hidden = false;
});
searching.hidden = true;
document.getElementById('search-zero').hidden = (results.length != 0);
});
}
};
DownloadManager.clearAll = function() {
DownloadManager.forEachItem(function(item) {
if (!item.div.hidden) {
item.erase();
// The onErased handler should circle back around to loadItems.
}
});
};
var kShowNewMax = 50;
var kOldMs = 1000 * 60 * 60 * 24 * 7;
// These settings can be tuned by modifying localStorage in dev-tools.
if ('kShowNewMax' in localStorage) {
kShowNewMax = parseInt(localStorage.kShowNewMax);
}
if ('kOldMs' in localStorage) {
kOldMs = parseInt(localStorage.kOldMs);
}
DownloadManager.loadItems = function() {
// Request up to kShowNewMax + 1, but only display kShowNewMax; the +1 is a
// probe to see if there are any older downloads.
// TODO(benjhayden) When https://codereview.chromium.org/16924017/ is
// released, set minimum_chrome_version and remove this try/catch.
try {
chrome.downloads.search({
orderBy: ['-startTime'],
limit: kShowNewMax + 1},
function(results) {
DownloadManager.loadItems.items = results;
DownloadManager.loadItems.onLoaded();
});
} catch (exc) {
chrome.downloads.search({
orderBy: '-startTime',
limit: kShowNewMax + 1},
function(results) {
DownloadManager.loadItems.items = results;
DownloadManager.loadItems.onLoaded();
});
}
};
DownloadManager.loadItems.items = [];
DownloadManager.loadItems.window_loaded = false;
DownloadManager.loadItems.onLoaded = function() {
if (!DownloadManager.loadItems.window_loaded) {
return;
}
DownloadManager.loadItems.items.forEach(function(item) {
DownloadManager.getOrCreate(item);
});
DownloadManager.loadItems.items = [];
DownloadManager.showNew();
};
DownloadManager.loadItems.onWindowLoaded = function() {
DownloadManager.loadItems.window_loaded = true;
DownloadManager.loadItems.onLoaded();
};
// If this extension is installed on a stable-channel chrome, where the
// downloads API is not available, do not use the downloads API, and link to the
// beta channel.
if (chrome.downloads) {
// Start searching ASAP, don't wait for onload.
DownloadManager.loadItems();
chrome.downloads.onCreated.addListener(function(item) {
DownloadManager.getOrCreate(item);
DownloadManager.showNew();
DownloadManager.startPollingProgress();
});
chrome.downloads.onChanged.addListener(function(delta) {
var item = DownloadManager.getItem(delta.id);
if (item) {
item.onChanged(delta);
}
});
chrome.downloads.onErased.addListener(function(id) {
var item = DownloadManager.getItem(id);
if (!item) {
return;
}
item.onErased();
DownloadManager.loadItems();
});
window.onload = function() {
ratchetWidth(
document.getElementById('q-outer').offsetWidth +
document.getElementById('clear-all').offsetWidth +
document.getElementById('open-folder').offsetWidth);
setLastOpened();
loadI18nMessages();
DownloadManager.loadItems.onWindowLoaded();
document.getElementById('older').onclick = function() {
DownloadManager.showOlder();
return false;
};
document.getElementById('q').onsearch = function() {
DownloadManager.onSearch();
};
document.getElementById('clear-all').onclick = function() {
DownloadManager.clearAll();
return false;
};
if (chrome.downloads.showDefaultFolder) {
document.getElementById('open-folder').onclick = function() {
chrome.downloads.showDefaultFolder();
return false;
};
} else {
document.getElementById('open-folder').hidden = true;
}
};
} else {
// The downloads API is not available.
// TODO(benjhayden) Remove this when minimum_chrome_version is set.
window.onload = function() {
loadI18nMessages();
var bad_version = document.getElementById('bad-chrome-version');
bad_version.hidden = false;
bad_version.onclick = function() {
chrome.tabs.create({url: bad_version.href});
return false;
};
document.getElementById('empty').style.display = 'none';
document.getElementById('q').style.display = 'none';
document.getElementById('open-folder').style.display = 'none';
document.getElementById('clear-all').style.display = 'none';
};
}