mirror of
https://github.com/mdn/webextensions-examples.git
synced 2026-04-16 06:18:35 +02:00
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:
@@ -1,3 +1,4 @@
|
||||
**/node_modules/**
|
||||
react-es6-popup/**/dist
|
||||
mocha-client-tests
|
||||
store-collected-images/webextension-plain/deps
|
||||
|
||||
36
store-collected-images/README.md
Normal file
36
store-collected-images/README.md
Normal 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.
|
||||
|
||||
[](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/).
|
||||
BIN
store-collected-images/screenshots/screenshot.png
Normal file
BIN
store-collected-images/screenshots/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
1
store-collected-images/webextension-plain/.eslintignore
Normal file
1
store-collected-images/webextension-plain/.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
deps
|
||||
3
store-collected-images/webextension-plain/.eslintrc
Normal file
3
store-collected-images/webextension-plain/.eslintrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
39
store-collected-images/webextension-plain/README.md
Normal file
39
store-collected-images/webextension-plain/README.md
Normal 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"),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
47
store-collected-images/webextension-plain/background.js
Normal file
47
store-collected-images/webextension-plain/background.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -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
1
store-collected-images/webextension-plain/deps/uuidv4.js
Normal file
1
store-collected-images/webextension-plain/deps/uuidv4.js
Normal 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)});
|
||||
BIN
store-collected-images/webextension-plain/images/icon.png
Normal file
BIN
store-collected-images/webextension-plain/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 B |
BIN
store-collected-images/webextension-plain/images/icon16.png
Normal file
BIN
store-collected-images/webextension-plain/images/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1008 B |
26
store-collected-images/webextension-plain/manifest.json
Executable file
26
store-collected-images/webextension-plain/manifest.json
Executable 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>"
|
||||
]
|
||||
}
|
||||
1
store-collected-images/webextension-plain/navigate-collection.css
Executable file
1
store-collected-images/webextension-plain/navigate-collection.css
Executable file
@@ -0,0 +1 @@
|
||||
@import "shared.css";
|
||||
21
store-collected-images/webextension-plain/navigate-collection.html
Executable file
21
store-collected-images/webextension-plain/navigate-collection.html
Executable 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>
|
||||
@@ -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'));
|
||||
12
store-collected-images/webextension-plain/popup.css
Executable file
12
store-collected-images/webextension-plain/popup.css
Executable file
@@ -0,0 +1,12 @@
|
||||
@import "shared.css";
|
||||
|
||||
html, body {
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
}
|
||||
22
store-collected-images/webextension-plain/popup.html
Executable file
22
store-collected-images/webextension-plain/popup.html
Executable 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>
|
||||
127
store-collected-images/webextension-plain/popup.js
Normal file
127
store-collected-images/webextension-plain/popup.js
Normal 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});
|
||||
}
|
||||
});
|
||||
22
store-collected-images/webextension-plain/shared.css
Normal file
22
store-collected-images/webextension-plain/shared.css
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"commonjs": true
|
||||
}
|
||||
}
|
||||
5
store-collected-images/webextension-with-webpack/.gitignore
vendored
Normal file
5
store-collected-images/webextension-with-webpack/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Ignore build artifacts and other files.
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
extension/dist
|
||||
node_modules
|
||||
32
store-collected-images/webextension-with-webpack/README.md
Normal file
32
store-collected-images/webextension-with-webpack/README.md
Normal 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 |
26
store-collected-images/webextension-with-webpack/extension/manifest.json
Executable file
26
store-collected-images/webextension-with-webpack/extension/manifest.json
Executable 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>"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import "shared.css";
|
||||
@@ -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>
|
||||
12
store-collected-images/webextension-with-webpack/extension/popup.css
Executable file
12
store-collected-images/webextension-with-webpack/extension/popup.css
Executable file
@@ -0,0 +1,12 @@
|
||||
@import "shared.css";
|
||||
|
||||
html, body {
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
}
|
||||
11
store-collected-images/webextension-with-webpack/extension/popup.html
Executable file
11
store-collected-images/webextension-with-webpack/extension/popup.html
Executable 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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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'));
|
||||
114
store-collected-images/webextension-with-webpack/src/popup.js
Executable file
114
store-collected-images/webextension-with-webpack/src/popup.js
Executable 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});
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
Reference in New Issue
Block a user