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
This commit is contained in:
Luca Greco
2017-07-20 00:06:46 +02:00
committed by wbamberg
parent 119ee4cb6c
commit 7b6b03a72c
40 changed files with 1803 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
**/node_modules/**
react-es6-popup/**/dist
mocha-client-tests
store-collected-images/webextension-plain/deps

View File

@@ -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/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1 @@
deps

View File

@@ -0,0 +1,3 @@
{
"parser": "babel-eslint"
}

View File

@@ -0,0 +1,39 @@
# "Image Reference Collector" example without a webpack build step (and React UI)
## Usage
This version of the example doesn't use Webpack and Babel to transpile the ES6 modules (and JSX)
into JavaScript bundle scripts, so it can be executed (using `web-ext run` or by installing it temporarily from "about:debugging#addons") and changed without any build step.
## NOTE on the plain JavaScript React UI
The UI of this example is based on React (as is the "build with webpack" version of this example), but it uses plain JavaScript instead of JSX (the "HTML"-like syntax usually used in "React"-based projects), and so the component UI hierarchy is composed of `React.createElement` function calls, e.g.
```
class MyReactComponent extends React.Component {
render() {
return (
<div className="important">
<h3>A title</h3>
<p>A text paragraph</p>
</div>
);
}
}
```
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"),
]);
}
}
```

View File

@@ -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);
}
});

View File

@@ -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<string>}
* 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<ArrayBuffer>}
* 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<ArrayBuffer>}
* 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<number>}
* 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<number>}
* 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<number>}
* 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<File>}
* 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<File>}
* 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<string[]>}
* 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<number>}
* 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<Blob|File|IDBPromisedMutableFile>}
* 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

File diff suppressed because one or more lines are too long

View File

@@ -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<o.length;f++)t(o[f]);return t}({1:[function(n,e,r){function o(n,e){var r=e||0,o=t;return o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+"-"+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]+o[n[r++]]}for(var t=[],i=0;i<256;++i)t[i]=(i+256).toString(16).substr(1);e.exports=o},{}],2:[function(n,e,r){(function(n){var r,o=n.crypto||n.msCrypto;if(o&&o.getRandomValues){var t=new Uint8Array(16);r=function(){return o.getRandomValues(t),t}}if(!r){var i=new Array(16);r=function(){for(var n,e=0;e<16;e++)0===(3&e)&&(n=4294967296*Math.random()),i[e]=n>>>((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)});

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -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",
"<all_urls>"
]
}

View File

@@ -0,0 +1 @@
@import "shared.css";

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="navigate-collection.css"/>
</head>
<body>
<div id="app">
<h3>Stored images</h3>
<input placeholder="filter image" class="image-filter">
<button class="reload-images">reload</button>
<button class="delete-images">delete</button>
<ul class="thumbnails"></ul>
</div>
<script src="/deps/idb-file-storage.js"></script>
<script src="/deps/uuidv4.js"></script>
<script src="/utils/handle-window-drag-and-drop.js"></script>
<script src="/utils/image-store.js"></script>
<script src="navigate-collection.js"></script>
</body>
</html>

View File

@@ -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'));

View File

@@ -0,0 +1,12 @@
@import "shared.css";
html, body {
width: 250px;
margin: 0;
padding: 0;
margin-left: 1em;
}
input {
width: 90%;
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css"/>
</head>
<body>
<div id="app">
<h3>Collected images</h3>
<p id="error-message"></p>
<input placeholder="collection name" class="collection-name">
<button class="save-collection">save</button>
<ul class="thumbnails">
<ul>
</div>
<script src="/deps/idb-file-storage.js"></script>
<script src="/deps/uuidv4.js"></script>
<script src="/utils/handle-window-drag-and-drop.js"></script>
<script src="/utils/image-store.js"></script>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -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});
}
});

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
{
"parser": "babel-eslint",
"env": {
"commonjs": true
}
}

View File

@@ -0,0 +1,5 @@
# Ignore build artifacts and other files.
.DS_Store
yarn.lock
extension/dist
node_modules

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -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",
"<all_urls>"
]
}

View File

@@ -0,0 +1 @@
@import "shared.css";

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="navigate-collection.css"/>
</head>
<body>
<div id="app"></div>
<script src="dist/navigate-collection.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
@import "shared.css";
html, body {
width: 250px;
margin: 0;
padding: 0;
margin-left: 1em;
}
input {
width: 90%;
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css"/>
</head>
<body>
<div id="app"></div>
<script src="dist/popup.js"></script>
</body>
</html>

View File

@@ -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);
}

View File

@@ -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"
]
}
}

View File

@@ -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);
}
});

View File

@@ -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 (
<div>
<h3>Stored images</h3>
<input placeholder="filter image" ref="imageFilter" onChange={this.onFilterUpdated}/>
<button onClick={this.onReload}>reload</button>
<button onClick={this.onDelete}>delete</button>
<ul className="thumbnails">
{
storedImages.map(({storedName, blobUrl}) => {
const onClickedImage = () => {
this.refs.imageFilter.value = storedName;
this.onFilterUpdated();
};
return <li key={storedName}><img src={blobUrl} onClick={onClickedImage}/></li>;
})
}
</ul>
</div>
);
}
}
ReactDOM.render(<NavigateCollectionUI/>, document.getElementById('app'));

View File

@@ -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 (
<div>
<h3>Collected images</h3>
{lastMessage && <p className={lastMessage.type}>{lastMessage.text}</p>}
<input placeholder="collection name" ref="collectionName"/>
<button onClick={this.onClick}>save</button>
<ul className="thumbnails">
{
collectedBlobs.map(({uuid, blobUrl}) => {
return <li key={uuid}><img src={blobUrl}/></li>;
})
}
</ul>
</div>
);
}
}
const popup = ReactDOM.render(<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});
}
});

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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',
};