From 78a25bb6c235afe0d7fda1d12cdd5606594cea3d Mon Sep 17 00:00:00 2001 From: daidr Date: Mon, 29 May 2023 23:44:10 +0800 Subject: [PATCH 01/25] Add sample download_filename_controller --- .../download_filename_controller/bg.js | 42 ++++++++ .../manifest.json | 11 +++ .../download_filename_controller/options.html | 40 ++++++++ .../download_filename_controller/options.js | 96 +++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 api-samples/downloads/download_filename_controller/bg.js create mode 100644 api-samples/downloads/download_filename_controller/manifest.json create mode 100644 api-samples/downloads/download_filename_controller/options.html create mode 100644 api-samples/downloads/download_filename_controller/options.js diff --git a/api-samples/downloads/download_filename_controller/bg.js b/api-samples/downloads/download_filename_controller/bg.js new file mode 100644 index 0000000000..477b8889e9 --- /dev/null +++ b/api-samples/downloads/download_filename_controller/bg.js @@ -0,0 +1,42 @@ +function matches(rule, item) { + if (rule.matcher == '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 + ); + } + if (rule.matcher == 'default') return item.filename == rule.match_param; + if (rule.matcher == 'url-regex') + return new RegExp(rule.match_param).test(item.url); + if (rule.matcher == 'default-regex') + return new RegExp(rule.match_param).test(item.filename); + return false; +} + +chrome.downloads.onDeterminingFilename.addListener(function (item, __suggest) { + function suggest(filename, conflictAction) { + __suggest({ filename: filename, conflictAction: conflictAction }); + } + 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(item.filename, 'overwrite'); + } else if (rule.action == 'prompt') { + suggest(item.filename, 'prompt'); + } + return; + } + } + suggest(item.filename); + }); + + // return true to indicate that suggest() was called asynchronously + return true; +}); 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..6987ece3ca --- /dev/null +++ b/api-samples/downloads/download_filename_controller/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "Download Filename Controller", + "description": "Download Filename Controller", + "version": "0.1", + "background": { + "service_worker": "bg.js" + }, + "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(); + }; +}; From 60bdf2f8f27b0abbe579b88bc8a57a0f6e011805 Mon Sep 17 00:00:00 2001 From: daidr Date: Tue, 30 May 2023 11:37:48 +0800 Subject: [PATCH 02/25] Add README --- .../downloads/download_filename_controller/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 api-samples/downloads/download_filename_controller/README.md 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..dee12bf10a --- /dev/null +++ b/api-samples/downloads/download_filename_controller/README.md @@ -0,0 +1,7 @@ +# chrome.downloads - `onDeterminingFilename` event + +A sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. + +## Overview + +In this sample, the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/#event-onDeterminingFilename) event is used to set conflict action for the files being downloaded. From 12e2e9de82777de5d9711e347588936d73a1b511 Mon Sep 17 00:00:00 2001 From: daidr Date: Wed, 31 May 2023 15:07:58 +0800 Subject: [PATCH 03/25] Add download_links sample --- .../downloads/download_links/README.md | 7 ++ .../downloads/download_links/manifest.json | 11 ++ .../downloads/download_links/popup.html | 21 ++++ api-samples/downloads/download_links/popup.js | 114 ++++++++++++++++++ .../downloads/download_links/send_links.js | 35 ++++++ 5 files changed, 188 insertions(+) create mode 100644 api-samples/downloads/download_links/README.md create mode 100644 api-samples/downloads/download_links/manifest.json create mode 100644 api-samples/downloads/download_links/popup.html create mode 100644 api-samples/downloads/download_links/popup.js create mode 100644 api-samples/downloads/download_links/send_links.js diff --git a/api-samples/downloads/download_links/README.md b/api-samples/downloads/download_links/README.md new file mode 100644 index 0000000000..2a1c305df5 --- /dev/null +++ b/api-samples/downloads/download_links/README.md @@ -0,0 +1,7 @@ +# chrome.downloads - `download` method + +A sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. + +## Overview + +In this sample, all available links on the active page will be listed in the popup. The [`chrome.downloads.download`](https://developer.chrome.com/docs/extensions/reference/downloads/#method-download) method is used to download all selected links. diff --git a/api-samples/downloads/download_links/manifest.json b/api-samples/downloads/download_links/manifest.json new file mode 100644 index 0000000000..a069038336 --- /dev/null +++ b/api-samples/downloads/download_links/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "Download Selected Links", + "description": "Select links on a page and download them.", + "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..13e74414ac --- /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); +} From 237cae9fd24c579eb6f5d124121f0d1db36cda52 Mon Sep 17 00:00:00 2001 From: daidr Date: Wed, 31 May 2023 17:23:57 +0800 Subject: [PATCH 04/25] Add download_open sample (WIP) --- .../download_open/_locales/en/messages.json | 14 +++++++ .../downloads/download_open/background.js | 39 ++++++++++++++++++ .../downloads/download_open/icon128.png | Bin 0 -> 511 bytes .../downloads/download_open/icon16.png | Bin 0 -> 161 bytes .../downloads/download_open/manifest.json | 15 +++++++ 5 files changed, 68 insertions(+) create mode 100644 api-samples/downloads/download_open/_locales/en/messages.json create mode 100644 api-samples/downloads/download_open/background.js create mode 100644 api-samples/downloads/download_open/icon128.png create mode 100644 api-samples/downloads/download_open/icon16.png create mode 100644 api-samples/downloads/download_open/manifest.json diff --git a/api-samples/downloads/download_open/_locales/en/messages.json b/api-samples/downloads/download_open/_locales/en/messages.json new file mode 100644 index 0000000000..129e664007 --- /dev/null +++ b/api-samples/downloads/download_open/_locales/en/messages.json @@ -0,0 +1,14 @@ +{ + "extName": { + "message": "Download and Open Button", + "description": "Extension name" + }, + "extDesc": { + "message": "Download and Open Context Menu Button", + "description": "Extension description" + }, + "openContextMenuTitle": { + "message": "Download and Open", + "description": "context menu button text" + } +} diff --git a/api-samples/downloads/download_open/background.js b/api-samples/downloads/download_open/background.js new file mode 100644 index 0000000000..026cbb93c7 --- /dev/null +++ b/api-samples/downloads/download_open/background.js @@ -0,0 +1,39 @@ +async function getOpeningIds() { + let { openWhenComplete: ids } = await chrome.storage.session.get([ + 'openWhenComplete' + ]); + return ids || []; +} + +async function setOpeningIds(ids) { + await chrome.storage.session.set({ openWhenComplete: ids }); +} + +chrome.downloads.onChanged.addListener(async function (delta) { + if (!delta.state || delta.state.current != 'complete') { + return; + } + const ids = await getOpeningIds(); + if (ids.indexOf(delta.id) < 0) { + return; + } + chrome.downloads.open(delta.id); + ids.splice(ids.indexOf(delta.id), 1); + await setOpeningIds(ids); +}); + +chrome.contextMenus.onClicked.addListener(async function (info, tab) { + const downloadId = await chrome.downloads.download({ url: info.linkUrl }); + const ids = await getOpeningIds(); + if (ids.indexOf(downloadId) >= 0) { + return; + } + ids.push(downloadId); + await setOpeningIds(ids); +}); + +chrome.contextMenus.create({ + id: 'open', + title: chrome.i18n.getMessage('openContextMenuTitle'), + contexts: ['link'] +}); diff --git a/api-samples/downloads/download_open/icon128.png b/api-samples/downloads/download_open/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..50b99a1c98048a690d1668349c0a26845951db24 GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&IR*HHxB}^rkdRO?05X6e01O~( zpfHdDk%f~Gk>KE92;<<1%O8NIh?WHT1vC8nVBk=%Ah5qbVZnh5_vhOeoDX>4xWP$^ zfq^mH)5S5QBJS;tgIR|S1Y846H>yTYo4e%gpa1L25ond;VeP52M~G`cMidiXl2?|BP%^9)mZH>o31;P?Skf z-z}ujz`(?TN<4Xf#3F`n z;SaK`8GZ+LrT6jO{dn!w|E9PDzyIm4X#ag{-_<8E)`AD>yzf1Y)_?Ff+V9=nYR32X zgOixr0pQ&s&x`3y8k4uQzdi UmZXu90gM9%Pgg&ebxsLQ0F5W;Qvd(} literal 0 HcmV?d00001 diff --git a/api-samples/downloads/download_open/icon16.png b/api-samples/downloads/download_open/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..0f3aceb1921d53e190003419bc817c5f04137866 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S0wixl{&NRX8J;eVAr*0FPaWiKP~d4zbW38E zY;3u1S;8#Y>U*PSmCUWU7rjTm&%afZ_*~pjIN$<8*JYV&B#NTbti}-I> z5;dj6P*_cTQfqI-zopr05kwNi2wiq literal 0 HcmV?d00001 diff --git a/api-samples/downloads/download_open/manifest.json b/api-samples/downloads/download_open/manifest.json new file mode 100644 index 0000000000..9ca5976464 --- /dev/null +++ b/api-samples/downloads/download_open/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "__MSG_extName__", + "version": "0.1", + "manifest_version": 3, + "description": "__MSG_extDesc__", + "icons": { + "16": "icon16.png", + "128": "icon128.png" + }, + "background": { + "service_worker": "background.js" + }, + "default_locale": "en", + "permissions": ["contextMenus", "downloads", "downloads.open", "storage"] +} From 7676d39b42a34273f1b71ccd8070f9ceaffe5ac0 Mon Sep 17 00:00:00 2001 From: daidr Date: Wed, 31 May 2023 21:45:18 +0800 Subject: [PATCH 05/25] Add downloads_overwrite sample --- api-samples/downloads/downloads_overwrite/README.md | 7 +++++++ api-samples/downloads/downloads_overwrite/bg.js | 9 +++++++++ .../downloads/downloads_overwrite/manifest.json | 10 ++++++++++ 3 files changed, 26 insertions(+) create mode 100644 api-samples/downloads/downloads_overwrite/README.md create mode 100644 api-samples/downloads/downloads_overwrite/bg.js create mode 100644 api-samples/downloads/downloads_overwrite/manifest.json diff --git a/api-samples/downloads/downloads_overwrite/README.md b/api-samples/downloads/downloads_overwrite/README.md new file mode 100644 index 0000000000..d38e54c76a --- /dev/null +++ b/api-samples/downloads/downloads_overwrite/README.md @@ -0,0 +1,7 @@ +# chrome.downloads + +A sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. + +## Overview + +In this sample, the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/#event-onDeterminingFilename) event is used to set conflict action to `overwrite` for all downloads. diff --git a/api-samples/downloads/downloads_overwrite/bg.js b/api-samples/downloads/downloads_overwrite/bg.js new file mode 100644 index 0000000000..f8c18f03b5 --- /dev/null +++ b/api-samples/downloads/downloads_overwrite/bg.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' + }); +}); diff --git a/api-samples/downloads/downloads_overwrite/manifest.json b/api-samples/downloads/downloads_overwrite/manifest.json new file mode 100644 index 0000000000..e88ffb2183 --- /dev/null +++ b/api-samples/downloads/downloads_overwrite/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "Downloads Overwrite Existing Files", + "description": "All downloads overwrite existing files instead of adding ' (1)', ' (2)', etc.", + "version": "0.1", + "background": { + "service_worker": "bg.js" + }, + "permissions": ["downloads"], + "manifest_version": 3 +} From e7b3ff33a7a2bd3df3611f2e685e19fe93a27b2f Mon Sep 17 00:00:00 2001 From: daidr Date: Thu, 1 Jun 2023 15:17:54 +0800 Subject: [PATCH 06/25] Update README.md --- .../downloads/download_filename_controller/README.md | 8 +++++++- api-samples/downloads/download_links/README.md | 8 +++++++- api-samples/downloads/download_links/popup.html | 2 +- api-samples/downloads/downloads_overwrite/README.md | 8 +++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/api-samples/downloads/download_filename_controller/README.md b/api-samples/downloads/download_filename_controller/README.md index dee12bf10a..06fbb05ab9 100644 --- a/api-samples/downloads/download_filename_controller/README.md +++ b/api-samples/downloads/download_filename_controller/README.md @@ -1,7 +1,13 @@ # chrome.downloads - `onDeterminingFilename` event -A sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. +A sample that demonstrates how to use the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. ## Overview In this sample, the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/#event-onDeterminingFilename) event is used to set 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. Add rules in the extension's options page and download files to see the effect. diff --git a/api-samples/downloads/download_links/README.md b/api-samples/downloads/download_links/README.md index 2a1c305df5..b61ecf3082 100644 --- a/api-samples/downloads/download_links/README.md +++ b/api-samples/downloads/download_links/README.md @@ -1,7 +1,13 @@ # chrome.downloads - `download` method -A sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. +A sample that demonstrates how to use the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads) API. ## Overview In this sample, all available links on the active page will be listed in the 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/popup.html b/api-samples/downloads/download_links/popup.html index 13e74414ac..9f29a208ae 100644 --- a/api-samples/downloads/download_links/popup.html +++ b/api-samples/downloads/download_links/popup.html @@ -1,7 +1,6 @@ - popup @@ -17,5 +16,6 @@ + diff --git a/api-samples/downloads/downloads_overwrite/README.md b/api-samples/downloads/downloads_overwrite/README.md index d38e54c76a..61561f010b 100644 --- a/api-samples/downloads/downloads_overwrite/README.md +++ b/api-samples/downloads/downloads_overwrite/README.md @@ -1,7 +1,13 @@ # chrome.downloads -A sample demonstrates using the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. +A sample that demonstrates how to use the [`chrome.downloads`](https://developer.chrome.com/docs/extensions/reference/downloads/) API. ## Overview In this sample, the [`chrome.downloads.onDeterminingFilename`](https://developer.chrome.com/docs/extensions/reference/downloads/#event-onDeterminingFilename) event is used to set conflict action to `overwrite` for all downloads. + +## 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. From a8bc45f602c6b183d5b2ae1a65afd161af180ea3 Mon Sep 17 00:00:00 2001 From: daidr Date: Thu, 1 Jun 2023 15:20:56 +0800 Subject: [PATCH 07/25] Rename background scripts --- .../downloads/download_filename_controller/manifest.json | 2 +- .../download_filename_controller/{bg.js => service-worker.js} | 0 api-samples/downloads/downloads_overwrite/manifest.json | 2 +- .../downloads/downloads_overwrite/{bg.js => service-worker.js} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename api-samples/downloads/download_filename_controller/{bg.js => service-worker.js} (100%) rename api-samples/downloads/downloads_overwrite/{bg.js => service-worker.js} (100%) diff --git a/api-samples/downloads/download_filename_controller/manifest.json b/api-samples/downloads/download_filename_controller/manifest.json index 6987ece3ca..ad3b085ef2 100644 --- a/api-samples/downloads/download_filename_controller/manifest.json +++ b/api-samples/downloads/download_filename_controller/manifest.json @@ -3,7 +3,7 @@ "description": "Download Filename Controller", "version": "0.1", "background": { - "service_worker": "bg.js" + "service_worker": "service-worker.js" }, "options_page": "options.html", "permissions": ["downloads", "storage"], diff --git a/api-samples/downloads/download_filename_controller/bg.js b/api-samples/downloads/download_filename_controller/service-worker.js similarity index 100% rename from api-samples/downloads/download_filename_controller/bg.js rename to api-samples/downloads/download_filename_controller/service-worker.js diff --git a/api-samples/downloads/downloads_overwrite/manifest.json b/api-samples/downloads/downloads_overwrite/manifest.json index e88ffb2183..47b01cb306 100644 --- a/api-samples/downloads/downloads_overwrite/manifest.json +++ b/api-samples/downloads/downloads_overwrite/manifest.json @@ -3,7 +3,7 @@ "description": "All downloads overwrite existing files instead of adding ' (1)', ' (2)', etc.", "version": "0.1", "background": { - "service_worker": "bg.js" + "service_worker": "service-worker.js" }, "permissions": ["downloads"], "manifest_version": 3 diff --git a/api-samples/downloads/downloads_overwrite/bg.js b/api-samples/downloads/downloads_overwrite/service-worker.js similarity index 100% rename from api-samples/downloads/downloads_overwrite/bg.js rename to api-samples/downloads/downloads_overwrite/service-worker.js From d66084f9ad76900087c4613161259bd885c000fd Mon Sep 17 00:00:00 2001 From: daidr Date: Fri, 2 Jun 2023 22:19:56 +0800 Subject: [PATCH 08/25] Remove download_open sample --- .../download_open/_locales/en/messages.json | 14 ------- .../downloads/download_open/background.js | 39 ------------------ .../downloads/download_open/icon128.png | Bin 511 -> 0 bytes .../downloads/download_open/icon16.png | Bin 161 -> 0 bytes .../downloads/download_open/manifest.json | 15 ------- 5 files changed, 68 deletions(-) delete mode 100644 api-samples/downloads/download_open/_locales/en/messages.json delete mode 100644 api-samples/downloads/download_open/background.js delete mode 100644 api-samples/downloads/download_open/icon128.png delete mode 100644 api-samples/downloads/download_open/icon16.png delete mode 100644 api-samples/downloads/download_open/manifest.json diff --git a/api-samples/downloads/download_open/_locales/en/messages.json b/api-samples/downloads/download_open/_locales/en/messages.json deleted file mode 100644 index 129e664007..0000000000 --- a/api-samples/downloads/download_open/_locales/en/messages.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extName": { - "message": "Download and Open Button", - "description": "Extension name" - }, - "extDesc": { - "message": "Download and Open Context Menu Button", - "description": "Extension description" - }, - "openContextMenuTitle": { - "message": "Download and Open", - "description": "context menu button text" - } -} diff --git a/api-samples/downloads/download_open/background.js b/api-samples/downloads/download_open/background.js deleted file mode 100644 index 026cbb93c7..0000000000 --- a/api-samples/downloads/download_open/background.js +++ /dev/null @@ -1,39 +0,0 @@ -async function getOpeningIds() { - let { openWhenComplete: ids } = await chrome.storage.session.get([ - 'openWhenComplete' - ]); - return ids || []; -} - -async function setOpeningIds(ids) { - await chrome.storage.session.set({ openWhenComplete: ids }); -} - -chrome.downloads.onChanged.addListener(async function (delta) { - if (!delta.state || delta.state.current != 'complete') { - return; - } - const ids = await getOpeningIds(); - if (ids.indexOf(delta.id) < 0) { - return; - } - chrome.downloads.open(delta.id); - ids.splice(ids.indexOf(delta.id), 1); - await setOpeningIds(ids); -}); - -chrome.contextMenus.onClicked.addListener(async function (info, tab) { - const downloadId = await chrome.downloads.download({ url: info.linkUrl }); - const ids = await getOpeningIds(); - if (ids.indexOf(downloadId) >= 0) { - return; - } - ids.push(downloadId); - await setOpeningIds(ids); -}); - -chrome.contextMenus.create({ - id: 'open', - title: chrome.i18n.getMessage('openContextMenuTitle'), - contexts: ['link'] -}); diff --git a/api-samples/downloads/download_open/icon128.png b/api-samples/downloads/download_open/icon128.png deleted file mode 100644 index 50b99a1c98048a690d1668349c0a26845951db24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&IR*HHxB}^rkdRO?05X6e01O~( zpfHdDk%f~Gk>KE92;<<1%O8NIh?WHT1vC8nVBk=%Ah5qbVZnh5_vhOeoDX>4xWP$^ zfq^mH)5S5QBJS;tgIR|S1Y846H>yTYo4e%gpa1L25ond;VeP52M~G`cMidiXl2?|BP%^9)mZH>o31;P?Skf z-z}ujz`(?TN<4Xf#3F`n z;SaK`8GZ+LrT6jO{dn!w|E9PDzyIm4X#ag{-_<8E)`AD>yzf1Y)_?Ff+V9=nYR32X zgOixr0pQ&s&x`3y8k4uQzdi UmZXu90gM9%Pgg&ebxsLQ0F5W;Qvd(} diff --git a/api-samples/downloads/download_open/icon16.png b/api-samples/downloads/download_open/icon16.png deleted file mode 100644 index 0f3aceb1921d53e190003419bc817c5f04137866..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S0wixl{&NRX8J;eVAr*0FPaWiKP~d4zbW38E zY;3u1S;8#Y>U*PSmCUWU7rjTm&%afZ_*~pjIN$<8*JYV&B#NTbti}-I> z5;dj6P*_cTQfqI-zopr05kwNi2wiq diff --git a/api-samples/downloads/download_open/manifest.json b/api-samples/downloads/download_open/manifest.json deleted file mode 100644 index 9ca5976464..0000000000 --- a/api-samples/downloads/download_open/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "__MSG_extName__", - "version": "0.1", - "manifest_version": 3, - "description": "__MSG_extDesc__", - "icons": { - "16": "icon16.png", - "128": "icon128.png" - }, - "background": { - "service_worker": "background.js" - }, - "default_locale": "en", - "permissions": ["contextMenus", "downloads", "downloads.open", "storage"] -} From 85d32ccf6cc89d0ed9250146aa1ebed586bd96e5 Mon Sep 17 00:00:00 2001 From: daidr Date: Sat, 3 Jun 2023 17:33:03 +0800 Subject: [PATCH 09/25] Add download_manager sample --- .../_locales/en/messages.json | 222 +++++ .../downloads/download_manager/icon128.png | Bin 0 -> 511 bytes .../downloads/download_manager/icon19.png | Bin 0 -> 161 bytes .../downloads/download_manager/icon38.png | Bin 0 -> 194 bytes .../downloads/download_manager/icons.html | 11 + .../downloads/download_manager/icons.js | 8 + .../downloads/download_manager/manifest.json | 23 + .../downloads/download_manager/popup.css | 235 ++++++ .../downloads/download_manager/popup.html | 215 +++++ .../downloads/download_manager/popup.js | 783 ++++++++++++++++++ .../download_manager/service-worker.js | 259 ++++++ 11 files changed, 1756 insertions(+) create mode 100644 api-samples/downloads/download_manager/_locales/en/messages.json create mode 100644 api-samples/downloads/download_manager/icon128.png create mode 100644 api-samples/downloads/download_manager/icon19.png create mode 100644 api-samples/downloads/download_manager/icon38.png create mode 100644 api-samples/downloads/download_manager/icons.html create mode 100644 api-samples/downloads/download_manager/icons.js create mode 100644 api-samples/downloads/download_manager/manifest.json create mode 100644 api-samples/downloads/download_manager/popup.css create mode 100644 api-samples/downloads/download_manager/popup.html create mode 100644 api-samples/downloads/download_manager/popup.js create mode 100644 api-samples/downloads/download_manager/service-worker.js 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..5fa9b0ad32 --- /dev/null +++ b/api-samples/downloads/download_manager/_locales/en/messages.json @@ -0,0 +1,222 @@ +{ + "extName": { + "message": "Download Manager Button", + "description": "Extension name" + }, + "extDesc": { + "message": "Browser Action Download Manager User Interface for Google Chrome", + "description": "Extension description" + }, + "badChromeVersion": { + "message": "The downloads API is only available on the canary, dev, and beta channels.", + "description": "" + }, + "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": "" }, + "openWhenCompleteFinishing": { + "message": "Opening in just a moment", + "description": "" + }, + "timeLeftFinishing": { + "message": "finishing...", + "description": "" + }, + "openWhenCompleteDays": { + "message": "Opening in $days$d $hours$h", + "description": "", + "placeholders": { + "days": { + "content": "$1", + "example": "2" + }, + "hours": { + "content": "$2", + "example": "23" + } + } + }, + "timeLeftDays": { + "message": "$days$d $hours$h left", + "description": "", + "placeholders": { + "days": { + "content": "$1", + "example": "2" + }, + "hours": { + "content": "$2", + "example": "23" + } + } + }, + "openWhenCompleteHours": { + "message": "Opening in $hours$h $mins$m", + "description": "", + "placeholders": { + "hours": { + "content": "$1", + "example": "23" + }, + "mins": { + "content": "$2", + "example": "59" + } + } + }, + "timeLeftHours": { + "message": "$hours$h $mins$m left", + "description": "", + "placeholders": { + "hours": { + "content": "$1", + "example": "23" + }, + "mins": { + "content": "$2", + "example": "59" + } + } + }, + "openWhenCompleteMinutes": { + "message": "Opening in $mins$m $sec$s", + "description": "", + "placeholders": { + "mins": { + "content": "$1", + "example": "59" + }, + "sec": { + "content": "$2", + "example": "59" + } + } + }, + "timeLeftMinutes": { + "message": "$mins$m $sec$s left", + "description": "", + "placeholders": { + "mins": { + "content": "$1", + "example": "59" + }, + "sec": { + "content": "$2", + "example": "59" + } + } + }, + "openWhenCompleteSeconds": { + "message": "Opening in $sec$s", + "description": "", + "placeholders": { + "sec": { + "content": "$1", + "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 0000000000000000000000000000000000000000..4d3610dd90df97a755ba2d09e477f40d44513930 GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&IR*HHxB}^rkdRO?05X6e01O~( zpfHdDk%f~Gk>KE92;<<1%O8NIh?WHT1vC8nVBk=%Ah5qbVZnh5_vhOeoDX>4xWP$^ zfq^mH)5S5QBJS-C-@d~N0<4K@g$It^_&M|C@Ay(NolOp5QCi=XE>*KHupc0=Ro4tF-d?)|0+|wz^Gx!gjPp{nmnQ>csNzZ5IAB^jxcn+)*-r&X< zrv5;bHN)?~HRHX9zt?4du0N>pp!UD`mF>T;?7R9T##-P&o%g+`r!^k@_4a!g>(2N- zJ~)Z#UOEzLXS#Eo>DF_qp=)5HE-Z%{cU*PS)!rXIYs`gy&%afd_{?L<(JuKFDJvo_Op0x*GQP6kAm_$;tBrNB zDXCN5BuXyk?MxJ|Bc!NJZS+GaES@97LKE-HTeJ z9SvT}9^{pAbT1Gwx1Jc@!QvPk`|;EEhp&rxkIS7l{-)MCGxTvvqKzIyd84eE+SXRp zX%gnw>Jw`h9&7wsud!Z2%g*?tY&(O>n}fB>H?SuZdEJ~7WVU?DdCjNAUO(pq* A im;MO|*!69?HRFVdjQTli^>zXs#o+1c=d#Wzp$Pz;dq>Rx literal 0 HcmV?d00001 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..548a80675a --- /dev/null +++ b/api-samples/downloads/download_manager/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "__MSG_extName__", + "version": "0.3", + "manifest_version": 3, + "description": "__MSG_extDesc__", + "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.shelf", "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..a5c78f1df5 --- /dev/null +++ b/api-samples/downloads/download_manager/popup.html @@ -0,0 +1,215 @@ + + + + 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..7f1c38350b --- /dev/null +++ b/api-samples/downloads/download_manager/popup.js @@ -0,0 +1,783 @@ +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('#bad-chrome-version', 'innerText', 'badChromeVersion'); + setProperty('.remove-file', 'title', 'removeFileTitle'); + + document.querySelector('.progress').style.minWidth = + getTextWidth( + formatBytes(1024 * 1024 * 1023.9) + + '/' + + formatBytes(1024 * 1024 * 1023.9) + ) + 'px'; + + // This only covers {timeLeft,openWhenComplete}{Finishing,Days}. If + // ...Hours/Minutes/Seconds could be longer for any locale, then this should + // test them. + 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(0 == i % 2, i < 2 ? 0 : (100 * 24 + 23) * 60 * 60 * 1000) + ) + ); + } + document.querySelector('body div.item span.time-left').style.minWidth = + max_time_left_width + 'px'; +} + +function getTextWidth(s) { + 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(openWhenComplete, ms) { + const prefix = openWhenComplete ? 'openWhenComplete' : '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); + if (item.canResume == undefined) { + DownloadItem.canResumeHack = true; + } + + item.div = document.querySelector('body>div.item').cloneNode(true); + item.div.id = 'item' + item.id; + item.div.item = item; + + 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.chilWdNodes), + 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.body.scrollTop }; + if ( + item.getElement('more') && + (pointInElement(mouse, item.div) || + pointInElement(mouse, item.getElement('more'))) + ) { + return; + } + if (item.getElement('more')) { + item.getElement('more').hidden = true; + } + window.removeEventListener('mousemove', item.more_mousemove); + }; + [item.div, item.getElement('more')] + .concat(item.getElement('more').children) + .forEach(function (elem) { + elem.onmouseover = function () { + arrayFrom(items_div.children).forEach(function (other) { + if (other.item != item) { + other.item.getElement('more').hidden = true; + } + }); + item.getElement('more').hidden = false; + item.getElement('more').style.top = + item.div.offsetTop + item.div.offsetHeight + 'px'; + item.getElement('more').style.left = item.div.offsetLeft + 'px'; + if ( + window.innerHeight < + parseInt(item.getElement('more').style.top) + + item.getElement('more').offsetHeight + ) { + item.getElement('more').style.top = + item.div.offsetTop - item.getElement('more').offsetHeight + 'px'; + } + window.addEventListener('mousemove', item.more_mousemove); + }; + }); + + if (item.referrer) { + item.getElement('referrer').href = item.referrer; + } else { + item.getElement('referrer').hidden = true; + } + item.getElement('url').href = item.url; + item.getElement('url').innerText = item.url; + item.render(); + } + 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); + if (DownloadItem.canResumeHack) { + item.canResume = in_progress && item.paused; + } + if (item.filename) { + item.basename = item.filename.substring( + Math.max( + item.filename.lastIndexOf('\\'), + item.filename.lastIndexOf('/') + ) + 1 + ); + } + if (item.estimatedEndTime) { + item.estimatedEndTime = new Date(item.estimatedEndTime); + } + if (item.endTime) { + item.endTime = new Date(item.endTime); + } + + if (item.filename && !item.icon_url) { + 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'; + 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) { + let openWhenComplete = + (await chrome.storage.local.get(['openWhenComplete'])) + .openWhenComplete || []; + openWhenComplete = openWhenComplete.indexOf(item.id) >= 0; + item.getElement('time-left').innerText = formatTimeLeft( + openWhenComplete, + item.estimatedEndTime.getTime() - now.getTime() + ); + } else { + item.getElement('time-left').innerText = String.fromCharCode(160); + } + } + + if (item.startTime) { + item.getElement('start-time').innerText = formatDateTime(item.startTime); + } + + ratchetWidth( + item.getElement('icon').offsetWidth + + item.getElement('file-url').offsetWidth + + item.getElement('cancel').offsetWidth + + item.getElement('pause').offsetWidth + + item.getElement('resume').offsetWidth + ); + ratchetWidth(item.getElement('more').offsetWidth); + + this.maybeAccept(); + } + 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; + } + chrome.runtime.sendMessage({ openWhenComplete: this.id }); + } + 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.canResumeHack = false; + +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(); + document.getElementById('older').onclick = function () { + DownloadManager.showOlder(); + return false; + }; + document.getElementById('q').onsearch = function () { + DownloadManager.onSearch(); + }; + document.getElementById('clear-all').onclick = function () { + DownloadManager.clearAll(); + return false; + }; + if (chrome.downloads.showDefaultFolder) { + document.getElementById('open-folder').onclick = function () { + chrome.downloads.showDefaultFolder(); + return false; + }; + } else { + document.getElementById('open-folder').hidden = true; + } +}; 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..64846406f3 --- /dev/null +++ b/api-samples/downloads/download_manager/service-worker.js @@ -0,0 +1,259 @@ +if (chrome.downloads.setShelfEnabled) chrome.downloads.setShelfEnabled(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; +} + +async function maybeOpen(id) { + let openWhenComplete = (await chrome.storage.local.get(['openWhenComplete'])) + .openWhenComplete; + + if (!openWhenComplete) { + openWhenComplete = []; + await chrome.storage.local.set({ openWhenComplete }); + } + const openNowIndex = openWhenComplete.indexOf(id); + if (openNowIndex >= 0) { + chrome.downloads.open(id); + openWhenComplete.splice(openNowIndex, 1); + await chrome.storage.local.set({ openWhenComplete }); + } +} + +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; + maybeOpen(item.id); + } + }); + + 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); + } +}; + +function isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +} + +chrome.downloads.onCreated.addListener(function (item) { + pollProgress(); +}); + +pollProgress(); + +async function openWhenComplete(downloadId) { + let ids = (await chrome.storage.local.get(['openWhenComplete'])) + .openWhenComplete; + + if (!ids) { + ids = []; + await chrome.storage.local.set({ openWhenComplete: ids }); + } + pollProgress.start(); + if (ids.indexOf(downloadId) >= 0) { + return; + } + ids.push(downloadId); + await chrome.storage.local.set({ openWhenComplete: ids }); +} + +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' + }); + }); + } + if (isNumber(request.openWhenComplete)) { + openWhenComplete(request.openWhenComplete); + } +}); From 48d12996ee04cd7c8a30b1d65ba95120aa84c0e7 Mon Sep 17 00:00:00 2001 From: daidr Date: Sat, 3 Jun 2023 23:17:32 +0800 Subject: [PATCH 10/25] Remove badChromeVersion --- .../downloads/download_manager/_locales/en/messages.json | 4 ---- api-samples/downloads/download_manager/popup.html | 5 ----- api-samples/downloads/download_manager/popup.js | 1 - 3 files changed, 10 deletions(-) diff --git a/api-samples/downloads/download_manager/_locales/en/messages.json b/api-samples/downloads/download_manager/_locales/en/messages.json index 5fa9b0ad32..95d62cf21c 100644 --- a/api-samples/downloads/download_manager/_locales/en/messages.json +++ b/api-samples/downloads/download_manager/_locales/en/messages.json @@ -7,10 +7,6 @@ "message": "Browser Action Download Manager User Interface for Google Chrome", "description": "Extension description" }, - "badChromeVersion": { - "message": "The downloads API is only available on the canary, dev, and beta channels.", - "description": "" - }, "tabTitle": { "message": "Downloads", "description": "tab title" diff --git a/api-samples/downloads/download_manager/popup.html b/api-samples/downloads/download_manager/popup.html index a5c78f1df5..69613dbcea 100644 --- a/api-samples/downloads/download_manager/popup.html +++ b/api-samples/downloads/download_manager/popup.html @@ -10,11 +10,6 @@