1
0
Fork 0
mirror of https://github.com/futurepress/epub.js.git synced 2025-10-02 14:49:16 +02:00

Add Storage

This commit is contained in:
Fred Chasen 2018-10-28 22:31:24 -07:00
parent 14af4539f5
commit 363be16fd9
11 changed files with 590 additions and 12 deletions

97
examples/offline.html Normal file
View file

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EPUB.js Storage Example</title>
<script src="../node_modules/jszip/dist/jszip.min.js"></script>
<script src="../node_modules/localforage/dist/localforage.min.js"></script>
<script src="../dist/epub.js"></script>
<link rel="stylesheet" type="text/css" href="examples.css">
<style type="text/css">
#offline {
position: fixed;
top: -40px;
left: 0;
background-color: yellow;
width: 100%;
text-align: center;
padding: 10px 0;
transition: top .5s;
z-index: 99;
}
</style>
</head>
<body>
<div id="offline">You are offline. Loading from Storage.</div>
<div id="viewer" class="spreads"></div>
<div id="prev" class="arrow"></div>
<div id="next" class="arrow"></div>
<script>
var book = ePub("https://s3.amazonaws.com/moby-dick/", {
store: "epubjs-test"
});
var rendition = book.renderTo("viewer", {
width: "100%",
height: 600
});
var displayed = rendition.display();
displayed.then(function(renderer){
// Add all resources to the store
// Add `true` to force re-saving resources
book.storage.add(book.resources, true).then(() => {
console.log("stored");
})
});
var next = document.getElementById("next");
next.addEventListener("click", function(){
rendition.next();
}, false);
var prev = document.getElementById("prev");
prev.addEventListener("click", function(){
rendition.prev();
}, false);
var keyListener = function(e){
// Left Key
if ((e.keyCode || e.which) == 37) {
rendition.prev();
}
// Right Key
if ((e.keyCode || e.which) == 39) {
rendition.next();
}
};
rendition.on("keyup", keyListener);
document.addEventListener("keyup", keyListener, false);
var msg = document.getElementById('offline');
book.storage.on("online", function () {
console.log("online");
msg.style.top = "-40px";
});
book.storage.on("offline", function () {
console.log("offline");
msg.style.top = "0px";
});
</script>
</body>
</html>

18
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "epubjs", "name": "epubjs",
"version": "0.3.74", "version": "0.3.75",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -49,6 +49,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/localforage": {
"version": "0.0.34",
"resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz",
"integrity": "sha1-XjHDLdh5HsS5/z70fJy1Wy0NlDg=",
"requires": {
"localforage": "*"
}
},
"@types/node": { "@types/node": {
"version": "10.12.0", "version": "10.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz",
@ -10804,6 +10812,14 @@
"json5": "^0.5.0" "json5": "^0.5.0"
} }
}, },
"localforage": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.2.tgz",
"integrity": "sha1-+kRCYC+Abt0rympUq05lbwMfEhw=",
"requires": {
"lie": "3.1.1"
}
},
"locate-path": { "locate-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",

View file

@ -75,13 +75,15 @@
"webpack-dev-server": "^2.11.2" "webpack-dev-server": "^2.11.2"
}, },
"dependencies": { "dependencies": {
"@types/jszip": "^3.1.4",
"@types/localforage": "0.0.34",
"event-emitter": "^0.3.5", "event-emitter": "^0.3.5",
"jszip": "^3.1.5", "jszip": "^3.1.5",
"localforage": "^1.7.2",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"marks-pane": "^1.0.9", "marks-pane": "^1.0.9",
"path-webpack": "0.0.3", "path-webpack": "0.0.3",
"stream-browserify": "^2.0.1", "stream-browserify": "^2.0.1",
"xmldom": "^0.1.27", "xmldom": "^0.1.27"
"@types/jszip": "^3.1.4"
} }
} }

View file

