diff --git a/api-samples/downloads/download_filename_controller/README.md b/api-samples/downloads/download_filename_controller/README.md new file mode 100644 index 0000000000..85832a47f3 --- /dev/null +++ b/api-samples/downloads/download_filename_controller/README.md @@ -0,0 +1,14 @@ +# chrome.downloads - Download Filename Controller + +This sample uses the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API to control the names of files being downloaded. + +## Overview + +This sample lets you add rules to the extension's options page to set the conflict action for the files being downloaded. + +## Running this extension + +1. Clone this repository. +2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +3. Click the extension's action icon to open the options page. +4. Add rules in the extension's options page and download files to see the effect. diff --git a/api-samples/downloads/download_filename_controller/manifest.json b/api-samples/downloads/download_filename_controller/manifest.json new file mode 100644 index 0000000000..b0f1721968 --- /dev/null +++ b/api-samples/downloads/download_filename_controller/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "Download Filename Controller", + "description": "Demonstrates using the chrome.downloads API to control the names of files being downloaded.", + "version": "0.1", + "background": { + "service_worker": "service-worker.js" + }, + "action": {}, + "options_page": "options.html", + "permissions": ["downloads", "storage"], + "manifest_version": 3 +} diff --git a/api-samples/downloads/download_filename_controller/options.html b/api-samples/downloads/download_filename_controller/options.html new file mode 100644 index 0000000000..cd2f3801fb --- /dev/null +++ b/api-samples/downloads/download_filename_controller/options.html @@ -0,0 +1,40 @@ + + + + Download Filename Controller + + + + +
+ + + + + + + + + + diff --git a/api-samples/downloads/download_filename_controller/options.js b/api-samples/downloads/download_filename_controller/options.js new file mode 100644 index 0000000000..80bb4a0c61 --- /dev/null +++ b/api-samples/downloads/download_filename_controller/options.js @@ -0,0 +1,96 @@ +class Rule { + constructor(data) { + const rules = document.getElementById('rules'); + this.node = document.getElementById('rule-template').cloneNode(true); + this.node.id = 'rule' + Rule.next_id++; + this.node.rule = this; + rules.appendChild(this.node); + this.node.hidden = false; + + if (data) { + this.getElement('matcher').value = data.matcher; + this.getElement('match-param').value = data.match_param; + this.getElement('action').value = data.action; + this.getElement('enabled').checked = data.enabled; + } + + this.getElement('enabled-label').htmlFor = this.getElement('enabled').id = + this.node.id + '-enabled'; + + this.render(); + + this.getElement('matcher').onchange = storeRules; + this.getElement('match-param').onkeyup = storeRules; + this.getElement('action').onchange = storeRules; + this.getElement('enabled').onchange = storeRules; + + const rule = this; + this.getElement('move-up').onclick = function () { + const sib = rule.node.previousSibling; + rule.node.parentNode.removeChild(rule.node); + sib.parentNode.insertBefore(rule.node, sib); + storeRules(); + }; + this.getElement('move-down').onclick = function () { + const parentNode = rule.node.parentNode; + const sib = rule.node.nextSibling.nextSibling; + parentNode.removeChild(rule.node); + if (sib) { + parentNode.insertBefore(rule.node, sib); + } else { + parentNode.appendChild(rule.node); + } + storeRules(); + }; + this.getElement('remove').onclick = function () { + rule.node.parentNode.removeChild(rule.node); + storeRules(); + }; + storeRules(); + } + + getElement(name) { + return document.querySelector('#' + this.node.id + ' .' + name); + } + + render() { + this.getElement('move-up').disabled = !this.node.previousSibling; + this.getElement('move-down').disabled = !this.node.nextSibling; + } +} + +Rule.next_id = 0; + +async function loadRules() { + const { rules } = await chrome.storage.local.get('rules'); + try { + rules.forEach(function (rule) { + new Rule(rule); + }); + } catch (e) { + await chrome.storage.local.set({ rules: [] }); + } +} + +async function storeRules() { + await chrome.storage.local.set({ + rules: [...document.getElementById('rules').childNodes].map(function ( + node + ) { + node.rule.render(); + return { + matcher: node.rule.getElement('matcher').value, + match_param: node.rule.getElement('match-param').value, + action: node.rule.getElement('action').value, + enabled: node.rule.getElement('enabled').checked + }; + }) + }); +} + +window.onload = function () { + loadRules(); + document.getElementById('new').onclick = function () { + new Rule(); + }; +}; diff --git a/api-samples/downloads/download_filename_controller/service-worker.js b/api-samples/downloads/download_filename_controller/service-worker.js new file mode 100644 index 0000000000..7ad6ac7199 --- /dev/null +++ b/api-samples/downloads/download_filename_controller/service-worker.js @@ -0,0 +1,48 @@ +function matches(rule, item) { + switch (rule.matcher) { + case 'hostname': { + const link = new URL(item.url); + const host = + rule.match_param.indexOf(':') < 0 ? link.hostname : link.host; + return ( + host.indexOf(rule.match_param.toLowerCase()) == + host.length - rule.match_param.length + ); + } + case 'default': + return item.filename == rule.match_param; + case 'url-regex': + return new RegExp(rule.match_param).test(item.url); + case 'default-regex': + return new RegExp(rule.match_param).test(item.filename); + default: + return false; + } +} + +chrome.downloads.onDeterminingFilename.addListener(function (item, suggest) { + chrome.storage.local.get('rules').then(({ rules }) => { + if (!rules) { + rules = []; + chrome.storage.local.set({ rules }); + } + for (let rule of rules) { + if (rule.enabled && matches(rule, item)) { + if (rule.action == 'overwrite') { + suggest({ filename: item.filename, conflictAction: 'overwrite' }); + } else if (rule.action == 'prompt') { + suggest({ filename: item.filename, conflictAction: 'prompt' }); + } + return; + } + } + suggest({ filename: item.filename }); + }); + + // return true to indicate that suggest() was called asynchronously + return true; +}); + +chrome.action.onClicked.addListener(function () { + chrome.runtime.openOptionsPage(); +}); diff --git a/api-samples/downloads/download_links/README.md b/api-samples/downloads/download_links/README.md new file mode 100644 index 0000000000..d0ff4be4ed --- /dev/null +++ b/api-samples/downloads/download_links/README.md @@ -0,0 +1,13 @@ +# chrome.downloads - Download Links + +This sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads) API to download links visible on the active page. + +## Overview + +This sample lists all available links from the active page in a popup. The [`chrome.downloads.download()`](https://developer.chrome.com/docs/extensions/reference/downloads/#method-download) method is used to download all selected links. + +## Running this extension + +1. Clone this repository. +2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +3. Open the popup and select links to download. diff --git a/api-samples/downloads/download_links/manifest.json b/api-samples/downloads/download_links/manifest.json new file mode 100644 index 0000000000..0094187e5b --- /dev/null +++ b/api-samples/downloads/download_links/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "Download Selected Links", + "description": "Uses the chrome.downloads.download() method to list all available links from the active page in a popup.", + "version": "0.1", + "permissions": ["downloads", "scripting"], + "host_permissions": [""], + "action": { + "default_popup": "popup.html" + }, + "manifest_version": 3 +} diff --git a/api-samples/downloads/download_links/popup.html b/api-samples/downloads/download_links/popup.html new file mode 100644 index 0000000000..9f29a208ae --- /dev/null +++ b/api-samples/downloads/download_links/popup.html @@ -0,0 +1,21 @@ + + + + popup + + + + + +
+ + + + + + + + + + + diff --git a/api-samples/downloads/download_links/popup.js b/api-samples/downloads/download_links/popup.js new file mode 100644 index 0000000000..5b169de14f --- /dev/null +++ b/api-samples/downloads/download_links/popup.js @@ -0,0 +1,114 @@ +// This extension demonstrates using chrome.downloads.download() to +// download URLs. + +const allLinks = []; +let visibleLinks = []; + +// Display all visible links. +function showLinks() { + const linksTable = document.getElementById('links'); + while (linksTable.children.length > 1) { + linksTable.removeChild(linksTable.children[linksTable.children.length - 1]); + } + for (let i = 0; i < visibleLinks.length; ++i) { + const row = document.createElement('tr'); + const col0 = document.createElement('td'); + const col1 = document.createElement('td'); + const checkbox = document.createElement('input'); + checkbox.checked = true; + checkbox.type = 'checkbox'; + checkbox.id = 'check' + i; + col0.appendChild(checkbox); + col1.innerText = visibleLinks[i]; + col1.style.whiteSpace = 'nowrap'; + col1.onclick = function () { + checkbox.checked = !checkbox.checked; + }; + row.appendChild(col0); + row.appendChild(col1); + linksTable.appendChild(row); + } +} + +// Toggle the checked state of all visible links. +function toggleAll() { + const checked = document.getElementById('toggle_all').checked; + for (let i = 0; i < visibleLinks.length; ++i) { + document.getElementById('check' + i).checked = checked; + } +} + +// Download all visible checked links. +function downloadCheckedLinks() { + for (let i = 0; i < visibleLinks.length; ++i) { + if (document.getElementById('check' + i).checked) { + chrome.downloads.download({ url: visibleLinks[i] }); + } + } + window.close(); +} + +// Re-filter allLinks into visibleLinks and reshow visibleLinks. +function filterLinks() { + const filterValue = document.getElementById('filter').value; + if (document.getElementById('regex').checked) { + visibleLinks = allLinks.filter(function (link) { + return link.match(filterValue); + }); + } else { + const terms = filterValue.split(' '); + visibleLinks = allLinks.filter(function (link) { + for (let term of terms) { + if (term.length != 0) { + const expected = term[0] != '-'; + if (!expected) { + term = term.slice(1); + if (term.length == 0) { + continue; + } + } + const found = -1 !== link.indexOf(term); + if (found != expected) { + return false; + } + } + } + return true; + }); + } + showLinks(); +} + +// Add links to allLinks and visibleLinks, sort and show them. send_links.js is +// injected into all frames of the active tab, so this listener may be called +// multiple times. +chrome.runtime.onMessage.addListener(function (links) { + allLinks.push(...links); + allLinks.sort(); + visibleLinks = allLinks; + showLinks(); +}); + +// Set up event handlers and inject send_links.js into all frames in the active +// tab. +window.onload = async function () { + document.getElementById('filter').onkeyup = filterLinks; + document.getElementById('regex').onchange = filterLinks; + document.getElementById('toggle_all').onchange = toggleAll; + document.getElementById('download0').onclick = downloadCheckedLinks; + document.getElementById('download1').onclick = downloadCheckedLinks; + + const { id: currentWindowId } = await chrome.windows.getCurrent(); + + const tabs = await chrome.tabs.query({ + active: true, + windowId: currentWindowId + }); + + const activeTabId = tabs[0].id; + + chrome.scripting.executeScript({ + target: { tabId: activeTabId, allFrames: true }, + files: ['send_links.js'] + }); +}; diff --git a/api-samples/downloads/download_links/send_links.js b/api-samples/downloads/download_links/send_links.js new file mode 100644 index 0000000000..4fb73ba020 --- /dev/null +++ b/api-samples/downloads/download_links/send_links.js @@ -0,0 +1,35 @@ +{ + // Send back to the popup a sorted deduped list of valid link URLs on this page. + // The popup injects this script into all frames in the active tab. + + let links = [...document.getElementsByTagName('a')]; + links = links.map(function (element) { + // Return an anchor's href attribute, stripping any URL fragment (hash '#'). + // If the html specifies a relative path, chrome converts it to an absolute + // URL. + let href = element.href; + const hashIndex = href.indexOf('#'); + if (hashIndex >= 0) { + href = href.slice(0, hashIndex); + } + return href; + }); + + links.sort(); + + // Remove duplicates and invalid URLs. + const kBadPrefix = 'javascript'; + for (let i = 0; i < links.length; ) { + if ( + (i > 0 && links[i] == links[i - 1]) || + links[i] == '' || + kBadPrefix == links[i].toLowerCase().substr(0, kBadPrefix.length) + ) { + links.splice(i, 1); + } else { + ++i; + } + } + + chrome.runtime.sendMessage(links); +} diff --git a/api-samples/downloads/download_manager/README.md b/api-samples/downloads/download_manager/README.md new file mode 100644 index 0000000000..863d97b46f --- /dev/null +++ b/api-samples/downloads/download_manager/README.md @@ -0,0 +1,13 @@ +# chrome.downloads - Download Manager + +A sample that demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads) API. + +## Overview + +This sample uses multiple [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads) APIs to implement a simple download manager. + +## Running this extension + +1. Clone this repository. +2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +3. Open the popup. diff --git a/api-samples/downloads/download_manager/_locales/en/messages.json b/api-samples/downloads/download_manager/_locales/en/messages.json new file mode 100644 index 0000000000..cfad8708c2 --- /dev/null +++ b/api-samples/downloads/download_manager/_locales/en/messages.json @@ -0,0 +1,154 @@ +{ + "tabTitle": { + "message": "Downloads", + "description": "tab title" + }, + "searchPlaceholder": { + "message": "Search Downloads", + "description": "" + }, + "clearAllTitle": { + "message": "Erase All Visible Downloads", + "description": "" + }, + "openDownloadsFolderTitle": { + "message": "Open Downloads Folder", + "description": "" + }, + "zeroItems": { + "message": "There are zero download items.", + "description": "" + }, + "searching": { + "message": "Teleporting lots of goats...", + "description": "" + }, + "zeroSearchResults": { + "message": "Zero matches", + "description": "" + }, + "managementPermissionInfo": { + "message": "Some files were downloaded by an extension.", + "description": "" + }, + "grantManagementPermission": { + "message": "Show links to extensions that download files.", + "description": "" + }, + "showOlderDownloads": { + "message": "Show Older Downloads", + "description": "" + }, + "loadingOlderDownloads": { + "message": "Loading Older Downloads...", + "description": "" + }, + "openTitle": { + "message": "Open", + "description": "" + }, + "pauseTitle": { + "message": "Pause", + "description": "" + }, + "resumeTitle": { + "message": "Resume", + "description": "" + }, + "cancelTitle": { + "message": "Cancel", + "description": "" + }, + "removeFileTitle": { + "message": "Remove file", + "description": "" + }, + "eraseTitle": { + "message": "Erase", + "description": "" + }, + "retryTitle": { + "message": "Retry", + "description": "" + }, + "referrerTitle": { + "message": "Referrer", + "description": "" + }, + "month0abbr": { "message": "Jan", "description": "" }, + "month1abbr": { "message": "Feb", "description": "" }, + "month2abbr": { "message": "Mar", "description": "" }, + "month3abbr": { "message": "Apr", "description": "" }, + "month4abbr": { "message": "May", "description": "" }, + "month5abbr": { "message": "Jun", "description": "" }, + "month6abbr": { "message": "Jul", "description": "" }, + "month7abbr": { "message": "Aug", "description": "" }, + "month8abbr": { "message": "Sep", "description": "" }, + "month9abbr": { "message": "Oct", "description": "" }, + "month10abbr": { "message": "Nov", "description": "" }, + "month11abbr": { "message": "Dec", "description": "" }, + "timeLeftFinishing": { + "message": "finishing...", + "description": "" + }, + "timeLeftDays": { + "message": "$days$d $hours$h left", + "description": "", + "placeholders": { + "days": { + "content": "$1", + "example": "2" + }, + "hours": { + "content": "$2", + "example": "23" + } + } + }, + "timeLeftHours": { + "message": "$hours$h $mins$m left", + "description": "", + "placeholders": { + "hours": { + "content": "$1", + "example": "23" + }, + "mins": { + "content": "$2", + "example": "59" + } + } + }, + "timeLeftMinutes": { + "message": "$mins$m $sec$s left", + "description": "", + "placeholders": { + "mins": { + "content": "$1", + "example": "59" + }, + "sec": { + "content": "$2", + "example": "59" + } + } + }, + "timeLeftSeconds": { + "message": "$sec$s left", + "description": "", + "placeholders": { + "sec": { + "content": "$1", + "example": "59" + } + } + }, + "errorRemoved": { + "message": "Removed", + "description": "" + }, + "showInFolderTitle": { + "message": "Show in Folder", + "description": "Alt text for show in folder icon" + } +} diff --git a/api-samples/downloads/download_manager/icon128.png b/api-samples/downloads/download_manager/icon128.png new file mode 100644 index 0000000000..4d3610dd90 Binary files /dev/null and b/api-samples/downloads/download_manager/icon128.png differ diff --git a/api-samples/downloads/download_manager/icon19.png b/api-samples/downloads/download_manager/icon19.png new file mode 100644 index 0000000000..0aa6033748 Binary files /dev/null and b/api-samples/downloads/download_manager/icon19.png differ diff --git a/api-samples/downloads/download_manager/icon38.png b/api-samples/downloads/download_manager/icon38.png new file mode 100644 index 0000000000..84563d7581 Binary files /dev/null and b/api-samples/downloads/download_manager/icon38.png differ diff --git a/api-samples/downloads/download_manager/icons.html b/api-samples/downloads/download_manager/icons.html new file mode 100644 index 0000000000..1a502096eb --- /dev/null +++ b/api-samples/downloads/download_manager/icons.html @@ -0,0 +1,11 @@ + + + + Icon Generator + + + + + + + diff --git a/api-samples/downloads/download_manager/icons.js b/api-samples/downloads/download_manager/icons.js new file mode 100644 index 0000000000..5a2750d53a --- /dev/null +++ b/api-samples/downloads/download_manager/icons.js @@ -0,0 +1,8 @@ +window.onload = function () { + const download = document.getElementById('download'); + download.onclick = function () { + chrome.runtime.sendMessage('icons'); + download.disabled = true; + return false; + }; +}; diff --git a/api-samples/downloads/download_manager/manifest.json b/api-samples/downloads/download_manager/manifest.json new file mode 100644 index 0000000000..adaeb65ad2 --- /dev/null +++ b/api-samples/downloads/download_manager/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "Download Manager Button", + "version": "0.3", + "manifest_version": 3, + "description": "Uses multiple chrome.downloads APIs to implement a simple download manager.", + "icons": { + "128": "icon128.png" + }, + "action": { + "default_icon": { + "19": "icon19.png", + "38": "icon38.png" + }, + "default_title": "__MSG_extName__", + "default_popup": "popup.html" + }, + "background": { + "service_worker": "service-worker.js" + }, + "default_locale": "en", + "optional_permissions": ["management"], + "permissions": ["downloads", "downloads.open", "downloads.ui", "storage"] +} diff --git a/api-samples/downloads/download_manager/popup.css b/api-samples/downloads/download_manager/popup.css new file mode 100644 index 0000000000..0cadf01f70 --- /dev/null +++ b/api-samples/downloads/download_manager/popup.css @@ -0,0 +1,235 @@ +#outer, +#empty, +#open-folder, +.file-url, +.time-left, +.more-left { + display: inline-block; +} + +#q-outer { + display: inline-block; + height: 1.7em; + overflow: hidden; +} + +#q { + margin-right: 2em; +} + +#head { + position: fixed; + z-index: 2; + width: 100%; + left: 0; + top: 0; + background: white; + border-bottom: 1px solid grey; +} + +#search-zero, +#items { + margin-top: 2em; +} + +#head, +#empty, +#searching, +#search-zero, +.item, +.start-time, +.complete-size, +.error, +.file-url-head, +.removed, +.open-filename, +.progress, +.time-left, +.url, +#text-width-probe { + white-space: nowrap; +} + +#head, +.item { + text-align: left; +} + +#empty { + vertical-align: bottom; + height: 2em; +} + +#q { + margin-left: 3%; +} + +.by-ext img, +svg { + width: 2em; + height: 2em; +} + +svg rect.border { + fill-opacity: 0; + stroke-width: 6; +} + +#open-folder svg, +.show-folder svg { + stroke: brown; + fill: white; + stroke-width: 3; +} + +#clear-all svg { + fill: white; + stroke-width: 3; +} + +#clear-all svg, +.remove-file svg, +.erase svg { + stroke: black; +} + +#older, +#loading-older, +.item { + margin-top: 1em; +} + +.more { + position: absolute; + border: 1px solid grey; + background: white; + z-index: 3; +} + +.complete-size, +.error, +.removed, +.open-filename, +.progress, +.time-left { + margin-right: 0.4em; +} + +.error { + color: darkred; +} + +.referrer svg { + stroke: blue; + fill-opacity: 0; + stroke-width: 7; +} + +.removed, +.open-filename { + max-width: 400px; + overflow: hidden; + display: inline-block; +} + +.remove-file svg { + fill-opacity: 0; + stroke-width: 5; +} + +.remove-file svg line { + stroke-width: 7; + stroke: red; +} + +.erase svg ellipse, +.erase svg line { + fill-opacity: 0; + stroke-width: 5; +} + +.pause svg { + stroke: #333; +} + +.resume svg { + stroke: rgb(68, 187, 68); + fill: rgb(68, 187, 68); +} + +.cancel svg line { + stroke-width: 7; +} + +.cancel svg { + stroke: rgb(204, 68, 68); +} + +.meter { + height: 0.5em; + position: relative; + background: grey; +} + +.meter > span { + display: block; + height: 100%; + background-color: rgb(43, 194, 83); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0, rgb(43, 194, 83)), + color-stop(1, rgb(84, 240, 84)) + ); + position: relative; + overflow: hidden; +} + +.meter > span:after { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-image: -webkit-gradient( + linear, + 0 0, + 100% 100%, + color-stop(0.25, rgba(255, 255, 255, 0.2)), + color-stop(0.25, transparent), + color-stop(0.5, transparent), + color-stop(0.5, rgba(255, 255, 255, 0.2)), + color-stop(0.75, rgba(255, 255, 255, 0.2)), + color-stop(0.75, transparent), + to(transparent) + ); + z-index: 1; + -webkit-background-size: 50px 50px; + background-size: 50px 50px; + animation: move 2s linear infinite; + overflow: hidden; +} + +@keyframes move { + 0% { + background-position: 0 0; + } + 100% { + background-position: 50px 50px; + } +} + +.url { + color: grey; + max-width: 700px; + overflow: hidden; + display: block; +} + +#text-width-probe { + position: absolute; + top: -100px; + left: 0; +} diff --git a/api-samples/downloads/download_manager/popup.html b/api-samples/downloads/download_manager/popup.html new file mode 100644 index 0000000000..69613dbcea --- /dev/null +++ b/api-samples/downloads/download_manager/popup.html @@ -0,0 +1,210 @@ + + + + popup + + + + + + +
+ + + +
+ + + +
+ + + + + diff --git a/api-samples/downloads/download_manager/popup.js b/api-samples/downloads/download_manager/popup.js new file mode 100644 index 0000000000..c1f6df246b --- /dev/null +++ b/api-samples/downloads/download_manager/popup.js @@ -0,0 +1,769 @@ +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 + ); +} + +async function setLastOpened() { + await chrome.storage.local.set({ 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('.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}{Finishing,Days}. If + // ...Hours/Minutes/Seconds could be longer for any locale, then this should + // test them. + let max_time_left_width = 0; + for (let i = 0; i < 4; ++i) { + max_time_left_width = Math.max( + max_time_left_width, + getTextWidth(formatTimeLeft(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) { + const probe = document.getElementById('text-width-probe'); + probe.innerText = s; + return probe.offsetWidth; +} + +function formatDateTime(date) { + const now = new Date(); + const 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'; + } + const prefixes = 'KMGTPEZY'; + let mul = 1024; + for (let 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(ms) { + const prefix = 'timeLeft'; + if (ms < 1000) { + return chrome.i18n.getMessage(prefix + 'Finishing'); + } + const days = parseInt(ms / (24 * 60 * 60 * 1000)); + const hours = parseInt(ms / (60 * 60 * 1000)) % 24; + if (days) { + return chrome.i18n.getMessage(prefix + 'Days', [days, hours]); + } + const minutes = parseInt(ms / (60 * 1000)) % 60; + if (hours) { + return chrome.i18n.getMessage(prefix + 'Hours', [hours, minutes]); + } + const 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) { + const current = parseInt(document.body.style.minWidth) || 0; + document.body.style.minWidth = Math.max(w, current) + 'px'; +} + +function ratchetHeight(h) { + const current = parseInt(document.body.style.minHeight) || 0; + document.body.style.minHeight = Math.max(h, current) + 'px'; +} + +function binarySearch(array, target, cmp) { + let 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); +} + +class DownloadItem { + constructor(data) { + let item = this; + for (let prop in data) { + item[prop] = data[prop]; + } + item.startTime = new Date(item.startTime); + item.div = document.querySelector('body>div.item').cloneNode(true); + item.div.id = 'item' + item.id; + item.div.item = item; + + let 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 { + const adjacent_div = + items_div.childNodes[ + binarySearch( + arrayFrom(items_div.childNodes), + item.startTime.getTime(), + function (target, other) { + return target - other.item.startTime.getTime(); + } + ) + ]; + const 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('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) { + const mouse = { x: evt.x, y: evt.y + document.documentElement.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(); + } + getElement(name) { + return document.querySelector('#item' + this.id + ' .' + name); + } + async render() { + let item = this; + const now = new Date(); + const in_progress = item.state == 'in_progress'; + const openable = + item.state != 'interrupted' && item.exists && !item.deleted; + + item.startTime = new Date(item.startTime); + 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) { + const icon_url = await chrome.downloads.getFileIcon(item.id, { + size: 32 + }); + 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'; + if (in_progress) { + item.getElement('open-filename').removeAttribute('href'); + } else { + item.getElement('open-filename').setAttribute('href', '#'); + } + 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; + + const 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) { + const result = await chrome.permissions.contains({ + permissions: ['management'] + }); + if (result) { + setByExtension(true); + } else { + setByExtension(false); + const managementPermissionDenied = ( + await chrome.storage.local.get(['managementPermissionDenied']) + ).managementPermissionDenied; + if (!managementPermissionDenied) { + document.getElementById( + 'request-management-permission' + ).hidden = false; + document.getElementById('grant-management-permission').onclick = + async function () { + const granted = await chrome.permissions.request({ + permissions: ['management'] + }); + setByExtension(granted); + if (!granted) { + await chrome.storage.local.set({ + managementPermissionDenied: true + }); + } + return false; + }; + } + } + } else { + setByExtension(false); + } + + if (!item.getElement('error').hidden) { + if (item.error) { + 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) { + item.getElement('time-left').innerText = formatTimeLeft( + 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(); + } + onChanged(delta) { + for (let 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(); + } + } + onErased() { + window.removeEventListener('mousemove', this.more_mousemove); + document.getElementById('items').removeChild(this.div); + } + show() { + chrome.downloads.show(this.id); + } + open() { + if (this.state == 'complete') { + chrome.downloads.open(this.id); + return; + } + } + removeFile() { + chrome.downloads.removeFile(this.id); + this.deleted = true; + this.render(); + } + erase() { + chrome.downloads.erase({ id: this.id }); + } + pause() { + chrome.downloads.pause(this.id); + } + resume() { + chrome.downloads.resume(this.id); + } + cancel() { + chrome.downloads.cancel(this.id); + } + async maybeAccept() { + // 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; + + let id = this.id; + await chrome.downloads.acceptDanger(id); + DownloadItem.prototype.maybeAccept.accepting_danger = false; + arrayFrom(document.getElementById('items').childNodes).forEach(function ( + item_div + ) { + item_div.item.maybeAccept(); + }); + } +} + +DownloadItem.prototype.maybeAccept.accepting_danger = false; + +let DownloadManager = {}; + +DownloadManager.showingOlder = false; + +DownloadManager.getItem = function (id) { + const item_div = document.getElementById('item' + id); + return item_div ? item_div.item : null; +}; + +DownloadManager.getOrCreate = function (data) { + const 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 = async function () { + DownloadManager.startPollingProgress.tid = -1; + const results = await chrome.downloads.search({ + state: 'in_progress', + paused: false + }); + if (!results.length) return; + results.forEach(function (result) { + const item = DownloadManager.getOrCreate(result); + for (let prop in result) { + item[prop] = result[prop]; + } + item.render(); + if (item.state == 'in_progress' && !item.paused) { + DownloadManager.startPollingProgress(); + } + }); +}; + +DownloadManager.showNew = function () { + const 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; + + const query_search = document.getElementById('q'); + query_search.hidden = !any_items; + + if (!any_items) { + return; + } + const old_ms = new Date().getTime() - kOldMs; + let any_hidden = false; + let 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 = async function () { + DownloadManager.showingOlder = true; + const loading_older_span = document.getElementById('loading-older'); + document.getElementById('older').hidden = true; + loading_older_span.hidden = false; + const results = await chrome.downloads.search({}); + results.forEach(function (result) { + const item = DownloadManager.getOrCreate(result); + item.div.hidden = false; + }); + loading_older_span.hidden = true; +}; + +DownloadManager.onSearch = async function () { + // split string by space, but ignore space in quotes + // http://stackoverflow.com/questions/16261635 + let 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; + }); + const searching = document.getElementById('searching'); + searching.hidden = false; + const results = await chrome.downloads.search({ query: query }); + 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. + } + }); +}; + +const kShowNewMax = 50; +const kOldMs = 1000 * 60 * 60 * 24 * 7; + +DownloadManager.loadItems = async function () { + // Request up to kShowNewMax + 1, but only display kShowNewMax; the +1 is a + // probe to see if there are any older downloads. + + const results = await chrome.downloads.search({ + orderBy: ['-startTime'], + limit: kShowNewMax + 1 + }); + 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(); +}; + +// 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) { + const item = DownloadManager.getItem(delta.id); + if (item) { + item.onChanged(delta); + } +}); + +chrome.downloads.onErased.addListener(function (id) { + const 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(); + DownloadManager.startPollingProgress(); + 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; + }; + document.getElementById('open-folder').onclick = function () { + chrome.downloads.showDefaultFolder(); + return false; + }; +}; diff --git a/api-samples/downloads/download_manager/service-worker.js b/api-samples/downloads/download_manager/service-worker.js new file mode 100644 index 0000000000..d90ac86069 --- /dev/null +++ b/api-samples/downloads/download_manager/service-worker.js @@ -0,0 +1,219 @@ +chrome.downloads.setUiOptions({ enabled: false }); + +const colors = { + progressColor: '#0d0', + arrow: '#555', + danger: 'red', + complete: 'green', + paused: 'grey', + background: 'white' +}; + +Math.TAU = 2 * Math.PI; // http://tauday.com/tau-manifesto + +function drawProgressArc(ctx, startAngle, endAngle) { + const center = ctx.canvas.width / 2; + ctx.lineWidth = Math.round(ctx.canvas.width * 0.1); + ctx.beginPath(); + ctx.moveTo(center, center); + ctx.arc(center, center, center * 0.9, startAngle, endAngle, false); + ctx.fill(); + ctx.stroke(); +} + +function drawUnknownProgressSpinner(ctx) { + const segments = 16; + const segArc = Math.TAU / segments; + for (let seg = 0; seg < segments; ++seg) { + ctx.fillStyle = ctx.strokeStyle = + seg % 2 == 0 ? colors.progressColor : colors.background; + drawProgressArc(ctx, (seg - 4) * segArc, (seg - 3) * segArc); + } +} + +function drawProgressSpinner(ctx, stage) { + ctx.fillStyle = ctx.strokeStyle = colors.progressColor; + const clocktop = -Math.TAU / 4; + drawProgressArc(ctx, clocktop, clocktop + stage * Math.TAU); +} + +function drawArrow(ctx) { + ctx.beginPath(); + ctx.lineWidth = Math.round(ctx.canvas.width * 0.1); + ctx.lineJoin = 'round'; + ctx.strokeStyle = ctx.fillStyle = colors.arrow; + const center = ctx.canvas.width / 2; + const minw2 = center * 0.2; + const maxw2 = center * 0.6; + const height2 = maxw2; + ctx.moveTo(center - minw2, center - height2); + ctx.lineTo(center + minw2, center - height2); + ctx.lineTo(center + minw2, center); + ctx.lineTo(center + maxw2, center); + ctx.lineTo(center, center + height2); + ctx.lineTo(center - maxw2, center); + ctx.lineTo(center - minw2, center); + ctx.lineTo(center - minw2, center - height2); + ctx.lineTo(center + minw2, center - height2); + ctx.stroke(); + ctx.fill(); +} + +function drawDangerBadge(ctx) { + const s = ctx.canvas.width / 100; + ctx.fillStyle = colors.danger; + ctx.strokeStyle = colors.background; + ctx.lineWidth = Math.round(s * 5); + const edge = ctx.canvas.width - ctx.lineWidth; + ctx.beginPath(); + ctx.moveTo(s * 75, s * 55); + ctx.lineTo(edge, edge); + ctx.lineTo(s * 55, edge); + ctx.lineTo(s * 75, s * 55); + ctx.lineTo(edge, edge); + ctx.fill(); + ctx.stroke(); +} + +function drawPausedBadge(ctx) { + const s = ctx.canvas.width / 100; + ctx.beginPath(); + ctx.strokeStyle = colors.background; + ctx.lineWidth = Math.round(s * 5); + ctx.rect(s * 55, s * 55, s * 15, s * 35); + ctx.fillStyle = colors.paused; + ctx.fill(); + ctx.stroke(); + ctx.rect(s * 75, s * 55, s * 15, s * 35); + ctx.fill(); + ctx.stroke(); +} + +function drawCompleteBadge(ctx) { + const s = ctx.canvas.width / 100; + ctx.beginPath(); + ctx.arc(s * 75, s * 75, s * 15, 0, Math.TAU, false); + ctx.fillStyle = colors.complete; + ctx.fill(); + ctx.strokeStyle = colors.background; + ctx.lineWidth = Math.round(s * 5); + ctx.stroke(); +} + +function drawIcon(side, options) { + const canvas = new OffscreenCanvas(side, side); + const ctx = canvas.getContext('2d'); + if (options.anyInProgress) { + if (options.anyMissingTotalBytes) { + drawUnknownProgressSpinner(ctx); + } else { + drawProgressSpinner( + ctx, + options.totalBytesReceived / options.totalTotalBytes + ); + } + } + drawArrow(ctx); + if (options.anyDangerous) { + drawDangerBadge(ctx); + } else if (options.anyPaused) { + drawPausedBadge(ctx); + } else if (options.anyRecentlyCompleted) { + drawCompleteBadge(ctx); + } + return canvas; +} + +function setActionIcon(options) { + const canvas1 = drawIcon(19, options); + const canvas2 = drawIcon(38, options); + const imageData = {}; + imageData['' + canvas1.width] = canvas1 + .getContext('2d') + .getImageData(0, 0, canvas1.width, canvas1.height); + imageData['' + canvas2.width] = canvas2 + .getContext('2d') + .getImageData(0, 0, canvas2.width, canvas2.height); + chrome.action.setIcon({ imageData: imageData }); +} + +async function pollProgress() { + pollProgress.tid = -1; + const items = await chrome.downloads.search({}); + + let popupLastOpened = (await chrome.storage.local.get(['popupLastOpened'])) + .popupLastOpened; + if (!popupLastOpened) { + popupLastOpened = new Date().getTime(); + await chrome.storage.local.set({ popupLastOpened }); + } + const options = { + anyMissingTotalBytes: false, + anyInProgress: false, + anyRecentlyCompleted: false, + anyPaused: false, + anyDangerous: false, + totalBytesReceived: 0, + totalTotalBytes: 0 + }; + items.forEach(function (item) { + if (item.state == 'in_progress') { + options.anyInProgress = true; + if (item.totalBytes) { + options.totalTotalBytes += item.totalBytes; + options.totalBytesReceived += item.bytesReceived; + } else { + options.anyMissingTotalBytes = true; + } + const dangerous = item.danger != 'safe' && item.danger != 'accepted'; + options.anyDangerous = options.anyDangerous || dangerous; + options.anyPaused = options.anyPaused || item.paused; + } else if (item.state == 'complete' && item.endTime && !item.error) { + options.anyRecentlyCompleted = + options.anyRecentlyCompleted || + new Date(item.endTime).getTime() >= popupLastOpened; + } + }); + + const targetIcon = JSON.stringify(options); + let currentIcon = (await chrome.storage.session.get(['currentIcon'])) + .currentIcon; + if (currentIcon != targetIcon) { + setActionIcon(options); + currentIcon = targetIcon; + await chrome.storage.session.set({ currentIcon }); + } + + if (options.anyInProgress && pollProgress.tid < 0) { + pollProgress.start(); + } +} +pollProgress.tid = -1; +pollProgress.MS = 200; + +pollProgress.start = function () { + if (pollProgress.tid < 0) { + pollProgress.tid = setTimeout(pollProgress, pollProgress.MS); + } +}; + +chrome.downloads.onCreated.addListener(function () { + pollProgress(); +}); + +pollProgress(); + +chrome.runtime.onMessage.addListener(function (request) { + if (request == 'poll') { + pollProgress.start(); + } + if (request == 'icons') { + [16, 19, 38, 128].forEach(function (s) { + const canvas = drawIcon(s); + chrome.downloads.download({ + url: canvas.toDataURL('image/png', 1.0), + filename: 'icon' + s + '.png' + }); + }); + } +}); diff --git a/api-samples/downloads/downloads_overwrite/README.md b/api-samples/downloads/downloads_overwrite/README.md new file mode 100644 index 0000000000..4a512d91ef --- /dev/null +++ b/api-samples/downloads/downloads_overwrite/README.md @@ -0,0 +1,13 @@ +# chrome.downloads + +Demonstrates using the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/) handler. + +## Overview + +This sample uses the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/#event-onDeterminingFilename) handler to override target filenames for all downloads instead of adding ' (1)', ' (2)', etc.. + +## Running this extension + +1. Clone this repository. +2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +3. Download a file twice to see the effect. diff --git a/api-samples/downloads/downloads_overwrite/manifest.json b/api-samples/downloads/downloads_overwrite/manifest.json new file mode 100644 index 0000000000..485d1f6883 --- /dev/null +++ b/api-samples/downloads/downloads_overwrite/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "Downloads Overwrite Existing Files", + "description": "Uses the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/#event-onDeterminingFilename) handler to override target file names for all downloads instead of adding ' (1)', ' (2)', etc.", + "version": "0.1", + "background": { + "service_worker": "service-worker.js" + }, + "permissions": ["downloads"], + "manifest_version": 3 +} diff --git a/api-samples/downloads/downloads_overwrite/service-worker.js b/api-samples/downloads/downloads_overwrite/service-worker.js new file mode 100644 index 0000000000..f8c18f03b5 --- /dev/null +++ b/api-samples/downloads/downloads_overwrite/service-worker.js @@ -0,0 +1,9 @@ +// Force all downloads to overwrite any existing files instead of inserting +// ' (1)', ' (2)', etc. + +chrome.downloads.onDeterminingFilename.addListener(function (item, suggest) { + suggest({ + filename: item.filename, + conflictAction: 'overwrite' + }); +});