diff --git a/.eslintignore b/.eslintignore
index 37f3e66..a0612c7 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
**/node_modules/**
react-es6-popup/**/dist
mocha-client-tests
+store-collected-images/webextension-plain/deps
diff --git a/store-collected-images/README.md b/store-collected-images/README.md
new file mode 100644
index 0000000..1733902
--- /dev/null
+++ b/store-collected-images/README.md
@@ -0,0 +1,36 @@
+# "Image Reference Collector" example
+
+## What it does
+
+This example adds a context menu which targets any image element in the webpage.
+When the context menu item is clicked, the add-on opens a window and
+adds the related image element to the preview list of the collected images.
+The user can then store the collected images by giving the collection a name
+and pressing the **save** button.
+
+Once a collection of reference images has been stored by the add-on, they
+can be navigated using the extension page that the add-on will open in a tab
+when the user press the add-on **browserAction**.
+
+## What it shows
+
+The main goal of this example is showing how to use the [idb-file-storage library](https://www.npmjs.com/package/idb-file-storage) to store and manipulate files in a WebExtension.
+
+* How to store blob into the add-on IndexedDB storage
+* How to list the stored blobs (optionally by filtering the listed blobs)
+* How to turn the stored blobs into blob urls to show them in the extension page
+* How to remove the stored blobs from the extension IndexedDB storage.
+
+[](https://youtu.be/t6aVqMMe2Rc)
+
+This example is written in two forms:
+
+- a plain webextension (which doesn't need any build step)
+- a webextension built using webpack
+
+The code that stores and retrieves the files from the IndexedDB storage is in the
+file named `utils/image-store.js` in both the example version.
+
+## Icons
+
+The icon for this add-on is provided by [icons8](https://icons8.com/).
diff --git a/store-collected-images/screenshots/screenshot.png b/store-collected-images/screenshots/screenshot.png
new file mode 100644
index 0000000..01341b8
Binary files /dev/null and b/store-collected-images/screenshots/screenshot.png differ
diff --git a/store-collected-images/webextension-plain/.eslintignore b/store-collected-images/webextension-plain/.eslintignore
new file mode 100644
index 0000000..1aa6280
--- /dev/null
+++ b/store-collected-images/webextension-plain/.eslintignore
@@ -0,0 +1 @@
+deps
\ No newline at end of file
diff --git a/store-collected-images/webextension-plain/.eslintrc b/store-collected-images/webextension-plain/.eslintrc
new file mode 100644
index 0000000..f0310c8
--- /dev/null
+++ b/store-collected-images/webextension-plain/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "parser": "babel-eslint"
+}
\ No newline at end of file
diff --git a/store-collected-images/webextension-plain/README.md b/store-collected-images/webextension-plain/README.md
new file mode 100644
index 0000000..a6e2ce5
--- /dev/null
+++ b/store-collected-images/webextension-plain/README.md
@@ -0,0 +1,39 @@
+# "Image Reference Collector" example without a webpack build step (and React UI)
+
+## Usage
+
+This version of the example doesn't use Webpack and Babel to transpile the ES6 modules (and JSX)
+into JavaScript bundle scripts, so it can be executed (using `web-ext run` or by installing it temporarily from "about:debugging#addons") and changed without any build step.
+
+## NOTE on the plain JavaScript React UI
+
+The UI of this example is based on React (as is the "build with webpack" version of this example), but it uses plain JavaScript instead of JSX (the "HTML"-like syntax usually used in "React"-based projects), and so the component UI hierarchy is composed of `React.createElement` function calls, e.g.
+
+```
+class MyReactComponent extends React.Component {
+ render() {
+ return (
+
+
A title
+
A text paragraph
+
+ );
+ }
+}
+```
+
+in plain Javascript (without JSX) this becomes:
+
+```
+// Shortcut for React components render methods.
+const el = React.createElement;
+
+class Popup extends React.Component {
+ render() {
+ return el("div", {className: "important"}, [
+ el("h3", {}, "A title"),
+ el("p", {}, "A text paragraph"),
+ ]);
+ }
+}
+```
diff --git a/store-collected-images/webextension-plain/background.js b/store-collected-images/webextension-plain/background.js
new file mode 100644
index 0000000..436131f
--- /dev/null
+++ b/store-collected-images/webextension-plain/background.js
@@ -0,0 +1,47 @@
+// Open the UI to navigate the collection images in a tab.
+browser.browserAction.onClicked.addListener(() => {
+ browser.tabs.create({url: "/navigate-collection.html"});
+});
+
+// Add a context menu action on every image element in the page.
+browser.contextMenus.create({
+ id: "collect-image",
+ title: "Add to the collected images",
+ contexts: ["image"],
+});
+
+// Manage pending collected images.
+let pendingCollectedUrls = [];
+browser.runtime.onMessage.addListener((msg) => {
+ if (msg.type === "get-pending-collected-urls") {
+ let urls = pendingCollectedUrls;
+ pendingCollectedUrls = [];
+ return Promise.resolve(urls);
+ }
+});
+
+// Handle the context menu action click events.
+browser.contextMenus.onClicked.addListener(async (info) => {
+ try {
+ await browser.runtime.sendMessage({
+ type: "new-collected-images",
+ url: info.srcUrl,
+ });
+ } catch (err) {
+ if (err.message.includes("Could not establish connection. Receiving end does not exist.")) {
+ // Add the url to the pending urls and open a popup.
+ pendingCollectedUrls.push(info.srcUrl);
+ try {
+ await browser.windows.create({
+ type: "popup", url: "/popup.html",
+ top: 0, left: 0, width: 300, height: 400,
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ return;
+ }
+
+ console.error(err);
+ }
+});
diff --git a/store-collected-images/webextension-plain/deps/idb-file-storage.js b/store-collected-images/webextension-plain/deps/idb-file-storage.js
new file mode 100644
index 0000000..984a7f8
--- /dev/null
+++ b/store-collected-images/webextension-plain/deps/idb-file-storage.js
@@ -0,0 +1,801 @@
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("idb-file-storage", ["exports"], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(exports);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod.exports);
+ global.IDBFiles = mod.exports;
+ }
+})(this, function (exports) {
+ "use strict";
+
+ /**
+ * @typedef {Object} IDBPromisedFileHandle.Metadata
+ * @property {number} size
+ * The size of the file in bytes.
+ * @property {Date} last Modified
+ * The time and date of the last change to the file.
+ */
+
+ /**
+ * @typedef {Object} IDBFileStorage.ListFilteringOptions
+ * @property {string} startsWith
+ * A string to be checked with `fileNameString.startsWith(...)`.
+ * @property {string} endsWith
+ * A string to be checked with `fileNameString.endsWith(...)`.
+ * @property {string} includes
+ * A string to be checked with `fileNameString.includes(...)`.
+ * @property {function} filterFn
+ * A function to be used to check the file name (`filterFn(fileNameString)`).
+ */
+
+ /**
+ * Wraps a DOMRequest into a promise, optionally transforming the result using the onsuccess
+ * callback.
+ *
+ * @param {IDBRequest|DOMRequest} req
+ * The DOMRequest instance to wrap in a Promise.
+ * @param {function} [onsuccess]
+ * An optional onsuccess callback which can transform the result before resolving it.
+ *
+ * @returns {Promise}
+ * The promise which wraps the request result, rejected if the request.onerror has been
+ * called.
+ */
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ exports.waitForDOMRequest = waitForDOMRequest;
+ exports.getFileStorage = getFileStorage;
+ function waitForDOMRequest(req, onsuccess) {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = onsuccess ? () => resolve(onsuccess(req.result)) : () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /**
+ * Wraps an IDBMutableFile's FileHandle with a nicer Promise-based API.
+ *
+ * Instances of this class are created from the
+ * {@link IDBPromisedMutableFile.open} method.
+ */
+ class IDBPromisedFileHandle {
+ /**
+ * @private private helper method used internally.
+ */
+ constructor({ file, lockedFile }) {
+ // All the following properties are private and it should not be needed
+ // while using the API.
+
+ /** @private */
+ this.file = file;
+ /** @private */
+ this.lockedFile = lockedFile;
+ /** @private */
+ this.writeQueue = Promise.resolve();
+ /** @private */
+ this.closed = undefined;
+ /** @private */
+ this.aborted = undefined;
+ }
+
+ /**
+ * @private private helper method used internally.
+ */
+ ensureLocked({ invalidMode } = {}) {
+ if (this.closed) {
+ throw new Error("FileHandle has been closed");
+ }
+
+ if (this.aborted) {
+ throw new Error("FileHandle has been aborted");
+ }
+
+ if (!this.lockedFile) {
+ throw new Error("Invalid FileHandled");
+ }
+
+ if (invalidMode && this.lockedFile.mode === invalidMode) {
+ throw new Error(`FileHandle should not be opened as '${this.lockedFile.mode}'`);
+ }
+ if (!this.lockedFile.active) {
+ // Automatically relock the file with the last open mode
+ this.file.reopenFileHandle(this);
+ }
+ }
+
+ // Promise-based MutableFile API
+
+ /**
+ * Provide access to the mode that has been used to open the {@link IDBPromisedMutableFile}.
+ *
+ * @type {"readonly"|"readwrite"|"writeonly"}
+ */
+ get mode() {
+ return this.lockedFile.mode;
+ }
+
+ /**
+ * A boolean property that is true if the lock is still active.
+ *
+ * @type {boolean}
+ */
+ get active() {
+ return this.lockedFile ? this.lockedFile.active : false;
+ }
+
+ /**
+ * Close the locked file (and wait for any written data to be flushed if needed).
+ *
+ * @returns {Promise}
+ * A promise which is resolved when the close request has been completed
+ */
+ async close() {
+ if (!this.lockedFile) {
+ throw new Error("FileHandle is not open");
+ }
+
+ // Wait the queued write to complete.
+ await this.writeQueue;
+
+ // Wait for flush request to complete if needed.
+ if (this.lockedFile.active && this.lockedFile.mode !== "readonly") {
+ await waitForDOMRequest(this.lockedFile.flush());
+ }
+
+ this.closed = true;
+ this.lockedFile = null;
+ this.writeQueue = Promise.resolve();
+ }
+
+ /**
+ * Abort any pending data request and set the instance as aborted.
+ *
+ * @returns {Promise}
+ * A promise which is resolved when the abort request has been completed
+ */
+ async abort() {
+ if (this.lockedFile.active) {
+ // NOTE: in the docs abort is reported to return a DOMRequest, but it doesn't seem
+ // to be the case. (https://developer.mozilla.org/en-US/docs/Web/API/LockedFile/abort)
+ this.lockedFile.abort();
+ }
+
+ this.aborted = true;
+ this.lockedFile = null;
+ this.writeQueue = Promise.resolve();
+ }
+
+ /**
+ * Get the file metadata (take a look to {@link IDBPromisedFileHandle.Metadata} for more info).
+ *
+ * @returns {Promise<{size: number, lastModified: Date}>}
+ * A promise which is resolved when the request has been completed
+ */
+ async getMetadata() {
+ this.ensureLocked();
+ return waitForDOMRequest(this.lockedFile.getMetadata());
+ }
+
+ /**
+ * Read a given amount of data from the file as Text (optionally starting from the specified
+ * location).
+ *
+ * @param {number} size
+ * The amount of data to read.
+ * @param {number} [location]
+ * The location where the request should start to read the data.
+ *
+ * @returns {Promise}
+ * A promise which resolves to the data read, when the request has been completed.
+ */
+ async readAsText(size, location) {
+ this.ensureLocked({ invalidMode: "writeonly" });
+ if (typeof location === "number") {
+ this.lockedFile.location = location;
+ }
+ return waitForDOMRequest(this.lockedFile.readAsText(size));
+ }
+
+ /**
+ * Read a given amount of data from the file as an ArrayBufer (optionally starting from the specified
+ * location).
+ *
+ * @param {number} size
+ * The amount of data to read.
+ * @param {number} [location]
+ * The location where the request should start to read the data.
+ *
+ * @returns {Promise}
+ * A promise which resolves to the data read, when the request has been completed.
+ */
+ async readAsArrayBuffer(size, location) {
+ this.ensureLocked({ invalidMode: "writeonly" });
+ if (typeof location === "number") {
+ this.lockedFile.location = location;
+ }
+ return waitForDOMRequest(this.lockedFile.readAsArrayBuffer(size));
+ }
+
+ /**
+ * Truncate the file (optionally at a specified location).
+ *
+ * @param {number} [location=0]
+ * The location where the file should be truncated.
+ *
+ * @returns {Promise}
+ * A promise which is resolved once the request has been completed.
+ */
+ async truncate(location = 0) {
+ this.ensureLocked({ invalidMode: "readonly" });
+ return waitForDOMRequest(this.lockedFile.truncate(location));
+ }
+
+ /**
+ * Append the passed data to the end of the file.
+ *
+ * @param {string|ArrayBuffer} data
+ * The data to append to the end of the file.
+ *
+ * @returns {Promise}
+ * A promise which is resolved once the request has been completed.
+ */
+ async append(data) {
+ this.ensureLocked({ invalidMode: "readonly" });
+ return waitForDOMRequest(this.lockedFile.append(data));
+ }
+
+ /**
+ * Write data into the file (optionally starting from a defined location in the file).
+ *
+ * @param {string|ArrayBuffer} data
+ * The data to write into the file.
+ * @param {number} location
+ * The location where the data should be written.
+ *
+ * @returns {Promise}
+ * A promise which is resolved to the location where the written data ends.
+ */
+ async write(data, location) {
+ this.ensureLocked({ invalidMode: "readonly" });
+ if (typeof location === "number") {
+ this.lockedFile.location = location;
+ }
+ return waitForDOMRequest(this.lockedFile.write(data),
+ // Resolves to the new location.
+ () => {
+ return this.lockedFile.location;
+ });
+ }
+
+ /**
+ * Queue data to be written into the file (optionally starting from a defined location in the file).
+ *
+ * @param {string|ArrayBuffer} data
+ * The data to write into the file.
+ * @param {number} location
+ * The location where the data should be written (when not specified the end of the previous
+ * queued write is used).
+ *
+ * @returns {Promise}
+ * A promise which is resolved once the request has been completed with the location where the
+ * file was after the data has been writted.
+ */
+ queuedWrite(data, location) {
+ const nextWriteRequest = async lastLocation => {
+ this.ensureLocked({ invalidMode: "readonly" });
+
+ if (typeof location === "number") {
+ return this.write(data, location);
+ }
+ return this.write(data, lastLocation);
+ };
+
+ this.writeQueue = this.writeQueue.then(nextWriteRequest);
+ return this.writeQueue;
+ }
+
+ /**
+ * Wait that any queued data has been written.
+ *
+ * @returns {Promise}
+ * A promise which is resolved once the request has been completed with the location where the
+ * file was after the data has been writted.
+ */
+ async waitForQueuedWrites() {
+ await this.writeQueue;
+ }
+ }
+
+ exports.IDBPromisedFileHandle = IDBPromisedFileHandle;
+ /**
+ * Wraps an IDBMutableFile with a nicer Promise-based API.
+ *
+ * Instances of this class are created from the
+ * {@link IDBFileStorage.createMutableFile} method.
+ */
+ class IDBPromisedMutableFile {
+ /**
+ * @private private helper method used internally.
+ */
+ constructor({ filesStorage, idb, fileName, fileType, mutableFile }) {
+ // All the following properties are private and it should not be needed
+ // while using the API.
+
+ /** @private */
+ this.filesStorage = filesStorage;
+ /** @private */
+ this.idb = idb;
+ /** @private */
+ this.fileName = fileName;
+ /** @private */
+ this.fileType = fileType;
+ /** @private */
+ this.mutableFile = mutableFile;
+ }
+
+ /**
+ * @private private helper method used internally.
+ */
+ reopenFileHandle(fileHandle) {
+ fileHandle.lockedFile = this.mutableFile.open(fileHandle.mode);
+ }
+
+ // API methods.
+
+ /**
+ * Open a mutable file for reading/writing data.
+ *
+ * @param {"readonly"|"readwrite"|"writeonly"} mode
+ * The mode of the created IDBPromisedFileHandle instance.
+ *
+ * @returns {IDBPromisedFileHandle}
+ * The created IDBPromisedFileHandle instance.
+ */
+ open(mode) {
+ if (this.lockedFile) {
+ throw new Error("MutableFile cannot be opened twice");
+ }
+ const lockedFile = this.mutableFile.open(mode);
+
+ return new IDBPromisedFileHandle({ file: this, lockedFile });
+ }
+
+ /**
+ * Get a {@link File} instance of this mutable file.
+ *
+ * @returns {Promise}
+ * A promise resolved to the File instance.
+ *
+ * To read the actual content of the mutable file as a File object,
+ * it is often better to use {@link IDBPromisedMutableFile.saveAsFileSnapshot}
+ * to save a persistent snapshot of the file in the IndexedDB store,
+ * or reading it directly using the {@link IDBPromisedFileHandle} instance
+ * returned by the {@link IDBPromisedMutableFile.open} method.
+ *
+ * The reason is that to be able to read the content of the returned file
+ * a lockfile have be keep the file open, e.d. as in the following example.
+ *
+ * @example
+ * ...
+ * let waitSnapshotStored;
+ * await mutableFile.runFileRequestGenerator(function* (lockedFile) {
+ * const file = yield lockedFile.mutableFile.getFile();
+ * // read the file content or turn it into a persistent object of its own
+ * // (e.g. by saving it back into IndexedDB as its snapshot in form of a File object,
+ * // or converted into a data url, a string or an array buffer)
+ *
+ * waitSnapshotStored = tmpFiles.put("${filename}/last_snapshot", file);
+ * }
+ *
+ * await waitSnapshotStored;
+ * let fileSnapshot = await tmpFiles.get("${filename}/last_snapshot");
+ * ...
+ * // now you can use fileSnapshot even if the mutableFile lock is not active anymore.
+ */
+ getFile() {
+ return waitForDOMRequest(this.mutableFile.getFile());
+ }
+
+ /**
+ * Persist the content of the mutable file into the files storage
+ * as a File, using the specified snapshot name and return the persisted File instance.
+ *
+ * @returns {Promise}
+ * A promise resolved to the File instance.
+ *
+ * @example
+ *
+ * const file = await mutableFile.persistAsFileSnapshot(`${filename}/last_snapshot`);
+ * const blobURL = URL.createObjectURL(file);
+ * ...
+ * // The blob URL is still valid even if the mutableFile is not active anymore.
+ */
+ async persistAsFileSnapshot(snapshotName) {
+ if (snapshotName === this.fileName) {
+ throw new Error("Snapshot name and the file name should be different");
+ }
+
+ const idb = await this.filesStorage.initializedDB();
+ await this.runFileRequestGenerator(function* () {
+ const file = yield this.mutableFile.getFile();
+ const objectStore = this.filesStorage.getObjectStoreTransaction({ idb, mode: "readwrite" });
+
+ yield objectStore.put(file, snapshotName);
+ }.bind(this));
+
+ return this.filesStorage.get(snapshotName);
+ }
+
+ /**
+ * Persist the this mutable file into its related IDBFileStorage.
+ *
+ * @returns {Promise}
+ * A promise resolved on the mutable file has been persisted into IndexedDB.
+ */
+ persist() {
+ return this.filesStorage.put(this.fileName, this);
+ }
+
+ /**
+ * Run a generator function which can run a sequence of FileRequests
+ * without the lockfile to become inactive.
+ *
+ * This method should be rarely needed, mostly to optimize a sequence of
+ * file operations without the file to be closed and automatically re-opened
+ * between two file requests.
+ *
+ * @param {function* (lockedFile) {...}} generatorFunction
+ * @param {"readonly"|"readwrite"|"writeonly"} mode
+ *
+ * @example
+ * (async function () {
+ * const tmpFiles = await IDBFiles.getFileStorage({name: "tmpFiles"});
+ * const mutableFile = await tmpFiles.createMutableFile("test-mutable-file.txt");
+ *
+ * let allFileData;
+ *
+ * function* fileOperations(lockedFile) {
+ * yield lockedFile.write("some data");
+ * yield lockedFile.write("more data");
+ * const metadata = yield lockedFile.getMetadata();
+ *
+ * lockedFile.location = 0;
+ * allFileData = yield lockedFile.readAsText(metadata.size);
+ * }
+ *
+ * await mutableFile.runFileRequestGenerator(fileOperations, "readwrite");
+ *
+ * console.log("File Data", allFileData);
+ * })();
+ */
+ async runFileRequestGenerator(generatorFunction, mode) {
+ if (generatorFunction.constructor.name !== "GeneratorFunction") {
+ throw new Error("runGenerator parameter should be a generator function");
+ }
+
+ await new Promise((resolve, reject) => {
+ const lockedFile = this.mutableFile.open(mode || "readwrite");
+ const fileRequestsIter = generatorFunction(lockedFile);
+
+ const processFileRequestIter = prevRequestResult => {
+ const nextFileRequest = fileRequestsIter.next(prevRequestResult);
+ if (nextFileRequest.done) {
+ resolve();
+ return;
+ } else if (!(nextFileRequest.value instanceof window.DOMRequest || nextFileRequest.value instanceof window.IDBRequest)) {
+ const error = new Error("FileRequestGenerator should only yield DOMRequest instances");
+ fileRequestsIter.throw(error);
+ reject(error);
+ return;
+ }
+
+ const request = nextFileRequest.value;
+ if (request.onsuccess || request.onerror) {
+ const error = new Error("DOMRequest onsuccess/onerror callbacks are already set");
+ fileRequestsIter.throw(error);
+ reject(error);
+ } else {
+ request.onsuccess = () => processFileRequestIter(request.result);
+ request.onerror = () => reject(request.error);
+ }
+ };
+
+ processFileRequestIter();
+ });
+ }
+ }
+
+ exports.IDBPromisedMutableFile = IDBPromisedMutableFile;
+ /**
+ * Provides a Promise-based API to store files into an IndexedDB.
+ *
+ * Instances of this class are created using the exported
+ * {@link getFileStorage} function.
+ */
+ class IDBFileStorage {
+ /**
+ * @private private helper method used internally.
+ */
+ constructor({ name, persistent } = {}) {
+ // All the following properties are private and it should not be needed
+ // while using the API.
+
+ /** @private */
+ this.name = name;
+ /** @private */
+ this.persistent = persistent;
+ /** @private */
+ this.indexedDBName = `IDBFilesStorage-DB-${this.name}`;
+ /** @private */
+ this.objectStorageName = "IDBFilesObjectStorage";
+ /** @private */
+ this.initializedPromise = undefined;
+
+ // TODO: evalutate schema migration between library versions?
+ /** @private */
+ this.version = 1.0;
+ }
+
+ /**
+ * @private private helper method used internally.
+ */
+ initializedDB() {
+ if (this.initializedPromise) {
+ return this.initializedPromise;
+ }
+
+ this.initializedPromise = (async () => {
+ if (window.IDBMutableFile && this.persistent) {
+ this.version = { version: this.version, storage: "persistent" };
+ }
+ const dbReq = indexedDB.open(this.indexedDBName, this.version);
+
+ dbReq.onupgradeneeded = () => {
+ const db = dbReq.result;
+ if (!db.objectStoreNames.contains(this.objectStorageName)) {
+ db.createObjectStore(this.objectStorageName);
+ }
+ };
+
+ return waitForDOMRequest(dbReq);
+ })();
+
+ return this.initializedPromise;
+ }
+
+ /**
+ * @private private helper method used internally.
+ */
+ getObjectStoreTransaction({ idb, mode } = {}) {
+ const transaction = idb.transaction([this.objectStorageName], mode);
+ return transaction.objectStore(this.objectStorageName);
+ }
+
+ /**
+ * Create a new IDBPromisedMutableFile instance (where the IDBMutableFile is supported)
+ *
+ * @param {string} fileName
+ * The fileName associated to the new IDBPromisedMutableFile instance.
+ * @param {string} [fileType="text"]
+ * The mime type associated to the file.
+ *
+ * @returns {IDBPromisedMutableFile}
+ * The newly created {@link IDBPromisedMutableFile} instance.
+ */
+ async createMutableFile(fileName, fileType = "text") {
+ if (!window.IDBMutableFile) {
+ throw new Error("This environment does not support IDBMutableFile");
+ }
+ const idb = await this.initializedDB();
+ const mutableFile = await waitForDOMRequest(idb.createMutableFile(fileName, fileType));
+ return new IDBPromisedMutableFile({
+ filesStorage: this, idb, fileName, fileType, mutableFile
+ });
+ }
+
+ /**
+ * Put a file object into the IDBFileStorage, it overwrites an existent file saved with the
+ * fileName if any.
+ *
+ * @param {string} fileName
+ * The key associated to the file in the IDBFileStorage.
+ * @param {Blob|File|IDBPromisedMutableFile|IDBMutableFile} file
+ * The file to be persisted.
+ *
+ * @returns {Promise}
+ * A promise resolved when the request has been completed.
+ */
+ async put(fileName, file) {
+ if (!fileName || typeof fileName !== "string") {
+ throw new Error("fileName parameter is mandatory");
+ }
+
+ if (!(file instanceof File) && !(file instanceof Blob) && !(window.IDBMutableFile && file instanceof window.IDBMutableFile) && !(file instanceof IDBPromisedMutableFile)) {
+ throw new Error(`Unable to persist ${fileName}. Unknown file type.`);
+ }
+
+ if (file instanceof IDBPromisedMutableFile) {
+ file = file.mutableFile;
+ }
+
+ const idb = await this.initializedDB();
+ const objectStore = this.getObjectStoreTransaction({ idb, mode: "readwrite" });
+ return waitForDOMRequest(objectStore.put(file, fileName));
+ }
+
+ /**
+ * Remove a file object from the IDBFileStorage.
+ *
+ * @param {string} fileName
+ * The fileName (the associated IndexedDB key) to remove from the IDBFileStorage.
+ *
+ * @returns {Promise}
+ * A promise resolved when the request has been completed.
+ */
+ async remove(fileName) {
+ if (!fileName) {
+ throw new Error("fileName parameter is mandatory");
+ }
+
+ const idb = await this.initializedDB();
+ const objectStore = this.getObjectStoreTransaction({ idb, mode: "readwrite" });
+ return waitForDOMRequest(objectStore.delete(fileName));
+ }
+
+ /**
+ * List the names of the files stored in the IDBFileStorage.
+ *
+ * (If any filtering options has been specified, only the file names that match
+ * all the filters are included in the result).
+ *
+ * @param {IDBFileStorage.ListFilteringOptions} options
+ * The optional filters to apply while listing the stored file names.
+ *
+ * @returns {Promise}
+ * A promise resolved to the array of the filenames that has been found.
+ */
+ async list(options) {
+ const idb = await this.initializedDB();
+ const objectStore = this.getObjectStoreTransaction({ idb });
+ const allKeys = await waitForDOMRequest(objectStore.getAllKeys());
+
+ let filteredKeys = allKeys;
+
+ if (options) {
+ filteredKeys = filteredKeys.filter(key => {
+ let match = true;
+
+ if (typeof options.startsWith === "string") {
+ match = match && key.startsWith(options.startsWith);
+ }
+
+ if (typeof options.endsWith === "string") {
+ match = match && key.endsWith(options.endsWith);
+ }
+
+ if (typeof options.includes === "string") {
+ match = match && key.includes(options.includes);
+ }
+
+ if (typeof options.filterFn === "function") {
+ match = match && options.filterFn(key);
+ }
+
+ return match;
+ });
+ }
+
+ return filteredKeys;
+ }
+
+ /**
+ * Count the number of files stored in the IDBFileStorage.
+ *
+ * (If any filtering options has been specified, only the file names that match
+ * all the filters are included in the final count).
+ *
+ * @param {IDBFileStorage.ListFilteringOptions} options
+ * The optional filters to apply while listing the stored file names.
+ *
+ * @returns {Promise}
+ * A promise resolved to the number of files that has been found.
+ */
+ async count(options) {
+ if (!options) {
+ const idb = await this.initializedDB();
+ const objectStore = this.getObjectStoreTransaction({ idb });
+ return waitForDOMRequest(objectStore.count());
+ }
+
+ const filteredKeys = await this.list(options);
+ return filteredKeys.length;
+ }
+
+ /**
+ * Retrieve a file stored in the IDBFileStorage by key.
+ *
+ * @param {string} fileName
+ * The key to use to retrieve the file from the IDBFileStorage.
+ *
+ * @returns {Promise}
+ * A promise resolved once the file stored in the IDBFileStorage has been retrieved.
+ */
+ async get(fileName) {
+ const idb = await this.initializedDB();
+ const objectStore = this.getObjectStoreTransaction({ idb });
+ return waitForDOMRequest(objectStore.get(fileName)).then(result => {
+ if (window.IDBMutableFile && result instanceof window.IDBMutableFile) {
+ return new IDBPromisedMutableFile({
+ filesStorage: this,
+ idb,
+ fileName,
+ fileType: result.type,
+ mutableFile: result
+ });
+ }
+
+ return result;
+ });
+ }
+
+ /**
+ * Remove all the file objects stored in the IDBFileStorage.
+ *
+ * @returns {Promise}
+ * A promise resolved once the IDBFileStorage has been cleared.
+ */
+ async clear() {
+ const idb = await this.initializedDB();
+ const objectStore = this.getObjectStoreTransaction({ idb, mode: "readwrite" });
+ return waitForDOMRequest(objectStore.clear());
+ }
+ }
+
+ exports.IDBFileStorage = IDBFileStorage;
+ /**
+ * Retrieve an IDBFileStorage instance by name (and it creates the indexedDB if it doesn't
+ * exist yet).
+ *
+ * @param {Object} [param]
+ * @param {string} [param.name="default"]
+ * The name associated to the IDB File Storage.
+ * @param {boolean} [param.persistent]
+ * Optionally enable persistent storage mode (not enabled by default).
+ *
+ * @returns {IDBFileStorage}
+ * The IDBFileStorage instance with the given name.
+ */
+ async function getFileStorage({ name, persistent } = {}) {
+ const filesStorage = new IDBFileStorage({ name: name || "default", persistent });
+ await filesStorage.initializedDB();
+ return filesStorage;
+ }
+
+ /**
+ * @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob
+ */
+
+ /**
+ * @external {DOMRequest} https://developer.mozilla.org/en/docs/Web/API/DOMRequest
+ */
+
+ /**
+ * @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File
+ */
+
+ /**
+ * @external {IDBMutableFile} https://developer.mozilla.org/en-US/docs/Web/API/IDBMutableFile
+ */
+
+ /**
+ * @external {IDBRequest} https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest
+ */
+});
+//# sourceMappingURL=idb-file-storage.js.map
diff --git a/store-collected-images/webextension-plain/deps/idb-file-storage.js.map b/store-collected-images/webextension-plain/deps/idb-file-storage.js.map
new file mode 100644
index 0000000..72cc8f0
--- /dev/null
+++ b/store-collected-images/webextension-plain/deps/idb-file-storage.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["idb-file-storage.js"],"names":["waitForDOMRequest","getFileStorage","req","onsuccess","Promise","resolve","reject","result","onerror","error","IDBPromisedFileHandle","constructor","file","lockedFile","writeQueue","closed","undefined","aborted","ensureLocked","invalidMode","Error","mode","active","reopenFileHandle","close","flush","abort","getMetadata","readAsText","size","location","readAsArrayBuffer","truncate","append","data","write","queuedWrite","nextWriteRequest","lastLocation","then","waitForQueuedWrites","IDBPromisedMutableFile","filesStorage","idb","fileName","fileType","mutableFile","fileHandle","open","getFile","persistAsFileSnapshot","snapshotName","initializedDB","runFileRequestGenerator","objectStore","getObjectStoreTransaction","put","bind","get","persist","generatorFunction","name","fileRequestsIter","processFileRequestIter","prevRequestResult","nextFileRequest","next","done","value","window","DOMRequest","IDBRequest","throw","request","IDBFileStorage","persistent","indexedDBName","objectStorageName","initializedPromise","version","IDBMutableFile","storage","dbReq","indexedDB","onupgradeneeded","db","objectStoreNames","contains","createObjectStore","transaction","createMutableFile","File","Blob","remove","delete","list","options","allKeys","getAllKeys","filteredKeys","filter","key","match","startsWith","endsWith","includes","filterFn","count","length","type","clear"],"mappings":"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","file":"idb-file-storage.js","sourcesContent":["\"use strict\";\n\n/**\n * @typedef {Object} IDBPromisedFileHandle.Metadata\n * @property {number} size\n * The size of the file in bytes.\n * @property {Date} last Modified\n * The time and date of the last change to the file.\n */\n\n/**\n * @typedef {Object} IDBFileStorage.ListFilteringOptions\n * @property {string} startsWith\n * A string to be checked with `fileNameString.startsWith(...)`.\n * @property {string} endsWith\n * A string to be checked with `fileNameString.endsWith(...)`.\n * @property {string} includes\n * A string to be checked with `fileNameString.includes(...)`.\n * @property {function} filterFn\n * A function to be used to check the file name (`filterFn(fileNameString)`).\n */\n\n/**\n * Wraps a DOMRequest into a promise, optionally transforming the result using the onsuccess\n * callback.\n *\n * @param {IDBRequest|DOMRequest} req\n * The DOMRequest instance to wrap in a Promise.\n * @param {function} [onsuccess]\n * An optional onsuccess callback which can transform the result before resolving it.\n *\n * @returns {Promise}\n * The promise which wraps the request result, rejected if the request.onerror has been\n * called.\n */\nexport function waitForDOMRequest(req, onsuccess) {\n return new Promise((resolve, reject) => {\n req.onsuccess = onsuccess ?\n (() => resolve(onsuccess(req.result))) : (() => resolve(req.result));\n req.onerror = () => reject(req.error);\n });\n}\n\n/**\n * Wraps an IDBMutableFile's FileHandle with a nicer Promise-based API.\n *\n * Instances of this class are created from the\n * {@link IDBPromisedMutableFile.open} method.\n */\nexport class IDBPromisedFileHandle {\n /**\n * @private private helper method used internally.\n */\n constructor({file, lockedFile}) {\n // All the following properties are private and it should not be needed\n // while using the API.\n\n /** @private */\n this.file = file;\n /** @private */\n this.lockedFile = lockedFile;\n /** @private */\n this.writeQueue = Promise.resolve();\n /** @private */\n this.closed = undefined;\n /** @private */\n this.aborted = undefined;\n }\n\n /**\n * @private private helper method used internally.\n */\n ensureLocked({invalidMode} = {}) {\n if (this.closed) {\n throw new Error(\"FileHandle has been closed\");\n }\n\n if (this.aborted) {\n throw new Error(\"FileHandle has been aborted\");\n }\n\n if (!this.lockedFile) {\n throw new Error(\"Invalid FileHandled\");\n }\n\n if (invalidMode && this.lockedFile.mode === invalidMode) {\n throw new Error(`FileHandle should not be opened as '${this.lockedFile.mode}'`);\n }\n if (!this.lockedFile.active) {\n // Automatically relock the file with the last open mode\n this.file.reopenFileHandle(this);\n }\n }\n\n // Promise-based MutableFile API\n\n /**\n * Provide access to the mode that has been used to open the {@link IDBPromisedMutableFile}.\n *\n * @type {\"readonly\"|\"readwrite\"|\"writeonly\"}\n */\n get mode() {\n return this.lockedFile.mode;\n }\n\n /**\n * A boolean property that is true if the lock is still active.\n *\n * @type {boolean}\n */\n get active() {\n return this.lockedFile ? this.lockedFile.active : false;\n }\n\n /**\n * Close the locked file (and wait for any written data to be flushed if needed).\n *\n * @returns {Promise}\n * A promise which is resolved when the close request has been completed\n */\n async close() {\n if (!this.lockedFile) {\n throw new Error(\"FileHandle is not open\");\n }\n\n // Wait the queued write to complete.\n await this.writeQueue;\n\n // Wait for flush request to complete if needed.\n if (this.lockedFile.active && this.lockedFile.mode !== \"readonly\") {\n await waitForDOMRequest(this.lockedFile.flush());\n }\n\n this.closed = true;\n this.lockedFile = null;\n this.writeQueue = Promise.resolve();\n }\n\n /**\n * Abort any pending data request and set the instance as aborted.\n *\n * @returns {Promise}\n * A promise which is resolved when the abort request has been completed\n */\n async abort() {\n if (this.lockedFile.active) {\n // NOTE: in the docs abort is reported to return a DOMRequest, but it doesn't seem\n // to be the case. (https://developer.mozilla.org/en-US/docs/Web/API/LockedFile/abort)\n this.lockedFile.abort();\n }\n\n this.aborted = true;\n this.lockedFile = null;\n this.writeQueue = Promise.resolve();\n }\n\n /**\n * Get the file metadata (take a look to {@link IDBPromisedFileHandle.Metadata} for more info).\n *\n * @returns {Promise<{size: number, lastModified: Date}>}\n * A promise which is resolved when the request has been completed\n */\n async getMetadata() {\n this.ensureLocked();\n return waitForDOMRequest(this.lockedFile.getMetadata());\n }\n\n /**\n * Read a given amount of data from the file as Text (optionally starting from the specified\n * location).\n *\n * @param {number} size\n * The amount of data to read.\n * @param {number} [location]\n * The location where the request should start to read the data.\n *\n * @returns {Promise}\n * A promise which resolves to the data read, when the request has been completed.\n */\n async readAsText(size, location) {\n this.ensureLocked({invalidMode: \"writeonly\"});\n if (typeof location === \"number\") {\n this.lockedFile.location = location;\n }\n return waitForDOMRequest(this.lockedFile.readAsText(size));\n }\n\n /**\n * Read a given amount of data from the file as an ArrayBufer (optionally starting from the specified\n * location).\n *\n * @param {number} size\n * The amount of data to read.\n * @param {number} [location]\n * The location where the request should start to read the data.\n *\n * @returns {Promise}\n * A promise which resolves to the data read, when the request has been completed.\n */\n async readAsArrayBuffer(size, location) {\n this.ensureLocked({invalidMode: \"writeonly\"});\n if (typeof location === \"number\") {\n this.lockedFile.location = location;\n }\n return waitForDOMRequest(this.lockedFile.readAsArrayBuffer(size));\n }\n\n /**\n * Truncate the file (optionally at a specified location).\n *\n * @param {number} [location=0]\n * The location where the file should be truncated.\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed.\n */\n async truncate(location = 0) {\n this.ensureLocked({invalidMode: \"readonly\"});\n return waitForDOMRequest(this.lockedFile.truncate(location));\n }\n\n /**\n * Append the passed data to the end of the file.\n *\n * @param {string|ArrayBuffer} data\n * The data to append to the end of the file.\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed.\n */\n async append(data) {\n this.ensureLocked({invalidMode: \"readonly\"});\n return waitForDOMRequest(this.lockedFile.append(data));\n }\n\n /**\n * Write data into the file (optionally starting from a defined location in the file).\n *\n * @param {string|ArrayBuffer} data\n * The data to write into the file.\n * @param {number} location\n * The location where the data should be written.\n *\n * @returns {Promise}\n * A promise which is resolved to the location where the written data ends.\n */\n async write(data, location) {\n this.ensureLocked({invalidMode: \"readonly\"});\n if (typeof location === \"number\") {\n this.lockedFile.location = location;\n }\n return waitForDOMRequest(\n this.lockedFile.write(data),\n // Resolves to the new location.\n () => {\n return this.lockedFile.location;\n }\n );\n }\n\n /**\n * Queue data to be written into the file (optionally starting from a defined location in the file).\n *\n * @param {string|ArrayBuffer} data\n * The data to write into the file.\n * @param {number} location\n * The location where the data should be written (when not specified the end of the previous\n * queued write is used).\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed with the location where the\n * file was after the data has been writted.\n */\n queuedWrite(data, location) {\n const nextWriteRequest = async lastLocation => {\n this.ensureLocked({invalidMode: \"readonly\"});\n\n if (typeof location === \"number\") {\n return this.write(data, location);\n }\n return this.write(data, lastLocation);\n };\n\n this.writeQueue = this.writeQueue.then(nextWriteRequest);\n return this.writeQueue;\n }\n\n /**\n * Wait that any queued data has been written.\n *\n * @returns {Promise}\n * A promise which is resolved once the request has been completed with the location where the\n * file was after the data has been writted.\n */\n async waitForQueuedWrites() {\n await this.writeQueue;\n }\n}\n\n/**\n * Wraps an IDBMutableFile with a nicer Promise-based API.\n *\n * Instances of this class are created from the\n * {@link IDBFileStorage.createMutableFile} method.\n */\nexport class IDBPromisedMutableFile {\n /**\n * @private private helper method used internally.\n */\n constructor({filesStorage, idb, fileName, fileType, mutableFile}) {\n // All the following properties are private and it should not be needed\n // while using the API.\n\n /** @private */\n this.filesStorage = filesStorage;\n /** @private */\n this.idb = idb;\n /** @private */\n this.fileName = fileName;\n /** @private */\n this.fileType = fileType;\n /** @private */\n this.mutableFile = mutableFile;\n }\n\n /**\n * @private private helper method used internally.\n */\n reopenFileHandle(fileHandle) {\n fileHandle.lockedFile = this.mutableFile.open(fileHandle.mode);\n }\n\n // API methods.\n\n /**\n * Open a mutable file for reading/writing data.\n *\n * @param {\"readonly\"|\"readwrite\"|\"writeonly\"} mode\n * The mode of the created IDBPromisedFileHandle instance.\n *\n * @returns {IDBPromisedFileHandle}\n * The created IDBPromisedFileHandle instance.\n */\n open(mode) {\n if (this.lockedFile) {\n throw new Error(\"MutableFile cannot be opened twice\");\n }\n const lockedFile = this.mutableFile.open(mode);\n\n return new IDBPromisedFileHandle({file: this, lockedFile});\n }\n\n /**\n * Get a {@link File} instance of this mutable file.\n *\n * @returns {Promise}\n * A promise resolved to the File instance.\n *\n * To read the actual content of the mutable file as a File object,\n * it is often better to use {@link IDBPromisedMutableFile.saveAsFileSnapshot}\n * to save a persistent snapshot of the file in the IndexedDB store,\n * or reading it directly using the {@link IDBPromisedFileHandle} instance\n * returned by the {@link IDBPromisedMutableFile.open} method.\n *\n * The reason is that to be able to read the content of the returned file\n * a lockfile have be keep the file open, e.d. as in the following example.\n *\n * @example\n * ...\n * let waitSnapshotStored;\n * await mutableFile.runFileRequestGenerator(function* (lockedFile) {\n * const file = yield lockedFile.mutableFile.getFile();\n * // read the file content or turn it into a persistent object of its own\n * // (e.g. by saving it back into IndexedDB as its snapshot in form of a File object,\n * // or converted into a data url, a string or an array buffer)\n *\n * waitSnapshotStored = tmpFiles.put(\"${filename}/last_snapshot\", file);\n * }\n *\n * await waitSnapshotStored;\n * let fileSnapshot = await tmpFiles.get(\"${filename}/last_snapshot\");\n * ...\n * // now you can use fileSnapshot even if the mutableFile lock is not active anymore.\n */\n getFile() {\n return waitForDOMRequest(this.mutableFile.getFile());\n }\n\n /**\n * Persist the content of the mutable file into the files storage\n * as a File, using the specified snapshot name and return the persisted File instance.\n *\n * @returns {Promise}\n * A promise resolved to the File instance.\n *\n * @example\n *\n * const file = await mutableFile.persistAsFileSnapshot(`${filename}/last_snapshot`);\n * const blobURL = URL.createObjectURL(file);\n * ...\n * // The blob URL is still valid even if the mutableFile is not active anymore.\n */\n async persistAsFileSnapshot(snapshotName) {\n if (snapshotName === this.fileName) {\n throw new Error(\"Snapshot name and the file name should be different\");\n }\n\n const idb = await this.filesStorage.initializedDB();\n await this.runFileRequestGenerator(function* () {\n const file = yield this.mutableFile.getFile();\n const objectStore = this.filesStorage.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n\n yield objectStore.put(file, snapshotName);\n }.bind(this));\n\n return this.filesStorage.get(snapshotName);\n }\n\n /**\n * Persist the this mutable file into its related IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved on the mutable file has been persisted into IndexedDB.\n */\n persist() {\n return this.filesStorage.put(this.fileName, this);\n }\n\n /**\n * Run a generator function which can run a sequence of FileRequests\n * without the lockfile to become inactive.\n *\n * This method should be rarely needed, mostly to optimize a sequence of\n * file operations without the file to be closed and automatically re-opened\n * between two file requests.\n *\n * @param {function* (lockedFile) {...}} generatorFunction\n * @param {\"readonly\"|\"readwrite\"|\"writeonly\"} mode\n *\n * @example\n * (async function () {\n * const tmpFiles = await IDBFiles.getFileStorage({name: \"tmpFiles\"});\n * const mutableFile = await tmpFiles.createMutableFile(\"test-mutable-file.txt\");\n *\n * let allFileData;\n *\n * function* fileOperations(lockedFile) {\n * yield lockedFile.write(\"some data\");\n * yield lockedFile.write(\"more data\");\n * const metadata = yield lockedFile.getMetadata();\n *\n * lockedFile.location = 0;\n * allFileData = yield lockedFile.readAsText(metadata.size);\n * }\n *\n * await mutableFile.runFileRequestGenerator(fileOperations, \"readwrite\");\n *\n * console.log(\"File Data\", allFileData);\n * })();\n */\n async runFileRequestGenerator(generatorFunction, mode) {\n if (generatorFunction.constructor.name !== \"GeneratorFunction\") {\n throw new Error(\"runGenerator parameter should be a generator function\");\n }\n\n await new Promise((resolve, reject) => {\n const lockedFile = this.mutableFile.open(mode || \"readwrite\");\n const fileRequestsIter = generatorFunction(lockedFile);\n\n const processFileRequestIter = prevRequestResult => {\n const nextFileRequest = fileRequestsIter.next(prevRequestResult);\n if (nextFileRequest.done) {\n resolve();\n return;\n } else if (!(nextFileRequest.value instanceof window.DOMRequest ||\n nextFileRequest.value instanceof window.IDBRequest)) {\n const error = new Error(\"FileRequestGenerator should only yield DOMRequest instances\");\n fileRequestsIter.throw(error);\n reject(error);\n return;\n }\n\n const request = nextFileRequest.value;\n if (request.onsuccess || request.onerror) {\n const error = new Error(\"DOMRequest onsuccess/onerror callbacks are already set\");\n fileRequestsIter.throw(error);\n reject(error);\n } else {\n request.onsuccess = () => processFileRequestIter(request.result);\n request.onerror = () => reject(request.error);\n }\n };\n\n processFileRequestIter();\n });\n }\n}\n\n/**\n * Provides a Promise-based API to store files into an IndexedDB.\n *\n * Instances of this class are created using the exported\n * {@link getFileStorage} function.\n */\nexport class IDBFileStorage {\n /**\n * @private private helper method used internally.\n */\n constructor({name, persistent} = {}) {\n // All the following properties are private and it should not be needed\n // while using the API.\n\n /** @private */\n this.name = name;\n /** @private */\n this.persistent = persistent;\n /** @private */\n this.indexedDBName = `IDBFilesStorage-DB-${this.name}`;\n /** @private */\n this.objectStorageName = \"IDBFilesObjectStorage\";\n /** @private */\n this.initializedPromise = undefined;\n\n // TODO: evalutate schema migration between library versions?\n /** @private */\n this.version = 1.0;\n }\n\n /**\n * @private private helper method used internally.\n */\n initializedDB() {\n if (this.initializedPromise) {\n return this.initializedPromise;\n }\n\n this.initializedPromise = (async () => {\n if (window.IDBMutableFile && this.persistent) {\n this.version = {version: this.version, storage: \"persistent\"};\n }\n const dbReq = indexedDB.open(this.indexedDBName, this.version);\n\n dbReq.onupgradeneeded = () => {\n const db = dbReq.result;\n if (!db.objectStoreNames.contains(this.objectStorageName)) {\n db.createObjectStore(this.objectStorageName);\n }\n };\n\n return waitForDOMRequest(dbReq);\n })();\n\n return this.initializedPromise;\n }\n\n /**\n * @private private helper method used internally.\n */\n getObjectStoreTransaction({idb, mode} = {}) {\n const transaction = idb.transaction([this.objectStorageName], mode);\n return transaction.objectStore(this.objectStorageName);\n }\n\n /**\n * Create a new IDBPromisedMutableFile instance (where the IDBMutableFile is supported)\n *\n * @param {string} fileName\n * The fileName associated to the new IDBPromisedMutableFile instance.\n * @param {string} [fileType=\"text\"]\n * The mime type associated to the file.\n *\n * @returns {IDBPromisedMutableFile}\n * The newly created {@link IDBPromisedMutableFile} instance.\n */\n async createMutableFile(fileName, fileType = \"text\") {\n if (!window.IDBMutableFile) {\n throw new Error(\"This environment does not support IDBMutableFile\");\n }\n const idb = await this.initializedDB();\n const mutableFile = await waitForDOMRequest(\n idb.createMutableFile(fileName, fileType)\n );\n return new IDBPromisedMutableFile({\n filesStorage: this, idb, fileName, fileType, mutableFile\n });\n }\n\n /**\n * Put a file object into the IDBFileStorage, it overwrites an existent file saved with the\n * fileName if any.\n *\n * @param {string} fileName\n * The key associated to the file in the IDBFileStorage.\n * @param {Blob|File|IDBPromisedMutableFile|IDBMutableFile} file\n * The file to be persisted.\n *\n * @returns {Promise}\n * A promise resolved when the request has been completed.\n */\n async put(fileName, file) {\n if (!fileName || typeof fileName !== \"string\") {\n throw new Error(\"fileName parameter is mandatory\");\n }\n\n if (!(file instanceof File) && !(file instanceof Blob) &&\n !(window.IDBMutableFile && file instanceof window.IDBMutableFile) &&\n !(file instanceof IDBPromisedMutableFile)) {\n throw new Error(`Unable to persist ${fileName}. Unknown file type.`);\n }\n\n if (file instanceof IDBPromisedMutableFile) {\n file = file.mutableFile;\n }\n\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n return waitForDOMRequest(objectStore.put(file, fileName));\n }\n\n /**\n * Remove a file object from the IDBFileStorage.\n *\n * @param {string} fileName\n * The fileName (the associated IndexedDB key) to remove from the IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved when the request has been completed.\n */\n async remove(fileName) {\n if (!fileName) {\n throw new Error(\"fileName parameter is mandatory\");\n }\n\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n return waitForDOMRequest(objectStore.delete(fileName));\n }\n\n /**\n * List the names of the files stored in the IDBFileStorage.\n *\n * (If any filtering options has been specified, only the file names that match\n * all the filters are included in the result).\n *\n * @param {IDBFileStorage.ListFilteringOptions} options\n * The optional filters to apply while listing the stored file names.\n *\n * @returns {Promise}\n * A promise resolved to the array of the filenames that has been found.\n */\n async list(options) {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb});\n const allKeys = await waitForDOMRequest(objectStore.getAllKeys());\n\n let filteredKeys = allKeys;\n\n if (options) {\n filteredKeys = filteredKeys.filter(key => {\n let match = true;\n\n if (typeof options.startsWith === \"string\") {\n match = match && key.startsWith(options.startsWith);\n }\n\n if (typeof options.endsWith === \"string\") {\n match = match && key.endsWith(options.endsWith);\n }\n\n if (typeof options.includes === \"string\") {\n match = match && key.includes(options.includes);\n }\n\n if (typeof options.filterFn === \"function\") {\n match = match && options.filterFn(key);\n }\n\n return match;\n });\n }\n\n return filteredKeys;\n }\n\n /**\n * Count the number of files stored in the IDBFileStorage.\n *\n * (If any filtering options has been specified, only the file names that match\n * all the filters are included in the final count).\n *\n * @param {IDBFileStorage.ListFilteringOptions} options\n * The optional filters to apply while listing the stored file names.\n *\n * @returns {Promise}\n * A promise resolved to the number of files that has been found.\n */\n async count(options) {\n if (!options) {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb});\n return waitForDOMRequest(objectStore.count());\n }\n\n const filteredKeys = await this.list(options);\n return filteredKeys.length;\n }\n\n /**\n * Retrieve a file stored in the IDBFileStorage by key.\n *\n * @param {string} fileName\n * The key to use to retrieve the file from the IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved once the file stored in the IDBFileStorage has been retrieved.\n */\n async get(fileName) {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb});\n return waitForDOMRequest(objectStore.get(fileName)).then(result => {\n if (window.IDBMutableFile && result instanceof window.IDBMutableFile) {\n return new IDBPromisedMutableFile({\n filesStorage: this,\n idb,\n fileName,\n fileType: result.type,\n mutableFile: result\n });\n }\n\n return result;\n });\n }\n\n /**\n * Remove all the file objects stored in the IDBFileStorage.\n *\n * @returns {Promise}\n * A promise resolved once the IDBFileStorage has been cleared.\n */\n async clear() {\n const idb = await this.initializedDB();\n const objectStore = this.getObjectStoreTransaction({idb, mode: \"readwrite\"});\n return waitForDOMRequest(objectStore.clear());\n }\n}\n\n/**\n * Retrieve an IDBFileStorage instance by name (and it creates the indexedDB if it doesn't\n * exist yet).\n *\n * @param {Object} [param]\n * @param {string} [param.name=\"default\"]\n * The name associated to the IDB File Storage.\n * @param {boolean} [param.persistent]\n * Optionally enable persistent storage mode (not enabled by default).\n *\n * @returns {IDBFileStorage}\n * The IDBFileStorage instance with the given name.\n */\nexport async function getFileStorage({name, persistent} = {}) {\n const filesStorage = new IDBFileStorage({name: name || \"default\", persistent});\n await filesStorage.initializedDB();\n return filesStorage;\n}\n\n/**\n * @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob\n */\n\n/**\n * @external {DOMRequest} https://developer.mozilla.org/en/docs/Web/API/DOMRequest\n */\n\n/**\n * @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File\n */\n\n/**\n * @external {IDBMutableFile} https://developer.mozilla.org/en-US/docs/Web/API/IDBMutableFile\n */\n\n/**\n * @external {IDBRequest} https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest\n */\n"]}
\ No newline at end of file
diff --git a/store-collected-images/webextension-plain/deps/uuidv4.js b/store-collected-images/webextension-plain/deps/uuidv4.js
new file mode 100644
index 0000000..76d869b
--- /dev/null
+++ b/store-collected-images/webextension-plain/deps/uuidv4.js
@@ -0,0 +1 @@
+!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.uuidv4=n()}}(function(){return function n(e,r,o){function t(f,u){if(!r[f]){if(!e[f]){var a="function"==typeof require&&require;if(!u&&a)return a(f,!0);if(i)return i(f,!0);var d=new Error("Cannot find module '"+f+"'");throw d.code="MODULE_NOT_FOUND",d}var l=r[f]={exports:{}};e[f][0].call(l.exports,function(n){var r=e[f][1][n];return t(r?r:n)},l,l.exports,n,e,r,o)}return r[f].exports}for(var i="function"==typeof require&&require,f=0;f>>((3&e)<<3)&255;return i}}e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,r){function o(n,e,r){var o=e&&r||0;"string"==typeof n&&(e="binary"==n?new Array(16):null,n=null),n=n||{};var f=n.random||(n.rng||t)();if(f[6]=15&f[6]|64,f[8]=63&f[8]|128,e)for(var u=0;u<16;++u)e[o+u]=f[u];return e||i(f)}var t=n("./lib/rng"),i=n("./lib/bytesToUuid");e.exports=o},{"./lib/bytesToUuid":1,"./lib/rng":2}]},{},[3])(3)});
\ No newline at end of file
diff --git a/store-collected-images/webextension-plain/images/icon.png b/store-collected-images/webextension-plain/images/icon.png
new file mode 100644
index 0000000..81fede1
Binary files /dev/null and b/store-collected-images/webextension-plain/images/icon.png differ
diff --git a/store-collected-images/webextension-plain/images/icon16.png b/store-collected-images/webextension-plain/images/icon16.png
new file mode 100644
index 0000000..8d4b5cc
Binary files /dev/null and b/store-collected-images/webextension-plain/images/icon16.png differ
diff --git a/store-collected-images/webextension-plain/manifest.json b/store-collected-images/webextension-plain/manifest.json
new file mode 100755
index 0000000..1b9cee0
--- /dev/null
+++ b/store-collected-images/webextension-plain/manifest.json
@@ -0,0 +1,26 @@
+{
+ "manifest_version": 2,
+ "name": "store-collected-images",
+ "version": "1.0",
+
+ "icons": {
+ "16": "images/icon16.png",
+ "48": "images/icon.png"
+ },
+
+ "browser_action": {
+ "default_icon": {
+ "48": "images/icon.png"
+ },
+ "default_title": "Collected Images"
+ },
+
+ "background": {
+ "scripts": ["background.js"]
+ },
+
+ "permissions": [
+ "contextMenus",
+ ""
+ ]
+}
diff --git a/store-collected-images/webextension-plain/navigate-collection.css b/store-collected-images/webextension-plain/navigate-collection.css
new file mode 100755
index 0000000..0919083
--- /dev/null
+++ b/store-collected-images/webextension-plain/navigate-collection.css
@@ -0,0 +1 @@
+@import "shared.css";
\ No newline at end of file
diff --git a/store-collected-images/webextension-plain/navigate-collection.html b/store-collected-images/webextension-plain/navigate-collection.html
new file mode 100755
index 0000000..06b0889
--- /dev/null
+++ b/store-collected-images/webextension-plain/navigate-collection.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const popup = ReactDOM.render(, document.getElementById('app'));
+
+async function fetchBlobFromUrl(fetchUrl) {
+ const res = await fetch(fetchUrl);
+ const blob = await res.blob();
+
+ return {
+ blob,
+ blobUrl: URL.createObjectURL(blob),
+ fetchUrl,
+ uuid: uuidV4(),
+ };
+}
+
+preventWindowDragAndDrop();
+
+browser.runtime.onMessage.addListener(async (msg) => {
+ if (msg.type === "new-collected-images") {
+ let collectedBlobs = popup.state.collectedBlobs || [];
+ const fetchRes = await fetchBlobFromUrl(msg.url);
+ collectedBlobs.push(fetchRes);
+ popup.setState({collectedBlobs});
+ return true;
+ }
+});
+
+browser.runtime.sendMessage({type: "get-pending-collected-urls"}).then(async res => {
+ let collectedBlobs = popup.state.collectedBlobs || [];
+
+ for (const url of res) {
+ const fetchRes = await fetchBlobFromUrl(url);
+ collectedBlobs.push(fetchRes);
+ popup.setState({collectedBlobs});
+ }
+});
diff --git a/store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js b/store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js
new file mode 100644
index 0000000..5b0ef7b
--- /dev/null
+++ b/store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js
@@ -0,0 +1,12 @@
+"use strict";
+
+function preventDefault(ev) {
+ ev.preventDefault();
+ return true;
+}
+
+export default function preventWindowDragAndDrop() {
+ window.ondragover = preventDefault;
+ window.ondragend = preventDefault;
+ window.ondrop = preventDefault;
+}
diff --git a/store-collected-images/webextension-with-webpack/src/utils/image-store.js b/store-collected-images/webextension-with-webpack/src/utils/image-store.js
new file mode 100644
index 0000000..b9da5ea
--- /dev/null
+++ b/store-collected-images/webextension-with-webpack/src/utils/image-store.js
@@ -0,0 +1,51 @@
+"use strict";
+
+// Import the `getFileStorage` helper from the idb-file-storage npm dependency.
+import {getFileStorage} from 'idb-file-storage/src/idb-file-storage';
+
+export async function saveCollectedBlobs(collectionName, collectedBlobs) {
+ // Retrieve a named file storage (it creates a new one if it doesn't exist yet).
+ const storedImages = await getFileStorage({name: "stored-images"});
+
+ for (const item of collectedBlobs) {
+ // Save all the collected blobs in an IndexedDB key named based on the collectionName
+ // and a randomly generated uuid.
+ await storedImages.put(`${collectionName}/${item.uuid}`, item.blob);
+ }
+}
+
+export async function loadStoredImages(filter) {
+ // Retrieve a named file storage (it creates a new one if it doesn't exist yet).
+ const imagesStore = await getFileStorage({name: "stored-images"});
+
+ let listOptions = filter ? {includes: filter} : undefined;
+
+ // List the existent stored files (optionally filtered).
+ const imagesList = await imagesStore.list(listOptions);
+
+ let storedImages = [];
+
+ for (const storedName of imagesList) {
+ // Retrieve the stored blob by name.
+ const blob = await imagesStore.get(storedName);
+
+ // convert the Blob object into a blob URL and store it into the
+ // array of the results returned by this function.
+ storedImages.push({storedName, blobUrl: URL.createObjectURL(blob)});
+ }
+
+ return storedImages;
+}
+
+export async function removeStoredImages(storedImages) {
+ // Retrieve a named file storage (it creates a new one if it doesn't exist yet).
+ const imagesStore = await getFileStorage({name: "stored-images"});
+
+ for (const storedImage of storedImages) {
+ // Revoke the blob URL.
+ URL.revokeObjectURL(storedImage.blobUrl);
+
+ // Remove the stored blob by name.
+ await imagesStore.remove(storedImage.storedName);
+ }
+}
diff --git a/store-collected-images/webextension-with-webpack/webpack.config.js b/store-collected-images/webextension-with-webpack/webpack.config.js
new file mode 100644
index 0000000..a9c4df2
--- /dev/null
+++ b/store-collected-images/webextension-with-webpack/webpack.config.js
@@ -0,0 +1,52 @@
+/* eslint-env node */
+
+const path = require('path');
+const webpack = require('webpack');
+
+module.exports = {
+ entry: {
+ // Each entry in here would declare a file that needs to be transpiled
+ // and included in the extension source.
+ // For example, you could add a background script like:
+ background: 'background.js',
+ popup: 'popup.js',
+ 'navigate-collection': 'navigate-collection.js',
+ },
+ output: {
+ // This copies each source entry into the extension dist folder named
+ // after its entry config key.
+ path: path.join(__dirname, 'extension', 'dist'),
+ filename: '[name].js',
+ },
+ module: {
+ rules: [{
+ exclude: ['/node_modules/', '!/node_modules/idb-file-storage'],
+ test: /\.js$/,
+ use: [
+ // This transpiles all code (except for third party modules) using Babel.
+ {
+ // Babel options are in .babelrc
+ loader: 'babel-loader',
+ },
+ ]
+ }]
+ },
+ resolve: {
+ // This allows you to import modules just like you would in a NodeJS app.
+ extensions: ['.js', '.jsx'],
+ modules: [
+ path.join(__dirname, 'src'),
+ 'node_modules',
+ ],
+ },
+ plugins: [
+ // Since some NodeJS modules expect to be running in Node, it is helpful
+ // to set this environment var to avoid reference errors.
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ }),
+ ],
+ // This will expose source map files so that errors will point to your
+ // original source files instead of the transpiled files.
+ devtool: 'sourcemap',
+};