@ -13,6 +13,7 @@ import Rendition from "./rendition";
import Archive from "./archive"; import Archive from "./archive";
import request from "./utils/request"; import request from "./utils/request";
import EpubCFI from "./epubcfi"; import EpubCFI from "./epubcfi";
import Store from "./store";
import { EPUBJS_VERSION, EVENTS } from "./utils/constants"; import { EPUBJS_VERSION, EVENTS } from "./utils/constants";
const CONTAINER_PATH = "META-INF/container.xml"; const CONTAINER_PATH = "META-INF/container.xml";
@ -39,6 +40,7 @@ const INPUT_TYPE = {
* @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs * @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs
* @param {method} [options.canonical] optional function to determine canonical urls for a path * @param {method} [options.canonical] optional function to determine canonical urls for a path
* @param {string} [options.openAs] optional string to determine the input type * @param {string} [options.openAs] optional string to determine the input type
* @param {string} [options.store=false] cache the contents in local storage, value should be the name of the reader
* @returns {Book} * @returns {Book}
* @example new Book("/path/to/book.epub", {}) * @example new Book("/path/to/book.epub", {})
* @example new Book({ replacements: "blobUrl" }) * @example new Book({ replacements: "blobUrl" })
@ -60,7 +62,8 @@ class Book {
encoding: undefined, encoding: undefined,
replacements: undefined, replacements: undefined,
canonical: undefined, canonical: undefined,
openAs: undefined openAs: undefined,
store: undefined
}); });
extend(this.settings, options); extend(this.settings, options);
@ -173,6 +176,13 @@ class Book {
*/ */
this.archive = undefined; this.archive = undefined;
/**
* @member {Store} storage
* @memberof Book
* @private
*/
this.storage = undefined;
/** /**
* @member {Resources} resources * @member {Resources} resources
* @memberof Book * @memberof Book
@ -202,6 +212,9 @@ class Book {
this.packaging = undefined; this.packaging = undefined;
// this.toc = undefined; // this.toc = undefined;
if (this.settings.store) {
this.store();
}
if(url) { if(url) {
this.open(url, this.settings.openAs).catch((error) => { this.open(url, this.settings.openAs).catch((error) => {
@ -233,7 +246,7 @@ class Book {
} else if (type === INPUT_TYPE.EPUB) { } else if (type === INPUT_TYPE.EPUB) {
this.archived = true; this.archived = true;
this.url = new Url("/", ""); this.url = new Url("/", "");
opening = this.request(input, "binary",this.settings.requestCredentials) opening = this.request(input, "binary", this.settings.requestCredentials)
.then(this.openEpub.bind(this)); .then(this.openEpub.bind(this));
} else if(type == INPUT_TYPE.OPF) { } else if(type == INPUT_TYPE.OPF) {
this.url = new Url(input); this.url = new Url(input);
@ -318,13 +331,10 @@ class Book {
* @return {Promise} returns a promise with the requested resource * @return {Promise} returns a promise with the requested resource
*/ */
load(path) { load(path) {
var resolved; var resolved = this.resolve(path);
if(this.archived) { if(this.archived) {
resolved = this.resolve(path);
return this.archive.request(resolved); return this.archive.request(resolved);
} else { } else {
resolved = this.resolve(path);
return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders); return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders);
} }
} }
@ -558,6 +568,61 @@ class Book {
return this.archive.open(input, encoding); return this.archive.open(input, encoding);
} }
/**
* Store the epubs contents
* @private
* @param {binary} input epub data
* @param {string} [encoding]
* @return {Store}
*/
store() {
// Use "blobUrl" or "base64" for replacements
let replacementsSetting = this.settings.replacements && this.settings.replacements !== "none";
// Save original url
let originalUrl = this.url;
// Save original request method
let requester = this.settings.requestMethod || request.bind(this);
// Create new Store
this.storage = new Store(this.settings.store, requester, this.resolve.bind(this));
// Replace request method to go through store
this.request = this.storage.request.bind(this.storage);
this.opened.then(() => {
if (this.archived) {
this.storage.requester = this.archive.request.bind(this.archive);
}
// Substitute hook
let substituteResources = (output, section) => {
section.output = this.resources.substitute(output, section.url);
};
// Set to use replacements
this.resources.settings.replacements = replacementsSetting || "blobUrl";
// Create replacement urls
this.resources.replacements().
then(() => {
return this.resources.replaceCss();
});
this.storage.on("offline", () => {
// Remove url to use relative resolving for hrefs
this.url = new Url("/", "");
// Add hook to replace resources in contents
this.spine.hooks.serialize.register(substituteResources);
});
this.storage.on("online", () => {
// Restore original url
this.url = originalUrl;
// Remove hook
this.spine.hooks.serialize.deregister(substituteResources);
});
});
return this.storage;
}
/** /**
* Get the cover url * Get the cover url
* @return {string} coverUrl * @return {string} coverUrl

View file

@ -457,7 +457,7 @@ class DefaultViewManager {
} }
} }
}.bind(this), (err) => { }.bind(this), (err) => {
displaying.reject(err); return err;
}) })
.then(function(){ .then(function(){
this.views.show(); this.views.show();
@ -529,7 +529,7 @@ class DefaultViewManager {
} }
} }
}.bind(this), (err) => { }.bind(this), (err) => {
displaying.reject(err); return err;
}) })
.then(function(){ .then(function(){
if(this.isPaginated && this.settings.axis === "horizontal") { if(this.isPaginated && this.settings.axis === "horizontal") {

349
src/store.js Normal file
View file

@ -0,0 +1,349 @@
import {defer, isXml, parse} from "./utils/core";
import httpRequest from "./utils/request";
import mime from "../libs/mime/mime";
import Path from "./utils/path";
import EventEmitter from "event-emitter";
/**
* Handles saving and requesting files from local storage
* @class
*/
class Store {
constructor(name, requester, resolver) {
this.urlCache = {};
this.requester = requester || httpRequest;
this.resolver = resolver;
this.online = true;
this.checkRequirements();
// This should be the name of the application for modals
localforage.config({
name: name || 'epubjs'
});
this.addListeners();
}
/**
* Checks to see if localForage exists in global namspace,
* Requires localForage if it isn't there
* @private
*/
checkRequirements(){
try {
if (typeof localForage === "undefined") {
let localForage = require("localforage");
}
} catch (e) {
throw new Error("localForage lib not loaded");
}
}
/**
* Add online and offline event listeners
* @private
*/
addListeners() {
window.addEventListener('online', this.status.bind(this));
window.addEventListener('offline', this.status.bind(this));
}
/**
* Remove online and offline event listeners
* @private
*/
removeListeners() {
window.removeEventListener('online', this.status.bind(this));
window.removeEventListener('offline', this.status.bind(this));
}
/**
* Update the online / offline status
* @private
*/
status(event) {
let online = navigator.onLine;
this.online = online;
if (online) {
this.emit("online", this);
} else {
this.emit("offline", this);
}
}
/**
* Add all of a book resources to the store
* @param {Resources} resources book resources
* @param {boolean} [force] force resaving resources
* @return {Promise<object>} store objects
*/
add(resources, force) {
let mapped = resources.resources.map((item) => {
let { href } = item;
let url = this.resolver(href);
let encodedUrl = window.encodeURIComponent(url);
return localforage.getItem(encodedUrl).then((item) => {
if (!item || force) {
return this.requester(url, "binary")
.then((data) => {
return localforage.setItem(encodedUrl, data);
});
} else {
return item;
}
});
});
return Promise.all(mapped);
}
/**
* Request a url from storage
* @param {string} url a url to request from storage
* @param {string} [type] specify the type of the returned result
* @param {boolean} [withCredentials]
* @param {object} [headers]
* @return {Promise<Blob | string | JSON | Document | XMLDocument>}
*/
request(url, type, withCredentials, headers){
var deferred = new defer();
var response;
var path = new Path(url);
if (this.online) {
return this.requester(url, type, withCredentials, headers).then((data) => {
// from network
let encodedUrl = window.encodeURIComponent(url);
localforage.getItem(encodedUrl).then((result) => {
if (!result) {
this.requester(url, "binary", withCredentials, headers).then((data) => {
localforage.setItem(encodedUrl, data);
});
}
});
return data;
})
} else {
// If type isn't set, determine it from the file extension
if(!type) {
type = path.extension;
}
if(type == "blob"){
response = this.getBlob(url);
} else {
response = this.getText(url);
}
return response.then((r) => {
var deferred = new defer();
var result;
if (r) {
result = this.handleResponse(r, type);
deferred.resolve(result);
} else {
deferred.reject({
message : "File not found in storage: " + url,
stack : new Error().stack
});
}
return deferred.promise;
});
}
}
/**
* Handle the response from request
* @private
* @param {any} response
* @param {string} [type]
* @return {any} the parsed result
*/
handleResponse(response, type){
var r;
if(type == "json") {
r = JSON.parse(response);
}
else
if(isXml(type)) {
r = parse(response, "text/xml");
}
else
if(type == "xhtml") {
r = parse(response, "application/xhtml+xml");
}
else
if(type == "html" || type == "htm") {
r = parse(response, "text/html");
} else {
r = response;
}
return r;
}
/**
* Get a Blob from Storage by Url
* @param {string} url
* @param {string} [mimeType]
* @return {Blob}
*/
getBlob(url, mimeType){
let encodedUrl = window.encodeURIComponent(url);
return localforage.getItem(encodedUrl).then(function(uint8array) {
if(!uint8array) return;
mimeType = mimeType || mime.lookup(url);
return new Blob([uint8array], {type : mimeType});
});
}
/**
* Get Text from Storage by Url
* @param {string} url
* @param {string} [mimeType]
* @return {string}
*/
getText(url, mimeType){
let encodedUrl = window.encodeURIComponent(url);
mimeType = mimeType || mime.lookup(url);
return localforage.getItem(encodedUrl).then(function(uint8array) {
var deferred = new defer();
var reader = new FileReader();
var blob = new Blob([uint8array], {type : mimeType});
if(!blob) return;
reader.addEventListener("loadend", () => {
deferred.resolve(reader.result);
});
reader.readAsText(blob, mimeType);
return deferred.promise;
});
}
/**
* Get a base64 encoded result from Storage by Url
* @param {string} url
* @param {string} [mimeType]
* @return {string} base64 encoded
*/
getBase64(url, mimeType){
let encodedUrl = window.encodeURIComponent(url);
mimeType = mimeType || mime.lookup(url);
return localforage.getItem(encodedUrl).then((uint8array) => {
var deferred = new defer();
var reader = new FileReader();
var blob = new Blob([uint8array], {type : mimeType});
if(!blob) return;
reader.addEventListener("loadend", () => {
deferred.resolve(reader.result);
});
reader.readAsDataURL(blob, mimeType);
return deferred.promise;
});
}
/**
* Create a Url from a stored item
* @param {string} url
* @param {object} [options.base64] use base64 encoding or blob url
* @return {Promise} url promise with Url string
*/
createUrl(url, options){
var deferred = new defer();
var _URL = window.URL || window.webkitURL || window.mozURL;
var tempUrl;
var response;
var useBase64 = options && options.base64;
if(url in this.urlCache) {
deferred.resolve(this.urlCache[url]);
return deferred.promise;
}
if (useBase64) {
response = this.getBase64(url);
if (response) {
response.then(function(tempUrl) {
this.urlCache[url] = tempUrl;
deferred.resolve(tempUrl);
}.bind(this));
}
} else {
response = this.getBlob(url);
if (response) {
response.then(function(blob) {
tempUrl = _URL.createObjectURL(blob);
this.urlCache[url] = tempUrl;
deferred.resolve(tempUrl);
}.bind(this));
}
}
if (!response) {
deferred.reject({
message : "File not found in storage: " + url,
stack : new Error().stack
});
}
return deferred.promise;
}
/**
* Revoke Temp Url for a achive item
* @param {string} url url of the item in the store
*/
revokeUrl(url){
var _URL = window.URL || window.webkitURL || window.mozURL;
var fromCache = this.urlCache[url];
if(fromCache) _URL.revokeObjectURL(fromCache);
}
destroy() {
var _URL = window.URL || window.webkitURL || window.mozURL;
for (let fromCache in this.urlCache) {
_URL.revokeObjectURL(fromCache);
}
this.urlCache = {};
this.removeListeners();
}
}
EventEmitter(Store.prototype);
export default Store;

