From 7b6b03a72c0c4a39877de5dde8f460f037cdfa00 Mon Sep 17 00:00:00 2001 From: Luca Greco Date: Thu, 20 Jul 2017 00:06:46 +0200 Subject: [PATCH] Add an indexedDB file storage example: image-reference-collector (#224) * new example: image-reference-collector (indexedDB file storage demo) * fix: added missing deps, updated all npm dependencies and webpack config to v.2 * chore: Renamed the example to store-collected-images * chore: Removed from utils/image-store any direct call to the UI code * move example built using webpack into its own subdir * tweak browser action title * added plain webextension example (without webpack build step) * added README.md file to plain webextension example * small changed based on the review comments * fixed typo in store-collected-images example (webpack-based version) * Remove React from the store-collected-images (plain webextension version) * Fix eslint errors on store-collected-images example (both versions) * Fix some typos in the README files --- .eslintignore | 1 + store-collected-images/README.md | 36 + .../screenshots/screenshot.png | Bin 0 -> 94067 bytes .../webextension-plain/.eslintignore | 1 + .../webextension-plain/.eslintrc | 3 + .../webextension-plain/README.md | 39 + .../webextension-plain/background.js | 47 + .../deps/idb-file-storage.js | 801 ++++++++++++++++++ .../deps/idb-file-storage.js.map | 1 + .../webextension-plain/deps/uuidv4.js | 1 + .../webextension-plain/images/icon.png | Bin 0 -> 695 bytes .../webextension-plain/images/icon16.png | Bin 0 -> 1008 bytes .../webextension-plain/manifest.json | 26 + .../navigate-collection.css | 1 + .../navigate-collection.html | 21 + .../webextension-plain/navigate-collection.js | 85 ++ .../webextension-plain/popup.css | 12 + .../webextension-plain/popup.html | 22 + .../webextension-plain/popup.js | 127 +++ .../webextension-plain/shared.css | 22 + .../utils/handle-window-drag-and-drop.js | 14 + .../webextension-plain/utils/image-store.js | 37 + .../webextension-with-webpack/.eslintrc | 6 + .../webextension-with-webpack/.gitignore | 5 + .../webextension-with-webpack/README.md | 32 + .../extension/images/icon.png | Bin 0 -> 695 bytes .../extension/images/icon16.png | Bin 0 -> 1008 bytes .../extension/manifest.json | 26 + .../extension/navigate-collection.css | 1 + .../extension/navigate-collection.html | 11 + .../extension/popup.css | 12 + .../extension/popup.html | 11 + .../extension/shared.css | 22 + .../webextension-with-webpack/package.json | 38 + .../src/background.js | 47 + .../src/navigate-collection.js | 66 ++ .../webextension-with-webpack/src/popup.js | 114 +++ .../src/utils/handle-window-drag-and-drop.js | 12 + .../src/utils/image-store.js | 51 ++ .../webpack.config.js | 52 ++ 40 files changed, 1803 insertions(+) create mode 100644 store-collected-images/README.md create mode 100644 store-collected-images/screenshots/screenshot.png create mode 100644 store-collected-images/webextension-plain/.eslintignore create mode 100644 store-collected-images/webextension-plain/.eslintrc create mode 100644 store-collected-images/webextension-plain/README.md create mode 100644 store-collected-images/webextension-plain/background.js create mode 100644 store-collected-images/webextension-plain/deps/idb-file-storage.js create mode 100644 store-collected-images/webextension-plain/deps/idb-file-storage.js.map create mode 100644 store-collected-images/webextension-plain/deps/uuidv4.js create mode 100644 store-collected-images/webextension-plain/images/icon.png create mode 100644 store-collected-images/webextension-plain/images/icon16.png create mode 100755 store-collected-images/webextension-plain/manifest.json create mode 100755 store-collected-images/webextension-plain/navigate-collection.css create mode 100755 store-collected-images/webextension-plain/navigate-collection.html create mode 100644 store-collected-images/webextension-plain/navigate-collection.js create mode 100755 store-collected-images/webextension-plain/popup.css create mode 100755 store-collected-images/webextension-plain/popup.html create mode 100644 store-collected-images/webextension-plain/popup.js create mode 100644 store-collected-images/webextension-plain/shared.css create mode 100644 store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js create mode 100644 store-collected-images/webextension-plain/utils/image-store.js create mode 100644 store-collected-images/webextension-with-webpack/.eslintrc create mode 100644 store-collected-images/webextension-with-webpack/.gitignore create mode 100644 store-collected-images/webextension-with-webpack/README.md create mode 100644 store-collected-images/webextension-with-webpack/extension/images/icon.png create mode 100644 store-collected-images/webextension-with-webpack/extension/images/icon16.png create mode 100755 store-collected-images/webextension-with-webpack/extension/manifest.json create mode 100755 store-collected-images/webextension-with-webpack/extension/navigate-collection.css create mode 100755 store-collected-images/webextension-with-webpack/extension/navigate-collection.html create mode 100755 store-collected-images/webextension-with-webpack/extension/popup.css create mode 100755 store-collected-images/webextension-with-webpack/extension/popup.html create mode 100644 store-collected-images/webextension-with-webpack/extension/shared.css create mode 100644 store-collected-images/webextension-with-webpack/package.json create mode 100644 store-collected-images/webextension-with-webpack/src/background.js create mode 100644 store-collected-images/webextension-with-webpack/src/navigate-collection.js create mode 100755 store-collected-images/webextension-with-webpack/src/popup.js create mode 100644 store-collected-images/webextension-with-webpack/src/utils/handle-window-drag-and-drop.js create mode 100644 store-collected-images/webextension-with-webpack/src/utils/image-store.js create mode 100644 store-collected-images/webextension-with-webpack/webpack.config.js 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. + +[![entension demo screencast](screenshots/screenshot.png "extension demo screencast")](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 0000000000000000000000000000000000000000..01341b8cf8a81699f0aedb7996b811ecfc23ae2a GIT binary patch literal 94067 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 +

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 0000000000000000000000000000000000000000..81fede1622308f35240b22b09d6cbf80b0e311e2 GIT binary patch literal 695 zcmV;o0!aOdP)S_9yHz*Q^VRaW{=dFJ3*AoAG2^8A5ayhwiXMCGlWQMzYM1g+)Uuk75Q1 z&atwFjpCRJEVwN(>HB(kg1|<*(v@JsH~F6Wd#|7V-tYb1tH(3aNJF4WQN}R318~?h z0(7eXA8=Kr1N8`S1ULe88*r^av93{U6e+TY6k44kU!@>r_UycPQf4t)osHQjvN_I+ zw+*;fph%S1gz0bWCB2+Iq=@ZOeAs7>?NVeb=3~+aZ0}(#eWlWVX57IGUp~ir^3w&K;OVk~|Q>94k8IZONyv`a>Eb6## zzt)eA#q#D^u%;izYPkDy z2REK&TDRs~;S3(UQ;{hi*#mUabl>C*JWu}v6XOkB59e_8Ng9HrLyR{%o(YnU*@ZOj zy~;tY9J>#=s#2`{&88S{VEQqEAn83b+q59*m@aZ&lN}vjP+( z9h^TIaSiZhIAdCSR)Au>0X|4H*8nF)`CK3?wU24-WH)|0haT|G3zwJJsUEGkyh)rY zQ#``#f}=L`yQ9sx;i*9HBhCy^kaX~Yl;Uw)6Z2$@^UEi8k2=mTpG?e?G1mdxA$?NW z5{}i~0O8sp`9MnXhLlsER6cXv@=0ZQL&_;1NGSt{YZo^sk8}QHgbxy}S*LuEXq-P8 dX>Euu{Q=Q@y%o=As~i9T002ovPDHLkV1mc9MHB!4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8d4b5cca81689281f2df3b8c5461e936193283d3 GIT binary patch literal 1008 zcmZ8f4NMzl82-RjHZ0R^*$+As4Hl?cW%inS5l4=@-#N@?j2LxlEPxI#;T>vz5l2BVkf?tQ-Jp7-bb z?&PT{hod5&jzkDWNsfrpAwCtlH$Dc>7dTJ831P##3CRfvb$syzox_6lbJ8Q}$q1Fj zB2-b0&8uk zvDRv=)jyctzDCH@(;sxYF5G$quik4Ta+}63_j)Tep5t|fSF7~zD2yd*2GtZOdyBdp zq~A#e*0HogdzP3zzt5^X~tcaf3c?gWWgFM{&@le0e}c*vj_J*!VhM!k;Dd9 zhy#GY5=7u&02{#N$JTjlox_9+o7~toAA-e$C+4t&V(b({`a|6TP9Gkc#rnxmFRcv^ z{Dt)ntQrlf2wH3QX-x~pnWe?RDmbA_XwLO%%~)l`SN;f;bvi%Ncxrm7PqpN&`%Z9< zyYVk}taJQ>9xGjQ+xeN^-#mnMc3eLYyxdEH4TgkyU`X3M(`WP5^mxF~F&4DVVWlCc z7!3NC=>W|vtc2iSWdhUTH* zH}`2M1x_Fg!_@MNyWtx@BTG+#|3zGopWhDG_mi|C|8&C=Dc$@b&{L0>#mJJt7Iy0B zVNugeS8XFKB4m$7&*u_7)OZs@J+j})iu?gJ72!CnGU3N4~lDm7A z1+&rh#}sVR#MhLTF0(!s_w8xT`!kD8ZvXl%f*|&HmH47QPad$aNzQlY+-~=?jVC7# zY0HB`_StQ*DGkC>?-k`p+k-m(nFIU!i!ajQRmIbT+qPa>ZAcSkBxNP0MA literal 0 HcmV?d00001 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 @@ + + + + + + + +
+

Stored images

+ + + +
    +
    + + + + + + + diff --git a/store-collected-images/webextension-plain/navigate-collection.js b/store-collected-images/webextension-plain/navigate-collection.js new file mode 100644 index 0000000..b5d49f9 --- /dev/null +++ b/store-collected-images/webextension-plain/navigate-collection.js @@ -0,0 +1,85 @@ +/* global loadStoredImages, removeStoredImages */ + +"use strict"; + +class NavigateCollectionUI { + constructor(containerEl) { + this.containerEl = containerEl; + + this.state = { + storedImages: [], + }; + + this.onFilterUpdated = this.onFilterUpdated.bind(this); + this.onReload = this.onFilterUpdated; + this.onDelete = this.onDelete.bind(this); + + this.containerEl.querySelector("button.reload-images").onclick = this.onReload; + this.containerEl.querySelector("button.delete-images").onclick = this.onDelete; + this.containerEl.querySelector("input.image-filter").onchange = this.onFilterUpdated; + + // Load the stored image once the component has been rendered in the page. + this.onFilterUpdated(); + } + + get imageFilterValue() { + return this.containerEl.querySelector("input.image-filter").value; + } + + set imageFilterValue(value) { + return this.containerEl.querySelector("input.image-filter").value = value; + } + + setState(state) { + // Merge the new state on top of the previous one and re-render everything. + this.state = Object.assign(this.state, state); + this.render(); + } + + componentDidMount() { + // Load the stored image once the component has been rendered in the page. + this.onFilterUpdated(); + } + + onFilterUpdated() { + loadStoredImages(this.imageFilterValue) + .then((storedImages) => { + this.setState({storedImages}); + }) + .catch(console.error); + } + + onDelete() { + const {storedImages} = this.state; + this.setState({storedImages: []}); + + removeStoredImages(storedImages).catch(console.error); + } + + render() { + const {storedImages} = this.state; + + const thumbnailsUl = this.containerEl.querySelector("ul.thumbnails"); + while (thumbnailsUl.firstChild) { + thumbnailsUl.removeChild(thumbnailsUl.firstChild); + } + + storedImages.forEach(({storedName, blobUrl}) => { + const onClickedImage = () => { + this.imageFilterValue = storedName; + this.onFilterUpdated(); + }; + const li = document.createElement("li"); + const img = document.createElement("img"); + li.setAttribute("id", storedName); + img.setAttribute("src", blobUrl); + img.onclick = onClickedImage; + + li.appendChild(img); + thumbnailsUl.appendChild(li); + }); + } +} + +// eslint-disable-next-line no-unused-vars +const navigateCollectionUI = new NavigateCollectionUI(document.getElementById('app')); diff --git a/store-collected-images/webextension-plain/popup.css b/store-collected-images/webextension-plain/popup.css new file mode 100755 index 0000000..2c7ea8d --- /dev/null +++ b/store-collected-images/webextension-plain/popup.css @@ -0,0 +1,12 @@ +@import "shared.css"; + +html, body { + width: 250px; + margin: 0; + padding: 0; + margin-left: 1em; +} + +input { + width: 90%; +} diff --git a/store-collected-images/webextension-plain/popup.html b/store-collected-images/webextension-plain/popup.html new file mode 100755 index 0000000..d7b4798 --- /dev/null +++ b/store-collected-images/webextension-plain/popup.html @@ -0,0 +1,22 @@ + + + + + + + +
    +

    Collected images

    +

    + + +
      +
        +
    + + + + + + + diff --git a/store-collected-images/webextension-plain/popup.js b/store-collected-images/webextension-plain/popup.js new file mode 100644 index 0000000..0b3fc6a --- /dev/null +++ b/store-collected-images/webextension-plain/popup.js @@ -0,0 +1,127 @@ +/* global saveCollectedBlobs, uuidv4, preventWindowDragAndDrop */ + +"use strict"; + +class Popup { + constructor(containerEl) { + this.containerEl = containerEl; + + this.state = { + collectedBlobs: [], + lastMessage: undefined, + }; + + this.onClick = this.onClick.bind(this); + + this.containerEl.querySelector("button.save-collection").onclick = this.onClick; + } + + get collectionNameValue() { + return this.containerEl.querySelector("input.collection-name").value; + } + + setState(state) { + // Merge the new state on top of the previous one and re-render everything. + this.state = Object.assign(this.state, state); + this.render(); + } + + onClick() { + if (!this.collectionNameValue) { + this.setState({ + lastMessage: {text: "The collection name is mandatory.", type: "error"}, + }); + + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + + return; + } + + saveCollectedBlobs(this.collectionNameValue, this.state.collectedBlobs) + .then(() => { + this.setState({ + lastMessage: {text: "All the collected images have been saved", type: "success"}, + collectedBlobs: [], + }); + + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + }) + .catch((err) => { + this.setState({ + lastMessage: {text: `Failed to save collected images: ${err}`, type: "error"}, + }); + + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + }); + } + + render() { + const {collectedBlobs, lastMessage} = this.state; + + const lastMessageEl = this.containerEl.querySelector("p#error-message"); + if (lastMessage) { + lastMessageEl.setAttribute("class", lastMessage.type); + lastMessageEl.textContent = lastMessage.text; + } else { + lastMessageEl.setAttribute("class", ""); + lastMessageEl.textContent = ""; + } + + const thumbnailsUl = this.containerEl.querySelector("ul.thumbnails"); + while (thumbnailsUl.firstChild) { + thumbnailsUl.removeChild(thumbnailsUl.firstChild); + } + + collectedBlobs.forEach(({uuid, blobUrl}) => { + const li = document.createElement("li"); + const img = document.createElement("img"); + li.setAttribute("id", uuid); + img.setAttribute("src", blobUrl); + li.appendChild(img); + + thumbnailsUl.appendChild(li); + }); + } +} + +const popup = new Popup(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-plain/shared.css b/store-collected-images/webextension-plain/shared.css new file mode 100644 index 0000000..e31f58c --- /dev/null +++ b/store-collected-images/webextension-plain/shared.css @@ -0,0 +1,22 @@ +ul.thumbnails { + padding: 0; +} + +ul.thumbnails li { + display: inline-block; + vertical-align: middle; + padding: 0.4em; +} + +.thumbnails img { + max-width: 50px; + max-height: 50px; +} + +.error { + background: rgba(255,0,0,0.4); +} + +.success { + background: rgba(0,255,0,0.4); +} \ No newline at end of file diff --git a/store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js b/store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js new file mode 100644 index 0000000..34b74ef --- /dev/null +++ b/store-collected-images/webextension-plain/utils/handle-window-drag-and-drop.js @@ -0,0 +1,14 @@ +/* exported preventWindowDragAndDrop */ + +"use strict"; + +function preventWindowDragAndDrop() { + function preventDefault(ev) { + ev.preventDefault(); + return true; + } + + window.ondragover = preventDefault; + window.ondragend = preventDefault; + window.ondrop = preventDefault; +} diff --git a/store-collected-images/webextension-plain/utils/image-store.js b/store-collected-images/webextension-plain/utils/image-store.js new file mode 100644 index 0000000..2c52716 --- /dev/null +++ b/store-collected-images/webextension-plain/utils/image-store.js @@ -0,0 +1,37 @@ +/* global IDBFiles */ +/* exported saveCollectedBlobs, loadStoredImages, removeStoredImages */ + +"use strict"; + +async function saveCollectedBlobs(collectionName, collectedBlobs) { + const storedImages = await IDBFiles.getFileStorage({name: "stored-images"}); + + for (const item of collectedBlobs) { + await storedImages.put(`${collectionName}/${item.uuid}`, item.blob); + } +} + +async function loadStoredImages(filter) { + const imagesStore = await IDBFiles.getFileStorage({name: "stored-images"}); + + let listOptions = filter ? {includes: filter} : undefined; + const imagesList = await imagesStore.list(listOptions); + + let storedImages = []; + + for (const storedName of imagesList) { + const blob = await imagesStore.get(storedName); + + storedImages.push({storedName, blobUrl: URL.createObjectURL(blob)}); + } + + return storedImages; +} + +async function removeStoredImages(storedImages) { + const imagesStore = await IDBFiles.getFileStorage({name: "stored-images"}); + for (const storedImage of storedImages) { + URL.revokeObjectURL(storedImage.blobUrl); + await imagesStore.remove(storedImage.storedName); + } +} diff --git a/store-collected-images/webextension-with-webpack/.eslintrc b/store-collected-images/webextension-with-webpack/.eslintrc new file mode 100644 index 0000000..1adf976 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/.eslintrc @@ -0,0 +1,6 @@ +{ + "parser": "babel-eslint", + "env": { + "commonjs": true + } +} \ No newline at end of file diff --git a/store-collected-images/webextension-with-webpack/.gitignore b/store-collected-images/webextension-with-webpack/.gitignore new file mode 100644 index 0000000..20ccaa9 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/.gitignore @@ -0,0 +1,5 @@ +# Ignore build artifacts and other files. +.DS_Store +yarn.lock +extension/dist +node_modules diff --git a/store-collected-images/webextension-with-webpack/README.md b/store-collected-images/webextension-with-webpack/README.md new file mode 100644 index 0000000..b768140 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/README.md @@ -0,0 +1,32 @@ +# "Image Reference Collector" example built with webpack (and React UI) + +## Usage + +This example is built using Babel and Webpack, and so the transpiled bundles have to +be built first: + +you need to change into the example subdirectory and install all +[NodeJS][nodejs] dependencies with [npm](http://npmjs.com/) or +[yarn](https://yarnpkg.com/): + + npm install + +You can build the extension using: + + npm run build + +This creates the source bundles for the WebExtension in the `extension` subdirectory, and +you can manually install the add-on on Firefox by loading the `extension` from the +"about:debugging#addons" page. + +You can also build the sources and start a new Firefox instance with the add-on installed +in one command: + + npm run start + +To start a webpack instance that automatically rebuilds the add-on when +you change the sources, in another shell window, you can run the following npm script: + + npm run build:watch + +While this npm script is running, any time you edit a file, it will be rebuilt automatically. diff --git a/store-collected-images/webextension-with-webpack/extension/images/icon.png b/store-collected-images/webextension-with-webpack/extension/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..81fede1622308f35240b22b09d6cbf80b0e311e2 GIT binary patch literal 695 zcmV;o0!aOdP)S_9yHz*Q^VRaW{=dFJ3*AoAG2^8A5ayhwiXMCGlWQMzYM1g+)Uuk75Q1 z&atwFjpCRJEVwN(>HB(kg1|<*(v@JsH~F6Wd#|7V-tYb1tH(3aNJF4WQN}R318~?h z0(7eXA8=Kr1N8`S1ULe88*r^av93{U6e+TY6k44kU!@>r_UycPQf4t)osHQjvN_I+ zw+*;fph%S1gz0bWCB2+Iq=@ZOeAs7>?NVeb=3~+aZ0}(#eWlWVX57IGUp~ir^3w&K;OVk~|Q>94k8IZONyv`a>Eb6## zzt)eA#q#D^u%;izYPkDy z2REK&TDRs~;S3(UQ;{hi*#mUabl>C*JWu}v6XOkB59e_8Ng9HrLyR{%o(YnU*@ZOj zy~;tY9J>#=s#2`{&88S{VEQqEAn83b+q59*m@aZ&lN}vjP+( z9h^TIaSiZhIAdCSR)Au>0X|4H*8nF)`CK3?wU24-WH)|0haT|G3zwJJsUEGkyh)rY zQ#``#f}=L`yQ9sx;i*9HBhCy^kaX~Yl;Uw)6Z2$@^UEi8k2=mTpG?e?G1mdxA$?NW z5{}i~0O8sp`9MnXhLlsER6cXv@=0ZQL&_;1NGSt{YZo^sk8}QHgbxy}S*LuEXq-P8 dX>Euu{Q=Q@y%o=As~i9T002ovPDHLkV1mc9MHB!4 literal 0 HcmV?d00001 diff --git a/store-collected-images/webextension-with-webpack/extension/images/icon16.png b/store-collected-images/webextension-with-webpack/extension/images/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..8d4b5cca81689281f2df3b8c5461e936193283d3 GIT binary patch literal 1008 zcmZ8f4NMzl82-RjHZ0R^*$+As4Hl?cW%inS5l4=@-#N@?j2LxlEPxI#;T>vz5l2BVkf?tQ-Jp7-bb z?&PT{hod5&jzkDWNsfrpAwCtlH$Dc>7dTJ831P##3CRfvb$syzox_6lbJ8Q}$q1Fj zB2-b0&8uk zvDRv=)jyctzDCH@(;sxYF5G$quik4Ta+}63_j)Tep5t|fSF7~zD2yd*2GtZOdyBdp zq~A#e*0HogdzP3zzt5^X~tcaf3c?gWWgFM{&@le0e}c*vj_J*!VhM!k;Dd9 zhy#GY5=7u&02{#N$JTjlox_9+o7~toAA-e$C+4t&V(b({`a|6TP9Gkc#rnxmFRcv^ z{Dt)ntQrlf2wH3QX-x~pnWe?RDmbA_XwLO%%~)l`SN;f;bvi%Ncxrm7PqpN&`%Z9< zyYVk}taJQ>9xGjQ+xeN^-#mnMc3eLYyxdEH4TgkyU`X3M(`WP5^mxF~F&4DVVWlCc z7!3NC=>W|vtc2iSWdhUTH* zH}`2M1x_Fg!_@MNyWtx@BTG+#|3zGopWhDG_mi|C|8&C=Dc$@b&{L0>#mJJt7Iy0B zVNugeS8XFKB4m$7&*u_7)OZs@J+j})iu?gJ72!CnGU3N4~lDm7A z1+&rh#}sVR#MhLTF0(!s_w8xT`!kD8ZvXl%f*|&HmH47QPad$aNzQlY+-~=?jVC7# zY0HB`_StQ*DGkC>?-k`p+k-m(nFIU!i!ajQRmIbT+qPa>ZAcSkBxNP0MA literal 0 HcmV?d00001 diff --git a/store-collected-images/webextension-with-webpack/extension/manifest.json b/store-collected-images/webextension-with-webpack/extension/manifest.json new file mode 100755 index 0000000..f99de3c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/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": ["dist/background.js"] + }, + + "permissions": [ + "contextMenus", + "" + ] +} diff --git a/store-collected-images/webextension-with-webpack/extension/navigate-collection.css b/store-collected-images/webextension-with-webpack/extension/navigate-collection.css new file mode 100755 index 0000000..0919083 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/navigate-collection.css @@ -0,0 +1 @@ +@import "shared.css"; \ No newline at end of file diff --git a/store-collected-images/webextension-with-webpack/extension/navigate-collection.html b/store-collected-images/webextension-with-webpack/extension/navigate-collection.html new file mode 100755 index 0000000..98fadb7 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/navigate-collection.html @@ -0,0 +1,11 @@ + + + + + + + +
    + + + diff --git a/store-collected-images/webextension-with-webpack/extension/popup.css b/store-collected-images/webextension-with-webpack/extension/popup.css new file mode 100755 index 0000000..2c7ea8d --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/popup.css @@ -0,0 +1,12 @@ +@import "shared.css"; + +html, body { + width: 250px; + margin: 0; + padding: 0; + margin-left: 1em; +} + +input { + width: 90%; +} diff --git a/store-collected-images/webextension-with-webpack/extension/popup.html b/store-collected-images/webextension-with-webpack/extension/popup.html new file mode 100755 index 0000000..a005aa0 --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/popup.html @@ -0,0 +1,11 @@ + + + + + + + +
    + + + diff --git a/store-collected-images/webextension-with-webpack/extension/shared.css b/store-collected-images/webextension-with-webpack/extension/shared.css new file mode 100644 index 0000000..e31f58c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/extension/shared.css @@ -0,0 +1,22 @@ +ul.thumbnails { + padding: 0; +} + +ul.thumbnails li { + display: inline-block; + vertical-align: middle; + padding: 0.4em; +} + +.thumbnails img { + max-width: 50px; + max-height: 50px; +} + +.error { + background: rgba(255,0,0,0.4); +} + +.success { + background: rgba(0,255,0,0.4); +} \ No newline at end of file diff --git a/store-collected-images/webextension-with-webpack/package.json b/store-collected-images/webextension-with-webpack/package.json new file mode 100644 index 0000000..ce6ec9c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/package.json @@ -0,0 +1,38 @@ +{ + "name": "store-collected-images", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "webpack --display-error-details --progress --colors", + "build:watch": "npm run build -- -w", + "start": "npm run build && web-ext run -s extension/" + }, + "author": "", + "license": "MPL-2.0", + "devDependencies": { + "babel-core": "6.24.1", + "babel-loader": "7.0.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-object-rest-spread": "6.23.0", + "babel-plugin-transform-es2015-modules-commonjs": "6.24.1", + "babel-preset-es2017": "6.24.1", + "babel-preset-react": "6.24.1", + "idb-file-storage": "^0.1.0", + "react": "15.5.4", + "react-dom": "15.5.4", + "uuid": "^3.0.1", + "web-ext": "1.9.1", + "webpack": "2.6.1" + }, + "babel": { + "presets": [ + "es2017", + "react" + ], + "plugins": [ + "transform-class-properties", + "transform-es2015-modules-commonjs" + ] + } +} diff --git a/store-collected-images/webextension-with-webpack/src/background.js b/store-collected-images/webextension-with-webpack/src/background.js new file mode 100644 index 0000000..436131f --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/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-with-webpack/src/navigate-collection.js b/store-collected-images/webextension-with-webpack/src/navigate-collection.js new file mode 100644 index 0000000..79ba2ad --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/navigate-collection.js @@ -0,0 +1,66 @@ +"use strict"; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import {loadStoredImages, removeStoredImages} from './utils/image-store'; + +class NavigateCollectionUI extends React.Component { + constructor(props) { + super(props); + this.state = { + storedImages: [], + }; + + this.onFilterUpdated = this.onFilterUpdated.bind(this); + this.onReload = this.onFilterUpdated; + + this.onDelete = this.onDelete.bind(this); + } + + componentDidMount() { + // Load the stored image once the component has been rendered in the page. + this.onFilterUpdated(); + } + + onFilterUpdated() { + loadStoredImages(this.refs.imageFilter.value) + .then((storedImages) => { + this.setState({storedImages}); + }) + .catch(console.error); + } + + onDelete() { + const {storedImages} = this.state; + this.setState({storedImages: []}); + + removeStoredImages(storedImages).catch(console.error); + } + + render() { + const {storedImages} = this.state; + + return ( +
    +

    Stored images

    + + + +
      + { + storedImages.map(({storedName, blobUrl}) => { + const onClickedImage = () => { + this.refs.imageFilter.value = storedName; + this.onFilterUpdated(); + }; + return
    • ; + }) + } +
    +
    + ); + } +} + +ReactDOM.render(, document.getElementById('app')); diff --git a/store-collected-images/webextension-with-webpack/src/popup.js b/store-collected-images/webextension-with-webpack/src/popup.js new file mode 100755 index 0000000..56d933c --- /dev/null +++ b/store-collected-images/webextension-with-webpack/src/popup.js @@ -0,0 +1,114 @@ +"use strict"; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import uuidV4 from 'uuid/v4'; + +import preventWindowDragAndDrop from './utils/handle-window-drag-and-drop'; +import {saveCollectedBlobs} from './utils/image-store'; + +class Popup extends React.Component { + constructor(props) { + super(props); + this.state = { + collectedBlobs: [], + lastMessage: undefined, + }; + + this.onClick = this.onClick.bind(this); + } + + onClick() { + if (!this.refs.collectionName.value) { + this.setState({ + lastMessage: {text: "The collection name is mandatory.", type: "error"}, + }); + + // Clear the error message after a 2s timeout. + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + + return; + } + + saveCollectedBlobs(this.refs.collectionName.value, this.state.collectedBlobs) + .then(() => { + this.setState({ + lastMessage: {text: "All the collected images have been saved", type: "success"}, + collectedBlobs: [], + }); + + // Clear the error message after a 2s timeout. + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + }) + .catch((err) => { + this.setState({ + lastMessage: {text: `Failed to save collected images: ${err}`, type: "error"}, + }); + + // Clear the error message after a 2s timeout. + setTimeout(() => { + this.setState({lastMessage: undefined}); + }, 2000); + }); + } + + render() { + const {collectedBlobs, lastMessage} = this.state; + + return ( +
    +

    Collected images

    + {lastMessage &&

    {lastMessage.text}

    } + + +
      + { + collectedBlobs.map(({uuid, blobUrl}) => { + return
    • ; + }) + } +
    +
    + ); + } +} + +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', +};