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
+
+
+
+
+
+ New Rule
+
+
+
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
+
+
+
+
+
+ Regex
+ Download All!
+
+ Download All!
+
+
+
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
+
+
+
+
+ Generate Manifest Icons
+
+
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'
+ });
+});