View file

@ -28,6 +28,21 @@ class Hook {
} }
} }
/**
* Removes a function
* @example this.content.deregister(function(){...});
*/
deregister(func){
let hook;
for (let i = 0; i < this.hooks.length; i++) {
hook = this.hooks[i];
if (hook === func) {
this.hooks.splice(i, 1);
break;
}
}
}
/** /**
* Triggers a hook to run all functions * Triggers a hook to run all functions
* @example this.content.trigger(args).then(function(){...}); * @example this.content.trigger(args).then(function(){...});

View file

@ -77,6 +77,12 @@ class Path {
* @returns {string} relative * @returns {string} relative
*/ */
relative (what) { relative (what) {
var isAbsolute = what && (what.indexOf("://") > -1);
if (isAbsolute) {
return what;
}
return path.relative(this.directory, what); return path.relative(this.directory, what);
} }

26
types/store.d.ts vendored Normal file
View file

@ -0,0 +1,26 @@
import localForage = require('localforage');
import Resources from "./resources";
export default class Store {
constructor();
add(resources: Resources, force?: boolean): Promise<Array<object>>;
request(url: string, type?: string, withCredentials?: boolean, headers?: object): Promise<Blob | string | JSON | Document | XMLDocument>;
getBlob(url: string, mimeType?: string): Promise<Blob>;
getText(url: string): Promise<string>;
getBase64(url: string, mimeType?: string): Promise<string>;
createUrl(url: string, options: { base64: boolean }): Promise<string>;
revokeUrl(url: string): void;
destroy(): void;
private checkRequirements(): void;
private handleResponse(response: any, type?: string): Blob | string | JSON | Document | XMLDocument;
}

View file

@ -8,6 +8,8 @@ export default class Hook {
register(func: Function): void; register(func: Function): void;
register(arr: Array<Function>): void; register(arr: Array<Function>): void;
deregister(func: Function): void;
trigger(...args: any[]): Promise<any>; trigger(...args: any[]): Promise<any>;
list(): Array<any>; list(): Array<any>;

View file

@ -1 +1 @@
export default function request(url: string, type: string, withCredentials: boolean, headers: object): Promise<Blob | string | JSON | Document | XMLDocument>; export default function request(url: string, type?: string, withCredentials?: boolean, headers?: object): Promise<Blob | string | JSON | Document | XMLDocument>;