├── demo-ui.png
├── assets
├── img
│ ├── loader.gif
│ ├── favicon.png
│ ├── favicon.svg
│ ├── search-clear.svg
│ ├── bookmark.svg
│ ├── bookmarked.svg
│ ├── next.svg
│ ├── prev.svg
│ ├── open-book.svg
│ ├── menu.svg
│ ├── bookmarks.svg
│ ├── upload.svg
│ ├── resize-full.svg
│ ├── resize-small.svg
│ ├── info.svg
│ ├── annotations.svg
│ ├── settings.svg
│ ├── toc.svg
│ └── search.svg
├── font
│ ├── fontello.eot
│ ├── fontello.ttf
│ ├── fontello.woff
│ └── fontello.svg
└── css
│ ├── toolbar.css
│ ├── common.css
│ ├── notedlg.css
│ ├── main.css
│ └── sidebar.css
├── .gitignore
├── .editorconfig
├── docs
└── keybindings.md
├── index.html
├── LICENSE
├── package.json
├── src
├── utils.js
├── sidebar
│ ├── metadata.js
│ ├── search.js
│ ├── toc.js
│ ├── annotations.js
│ ├── bookmarks.js
│ └── settings.js
├── notedlg.js
├── storage.js
├── sidebar.js
├── content.js
├── toolbar.js
├── reader.js
├── strings.js
└── ui.js
├── webpack.config.cjs
└── README.md
/demo-ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intity/epubreader-js/HEAD/demo-ui.png
--------------------------------------------------------------------------------
/assets/img/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intity/epubreader-js/HEAD/assets/img/loader.gif
--------------------------------------------------------------------------------
/assets/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intity/epubreader-js/HEAD/assets/img/favicon.png
--------------------------------------------------------------------------------
/assets/font/fontello.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intity/epubreader-js/HEAD/assets/font/fontello.eot
--------------------------------------------------------------------------------
/assets/font/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intity/epubreader-js/HEAD/assets/font/fontello.ttf
--------------------------------------------------------------------------------
/assets/font/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intity/epubreader-js/HEAD/assets/font/fontello.woff
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
3 | # Dependency directories
4 | node_modules/
5 |
6 | # Visual Studio Code
7 | .vscode
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 |
9 | [*.{js,ts,cjs,css,html}]
10 | charset = utf-8
11 | indent_style = tab
12 | indent_size = 4
13 |
14 | [*.{js,ts}]
15 | trim_trailing_whitespace = true
16 |
--------------------------------------------------------------------------------
/assets/img/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/keybindings.md:
--------------------------------------------------------------------------------
1 | # Keybindings
2 |
3 | The **epubreader-js** application supports the following keybindings.
4 |
5 | ## Font size
6 |
7 | | Key | Description |
8 | | ------------ | -------------------------- |
9 | | + | Increasing font size |
10 | | - | Decrease font size |
11 | | 0 | Reset font size to default |
12 |
13 | > The font size increases/decreases as a percentage in steps of 2 units.
14 |
15 | ## Navigation
16 |
17 | | Key | Description |
18 | | --------------------- | ------------------- |
19 | | ArrowLeft | Go to previous page |
20 | | ArrowRight | Go to next page |
--------------------------------------------------------------------------------
/assets/css/toolbar.css:
--------------------------------------------------------------------------------
1 | #toolbar {
2 | height: 58px;
3 | margin: 0;
4 | padding: 0;
5 | z-index: 10;
6 | display: grid;
7 | position: relative;
8 | }
9 |
10 | #toolbar .menu-1 {
11 | display: flex;
12 | grid-column: 1;
13 | justify-content: left;
14 | }
15 |
16 | #toolbar .menu-2 {
17 | display: flex;
18 | grid-column: 2;
19 | justify-content: right;
20 | }
21 |
22 | #toolbar .box {
23 | width: 58px;
24 | height: 36px;
25 | }
26 |
27 | #toolbar #btn-p.box,
28 | #toolbar #btn-n.box {
29 | display: none;
30 | }
31 |
32 | /**
33 | * iPhone 5 : 320 x 568
34 | */
35 | @media
36 | only screen and (width: 320px) and (orientation: portrait),
37 | only screen and (height: 320px) and (orientation: landscape) {
38 | #toolbar {
39 | height: 52px;
40 | }
41 |
42 | #toolbar .box {
43 | width: 52px;
44 | height: 30px;
45 | }
46 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 futurepress
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/assets/img/search-clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "epubreader-js",
3 | "version": "0.3.4",
4 | "homepage": "https://github.com/intity/epubreader-js",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/intity/epubreader-js.git"
8 | },
9 | "directories": {
10 | "dist": "dist",
11 | "docs": "docs"
12 | },
13 | "description": "epub reader",
14 | "bugs": {
15 | "url": "https://github.com/intity/epubreader-js/issues"
16 | },
17 | "main": "src/reader.js",
18 | "type": "module",
19 | "scripts": {
20 | "build": "webpack --mode=production --progress",
21 | "serve": "webpack serve",
22 | "minify": "webpack --mode=production --optimization-minimize --progress",
23 | "prepare": "npm run build && npm run minify"
24 | },
25 | "author": "fchasen@gmail.com",
26 | "license": "MIT",
27 | "contributors": [
28 | {
29 | "name": "vite",
30 | "email": "nuget@list.ru"
31 | }
32 | ],
33 | "devDependencies": {
34 | "copy-webpack-plugin": "^8.1.1",
35 | "webpack": "^5.99.9",
36 | "webpack-cli": "^6.0.1",
37 | "webpack-concat-files-plugin": "^0.5.2",
38 | "webpack-dev-server": "^5.2.2"
39 | },
40 | "dependencies": {
41 | "epubjs": "^0.3.93",
42 | "js-md5": "^0.7.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/assets/img/bookmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
35 |
--------------------------------------------------------------------------------
/assets/img/bookmarked.svg:
--------------------------------------------------------------------------------
1 |
2 |
35 |
--------------------------------------------------------------------------------
/assets/img/next.svg:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/assets/img/prev.svg:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/assets/img/open-book.svg:
--------------------------------------------------------------------------------
1 |
2 |
43 |
--------------------------------------------------------------------------------
/assets/img/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
43 |
--------------------------------------------------------------------------------
/assets/img/bookmarks.svg:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const d = (obj, prop) => obj ? obj[prop] : undefined
2 |
3 | const q = (src, dst, ext, prop) => {
4 | let val
5 | if (typeof dst[prop] === "boolean") {
6 | switch (prop) {
7 | case "annotations":
8 | case "bookmarks":
9 | val = dst[prop] ? src[prop] : dst[prop]
10 | break;
11 | default:
12 | val = dst[prop]
13 | break;
14 | }
15 | } else if (prop === "arrows") {
16 | val = dst[prop]
17 | } else {
18 | val = d(ext, prop) === undefined ? src[prop] : dst[prop]
19 | }
20 | return val
21 | }
22 |
23 | export const extend = (src, dst, ext) => {
24 | for (let prop in src) {
25 | if (prop === "bookPath") {
26 | continue
27 | } else if (dst[prop] instanceof Array) {
28 | dst[prop] = ext ? (src[prop] ? src[prop] : dst[prop]) : src[prop]
29 | } else if (dst[prop] instanceof Object) {
30 | extend(src[prop], dst[prop], d(ext, prop)) // recursive call
31 | } else {
32 | dst[prop] = ext ? q(src, dst, ext, prop) : src[prop]
33 | }
34 | }
35 | }
36 |
37 | export const uuid = () => {
38 | let d = new Date().getTime()
39 | const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
40 | let r = (d + Math.random() * 16) % 16 | 0
41 | d = Math.floor(d / 16)
42 | return (c === "x" ? r : (r & 0x7 | 0x8)).toString(16)
43 | })
44 | return uuid
45 | }
46 |
47 | export const detectMobile = () => {
48 | const matches = [
49 | /Android/i,
50 | /BlackBerry/i,
51 | /iPhone/i,
52 | /iPad/i,
53 | /iPod/i,
54 | /Windows Phone/i,
55 | /webOS/i
56 | ]
57 | return matches.some((i) => navigator.userAgent.match(i))
58 | }
--------------------------------------------------------------------------------
/src/sidebar/metadata.js:
--------------------------------------------------------------------------------
1 | import { UIBox, UIDiv, UIItem, UIList, UIPanel, UIText } from "../ui.js";
2 |
3 | export class MetadataPanel extends UIPanel {
4 |
5 | constructor(reader) {
6 |
7 | super();
8 | const container = new UIDiv().setClass("list-container");
9 | const strings = reader.strings;
10 | const labels = {};
11 | const key = "sidebar/metadata";
12 | const label = new UIText(strings.get(key)).setClass("label");
13 | this.add(new UIBox(label).addClass("header"));
14 | labels[key] = label;
15 |
16 | this.items = new UIList();
17 | this.setId("metadata");
18 | this.add(container);
19 |
20 | const init = (prop, meta) => {
21 | if (meta[prop] === undefined ||
22 | meta[prop] === null || (typeof meta[prop] === "string" && meta[prop].length === 0)) {
23 | return;
24 | }
25 | const item = new UIItem();
26 | const label = new UIText().setClass("label");
27 | const value = new UIText().setClass("value");
28 | label.setValue(strings.get(key + "/" + prop).toUpperCase());
29 | if (prop === "description") {
30 | value.dom.innerHTML = meta[prop];
31 | } else {
32 | value.setValue(meta[prop]);
33 | }
34 | labels[key + "/" + prop] = label;
35 | item.add([label, value]);
36 | this.items.add(item);
37 | }
38 |
39 | //-- events --//
40 |
41 | reader.on("metadata", (meta) => {
42 |
43 | this.items.clear();
44 | container.clear();
45 | container.add(this.items);
46 | document.title = meta.title;
47 | for (const prop in meta) {
48 | init(prop, meta);
49 | }
50 | });
51 |
52 | reader.on("languagechanged", (value) => {
53 |
54 | for (const prop in labels) {
55 | let text;
56 | if (prop === key) {
57 | text = strings.get(prop);
58 | } else {
59 | text = strings.get(prop).toUpperCase();
60 | }
61 | labels[prop].setValue(text);
62 | }
63 | });
64 | }
65 | }
--------------------------------------------------------------------------------
/assets/img/upload.svg:
--------------------------------------------------------------------------------
1 |
2 |
43 |
--------------------------------------------------------------------------------
/assets/img/resize-full.svg:
--------------------------------------------------------------------------------
1 |
2 |
47 |
--------------------------------------------------------------------------------
/assets/img/resize-small.svg:
--------------------------------------------------------------------------------
1 |
2 |
47 |
--------------------------------------------------------------------------------
/src/sidebar/search.js:
--------------------------------------------------------------------------------
1 | import { UIPanel, UIDiv, UIInput, UILink, UIList, UIItem, UIBox } from "../ui.js";
2 |
3 | export class SearchPanel extends UIPanel {
4 |
5 | constructor(reader) {
6 |
7 | super();
8 | const container = new UIDiv().setClass("list-container");
9 | const strings = reader.strings;
10 |
11 | let searchQuery = undefined;
12 | const search = new UIInput("search").setId("nav-q");
13 | search.dom.placeholder = strings.get("sidebar/search/placeholder");
14 | search.dom.onsearch = () => {
15 |
16 | const value = search.getValue();
17 |
18 | if (value.length === 0) {
19 | this.items.clear();
20 | } else if (searchQuery !== value) {
21 | this.items.clear();
22 | this.doSearch(value).then(results => {
23 |
24 | results.forEach(data => {
25 | this.set(data);
26 | });
27 | });
28 | }
29 | searchQuery = value;
30 | };
31 |
32 | this.setId("search");
33 | this.items = new UIList();
34 | container.add(this.items);
35 | this.add([new UIBox(search), container]);
36 | this.reader = reader;
37 | this.selector = undefined;
38 | //
39 | // improvement of the highlighting of keywords is required...
40 | //
41 | }
42 |
43 | /**
44 | * Searching the entire book
45 | * @param {*} q Query keyword
46 | * @returns The search result array.
47 | */
48 | async doSearch(q) {
49 |
50 | const book = this.reader.book;
51 | const results = await Promise.all(
52 | book.spine.spineItems.map(item => item.load(book.load.bind(book))
53 | .then(item.find.bind(item, q)).finally(item.unload.bind(item))));
54 | return await Promise.resolve([].concat.apply([], results));
55 | }
56 |
57 | set(data) {
58 |
59 | const link = new UILink("#" + data.cfi, data.excerpt);
60 | const item = new UIItem();
61 | link.dom.onclick = () => {
62 |
63 | if (this.selector && this.selector !== item)
64 | this.selector.unselect();
65 |
66 | item.select();
67 | this.selector = item;
68 | this.reader.rendition.display(data.cfi);
69 | return false;
70 | };
71 | item.add(link);
72 | this.items.add(item);
73 | }
74 | }
--------------------------------------------------------------------------------
/assets/img/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
35 |
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const CopyPlugin = require("copy-webpack-plugin")
3 |
4 | const config = {
5 | mode: "development",
6 | entry: {
7 | epubreader: "./src/reader.js"
8 | },
9 | output: {
10 | path: path.resolve(__dirname, "dist"),
11 | libraryTarget: "module"
12 | },
13 | externals: {
14 | "epubjs": "epubjs"
15 | },
16 | optimization: {
17 | usedExports: false
18 | },
19 | devServer: {
20 | static: {
21 | directory: path.join(__dirname, "dist")
22 | },
23 | hot: false,
24 | liveReload: true,
25 | compress: true,
26 | port: 8080
27 | },
28 | experiments: {
29 | outputModule: true
30 | },
31 | plugins: [
32 | new CopyPlugin({
33 | patterns: [
34 | {
35 | from: "node_modules/jszip/dist/jszip.min.js",
36 | to: "js/libs/jszip.min.js",
37 | toType: "file",
38 | force: true
39 | },
40 | {
41 | from: "node_modules/js-md5/build/md5.min.js",
42 | to: "js/libs/md5.min.js",
43 | toType: "file",
44 | force: true
45 | },
46 | {
47 | from: "node_modules/epubjs/dist/epub.min.js",
48 | to: "js/libs/epub.min.js",
49 | toType: "file",
50 | force: true
51 | },
52 | {
53 | from: "assets",
54 | to: "assets",
55 | toType: "dir",
56 | force: true
57 | },
58 | {
59 | from: "index.html",
60 | to: "index.html",
61 | toType: "file",
62 | force: true,
63 | transform: (content, absoluteFrom) => {
64 | return content.toString().replace(/dist\//g, "")
65 | }
66 | }
67 | ]
68 | })
69 | ],
70 | performance: {
71 | hints: false
72 | }
73 | }
74 |
75 | module.exports = (env, args) => {
76 |
77 | config.devtool = env.WEBPACK_SERVE ? "eval-source-map" : "source-map"
78 |
79 | if (args.optimizationMinimize) {
80 | config.output.filename = "js/[name].min.js"
81 | config.output.sourceMapFilename = "js/[name].min.js.map"
82 | config.optimization.minimize = true
83 | } else {
84 | config.output.filename = "js/[name].js"
85 | config.output.sourceMapFilename = "js/[name].js.map"
86 | config.optimization.minimize = false
87 | }
88 |
89 | return config;
90 | }
--------------------------------------------------------------------------------
/src/notedlg.js:
--------------------------------------------------------------------------------
1 | import { uuid } from "./utils.js";
2 | import { UIBox, UIDiv, UIInput, UILabel } from "./ui.js";
3 |
4 | export class NoteDlg {
5 |
6 | constructor(reader) {
7 |
8 | const container = new UIDiv().setId("notedlg");
9 | const strings = reader.strings;
10 | const keys = [
11 | "notedlg/label",
12 | "notedlg/add"
13 | ];
14 | const label = new UILabel(strings.get(keys[0]), "note-input");
15 | const textBox = new UIInput("text", "").setId("note-input");
16 | textBox.dom.oninput = (e) => {
17 |
18 | this.update();
19 | e.preventDefault();
20 | };
21 |
22 | const addBtn = new UIInput("button", strings.get(keys[1]));
23 | addBtn.dom.disabled = true;
24 | addBtn.dom.onclick = (e) => {
25 |
26 | const note = {
27 | cfi: this.cfi,
28 | date: new Date(),
29 | text: textBox.getValue(),
30 | uuid: uuid()
31 | };
32 | this.range = undefined;
33 | reader.settings.annotations.push(note);
34 | reader.emit("noteadded", note);
35 | container.removeAttribute("class");
36 | e.preventDefault();
37 | addBtn.dom.blur();
38 | };
39 |
40 | this.update = () => {
41 |
42 | addBtn.dom.disabled = !(this.range && textBox.getValue().length > 0);
43 | };
44 |
45 | container.add(new UIBox([label, textBox, addBtn]).addClass("control"));
46 | document.body.appendChild(container.dom);
47 |
48 | //-- events --//
49 |
50 | reader.on("selected", (cfi, contents) => {
51 |
52 | this.cfi = cfi;
53 | this.range = contents.range(cfi);
54 | this.update();
55 | container.setClass("open");
56 | textBox.setValue("");
57 | });
58 |
59 | reader.on("unselected", () => {
60 |
61 | this.range = undefined;
62 | this.update();
63 | container.removeAttribute("class");
64 | });
65 |
66 | reader.on("languagechanged", (value) => {
67 |
68 | label.setTextContent(strings.get(keys[0]));
69 | addBtn.setValue(strings.get(keys[1]));
70 | });
71 | }
72 | }
--------------------------------------------------------------------------------
/src/sidebar/toc.js:
--------------------------------------------------------------------------------
1 | import { UIPanel, UIDiv, UIItem, UIList, UILink, UISpan, UIText, UIBox } from "../ui.js";
2 |
3 | export class TocPanel extends UIPanel {
4 |
5 | constructor(reader) {
6 |
7 | super();
8 | const container = new UIDiv().setClass("list-container");
9 | const strings = reader.strings;
10 | const keys = [
11 | "sidebar/contents"
12 | ];
13 | const label = new UIText(strings.get(keys[0])).setClass("label");
14 | this.reader = reader;
15 | this.selector = undefined; // save reference to selected tree item
16 | this.setId("contents");
17 | this.add(new UIBox(label).addClass("header"));
18 |
19 | //-- events --//
20 |
21 | reader.on("navigation", (toc) => {
22 |
23 | container.clear();
24 | container.add(this.generateToc(toc));
25 | this.add(container);
26 | });
27 |
28 | reader.on("languagechanged", (value) => {
29 |
30 | label.setValue(strings.get(keys[0]));
31 | });
32 | }
33 |
34 | generateToc(toc, parent) {
35 |
36 | const list = new UIList(parent);
37 |
38 | toc.forEach((chapter) => {
39 |
40 | const link = new UILink(chapter.href, chapter.label);
41 | const item = new UIItem(list).setId(chapter.id);
42 | const ibtn = new UISpan();
43 |
44 | link.dom.onclick = (e) => {
45 |
46 | if (this.selector && this.selector !== item)
47 | this.selector.unselect();
48 |
49 | item.select();
50 | this.selector = item;
51 | this.reader.settings.sectionId = chapter.id;
52 | this.reader.rendition.display(chapter.href);
53 | e.preventDefault();
54 | };
55 | item.add([ibtn, link]);
56 | this.reader.navItems[chapter.href] = {
57 | id: chapter.id,
58 | label: chapter.label
59 | };
60 |
61 | if (this.reader.settings.sectionId === chapter.id) {
62 | list.expand();
63 | item.select();
64 | this.selector = item;
65 | }
66 |
67 | if (chapter.subitems && chapter.subitems.length > 0) {
68 |
69 | const subItems = this.generateToc(chapter.subitems, item);
70 | ibtn.setClass("toggle-collapsed");
71 | ibtn.dom.onclick = () => {
72 |
73 | if (subItems.expanded) {
74 | subItems.collaps();
75 | ibtn.setClass("toggle-collapsed");
76 | } else {
77 | subItems.expand();
78 | ibtn.setClass("toggle-expanded");
79 | }
80 | return false;
81 | };
82 | item.add(subItems);
83 | }
84 |
85 | list.add(item);
86 | });
87 |
88 | return list;
89 | }
90 | }
--------------------------------------------------------------------------------
/src/storage.js:
--------------------------------------------------------------------------------
1 | export class Storage {
2 |
3 | constructor() {
4 |
5 | this.name = "epubreader-js";
6 | this.version = 1.0;
7 | this.db;
8 | this.indexedDB = window.indexedDB ||
9 | window.webkitIndexedDB ||
10 | window.mozIndexedDB ||
11 | window.OIndexedDB ||
12 | window.msIndexedDB;
13 |
14 | if (this.indexedDB === undefined) {
15 |
16 | console.error("The IndexedDB API not available in your browser.");
17 | }
18 | }
19 |
20 | init(callback) {
21 |
22 | if (this.indexedDB === undefined) {
23 | callback();
24 | return;
25 | }
26 |
27 | const time = Date.now();
28 | const onerror = (e) => console.error("IndexedDB", e);
29 | const request = indexedDB.open(this.name, this.version);
30 | request.onupgradeneeded = (e) => {
31 |
32 | const db = e.target.result;
33 | if (db.objectStoreNames.contains("entries") === false) {
34 | db.createObjectStore("entries");
35 | }
36 | }
37 |
38 | request.onsuccess = (e) => {
39 |
40 | this.db = e.target.result;
41 | this.db.onerror = onerror;
42 | callback();
43 | console.log(`storage.init: ${Date.now() - time} ms`);
44 | }
45 |
46 | request.onerror = onerror;
47 | }
48 |
49 | get(callback) {
50 |
51 | if (this.db === undefined) {
52 | callback();
53 | return;
54 | }
55 |
56 | const time = Date.now();
57 | const transaction = this.db.transaction(["entries"], "readwrite");
58 | const objectStore = transaction.objectStore("entries");
59 | const request = objectStore.get(0);
60 | request.onsuccess = (e) => {
61 |
62 | callback(e.target.result);
63 | console.log(`storage.get: ${Date.now() - time} ms`);
64 | }
65 | }
66 |
67 | set(data, callback) {
68 |
69 | if (this.db === undefined) {
70 | callback();
71 | return;
72 | }
73 |
74 | const time = Date.now();
75 | const transaction = this.db.transaction(["entries"], "readwrite");
76 | const objectStore = transaction.objectStore("entries");
77 | const request = objectStore.put(data, 0);
78 | request.onsuccess = () => {
79 |
80 | callback();
81 | console.log(`storage.set: ${Date.now() - time} ms`);
82 | }
83 | }
84 |
85 | clear() {
86 |
87 | if (this.db === undefined) {
88 | return;
89 | }
90 |
91 | const time = Date.now();
92 | const transaction = this.db.transaction(["entries"], "readwrite");
93 | const objectStore = transaction.objectStore("entries");
94 | const request = objectStore.clear();
95 | request.onsuccess = () => {
96 |
97 | console.log(`storage.clear: ${Date.now() - time} ms`);
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/assets/img/annotations.svg:
--------------------------------------------------------------------------------
1 |
2 |
58 |
--------------------------------------------------------------------------------
/src/sidebar.js:
--------------------------------------------------------------------------------
1 | import { UITabbedPanel, UIInput, UIDiv } from "./ui.js";
2 | import { TocPanel } from "./sidebar/toc.js";
3 | import { BookmarksPanel } from "./sidebar/bookmarks.js";
4 | import { AnnotationsPanel } from "./sidebar/annotations.js";
5 | import { SearchPanel } from "./sidebar/search.js";
6 | import { SettingsPanel } from "./sidebar/settings.js";
7 | import { MetadataPanel } from "./sidebar/metadata.js";
8 |
9 | export class Sidebar {
10 |
11 | constructor(reader) {
12 |
13 | const strings = reader.strings;
14 | const controls = reader.settings;
15 | const keys = [
16 | "sidebar/close",
17 | "sidebar/contents",
18 | "sidebar/bookmarks",
19 | "sidebar/annotations",
20 | "sidebar/search",
21 | "sidebar/settings",
22 | "sidebar/metadata"
23 | ];
24 |
25 | const container = new UITabbedPanel("vertical").setId("sidebar");
26 |
27 | const openerBox = new UIDiv().setId("btn-p").addClass("box");
28 | const openerBtn = new UIInput("button");
29 | openerBtn.setTitle(strings.get(keys[0]));
30 | openerBtn.dom.onclick = (e) => {
31 |
32 | reader.emit("sidebaropener", false);
33 | e.preventDefault();
34 | openerBtn.dom.blur();
35 | };
36 | openerBox.add(openerBtn);
37 | container.addMenu(openerBox);
38 |
39 | container.addTab("btn-t", strings.get(keys[1]), new TocPanel(reader));
40 | if (controls.bookmarks) {
41 | container.addTab("btn-d", strings.get(keys[2]), new BookmarksPanel(reader));
42 | }
43 | if (controls.annotations) {
44 | container.addTab("btn-a", strings.get(keys[3]), new AnnotationsPanel(reader));
45 | }
46 | container.addTab("btn-s", strings.get(keys[4]), new SearchPanel(reader));
47 | container.addTab("btn-c", strings.get(keys[5]), new SettingsPanel(reader));
48 | container.addTab("btn-i", strings.get(keys[6]), new MetadataPanel(reader));
49 | container.select("btn-t");
50 |
51 | document.body.appendChild(container.dom);
52 |
53 | //-- events --//
54 |
55 | reader.on("sidebaropener", (value) => {
56 |
57 | if (value) {
58 | container.setClass("open");
59 | } else {
60 | container.removeAttribute("class");
61 | }
62 | });
63 |
64 | reader.on("languagechanged", (value) => {
65 |
66 | openerBtn.setTitle(strings.get(keys[0]));
67 | container.setLabel("btn-t", strings.get(keys[1]));
68 | if (controls.bookmarks) {
69 | container.setLabel("btn-d", strings.get(keys[2]));
70 | }
71 | if (controls.annotations) {
72 | container.setLabel("btn-a", strings.get(keys[3]));
73 | }
74 | container.setLabel("btn-s", strings.get(keys[4]));
75 | container.setLabel("btn-c", strings.get(keys[5]));
76 | container.setLabel("btn-i", strings.get(keys[6]));
77 | });
78 | }
79 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # epubreader-js
2 |
3 | 
4 |
5 | ## About the Reader
6 |
7 | The **epubreader-js** application is based on the [epub.js](https://github.com/futurepress/epub.js) library and is a fork of the [epubjs-reader](https://github.com/futurepress/epubjs-reader) repository.
8 |
9 | ## Getting Started
10 |
11 | Open up [epubreader-js](https://intity.github.io/epubreader-js/) in a browser.
12 |
13 | You can change the ePub it opens by passing a link to `bookPath` in the url:
14 |
15 | `?bookPath=https://s3.amazonaws.com/epubjs/books/alice.epub`
16 |
17 | ## Running Locally
18 |
19 | Install [node.js](https://nodejs.org/en/)
20 |
21 | Then install the project dependences with npm
22 |
23 | ```javascript
24 | npm install
25 | ```
26 |
27 | You can run the reader locally with the command
28 |
29 | ```javascript
30 | npm run serve
31 | ```
32 |
33 | Builds are concatenated and minified using [webpack](https://github.com/webpack/webpack)
34 |
35 | To generate a new build run
36 |
37 | ```javascript
38 | npm run build
39 | ```
40 |
41 | or rebuilding all *.js files
42 |
43 | ```javascript
44 | npm run prepare
45 | ```
46 |
47 | ## Pre-configuration
48 |
49 | The **epubreader-js** application settings is a JavaScript object that you pass as an argument to the `Reader` constructor. You can make preliminary settings in the file [index.html](dist/index.html). For example, this is what the default `Reader` initialization looks like:
50 |
51 | ```html
52 |
58 | ```
59 |
60 | Let's say we want to disable the `openbook` feature, which is designed to open an epub file on a personal computer. This can be useful for integrating a public library into your site. Let's do this:
61 |
62 | ```html
63 |
68 | ```
69 |
70 | > Note that the `{{bookPath}}` replacement token is used to define the `url` string variable. This simple solution will allow you to set up a route to pass the target URL.
71 |
72 | ## Features
73 |
74 | The **epubreader-js** application supports the following features:
75 |
76 | - Initial support for mobile devices
77 | - Saving settings in the browser’s local storage
78 | - Opening a book file from the device’s file system
79 | - Bookmarks
80 | - Annotations
81 | - Search by sections of the book
82 | - Output epub metadata
83 | - [Keybindings](docs/keybindings.md)
84 |
--------------------------------------------------------------------------------
/assets/css/common.css:
--------------------------------------------------------------------------------
1 | #sidebar, #notedlg {
2 | display: grid;
3 | overflow: hidden;
4 | position: absolute;
5 | box-shadow: none;
6 | font-family: Roboto, 'Segoe UI', Tahoma, sans-serif;
7 | background-color: #fff;
8 | }
9 |
10 | .box {
11 | border: 0;
12 | padding: 11px 0;
13 | display: grid;
14 | min-height: 36px;
15 | }
16 |
17 | [id^="btn-"].box input {
18 | width: 36px;
19 | height: 36px;
20 | border: 0;
21 | margin: 0 11px;
22 | cursor: pointer;
23 | padding: 0;
24 | outline: none;
25 | opacity: 0.5;
26 | background-size: 24px 24px;
27 | background-color: transparent;
28 | background-repeat: no-repeat;
29 | background-position: center;
30 | }
31 |
32 | [id^="btn-"].box input:hover {
33 | opacity: 0.7;
34 | background-color: #F2F3F4;
35 | border-radius: 18px;
36 | }
37 |
38 | [id^="btn-"].box.selected input {
39 | opacity: 0.7;
40 | background-color: #F2F3F4;
41 | border-radius: 18px;
42 | }
43 |
44 | [id^="btn-"].box input[type=file]::file-selector-button,
45 | [id^="btn-"].box input[type=file]::-webkit-file-upload-button {
46 | content: none;
47 | visibility: hidden;
48 | }
49 |
50 | #btn-m.box input {
51 | background-image: url("../img/menu.svg");
52 | }
53 |
54 | #btn-p.box input {
55 | background-image: url("../img/prev.svg");
56 | }
57 |
58 | #btn-n.box input {
59 | background-image: url("../img/next.svg");
60 | }
61 |
62 | #btn-o.box input {
63 | background-image: url("../img/open-book.svg");
64 | }
65 |
66 | #btn-b.box input {
67 | background-image: url("../img/bookmark.svg");
68 | }
69 |
70 | #btn-b.box.bookmarked input {
71 | background-image: url("../img/bookmarked.svg");
72 | }
73 |
74 | #btn-d.box input {
75 | background-image: url("../img/bookmarks.svg");
76 | }
77 |
78 | #btn-f.box input {
79 | background-image: url("../img/resize-full.svg");
80 | }
81 |
82 | #btn-f.box.resize-small input {
83 | background-image: url("../img/resize-small.svg");
84 | }
85 |
86 | #btn-t.box input {
87 | background-image: url("../img/toc.svg");
88 | }
89 |
90 | #btn-a.box input {
91 | background-image: url("../img/annotations.svg");
92 | }
93 |
94 | #btn-s.box input {
95 | background-image: url("../img/search.svg");
96 | }
97 |
98 | #btn-c.box input {
99 | background-image: url("../img/settings.svg");
100 | }
101 |
102 | #btn-i.box input {
103 | background-image: url("../img/info.svg");
104 | }
105 |
106 | /**
107 | * iPhone 5 : 320 x 568
108 | */
109 | @media
110 | only screen and (width: 320px) and (orientation: portrait),
111 | only screen and (height: 320px) and (orientation: landscape) {
112 | .box {
113 | min-height: 30px;
114 | }
115 |
116 | [id^="btn-"].box input {
117 | width: 30px;
118 | height: 30px;
119 | }
120 | }
--------------------------------------------------------------------------------
/assets/img/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
35 |
--------------------------------------------------------------------------------
/assets/img/toc.svg:
--------------------------------------------------------------------------------
1 |
2 |
59 |
--------------------------------------------------------------------------------
/src/content.js:
--------------------------------------------------------------------------------
1 | import { UIDiv, UISpan } from "./ui.js";
2 |
3 | export class Content {
4 |
5 | constructor(reader) {
6 |
7 | const settings = reader.settings;
8 | const container = new UIDiv().setId("content");
9 |
10 | let prev;
11 | if (settings.arrows === "content") {
12 |
13 | prev = new UIDiv().setId("prev").setClass("arrow");
14 | prev.dom.onclick = (e) => {
15 |
16 | reader.emit("prev");
17 | e.preventDefault();
18 | };
19 | prev.add(new UISpan("<"));
20 | container.add(prev);
21 | }
22 |
23 | const viewer = new UIDiv().setId("viewer");
24 | container.add(viewer);
25 |
26 | let next;
27 | if (settings.arrows === "content") {
28 | next = new UIDiv().setId("next").setClass("arrow");
29 | next.dom.onclick = (e) => {
30 |
31 | reader.emit("next");
32 | e.preventDefault();
33 | };
34 | next.add(new UISpan(">"));
35 | container.add(next);
36 | }
37 |
38 | const loader = new UIDiv().setId("loader");
39 | const divider = new UIDiv().setId("divider");
40 | const overlay = new UIDiv().setId("overlay");
41 | overlay.dom.onclick = (e) => {
42 | reader.emit("sidebaropener", false);
43 | e.preventDefault();
44 | };
45 |
46 | container.add([loader, divider, overlay]);
47 | document.body.appendChild(container.dom);
48 |
49 | //-- events --//
50 |
51 | reader.on("bookready", (cfg) => {
52 |
53 | viewer.setClass(cfg.flow);
54 | loader.dom.style.display = "block";
55 | });
56 |
57 | reader.on("bookloaded", () => {
58 |
59 | loader.dom.style.display = "none";
60 | });
61 |
62 | reader.on("layout", (props) => {
63 |
64 | if (props.spread && props.width > props.spreadWidth) {
65 | divider.dom.style.display = "block";
66 | } else {
67 | divider.dom.style.display = "none";
68 | }
69 | });
70 |
71 | reader.on("flowchanged", (value) => {
72 |
73 | viewer.setClass(value);
74 | });
75 |
76 | reader.on("relocated", (location) => {
77 |
78 | if (settings.arrows === "content") {
79 | if (location.atStart) {
80 | prev.addClass("disabled");
81 | } else {
82 | prev.removeClass("disabled");
83 | }
84 | if (location.atEnd) {
85 | next.addClass("disabled");
86 | } else {
87 | next.removeClass("disabled");
88 | }
89 | }
90 | });
91 |
92 | reader.on("prev", () => {
93 |
94 | if (settings.arrows === "content") {
95 | prev.addClass("active");
96 | setTimeout(() => { prev.removeClass("active"); }, 100);
97 | }
98 | });
99 |
100 | reader.on("next", () => {
101 |
102 | if (settings.arrows === "content") {
103 | next.addClass("active");
104 | setTimeout(() => { next.removeClass("active"); }, 100);
105 | }
106 | });
107 |
108 | reader.on("sidebaropener", (value) => {
109 |
110 | overlay.dom.style.display = value ? "block" : "none";
111 | });
112 |
113 | reader.on("viewercleanup", () => {
114 |
115 | viewer.clear();
116 | });
117 | }
118 | }
--------------------------------------------------------------------------------
/src/sidebar/annotations.js:
--------------------------------------------------------------------------------
1 | import { UIPanel, UIDiv, UITextArea, UIInput, UILink, UIList, UIItem, UISpan, UIText, UIBox } from "../ui.js";
2 |
3 | export class AnnotationsPanel extends UIPanel {
4 |
5 | constructor(reader) {
6 |
7 | super();
8 | const container = new UIDiv().setClass("list-container");
9 | const strings = reader.strings;
10 | const keys = [
11 | "sidebar/annotations",
12 | "sidebar/annotations/clear"
13 | ];
14 | const headerLabel = new UIText(strings.get(keys[0])).setClass("label");
15 | const clearBtn = new UIInput("button", strings.get(keys[1]));
16 | clearBtn.dom.onclick = (e) => {
17 |
18 | this.clearNotes();
19 | e.preventDefault();
20 | };
21 | this.add(new UIBox([headerLabel, clearBtn]).addClass("header"));
22 | this.selector = undefined;
23 | this.notes = new UIList();
24 | container.add(this.notes);
25 | this.setId("annotations");
26 | this.add(container);
27 | this.reader = reader;
28 | this.update = () => {
29 |
30 | clearBtn.dom.disabled = reader.settings.annotations.length === 0;
31 | };
32 |
33 | //-- events --//
34 |
35 | reader.on("bookready", (cfg) => {
36 |
37 | cfg.annotations.forEach((note) => {
38 |
39 | this.set(note);
40 | });
41 | this.update();
42 | });
43 |
44 | reader.on("noteadded", (note) => {
45 |
46 | this.set(note);
47 | this.update();
48 | });
49 |
50 | reader.on("languagechanged", (value) => {
51 |
52 | headerLabel.setValue(strings.get(keys[0]));
53 | clearBtn.setValue(strings.get(keys[1]));
54 | });
55 | }
56 |
57 | set(note) {
58 |
59 | const link = new UILink("#" + note.cfi, note.text);
60 | const item = new UIItem().setId("note-" + note.uuid);
61 | const btnr = new UISpan().setClass("btn-remove");
62 | const call = () => { };
63 |
64 | link.dom.onclick = (e) => {
65 |
66 | if (this.selector && this.selector !== item) {
67 | this.selector.unselect();
68 | }
69 | item.select();
70 | this.selector = item;
71 | this.reader.rendition.display(note.cfi);
72 | e.preventDefault();
73 | };
74 |
75 | btnr.dom.onclick = (e) => {
76 |
77 | this.removeNote(note);
78 | e.preventDefault();
79 | };
80 |
81 | item.add([link, btnr]);
82 | this.notes.add(item);
83 | this.reader.rendition.annotations.add(
84 | "highlight", note.cfi, {}, call, "note-highlight", {});
85 | this.update();
86 | }
87 |
88 | removeNote(note) {
89 |
90 | const index = this.reader.settings.annotations.indexOf(note);
91 | if (index === -1)
92 | return;
93 |
94 | this.notes.remove(index);
95 | this.reader.settings.annotations.splice(index, 1);
96 | this.reader.rendition.annotations.remove(note.cfi, "highlight");
97 | this.update();
98 | }
99 |
100 | clearNotes() {
101 |
102 | this.reader.settings.annotations.forEach(note => {
103 | this.reader.rendition.annotations.remove(note.cfi, "highlight");
104 | });
105 | this.notes.clear();
106 | this.reader.settings.annotations = [];
107 | this.update();
108 | }
109 | }
--------------------------------------------------------------------------------
/assets/css/notedlg.css:
--------------------------------------------------------------------------------
1 | #notedlg {
2 | top: 0;
3 | width: 100%;
4 | height: 58px;
5 | z-index: 20;
6 | font-size: small;
7 | transform: translateY(-58px);
8 | transition: 0.25s;
9 | justify-items: center;
10 | }
11 |
12 | #notedlg.open {
13 | transform: translateY(0);
14 | box-shadow: 0 0 16px rgba(0, 0, 0, .28);
15 | }
16 |
17 | #notedlg .box {
18 | min-width: 390px;
19 | }
20 |
21 | #notedlg .box.control {
22 | display: grid;
23 | align-items: center;
24 | grid-template: 36px / 80px auto 80px;
25 | }
26 |
27 | #notedlg .box label {
28 | text-align: center;
29 | margin-left: 11px;
30 | font-weight: 500;
31 | }
32 |
33 | #notedlg .box input[type="text"] {
34 | margin: 0 11px;
35 | padding: 3px;
36 | border-width: 1px;
37 | border-style: solid;
38 | border-radius: 3px;
39 | outline-color: #1A73E8 !important;
40 | }
41 |
42 | #notedlg .box input[type="button"] {
43 | color: #fff !important;
44 | margin-right: 11px;
45 | height: 36px;
46 | border: 0;
47 | border-radius: 4px;
48 | background-color: #1A73E8 !important;
49 | font-weight: bold !important;
50 | }
51 |
52 | #notedlg .box input[type=button]:hover {
53 | box-shadow: 0 0 2px rgba(0,0,0,.28);
54 | background-color: #1B66C9 !important;
55 | }
56 |
57 | #notedlg .box input[type="button"]:disabled {
58 | cursor: auto;
59 | box-shadow: none;
60 | background-color: #BDC3C7 !important;
61 | }
62 |
63 | /**
64 | * iPhone 5 : 320 x 568
65 | */
66 | @media
67 | only screen and (width: 320px) and (orientation: portrait),
68 | only screen and (height: 320px) and (orientation: landscape) {
69 | #notedlg {
70 | height: 52px;
71 | }
72 |
73 | #notedlg .box {
74 | min-width: 320px;
75 | }
76 |
77 | #notedlg .box.control {
78 | grid-template: 30px / 80px 160px 80px;
79 | }
80 |
81 | #notedlg .box input[type="button"] {
82 | height: 30px;
83 | }
84 | }
85 |
86 | /**
87 | * iPhone 6/7/8 : 375 x 667
88 | * iPhone X : 375 x 812
89 | */
90 | @media
91 | only screen and (width: 375px) and (orientation: portrait),
92 | only screen and (height: 375px) and (orientation: landscape) {
93 | #notedlg .box {
94 | min-width: 375px;
95 | }
96 | }
97 |
98 | /**
99 | * Pixel 7 : 412 x 915
100 | */
101 | @media
102 | only screen and (width: 412px) and (orientation: portrait),
103 | only screen and (height: 412px) and (orientation: landscape) {
104 | #notedlg .box {
105 | min-width: 412px;
106 | }
107 | }
108 |
109 | /**
110 | * iPhone 6/7/8 Plus : 414 x 736
111 | */
112 | @media
113 | only screen and (width: 414px) and (orientation: portrait),
114 | only screen and (height: 414px) and (orientation: landscape) {
115 | #notedlg .box {
116 | min-width: 414px;
117 | }
118 | }
119 |
120 | /**
121 | * iPhone 14/15 Pro Max : 430 x 932
122 | */
123 | @media
124 | only screen and (width: 430px) and (orientation: portrait),
125 | only screen and (height: 430px) and (orientation: landscape) {
126 | #notedlg .box {
127 | min-width: 430px;
128 | }
129 | }
--------------------------------------------------------------------------------
/src/sidebar/bookmarks.js:
--------------------------------------------------------------------------------
1 | import { UIPanel, UIDiv, UIRow, UIInput, UILink, UIList, UIItem, UIText, UIBox, UISpan } from "../ui.js";
2 |
3 | export class BookmarksPanel extends UIPanel {
4 |
5 | constructor(reader) {
6 |
7 | super();
8 | const container = new UIDiv().setClass("list-container");
9 | const strings = reader.strings;
10 | const keys = [
11 | "sidebar/bookmarks",
12 | "sidebar/bookmarks/clear"
13 | ];
14 | const headerLabel = new UIText(strings.get(keys[0])).setClass("label");
15 | const clearBtn = new UIInput("button", strings.get(keys[1]));
16 | clearBtn.dom.onclick = (e) => {
17 |
18 | this.clearBookmarks();
19 | reader.emit("bookmarked", false);
20 | e.preventDefault();
21 | };
22 | this.add(new UIBox([headerLabel, clearBtn]).addClass("header"));
23 | this.selector = undefined;
24 | this.bookmarks = new UIList();
25 | container.add(this.bookmarks);
26 | this.setId("bookmarks");
27 | this.add(container);
28 | this.reader = reader;
29 |
30 | const update = () => {
31 |
32 | clearBtn.dom.disabled = reader.settings.bookmarks.length === 0;
33 | };
34 |
35 | //-- events --//
36 |
37 | reader.on("displayed", (renderer, cfg) => {
38 |
39 | cfg.bookmarks.forEach((cfi) => {
40 |
41 | this.setBookmark(cfi);
42 | });
43 | update();
44 | });
45 |
46 | reader.on("relocated", (location) => {
47 |
48 | this.locationCfi = location.start.cfi; // save location cfi
49 | });
50 |
51 | reader.on("bookmarked", (boolean, cfi) => {
52 |
53 | if (boolean) {
54 | this.appendBookmark();
55 | } else {
56 | this.removeBookmark(cfi);
57 | }
58 | update();
59 | });
60 |
61 | reader.on("languagechanged", (value) => {
62 |
63 | headerLabel.setValue(strings.get(keys[0]));
64 | clearBtn.setValue(strings.get(keys[1]));
65 | });
66 | }
67 |
68 | appendBookmark() {
69 |
70 | const cfi = this.locationCfi;
71 | if (this.reader.isBookmarked(cfi) > -1) {
72 | return;
73 | }
74 | this.setBookmark(cfi);
75 | this.reader.settings.bookmarks.push(cfi);
76 | }
77 |
78 | removeBookmark(cfi) {
79 |
80 | const _cfi = cfi || this.locationCfi;
81 | const index = this.reader.isBookmarked(_cfi);
82 | if (index === -1) {
83 | return;
84 | }
85 | this.bookmarks.remove(index);
86 | this.reader.settings.bookmarks.splice(index, 1);
87 | }
88 |
89 | clearBookmarks() {
90 |
91 | this.bookmarks.clear();
92 | this.reader.settings.bookmarks = [];
93 | }
94 |
95 | setBookmark(cfi) {
96 |
97 | const link = new UILink();
98 | const item = new UIItem();
99 | const btnr = new UISpan().setClass("btn-remove");
100 | const navItem = this.reader.navItemFromCfi(cfi);
101 | let idref;
102 | let label;
103 |
104 | if (navItem === undefined) {
105 | const spineItem = this.reader.book.spine.get(cfi);
106 | idref = spineItem.idref;
107 | label = spineItem.idref
108 | } else {
109 | idref = navItem.id;
110 | label = navItem.label;
111 | }
112 |
113 | link.setHref("#" + cfi);
114 | link.dom.onclick = (e) => {
115 |
116 | if (this.selector && this.selector !== item) {
117 | this.selector.unselect();
118 | }
119 | item.select();
120 | this.selector = item;
121 | this.reader.rendition.display(cfi);
122 | e.preventDefault();
123 | };
124 | link.setTextContent(label);
125 |
126 | btnr.dom.onclick = (e) => {
127 |
128 | this.reader.emit("bookmarked", false, cfi);
129 | e.preventDefault();
130 | };
131 |
132 | item.add([link, btnr]);
133 | item.setId(idref);
134 | this.bookmarks.add(item);
135 | }
136 | }
--------------------------------------------------------------------------------
/assets/css/main.css:
--------------------------------------------------------------------------------
1 | @import url("common.css");
2 | @import url("toolbar.css");
3 | @import url("sidebar.css");
4 | @import url("notedlg.css");
5 |
6 | @font-face {
7 | font-family: 'fontello';
8 | src: url('../font/fontello.eot?60518104');
9 | src: url('../font/fontello.eot?60518104#iefix') format('embedded-opentype'),
10 | url('../font/fontello.woff?60518104') format('woff'),
11 | url('../font/fontello.ttf?60518104') format('truetype'),
12 | url('../font/fontello.svg?60518104#fontello') format('svg');
13 | font-weight: normal;
14 | font-style: normal;
15 | }
16 |
17 | body {
18 | margin: 0;
19 | padding: 0;
20 | overflow: hidden;
21 | }
22 |
23 | #content {
24 | top: 0;
25 | right: 0;
26 | width: 100%;
27 | height: 100%;
28 | display: flex;
29 | position: absolute;
30 | overflow: hidden;
31 | background: #fff;
32 | align-items: center;
33 | }
34 |
35 | #viewer {
36 | color: inherit;
37 | margin: 0 auto;
38 | display: flex;
39 | z-index: 2;
40 | overflow: hidden;
41 | align-items: center;
42 | vertical-align: middle;
43 | }
44 |
45 | #viewer.paginated {
46 | width: calc(100% - 116px);
47 | height: calc(100% - 116px);
48 | }
49 |
50 | #viewer.scrolled {
51 | top: 58px;
52 | width: calc(100% - 116px);
53 | height: calc(100% - 58px);
54 | position: sticky;
55 | }
56 |
57 | #viewer .epub-view {
58 | width: auto !important;
59 | }
60 |
61 | #viewer iframe {
62 | border: none;
63 | }
64 |
65 | #prev {
66 | text-align: start;
67 | }
68 |
69 | #next {
70 | text-align: end;
71 | }
72 |
73 | .arrow {
74 | flex: 1;
75 | padding: 0 10px;
76 | }
77 |
78 | .arrow:disabled,
79 | .arrow.disabled {
80 | visibility: hidden;
81 | }
82 |
83 | .arrow span {
84 | color: #E2E2E2;
85 | cursor: pointer;
86 | overflow: hidden;
87 | font-size: 64px;
88 | font-family: arial, sans-serif;
89 | font-weight: bold;
90 | vertical-align: middle;
91 |
92 | user-select: none;
93 | -webkit-user-select: none;
94 | -moz-user-select: none;
95 | -ms-user-select: none;
96 | }
97 |
98 | .arrow span:hover {
99 | color: #777;
100 | }
101 |
102 | .arrow:active > span,
103 | .arrow.active > span {
104 | color: #000;
105 | }
106 |
107 | #divider {
108 | left: 50%;
109 | height: calc(100% - 116px);
110 | margin: 0;
111 | padding: 0;
112 | display: none;
113 | position: absolute;
114 | border-left: 1px dotted #777;
115 | }
116 |
117 | #overlay {
118 | top: 0;
119 | left: 0;
120 | width: 100%;
121 | height: 100%;
122 | z-index: 10;
123 | display: none;
124 | position: fixed;
125 | background: rgba(0, 0, 0, 0.4);
126 | transition: all 0.25s;
127 | }
128 |
129 | #loader {
130 | top: calc(50% - 32px);
131 | left: calc(50% - 32px);
132 | width: 64px;
133 | height: 64px;
134 | position: absolute;
135 | background-size: 64px;
136 | background-image: url("../img/loader.gif");
137 | background-repeat: no-repeat;
138 | background-position: center;
139 | }
140 |
141 | /**
142 | * iPhone 5 : 320 x 568
143 | */
144 | @media
145 | only screen and (max-width: 320px) and (orientation: portrait),
146 | only screen and (max-width: 568px) and (orientation: landscape) {
147 | #viewer.scrolled {
148 | height: calc(100% - 52px);
149 | }
150 |
151 | .arrow span {
152 | font-size: 32px;
153 | }
154 | }
155 |
156 | /**
157 | * iPhone 5 : 320 x 568
158 | * iPhone 6/7/8 : 375 x 667
159 | * iPhone 11 Pro : 375 x 812
160 | * Pixel 7 : 412 x 915
161 | * iPhone 6/7/8 Plus : 414 x 736
162 | * iPhone 11 : 414 x 896
163 | * iPhone 12/14 : 390 x 844
164 | * iPhone 14/15 Pro Max : 430 x 932
165 | */
166 | @media
167 | only screen and (min-width: 320px) and (max-width: 430px) and (orientation: portrait),
168 | only screen and (min-width: 568px) and (max-width: 932px) and (orientation: landscape) {
169 | #viewer.paginated {
170 | width: calc(100% - 32px);
171 | }
172 |
173 | #viewer.scrolled {
174 | width: 100%;
175 | }
176 |
177 | .arrow span {
178 | font-size: 40px;
179 | }
180 | }
181 |
182 | /**
183 | * iPad Mini : 768 x 1024
184 | * iPad 10.2 : 810 x 1080
185 | * iPad Air (2020) : 820 x 1180
186 | * iPad Air : 834 x 1112
187 | * iPad Pro 11 : 834 x 1194
188 | * iPad Pro 12.9 : 1024 x 1366
189 | */
190 | @media
191 | only screen and (min-width: 768px) and (max-width: 1024px) and (orientation: portrait),
192 | only screen and (min-width: 1024px) and (max-width: 1366px) and (orientation: landscape) {
193 | #viewer.paginated {
194 | width: calc(100% - 116px);
195 | }
196 |
197 | #viewer.scrolled {
198 | width: 100%;
199 | }
200 |
201 | .arrow span {
202 | font-size: 50px;
203 | }
204 | }
--------------------------------------------------------------------------------
/assets/img/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/src/sidebar/settings.js:
--------------------------------------------------------------------------------
1 | import { UIPanel, UIRow, UISelect, UIInput, UILabel, UINumber, UIText, UIBox, UIDiv } from "../ui.js";
2 |
3 | export class SettingsPanel extends UIPanel {
4 |
5 | constructor(reader) {
6 |
7 | super();
8 | super.setId("settings");
9 |
10 | const strings = reader.strings;
11 | const keys = [
12 | "sidebar/settings",
13 | "sidebar/settings/language",
14 | "sidebar/settings/fontsize",
15 | "sidebar/settings/flow",
16 | "sidebar/settings/spread",
17 | "sidebar/settings/spread/minwidth"
18 | ];
19 | const headerLabel = new UIText(strings.get(keys[0])).setClass("label");
20 | this.add(new UIBox(headerLabel).addClass("header"));
21 |
22 | const languageLabel = new UILabel(strings.get(keys[1]), "language-ui");
23 | const languageRow = new UIRow();
24 | const language = new UISelect().setOptions({
25 | en: "English",
26 | fr: "French",
27 | ja: "Japanese",
28 | ru: "Russian",
29 | zh: "Chinese"
30 | });
31 | language.dom.onchange = (e) => {
32 |
33 | reader.emit("languagechanged", e.target.value);
34 | };
35 | language.setId("language-ui");
36 | languageRow.add(languageLabel);
37 | languageRow.add(language);
38 |
39 | const fontSizeLabel = new UILabel(strings.get(keys[2]), "fontsize");
40 | const fontSizeRow = new UIRow();
41 | const fontSize = new UINumber(100, 1);
42 | fontSize.dom.onchange = (e) => {
43 |
44 | reader.emit("styleschanged", {
45 | fontSize: parseInt(e.target.value)
46 | });
47 | };
48 | fontSize.setId("fontsize")
49 | fontSizeRow.add(fontSizeLabel);
50 | fontSizeRow.add(fontSize);
51 |
52 | //-- flow configure --//
53 |
54 | const flowLabel = new UILabel(strings.get(keys[3]), "flow");
55 | const flowRow = new UIRow();
56 | const flow = new UISelect().setOptions({
57 | paginated: "Paginated",
58 | scrolled: "Scrolled"
59 | });
60 | flow.dom.onchange = (e) => {
61 |
62 | reader.emit("flowchanged", e.target.value);
63 |
64 | if (e.target.value === "scrolled") {
65 | reader.emit("spreadchanged", {
66 | mod: "none",
67 | min: undefined
68 | });
69 | } else {
70 | reader.emit("spreadchanged", {
71 | mod: undefined,
72 | min: undefined
73 | });
74 | }
75 | };
76 | flow.setId("flow");
77 | flowRow.add(flowLabel);
78 | flowRow.add(flow);
79 |
80 | //-- spdead configure --//
81 |
82 | const minSpreadWidth = new UINumber(800, 1);
83 | const spreadLabel = new UILabel(strings.get(keys[4]), "spread");
84 | const spreadRow = new UIRow();
85 | const spread = new UISelect().setOptions({
86 | none: "None",
87 | auto: "Auto"
88 | });
89 | spread.dom.onchange = (e) => {
90 |
91 | reader.emit("spreadchanged", {
92 | mod: e.target.value,
93 | min: undefined
94 | });
95 | minSpreadWidth.dom.disabled = e.target.value === "none";
96 | };
97 | spread.setId("spread");
98 |
99 | spreadRow.add(spreadLabel);
100 | spreadRow.add(spread);
101 |
102 | const minSpreadWidthLabel = new UILabel(strings.get(keys[5]), "min-spread-width");
103 | const minSpreadWidthRow = new UIRow();
104 | minSpreadWidth.dom.onchange = (e) => {
105 |
106 | reader.emit("spreadchanged", {
107 | mod: undefined,
108 | min: parseInt(e.target.value)
109 | });
110 | };
111 | minSpreadWidth.setId("min-spread-width");
112 | minSpreadWidthRow.add(minSpreadWidthLabel);
113 | minSpreadWidthRow.add(minSpreadWidth);
114 |
115 | //-- pagination --//
116 |
117 | const paginationStr = strings.get("sidebar/settings/pagination");
118 | const paginationRow = new UIRow();
119 | const pagination = new UIInput("checkbox", false, paginationStr[1]);
120 | pagination.setId("pagination");
121 | pagination.dom.onclick = (e) => {
122 |
123 | // not implemented
124 | };
125 |
126 | paginationRow.add(new UILabel(paginationStr[0], "pagination"));
127 | paginationRow.add(pagination);
128 |
129 | this.add(new UIBox([
130 | languageRow,
131 | fontSizeRow,
132 | flowRow,
133 | spreadRow,
134 | minSpreadWidthRow,
135 | //paginationRow
136 | ]));
137 |
138 | //-- events --//
139 |
140 | reader.on("bookready", (cfg) => {
141 |
142 | language.setValue(cfg.language);
143 | fontSize.setValue(cfg.styles.fontSize);
144 | flow.setValue(cfg.flow);
145 | spread.setValue(cfg.spread.mod);
146 | minSpreadWidth.setValue(cfg.spread.min);
147 | minSpreadWidth.dom.disabled = cfg.spread.mod === "none";
148 | });
149 |
150 | reader.on("layout", (props) => {
151 |
152 | if (props.flow === "scrolled") {
153 | spread.setValue("none");
154 | spread.dom.disabled = true;
155 | minSpreadWidth.dom.disabled = true;
156 | } else {
157 | spread.dom.disabled = false;
158 | }
159 | });
160 |
161 | reader.on("languagechanged", (value) => {
162 |
163 | headerLabel.setTextContent(strings.get(keys[0]));
164 | languageLabel.setTextContent(strings.get(keys[1]));
165 | fontSizeLabel.setTextContent(strings.get(keys[2]));
166 | flowLabel.setTextContent(strings.get(keys[3]));
167 | spreadLabel.setTextContent(strings.get(keys[4]));
168 | minSpreadWidthLabel.setTextContent(strings.get(keys[5]));
169 | });
170 | }
171 | }
--------------------------------------------------------------------------------
/src/toolbar.js:
--------------------------------------------------------------------------------
1 | import { UIDiv, UIInput } from "./ui.js";
2 |
3 | export class Toolbar {
4 |
5 | constructor(reader) {
6 |
7 | const strings = reader.strings;
8 | const settings = reader.settings;
9 |
10 | const container = new UIDiv().setId("toolbar");
11 | const keys = [
12 | "toolbar/sidebar",
13 | "toolbar/prev",
14 | "toolbar/next",
15 | "toolbar/openbook",
16 | "toolbar/openbook/error",
17 | "toolbar/bookmark",
18 | "toolbar/fullscreen"
19 | ];
20 | const menu1 = new UIDiv().setClass("menu-1");
21 | const openerBox = new UIDiv().setId("btn-m").setClass("box");
22 | const openerBtn = new UIInput("button");
23 | openerBtn.dom.title = strings.get(keys[0]);
24 | openerBtn.dom.onclick = (e) => {
25 |
26 | reader.emit("sidebaropener", true);
27 | openerBtn.dom.blur();
28 | e.preventDefault();
29 | };
30 | openerBox.add(openerBtn);
31 | menu1.add(openerBox);
32 |
33 | let prevBox, prevBtn;
34 | let nextBox, nextBtn;
35 | if (settings.arrows === "toolbar") {
36 | prevBox = new UIDiv().setId("btn-p").setClass("box");
37 | prevBtn = new UIInput("button");
38 | prevBtn.setTitle(strings.get(keys[1]));
39 | prevBtn.dom.onclick = (e) => {
40 |
41 | reader.emit("prev");
42 | e.preventDefault();
43 | prevBtn.dom.blur();
44 | };
45 | prevBox.add(prevBtn);
46 | menu1.add(prevBox);
47 |
48 | nextBox = new UIDiv().setId("btn-n").setClass("box");
49 | nextBtn = new UIInput("button");
50 | nextBtn.dom.title = strings.get(keys[2]);
51 | nextBtn.dom.onclick = (e) => {
52 |
53 | reader.emit("next");
54 | e.preventDefault();
55 | nextBtn.dom.blur();
56 | };
57 | nextBox.add(nextBtn);
58 | menu1.add(nextBox);
59 | }
60 |
61 | const menu2 = new UIDiv().setClass("menu-2");
62 | let openbookBtn;
63 | if (settings.openbook) {
64 | const onload = (e) => {
65 |
66 | reader.storage.clear();
67 | reader.storage.set(e.target.result, () => {
68 | reader.unload();
69 | reader.init(e.target.result);
70 | const url = new URL(window.location.origin);
71 | window.history.pushState({}, "", url);
72 | });
73 | };
74 | const onerror = (e) => {
75 | console.error(e);
76 | };
77 | const openbookBox = new UIDiv().setId("btn-o").setClass("box");
78 | openbookBtn = new UIInput("file");
79 | openbookBtn.dom.title = strings.get(keys[3]);
80 | openbookBtn.dom.accept = "application/epub+zip";
81 | openbookBtn.dom.onchange = (e) => {
82 |
83 | if (e.target.files.length === 0)
84 | return;
85 |
86 | if (window.FileReader) {
87 |
88 | const fr = new FileReader();
89 | fr.onload = onload;
90 | fr.readAsArrayBuffer(e.target.files[0]);
91 | fr.onerror = onerror;
92 | } else {
93 | alert(strings.get(keys[4]));
94 | }
95 |
96 | };
97 | openbookBtn.dom.onclick = (e) => {
98 |
99 | openbookBtn.dom.blur();
100 | };
101 | openbookBox.add(openbookBtn);
102 | menu2.add(openbookBox);
103 | }
104 |
105 | let bookmarkBox, bookmarkBtn;
106 | if (settings.bookmarks) {
107 | bookmarkBox = new UIDiv().setId("btn-b").setClass("box");
108 | bookmarkBtn = new UIInput("button");
109 | bookmarkBtn.setTitle(strings.get(keys[5]));
110 | bookmarkBtn.dom.onclick = (e) => {
111 |
112 | const cfi = this.locationCfi;
113 | const val = reader.isBookmarked(cfi) === -1;
114 | reader.emit("bookmarked", val);
115 | e.preventDefault();
116 | bookmarkBtn.dom.blur();
117 | };
118 | bookmarkBox.add(bookmarkBtn);
119 | menu2.add(bookmarkBox);
120 | }
121 |
122 | let fullscreenBtn;
123 | if (settings.fullscreen) {
124 |
125 | const fullscreenBox = new UIDiv().setId("btn-f").setClass("box");
126 | fullscreenBtn = new UIInput("button");
127 | fullscreenBtn.setTitle(strings.get(keys[6]));
128 | fullscreenBtn.dom.onclick = (e) => {
129 |
130 | this.toggleFullScreen();
131 | e.preventDefault();
132 | };
133 |
134 | document.onkeydown = (e) => {
135 |
136 | if (e.key === "F11") {
137 | e.preventDefault();
138 | this.toggleFullScreen();
139 | }
140 | };
141 |
142 | document.onfullscreenchange = (e) => {
143 |
144 | const w = window.screen.width === e.target.clientWidth;
145 | const h = window.screen.height === e.target.clientHeight;
146 |
147 | if (w && h) {
148 | fullscreenBox.addClass("resize-small");
149 | } else {
150 | fullscreenBox.removeClass("resize-small");
151 | }
152 | };
153 | fullscreenBox.add(fullscreenBtn);
154 | menu2.add(fullscreenBox);
155 | }
156 |
157 | container.add([menu1, menu2]);
158 | document.body.appendChild(container.dom);
159 |
160 | //-- events --//
161 |
162 | reader.on("relocated", (location) => {
163 |
164 | if (settings.bookmarks) {
165 | const cfi = location.start.cfi;
166 | const val = reader.isBookmarked(cfi) === -1;
167 | if (val) {
168 | bookmarkBox.removeClass("bookmarked");
169 | } else {
170 | bookmarkBox.addClass("bookmarked");
171 | }
172 | this.locationCfi = cfi; // save location cfi
173 | }
174 | if (settings.arrows === "toolbar") {
175 | prevBox.dom.style.display = location.atStart ? "none" : "block";
176 | nextBox.dom.style.display = location.atEnd ? "none" : "block";
177 | }
178 | });
179 |
180 | reader.on("bookmarked", (boolean) => {
181 |
182 | if (boolean) {
183 | bookmarkBox.addClass("bookmarked");
184 | } else {
185 | bookmarkBox.removeClass("bookmarked");
186 | }
187 | });
188 |
189 | reader.on("languagechanged", (value) => {
190 |
191 | openerBtn.setTitle(strings.get(keys[0]));
192 |
193 | if (settings.arrows === "toolbar") {
194 | prevBtn.setTitle(strings.get(keys[1]));
195 | nextBtn.setTitle(strings.get(keys[2]));
196 | }
197 | if (settings.openbook) {
198 | openbookBtn.setTitle(strings.get(keys[3]));
199 | }
200 | if (settings.bookmarks) {
201 | bookmarkBtn.setTitle(strings.get(keys[5]));
202 | }
203 | if (settings.fullscreen) {
204 | fullscreenBtn.setTitle(strings.get(keys[6]));
205 | }
206 | });
207 | }
208 |
209 | toggleFullScreen() {
210 |
211 | document.activeElement.blur();
212 |
213 | if (document.fullscreenElement === null) {
214 | document.documentElement.requestFullscreen();
215 | } else if (document.exitFullscreen) {
216 | document.exitFullscreen();
217 | }
218 | }
219 | }
--------------------------------------------------------------------------------
/assets/font/fontello.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/assets/css/sidebar.css:
--------------------------------------------------------------------------------
1 | #sidebar {
2 | top: 0;
3 | width: 390px;
4 | height: 100vh;
5 | z-index: 12;
6 | transform: translateX(-390px);
7 | transition: 0.25s;
8 | grid-template-rows: 58px;
9 | }
10 |
11 | #sidebar.open {
12 | transform: translateX(0);
13 | box-shadow: 0 0 16px rgba(0,0,0,.28);
14 | }
15 |
16 | #sidebar .menu {
17 | grid-row: 1;
18 | grid-column: 1 / 3;
19 | height: 58px;
20 | border: 0;
21 | border-bottom: 1px solid #BDC3C7;
22 | position: sticky;
23 | }
24 |
25 | #sidebar .list-container {
26 | height: calc(100% - 73px);
27 | padding: 15px 0 0 0;
28 | overflow: auto;
29 | }
30 |
31 | /* sidebar tabs */
32 |
33 | #sidebar .tabs {
34 | grid-column: 1;
35 | grid-row: 2;
36 | width: 58px;
37 | }
38 |
39 | #sidebar .tab-selector {
40 | width: 58px;
41 | height: 36px;
42 | margin: 11px 0;
43 | position: absolute;
44 | transition: transform 0.5s;
45 | border-left: 2px solid #1A73E8 !important;
46 | }
47 |
48 | /* sidebar panels */
49 |
50 | #sidebar .panels {
51 | width: 332px;
52 | margin: 0;
53 | padding: 0;
54 | grid-column: 2;
55 | grid-row: 1 / 3;
56 | overflow: hidden;
57 | }
58 |
59 | #sidebar .panel {
60 | height: 100vh;
61 | margin: 0;
62 | padding: 0;
63 | position: sticky;
64 | font-size: small;
65 | }
66 |
67 | #sidebar .panel > .box {
68 | width: 100%;
69 | align-content: center;
70 | }
71 |
72 | #sidebar .panel .box.header span.label {
73 | color: #5F6368;
74 | font-size: 18px;
75 | font-style: normal;
76 | margin: 0 11px;
77 | }
78 |
79 | #sidebar .panel .box.header {
80 | align-items: center;
81 | grid-template: 36px / auto 116px;
82 | }
83 |
84 | #sidebar .panel input[class^="btn-"] {
85 | height: 21px;
86 | }
87 |
88 | #sidebar .panel input[type=button] {
89 | color: #fff !important;
90 | cursor: pointer;
91 | margin: 0 11px;
92 | padding: 0;
93 | height: 36px;
94 | border: 0;
95 | border-radius: 4px;
96 | background-color: #1A73E8 !important;
97 | font-weight: bold !important;
98 | }
99 |
100 | #sidebar .panel input[type=button]:hover {
101 | box-shadow: 0 0 2px rgba(0,0,0,.28);
102 | background-color: #1B66C9 !important;
103 | }
104 |
105 | #sidebar .panel input[type=button]:disabled {
106 | cursor: auto;
107 | box-shadow: none;
108 | background-color: #BDC3C7 !important;
109 | }
110 |
111 | #sidebar .panel ul {
112 | margin: 0;
113 | padding-right: 11px;
114 | }
115 |
116 | #sidebar .panel li {
117 | width: calc(100% - 2px);
118 | margin-left: 2px;
119 | list-style: none;
120 | font-weight: 500;
121 | }
122 |
123 | #sidebar .panel li:hover {
124 | background-color: #F2F3F4 !important;
125 | }
126 |
127 | #sidebar .panel li.selected {
128 | margin-left: 0px;
129 | border-left: 2px solid #1A73E8 !important;
130 | background-color: #F2F3F4 !important;
131 | }
132 |
133 | /* #sidebar .panel li.selected a {
134 | color: #797D7F !important;
135 | }
136 |
137 | #sidebar .panel li a {
138 | color: #515A5A;
139 | display: inline-block;
140 | font-weight: bold;
141 | text-decoration: none;
142 | }
143 |
144 | #sidebar .panel li a:hover {
145 | color: #797D7F !important;
146 | } */
147 |
148 | #sidebar .panel li #item-box {
149 | display: grid;
150 | padding: 6px 0;
151 | align-items: baseline;
152 | }
153 |
154 | #sidebar .panel li #item-box span {
155 | margin: 0;
156 | padding: 0;
157 | cursor: pointer;
158 | text-align: center;
159 | }
160 |
161 | #sidebar .panel li #item-box span.btn-remove {
162 | background-color: #BDC3C7 !important;
163 | }
164 |
165 | #sidebar .panel li #item-box span.btn-remove::before {
166 | content: 'X';
167 | }
168 |
169 | #sidebar .panel li #item-box span[class^="toggle-"]::before {
170 | color: #000 !important;
171 | opacity: 0.8;
172 |
173 | user-select: none;
174 | -webkit-user-select: none;
175 | -moz-user-select: none;
176 | -ms-user-select: none;
177 | }
178 |
179 | #sidebar .panel li > ul {
180 | width: calc(100% - 24px);
181 | padding: 0 0 0 24px;
182 | display: none;
183 | }
184 |
185 | #sidebar .row {
186 | margin: 0;
187 | padding: 6px 11px 0px 11px;
188 | display: flex;
189 | min-height: 23px;
190 | align-items: baseline;
191 | vertical-align: middle;
192 | }
193 |
194 | #sidebar .row label {
195 | flex: 1;
196 | padding: 1px;
197 | font-weight: 500;
198 | white-space: nowrap;
199 | text-overflow: ellipsis;
200 | overflow: hidden;
201 | }
202 |
203 | #sidebar .row select {
204 | flex: 1;
205 | width: 100%;
206 | padding: 2px;
207 | border-width: 1px;
208 | border-style: solid;
209 | border-radius: 3px;
210 | outline-color: #1A73E8 !important;
211 | }
212 |
213 | #sidebar .row input[type="checkbox"] {
214 | width: 16px;
215 | height: 16px;
216 | margin: 0;
217 | cursor: pointer;
218 | border-width: 1px;
219 | border-style: solid;
220 | border-radius: 2px;
221 |
222 | appearance: none;
223 | -webkit-appearance: none;
224 | -moz-appearance: none;
225 | }
226 |
227 | #sidebar .row input[type="checkbox"]:checked {
228 | appearance: auto;
229 | -webkit-appearance: auto;
230 | -moz-appearance: auto;
231 | }
232 |
233 | #sidebar .row input[type="number"] {
234 | flex: 0.9867;
235 | width: 100%;
236 | padding: 3px;
237 | border-width: 1px;
238 | border-style: solid;
239 | border-radius: 3px;
240 | outline-color: #1A73E8 !important;
241 | }
242 |
243 | /* toc panel */
244 |
245 | #contents.panel ul {
246 | padding-left: 0;
247 | }
248 |
249 | #contents.panel li {
250 | padding: 0;
251 | }
252 |
253 | #contents.panel li #item-box {
254 | grid-template: auto / 24px auto;
255 | }
256 |
257 | #contents.panel li a {
258 | width: calc(100% - 24px);
259 | }
260 |
261 | #contents.panel li #item-box span.toggle-collapsed::before {
262 | content: '\002B';
263 | }
264 |
265 | #contents.panel #item-box span.toggle-expanded::before {
266 | content: '\2212';
267 | }
268 |
269 | /* bookmarks panel */
270 |
271 | #bookmarks.panel ul {
272 | padding-left: 0px;
273 | }
274 |
275 | #bookmarks.panel li #item-box {
276 | grid-template: auto / auto 22px;
277 | padding: 6px;
278 | }
279 |
280 | /* search panel */
281 |
282 | #search.panel ul {
283 | padding-left: 0px;
284 | }
285 |
286 | #search.panel li #item-box {
287 | padding: 6px;
288 | }
289 |
290 | #search.panel input[type="search"] {
291 | width: calc(100% - 22px);
292 | height: 36px;
293 | border-color: #BDC3C7;
294 | border-width: 1px;
295 | border-style: solid;
296 | border-radius: 18px;
297 | margin: 0 11px;
298 | padding: 8px 12px;
299 | outline-color: #1A73E8;
300 | }
301 |
302 | /* annotations panel */
303 |
304 | #annotations.panel ul {
305 | padding-left: 0px;
306 | }
307 |
308 | #annotations.panel li #item-box {
309 | grid-template: auto / auto 22px;
310 | padding: 6px;
311 | }
312 |
313 | #annotations.panel li > a {
314 | width: calc(100% - 38px);
315 | margin: 0 6px;
316 | }
317 |
318 | /* metadata panel */
319 |
320 | #metadata.panel ul {
321 | padding-left: 0px;
322 | }
323 |
324 | #metadata.panel li #item-box {
325 | padding: 6px;
326 | justify-items: left;
327 | }
328 |
329 | #metadata.panel li #item-box span {
330 | cursor: auto;
331 | }
332 |
333 | #metadata.panel li #item-box span.label {
334 | font-weight: bold;
335 | }
336 |
337 | #metadata.panel li #item-box span.value {
338 | text-align: left;
339 | }
340 |
341 | /**
342 | * iPhone 5 : 320 x 568
343 | * iPhone 6/7/8 : 375 x 667
344 | * iPhone 11 : 375 x 812
345 | */
346 | @media
347 | only screen and (height: 320px) and (orientation: landscape),
348 | only screen and (height: 375px) and (orientation: landscape) {
349 | #sidebar .tabs {
350 | overflow: auto;
351 | }
352 | }
353 |
354 | /**
355 | * iPhone 5 : 320 x 568
356 | */
357 | @media
358 | only screen and (width: 320px) and (orientation: portrait),
359 | only screen and (height: 320px) and (orientation: landscape) {
360 | #sidebar {
361 | width: 320px;
362 | transform: translateX(-320px);
363 | grid-template-rows: 52px;
364 | }
365 |
366 | #sidebar .menu {
367 | height: 52px;
368 | }
369 |
370 | #sidebar .tabs {
371 | width: 52px;
372 | }
373 |
374 | #sidebar .panels {
375 | width: 268px;
376 | }
377 |
378 | #sidebar .panel .box.header {
379 | grid-template: 30px / auto 116px;
380 | }
381 |
382 | #sidebar .panel .box.header span.label {
383 | font-size: 16px;
384 | }
385 |
386 | #sidebar .panel .box.header input[type="button"] {
387 | height: 30px;
388 | }
389 |
390 | #sidebar .panel .box input[type="search"] {
391 | height: 30px;
392 | padding: 6px 10px;
393 | }
394 |
395 | #sidebar .tab-selector {
396 | display: none;
397 | }
398 |
399 | #sidebar .list-container {
400 | height: calc(100% - 67px);
401 | }
402 | }
403 |
404 | /**
405 | * iPhone 6/7/8 : 375 x 667
406 | * iPhone 11 : 375 x 812
407 | */
408 | @media
409 | only screen and (width: 375px) and (orientation: portrait),
410 | only screen and (height: 375px) and (orientation: landscape) {
411 | #sidebar {
412 | width: 375px;
413 | transform: translateX(-375px);
414 | }
415 |
416 | #sidebar .panels {
417 | width: 317px;
418 | }
419 |
420 | #sidebar .tab-selector {
421 | display: none;
422 | }
423 | }
424 |
425 | /**
426 | * Pixel 7 : 412 x 915
427 | */
428 | @media
429 | only screen and (width: 412px) and (orientation: portrait),
430 | only screen and (height: 412px) and (orientation: landscape) {
431 | #sidebar {
432 | width: 412px;
433 | transform: translateX(-412px);
434 | }
435 |
436 | #sidebar .panels {
437 | width: 354px;
438 | }
439 | }
440 |
441 | /**
442 | * iPhone 6/7/8 Plus : 414 x 736
443 | */
444 | @media
445 | only screen and (width: 414px) and (orientation: portrait),
446 | only screen and (height: 414px) and (orientation: landscape) {
447 | #sidebar {
448 | width: 414px;
449 | transform: translateX(-414px);
450 | }
451 |
452 | #sidebar .panels {
453 | width: 356px;
454 | }
455 | }
456 |
457 | /**
458 | * iPhone 14/15 Pro Max : 430 x 932
459 | */
460 | @media
461 | only screen and (width: 430px) and (orientation: portrait),
462 | only screen and (height: 430px) and (orientation: landscape) {
463 | #sidebar {
464 | width: 430px;
465 | transform: translateX(-430px);
466 | }
467 |
468 | #sidebar .panels {
469 | width: 372px;
470 | }
471 | }
--------------------------------------------------------------------------------
/src/reader.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from "event-emitter";
2 |
3 | import { extend, detectMobile } from "./utils.js";
4 | import { Storage } from "./storage.js";
5 | import { Strings } from "./strings.js";
6 | import { Toolbar } from "./toolbar.js";
7 | import { Content } from "./content.js";
8 | import { Sidebar } from "./sidebar.js";
9 | import { NoteDlg } from "./notedlg.js";
10 |
11 | export class Reader {
12 |
13 | constructor(bookPath, settings) {
14 |
15 | const preinit = (data) => {
16 | const url = new URL(window.location);
17 | let path = bookPath;
18 | if (settings && !settings.openbook) {
19 | path = bookPath;
20 | if (data) this.storage.clear();
21 | } else if (data && url.search.length === 0) {
22 | path = data;
23 | }
24 | this.cfgInit(path, settings);
25 | this.strings = new Strings(this);
26 | this.toolbar = new Toolbar(this);
27 | this.content = new Content(this);
28 | this.sidebar = new Sidebar(this);
29 | if (this.settings.annotations) {
30 | this.notedlg = new NoteDlg(this);
31 | }
32 | this.init();
33 | }
34 |
35 | this.settings = undefined;
36 | this.isMobile = detectMobile();
37 | this.storage = new Storage();
38 | const openbook = settings && settings.openbook;
39 |
40 | if (this.storage.indexedDB && (!settings || openbook)) {
41 | this.storage.init(() => this.storage.get((data) => preinit(data)));
42 | } else {
43 | preinit();
44 | }
45 |
46 | window.onbeforeunload = this.unload.bind(this);
47 | window.onhashchange = this.hashChanged.bind(this);
48 | window.onkeydown = this.keyboardHandler.bind(this);
49 | window.onwheel = (e) => {
50 | if (e.ctrlKey) {
51 | e.preventDefault();
52 | }
53 | };
54 | }
55 |
56 | /**
57 | * Initialize book.
58 | * @param {*} bookPath
59 | * @param {*} settings
60 | */
61 | init(bookPath, settings) {
62 |
63 | this.emit("viewercleanup");
64 | this.navItems = {};
65 |
66 | if (arguments.length > 0) {
67 |
68 | this.cfgInit(bookPath, settings);
69 | }
70 |
71 | this.book = ePub(this.settings.bookPath);
72 | this.rendition = this.book.renderTo("viewer", {
73 | manager: this.settings.manager,
74 | flow: this.settings.flow,
75 | spread: this.settings.spread.mod,
76 | minSpreadWidth: this.settings.spread.min,
77 | width: "100%",
78 | height: "100%",
79 | snap: true
80 | });
81 |
82 | const cfi = this.settings.previousLocationCfi;
83 | if (cfi) {
84 | this.displayed = this.rendition.display(cfi);
85 | } else {
86 | this.displayed = this.rendition.display();
87 | }
88 |
89 | this.displayed.then((renderer) => {
90 | this.emit("displayed", renderer, this.settings);
91 | });
92 |
93 | this.book.ready.then(() => {
94 | this.emit("bookready", this.settings);
95 | }).then(() => {
96 | this.emit("bookloaded");
97 | });
98 |
99 | this.book.loaded.metadata.then((meta) => {
100 | this.emit("metadata", meta);
101 | });
102 |
103 | this.book.loaded.navigation.then((toc) => {
104 | this.emit("navigation", toc);
105 | });
106 |
107 | this.rendition.on("click", (e) => {
108 | const selection = e.view.document.getSelection();
109 | if (selection.type !== "Range") {
110 | this.emit("unselected");
111 | }
112 | });
113 |
114 | this.rendition.on("layout", (props) => {
115 | this.emit("layout", props);
116 | });
117 |
118 | this.rendition.on("selected", (cfiRange, contents) => {
119 | this.setLocation(cfiRange);
120 | this.emit("selected", cfiRange, contents);
121 | });
122 |
123 | this.rendition.on("relocated", (location) => {
124 | this.setLocation(location.start.cfi);
125 | this.emit("relocated", location);
126 | });
127 |
128 | this.rendition.on("keydown", this.keyboardHandler.bind(this));
129 |
130 | this.on("prev", () => {
131 | if (this.book.package.metadata.direction === "rtl") {
132 | this.rendition.next();
133 | } else {
134 | this.rendition.prev();
135 | }
136 | });
137 |
138 | this.on("next", () => {
139 | if (this.book.package.metadata.direction === "rtl") {
140 | this.rendition.prev();
141 | } else {
142 | this.rendition.next();
143 | }
144 | });
145 |
146 | this.on("languagechanged", (value) => {
147 | this.settings.language = value;
148 | });
149 |
150 | this.on("flowchanged", (value) => {
151 | this.settings.flow = value;
152 | this.rendition.flow(value);
153 | });
154 |
155 | this.on("spreadchanged", (value) => {
156 | const mod = value.mod || this.settings.spread.mod;
157 | const min = value.min || this.settings.spread.min;
158 | this.settings.spread.mod = mod;
159 | this.settings.spread.min = min;
160 | this.rendition.spread(mod, min);
161 | });
162 |
163 | this.on("styleschanged", (value) => {
164 | const fontSize = value.fontSize;
165 | this.settings.styles.fontSize = fontSize;
166 | this.rendition.themes.fontSize(fontSize + "%");
167 | });
168 | }
169 |
170 | /* ------------------------------- Common ------------------------------- */
171 |
172 | navItemFromCfi(cfi) {
173 |
174 | // This feature was added to solve the problem of duplicate titles in
175 | // bookmarks. But this still has no solution because when reloading the
176 | // reader, rendition cannot get the range from the previously saved CFI.
177 | const range = this.rendition.getRange(cfi);
178 | const idref = range ? range.startContainer.parentNode.id : undefined;
179 | const location = this.rendition.currentLocation();
180 | const href = location.start.href;
181 | return this.navItems[href + "#" + idref] || this.navItems[href];
182 | }
183 |
184 | /* ------------------------------ Bookmarks ----------------------------- */
185 |
186 | /**
187 | * Verifying the current page in bookmarks.
188 | * @param {*} cfi
189 | * @returns The index of the bookmark if it exists, or -1 otherwise.
190 | */
191 | isBookmarked(cfi) {
192 |
193 | return this.settings.bookmarks.indexOf(cfi);
194 | }
195 |
196 | /* ----------------------------- Annotations ---------------------------- */
197 |
198 | isAnnotated(note) {
199 |
200 | return this.settings.annotations.indexOf(note);
201 | }
202 |
203 | /* ------------------------------ Settings ------------------------------ */
204 |
205 | /**
206 | * Initialize book settings.
207 | * @param {any} bookPath
208 | * @param {any} settings
209 | */
210 | cfgInit(bookPath, settings) {
211 |
212 | this.entryKey = md5(bookPath).toString();
213 | this.settings = {
214 | bookPath: bookPath,
215 | arrows: this.isMobile ? "none" : "content", // none | content | toolbar
216 | manager: this.isMobile ? "continuous" : "default",
217 | restore: true,
218 | history: true,
219 | openbook: this.storage.indexedDB ? true : false,
220 | language: "en",
221 | sectionId: undefined,
222 | bookmarks: [], // array | false
223 | annotations: [], // array | false
224 | flow: "paginated", // paginated | scrolled
225 | spread: {
226 | mod: "auto", // auto | none
227 | min: 800
228 | },
229 | styles: {
230 | fontSize: 100
231 | },
232 | pagination: undefined, // not implemented
233 | fullscreen: document.fullscreenEnabled
234 | };
235 |
236 | extend(settings || {}, this.settings);
237 |
238 | if (this.settings.restore) {
239 | this.applySavedSettings(settings || {});
240 | } else {
241 | this.removeSavedSettings();
242 | }
243 | }
244 |
245 | /**
246 | * Checks if the book setting can be retrieved from localStorage.
247 | * @returns true if the book key exists, or false otherwise.
248 | */
249 | isSaved() {
250 |
251 | return localStorage && localStorage.getItem(this.entryKey) !== null;
252 | }
253 |
254 | /**
255 | * Removing the current book settings from local storage.
256 | * @returns true if the book settings were deleted successfully, or false
257 | * otherwise.
258 | */
259 | removeSavedSettings() {
260 |
261 | if (!this.isSaved())
262 | return false;
263 |
264 | localStorage.removeItem(this.entryKey);
265 | return true;
266 | }
267 |
268 | /**
269 | * Applies saved settings from local storage.
270 | * @param {*} external External settings
271 | * @returns True if the settings were applied successfully, false otherwise.
272 | */
273 | applySavedSettings(external) {
274 |
275 | if (!this.isSaved())
276 | return false;
277 |
278 | let stored;
279 | try {
280 | stored = JSON.parse(localStorage.getItem(this.entryKey));
281 | } catch (e) {
282 | console.exception(e);
283 | }
284 |
285 | if (stored) {
286 | extend(stored, this.settings, external);
287 | return true;
288 | } else {
289 | return false;
290 | }
291 | }
292 |
293 | /**
294 | * Saving the current book settings in local storage.
295 | */
296 | saveSettings() {
297 |
298 | this.settings.previousLocationCfi = this.rendition.location.start.cfi;
299 | const cfg = Object.assign({}, this.settings);
300 | delete cfg.arrows;
301 | delete cfg.manager;
302 | delete cfg.history;
303 | delete cfg.restore;
304 | delete cfg.openbook;
305 | delete cfg.pagination;
306 | delete cfg.fullscreen;
307 | localStorage.setItem(this.entryKey, JSON.stringify(cfg));
308 | }
309 |
310 | setLocation(cfi) {
311 |
312 | const baseUrl = this.book.archived ? undefined : this.book.url;
313 | const url = new URL(window.location, baseUrl);
314 | url.hash = "#" + cfi;
315 |
316 | // Update the History Location
317 | if (this.settings.history && window.location.hash !== url.hash) {
318 | // Add CFI fragment to the history
319 | window.history.pushState({}, "", url);
320 | this.currentLocationCfi = cfi;
321 | }
322 | }
323 |
324 | //-- event handlers --//
325 |
326 | unload() {
327 |
328 | if (this.settings.restore && localStorage) {
329 | this.saveSettings();
330 | }
331 | }
332 |
333 | hashChanged() {
334 |
335 | const hash = window.location.hash.slice(1);
336 | this.rendition.display(hash);
337 | }
338 |
339 | keyboardHandler(e) {
340 |
341 | const step = 2;
342 | let value = this.settings.styles.fontSize;
343 |
344 | switch (e.key) {
345 |
346 | case "=":
347 | case "+":
348 | value += step;
349 | this.emit("styleschanged", { fontSize: value });
350 | break;
351 | case "-":
352 | value -= step;
353 | this.emit("styleschanged", { fontSize: value });
354 | break;
355 | case "0":
356 | value = 100;
357 | this.emit("styleschanged", { fontSize: value });
358 | break;
359 | case "ArrowLeft":
360 | this.emit("prev");
361 | break;
362 | case "ArrowRight":
363 | this.emit("next");
364 | break;
365 | }
366 | }
367 | }
368 |
369 | EventEmitter(Reader.prototype);
--------------------------------------------------------------------------------
/src/strings.js:
--------------------------------------------------------------------------------
1 | export class Strings {
2 |
3 | constructor(reader) {
4 |
5 | this.language = reader.settings.language || "en";
6 | this.values = {
7 | en: {
8 | "toolbar/sidebar": "Sidebar",
9 | "toolbar/prev": "Previous page",
10 | "toolbar/next": "Next page",
11 | "toolbar/openbook": "Open book",
12 | "toolbar/openbook/error": "Your browser does not support the required features.\nPlease use a modern browser such as Google Chrome, or Mozilla Firefox.",
13 | "toolbar/bookmark": "Add this page to bookmarks",
14 | "toolbar/fullscreen": "Fullscreen",
15 |
16 | "sidebar/close": "Close Sidebar",
17 | "sidebar/contents": "Contents",
18 | "sidebar/bookmarks": "Bookmarks",
19 | "sidebar/bookmarks/add": "Add",
20 | "sidebar/bookmarks/remove": "Remove",
21 | "sidebar/bookmarks/clear": "Clear",
22 | "sidebar/annotations": "Annotations",
23 | "sidebar/annotations/add": "Add",
24 | "sidebar/annotations/remove": "Remove",
25 | "sidebar/annotations/clear": "Clear",
26 | "sidebar/annotations/anchor": "Anchor",
27 | "sidebar/annotations/cancel": "Cancel",
28 | "sidebar/search": "Search",
29 | "sidebar/search/placeholder": "Search",
30 | "sidebar/settings": "Settings",
31 | "sidebar/settings/language": "Language",
32 | "sidebar/settings/fontsize": "Font size (%)",
33 | "sidebar/settings/flow": "Flow",
34 | "sidebar/settings/pagination": ["Pagination", "Generate pagination"],
35 | "sidebar/settings/spread": "Spread",
36 | "sidebar/settings/spread/minwidth": "Minimum spread width",
37 | "sidebar/metadata": "Metadata",
38 | "sidebar/metadata/title": "Title",
39 | "sidebar/metadata/creator": "Creator",
40 | "sidebar/metadata/description": "Description",
41 | "sidebar/metadata/pubdate": "Pubdate",
42 | "sidebar/metadata/publisher": "Publisher",
43 | "sidebar/metadata/identifier": "Identifier",
44 | "sidebar/metadata/language": "Language",
45 | "sidebar/metadata/rights": "Rights",
46 | "sidebar/metadata/modified_date": "Modified date",
47 | "sidebar/metadata/layout": "Layout", // rendition:layout
48 | "sidebar/metadata/flow": "Flow", // rendition:flow
49 | "sidebar/metadata/spread": "Spread", // rendition:spread
50 | "sidebar/metadata/direction": "Direction", // page-progression-direction
51 |
52 | "notedlg/label": "Note",
53 | "notedlg/add": "Add"
54 | },
55 | fr: {
56 | "toolbar/sidebar": "Barre latérale",
57 | "toolbar/prev": "???",
58 | "toolbar/next": "???",
59 | "toolbar/openbook": "Ouvrir un livre local",
60 | "toolbar/openbook/error": "Votre navigateur ne prend pas en charge les fonctions nécessaires.\nVeuillez utiliser un navigateur moderne tel que Google Chrome ou Mozilla Firefox.",
61 | "toolbar/bookmark": "Insérer un marque page ici",
62 | "toolbar/fullscreen": "Plein écran",
63 |
64 | "sidebar/close": "???",
65 | "sidebar/contents": "Sommaire",
66 | "sidebar/bookmarks": "Marque-pages",
67 | "sidebar/bookmarks/add": "Ajouter",
68 | "sidebar/bookmarks/remove": "Retirer",
69 | "sidebar/bookmarks/clear": "Tout enlever",
70 | "sidebar/annotations": "Annotations",
71 | "sidebar/annotations/add": "Ajouter",
72 | "sidebar/annotations/remove": "Retirer",
73 | "sidebar/annotations/clear": "Tout enlever",
74 | "sidebar/annotations/anchor": "Ancre",
75 | "sidebar/annotations/cancel": "Annuler",
76 | "sidebar/search": "Rechercher",
77 | "sidebar/search/placeholder": "rechercher",
78 | "sidebar/settings": "Réglages",
79 | "sidebar/settings/language": "Langue",
80 | "sidebar/settings/fontsize": "???",
81 | "sidebar/settings/flow": "???",
82 | "sidebar/settings/pagination": ["Pagination", "Établir une pagination"],
83 | "sidebar/settings/spread": "???",
84 | "sidebar/settings/spread/minwidth": "???",
85 | "sidebar/metadata": "???",
86 | "sidebar/metadata/title": "???",
87 | "sidebar/metadata/creator": "???",
88 | "sidebar/metadata/description": "???",
89 | "sidebar/metadata/pubdate": "???",
90 | "sidebar/metadata/publisher": "???",
91 | "sidebar/metadata/identifier": "???",
92 | "sidebar/metadata/language": "Langue",
93 | "sidebar/metadata/rights": "???",
94 | "sidebar/metadata/modified_date": "???",
95 | "sidebar/metadata/layout": "???",
96 | "sidebar/metadata/flow": "???",
97 | "sidebar/metadata/spread": "???",
98 | "sidebar/metadata/direction": "???",
99 |
100 | "notedlg/label": "???",
101 | "notedlg/add": "Ajouter"
102 | },
103 | ja: {
104 | "toolbar/sidebar": "サイドバー",
105 | "toolbar/prev": "???",
106 | "toolbar/next": "???",
107 | "toolbar/openbook": "本を開く",
108 | "toolbar/openbook/error": "ご利用のブラウザは必要な機能をサポートしていません。\nGoogle Chrome、Mozilla Firefox、その他のモダンなブラウザでご利用ください。",
109 | "toolbar/bookmark": "このページに栞を設定する",
110 | "toolbar/fullscreen": "フルスクリーン",
111 |
112 | "sidebar/close": "???",
113 | "sidebar/contents": "目次",
114 | "sidebar/bookmarks": "栞",
115 | "sidebar/bookmarks/add": "追加",
116 | "sidebar/bookmarks/remove": "削除",
117 | "sidebar/bookmarks/clear": "クリア",
118 | "sidebar/annotations": "注釈",
119 | "sidebar/annotations/add": "追加",
120 | "sidebar/bookmarks/remove": "削除",
121 | "sidebar/annotations/clear": "クリア",
122 | "sidebar/annotations/anchor": "アンカー",
123 | "sidebar/annotations/cancel": "キャンセル",
124 | "sidebar/search": "検索",
125 | "sidebar/search/placeholder": "検索",
126 | "sidebar/settings": "設定",
127 | "sidebar/settings/language": "表示言語",
128 | "sidebar/settings/fontsize": "???",
129 | "sidebar/settings/flow": "???",
130 | "sidebar/settings/pagination": ["ページネーション", "ページネーションを生成します。"],
131 | "sidebar/settings/spread": "???",
132 | "sidebar/settings/spread/minwidth": "???",
133 | "sidebar/metadata": "???",
134 | "sidebar/metadata/title": "???",
135 | "sidebar/metadata/creator": "???",
136 | "sidebar/metadata/description": "???",
137 | "sidebar/metadata/pubdate": "???",
138 | "sidebar/metadata/publisher": "???",
139 | "sidebar/metadata/identifier": "???",
140 | "sidebar/metadata/language": "表示言語",
141 | "sidebar/metadata/rights": "???",
142 | "sidebar/metadata/modified_date": "???",
143 | "sidebar/metadata/layout": "???",
144 | "sidebar/metadata/flow": "???",
145 | "sidebar/metadata/spread": "???",
146 | "sidebar/metadata/direction": "???",
147 |
148 | "notedlg/label": "???",
149 | "notedlg/add": "追加"
150 | },
151 | ru: {
152 | "toolbar/sidebar": "Боковая панель",
153 | "toolbar/prev": "Предыдущая страница",
154 | "toolbar/next": "Следущая страница",
155 | "toolbar/openbook": "Открыть книгу",
156 | "toolbar/openbook/error": "Ваш браузер не поддерживает необходимые функции.\nПожалуйста, используйте современный браузер, такой как Google Chrome или Mozilla Firefox.",
157 | "toolbar/bookmark": "Добавить эту страницу в закладки",
158 | "toolbar/fullscreen": "Полноэкранный режим",
159 |
160 | "sidebar/close": "Закрыть боковую панель",
161 | "sidebar/contents": "Содержание",
162 | "sidebar/bookmarks": "Закладки",
163 | "sidebar/bookmarks/add": "Добавить",
164 | "sidebar/bookmarks/remove": "Удалить",
165 | "sidebar/bookmarks/clear": "Очистить",
166 | "sidebar/annotations": "Аннотации",
167 | "sidebar/annotations/add": "Добавить",
168 | "sidebar/annotations/remove": "Удалить",
169 | "sidebar/annotations/clear": "Очистить",
170 | "sidebar/annotations/anchor": "Метка",
171 | "sidebar/annotations/cancel": "Отмена",
172 | "sidebar/search": "Поиск",
173 | "sidebar/search/placeholder": "Поиск",
174 | "sidebar/settings": "Настройки",
175 | "sidebar/settings/language": "Язык",
176 | "sidebar/settings/fontsize": "Размер шрифта",
177 | "sidebar/settings/flow": "Поток",
178 | "sidebar/settings/pagination": ["Нумерация страниц", "Генерировать нумерацию страниц"],
179 | "sidebar/settings/spread": "Разворот",
180 | "sidebar/settings/spread/minwidth": "Мин. ширина колонки",
181 | "sidebar/metadata": "Метаданные",
182 | "sidebar/metadata/title": "Заголовок",
183 | "sidebar/metadata/creator": "Автор",
184 | "sidebar/metadata/description": "Описание",
185 | "sidebar/metadata/pubdate": "Дата публикации",
186 | "sidebar/metadata/publisher": "Издатель",
187 | "sidebar/metadata/identifier": "Идентификатор",
188 | "sidebar/metadata/language": "Язык",
189 | "sidebar/metadata/rights": "Лицензия",
190 | "sidebar/metadata/modified_date": "Дата изменения",
191 | "sidebar/metadata/layout": "Макет",
192 | "sidebar/metadata/flow": "Поток",
193 | "sidebar/metadata/spread": "Разворот",
194 | "sidebar/metadata/direction": "Направление",
195 |
196 | "notedlg/label": "Заметка",
197 | "notedlg/add": "Добавить"
198 | },
199 | zh: {
200 | "toolbar/sidebar": "侧边栏",
201 | "toolbar/prev": "上一页",
202 | "toolbar/next": "下一页",
203 | "toolbar/openbook": "打开书籍",
204 | "toolbar/openbook/error": "您的浏览器不支持所需功能。\n请使用现代浏览器如谷歌Chrome或火狐Firefox。",
205 | "toolbar/bookmark": "加为书签",
206 | "toolbar/fullscreen": "全屏",
207 |
208 | "sidebar/close": "关闭侧边栏",
209 | "sidebar/contents": "目录",
210 | "sidebar/bookmarks": "书签",
211 | "sidebar/bookmarks/add": "添加",
212 | "sidebar/bookmarks/remove": "移除",
213 | "sidebar/bookmarks/clear": "清空",
214 | "sidebar/annotations": "注解",
215 | "sidebar/annotations/add": "添加",
216 | "sidebar/annotations/remove": "移除",
217 | "sidebar/annotations/clear": "清空",
218 | "sidebar/annotations/anchor": "锚定",
219 | "sidebar/annotations/cancel": "取消",
220 |
221 | "sidebar/search": "搜索",
222 | "sidebar/search/placeholder": "搜索",
223 | "sidebar/settings": "设置",
224 | "sidebar/settings/language": "语言",
225 | "sidebar/settings/fontsize": "字体大小 (%)",
226 | "sidebar/settings/flow": "流模式", // Scrolled = "滚动模式"
227 | "sidebar/settings/pagination": ["分页模式", "生成分页"],
228 | "sidebar/settings/spread": "双页布局",
229 | "sidebar/settings/spread/minwidth": "最小双页宽度",
230 | "sidebar/metadata": "元数据",
231 | "sidebar/metadata/title": "标题",
232 | "sidebar/metadata/creator": "作者",
233 | "sidebar/metadata/description": "描述",
234 | "sidebar/metadata/pubdate": "出版日期",
235 | "sidebar/metadata/publisher": "出版商",
236 | "sidebar/metadata/identifier": "标识符",
237 | "sidebar/metadata/language": "语言",
238 | "sidebar/metadata/rights": "版权",
239 | "sidebar/metadata/modified_date": "修改日期",
240 | "sidebar/metadata/layout": "布局", // rendition:layout
241 | "sidebar/metadata/flow": "流模式", // rendition:flow
242 | "sidebar/metadata/spread": "双页布局", // rendition:spread
243 | "sidebar/metadata/direction": "阅读方向", // page-progression-direction
244 |
245 | "notedlg/label": "笔记",
246 | "notedlg/add": "添加"
247 | },
248 | };
249 |
250 | reader.on("languagechanged", (value) => {
251 | this.language = value;
252 | });
253 | }
254 |
255 | get(key) { return this.values[this.language][key] || "???"; }
256 | }
--------------------------------------------------------------------------------
/src/ui.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author mrdoob https://github.com/mrdoob/ui.js
3 | */
4 |
5 | const ERROR_MSG = "is not an instance of UIElement.";
6 |
7 | /**
8 | * UIElement
9 | * @param {string} tag
10 | */
11 | export class UIElement {
12 |
13 | constructor(tag) {
14 |
15 | this.dom = document.createElement(tag);
16 | }
17 |
18 | add() {
19 |
20 | for (let i = 0; i < arguments.length; i++) {
21 |
22 | const argument = arguments[i];
23 |
24 | if (argument instanceof UIElement) {
25 |
26 | this.dom.appendChild(argument.dom);
27 |
28 | } else if (Array.isArray(argument)) {
29 |
30 | for (let j = 0; j < argument.length; j++) {
31 |
32 | const element = argument[j];
33 |
34 | if (element instanceof UIElement) {
35 |
36 | this.dom.appendChild(element.dom);
37 | } else {
38 |
39 | console.error("UIElement:", element, ERROR_MSG);
40 | }
41 | }
42 | } else {
43 |
44 | console.error("UIElement:", argument, ERROR_MSG);
45 | }
46 | }
47 | return this;
48 | }
49 |
50 | remove() {
51 |
52 | for (let i = 0; i < arguments.length; i++) {
53 |
54 | const argument = arguments[i];
55 |
56 | if (argument instanceof UIElement) {
57 |
58 | this.dom.removeChild(argument.dom);
59 |
60 | } else if (Number.isInteger(argument)) {
61 |
62 | this.dom.removeChild(this.dom.childNodes[argument]);
63 | } else {
64 |
65 | console.error("UIElement:", argument, ERROR_MSG);
66 | }
67 | }
68 | return this;
69 | }
70 |
71 | clear() {
72 |
73 | while (this.dom.children.length) {
74 |
75 | this.dom.removeChild(this.dom.lastChild);
76 | }
77 | return this;
78 | }
79 |
80 | setId(id) {
81 |
82 | this.dom.id = id;
83 | return this;
84 | }
85 |
86 | getId() {
87 |
88 | return this.dom.id;
89 | }
90 |
91 | removeAttribute(name) {
92 |
93 | this.dom.removeAttribute(name);
94 | return this;
95 | }
96 |
97 | setClass(name) {
98 |
99 | this.dom.className = name;
100 | return this;
101 | }
102 |
103 | addClass(name) {
104 |
105 | this.dom.classList.add(name);
106 | return this;
107 | }
108 |
109 | removeClass(name) {
110 |
111 | this.dom.classList.remove(name);
112 | return this;
113 | }
114 |
115 | setStyle(key, value) {
116 |
117 | this.dom.style[key] = value;
118 | return this;
119 | }
120 |
121 | getTitle() {
122 |
123 | return this.dom.title;
124 | }
125 |
126 | setTitle(title) {
127 |
128 | if (this.dom.title !== title && title)
129 | this.dom.title = title;
130 | return this;
131 | }
132 |
133 | getTextContent() {
134 |
135 | return this.dom.textContent;
136 | }
137 |
138 | setTextContent(text) {
139 |
140 | if (this.dom.textContent !== text && text)
141 | this.dom.textContent = text;
142 | return this;
143 | }
144 |
145 | getBoundingClientRect() {
146 |
147 | return this.dom.getBoundingClientRect();
148 | }
149 | }
150 |
151 | /**
152 | * UISpan
153 | * @param {string} text
154 | */
155 | export class UISpan extends UIElement {
156 |
157 | constructor(text) {
158 |
159 | super("span");
160 | this.setTextContent(text);
161 | }
162 | }
163 |
164 | /**
165 | * UIDiv
166 | */
167 | export class UIDiv extends UIElement {
168 |
169 | constructor() {
170 |
171 | super("div");
172 | }
173 | }
174 |
175 | /**
176 | * UIRow
177 | */
178 | export class UIRow extends UIDiv {
179 |
180 | constructor() {
181 |
182 | super();
183 |
184 | this.dom.className = "row";
185 | }
186 | }
187 |
188 | /**
189 | * UIPanel
190 | */
191 | export class UIPanel extends UIDiv {
192 |
193 | constructor() {
194 |
195 | super();
196 |
197 | this.dom.className = "panel";
198 | }
199 | }
200 |
201 | /**
202 | * UILabel
203 | * @param {string} text
204 | * @param {string} id
205 | */
206 | export class UILabel extends UIElement {
207 |
208 | constructor(text, id) {
209 |
210 | super("label");
211 |
212 | this.dom.textContent = text;
213 | if (id) this.dom.htmlFor = id;
214 | }
215 | }
216 |
217 | /**
218 | * UILink
219 | * @param {string} href
220 | * @param {string} text
221 | */
222 | export class UILink extends UIElement {
223 |
224 | constructor(href, text) {
225 |
226 | super("a");
227 |
228 | this.dom.href = href || "#";
229 | this.dom.textContent = text || "";
230 | }
231 |
232 | setHref(url) {
233 |
234 | this.dom.href = url;
235 | return this;
236 | }
237 | }
238 |
239 | /**
240 | * UIText
241 | * @param {string} text
242 | */
243 | export class UIText extends UISpan {
244 |
245 | constructor(text) {
246 |
247 | super();
248 |
249 | this.dom.textContent = text;
250 | }
251 |
252 | getValue() {
253 |
254 | return this.dom.textContent;
255 | }
256 |
257 | setValue(text) {
258 |
259 | this.dom.textContent = text;
260 | return this;
261 | }
262 | }
263 |
264 | /**
265 | * UITextArea
266 | */
267 | export class UITextArea extends UIElement {
268 |
269 | constructor() {
270 |
271 | super("textarea");
272 |
273 | this.dom.spellcheck = false;
274 | this.dom.onkeydown = (e) => {
275 |
276 | e.stopPropagation();
277 | };
278 | }
279 |
280 | getValue() {
281 |
282 | return this.dom.value;
283 | }
284 |
285 | setValue(value) {
286 |
287 | this.dom.value = value;
288 | return this;
289 | }
290 | }
291 |
292 | /**
293 | * UISelect
294 | */
295 | export class UISelect extends UIElement {
296 |
297 | constructor() {
298 |
299 | super("select");
300 | }
301 |
302 | setMultiple(boolean) {
303 |
304 | this.dom.multiple = boolean || false;
305 | return this;
306 | }
307 |
308 | setOptions(options) {
309 |
310 | const selected = this.dom.value;
311 | this.clear();
312 |
313 | for (const key in options) {
314 |
315 | const option = document.createElement("option");
316 | option.value = key;
317 | option.text = options[key];
318 | this.dom.appendChild(option);
319 | }
320 | this.dom.value = selected;
321 | return this;
322 | }
323 |
324 | getValue() {
325 |
326 | return this.dom.value;
327 | }
328 |
329 | setValue(value) {
330 |
331 | value = String(value);
332 |
333 | if (this.dom.value !== value)
334 | this.dom.value = value;
335 | return this;
336 | }
337 | }
338 |
339 | /**
340 | * UIInput
341 | * @param {*} type
342 | * @param {*} value
343 | * @param {*} title
344 | */
345 | export class UIInput extends UIElement {
346 |
347 | constructor(type, value, title) {
348 |
349 | super("input");
350 |
351 | this.dom.type = type;
352 | this.dom.onkeydown = (e) => {
353 |
354 | e.stopPropagation();
355 | };
356 | this.setValue(value);
357 | this.setTitle(title);
358 | }
359 |
360 | getName() {
361 |
362 | return this.dom.name;
363 | }
364 |
365 | setName(name) {
366 |
367 | this.dom.name = name;
368 | return this;
369 | }
370 |
371 | getType() {
372 |
373 | return this.dom.type;
374 | }
375 |
376 | setType(type) {
377 |
378 | this.dom.type = type;
379 | return this;
380 | }
381 |
382 | getValue() {
383 |
384 | return this.dom.value;
385 | }
386 |
387 | setValue(value) {
388 |
389 | if (this.dom.value !== value && value !== undefined)
390 | this.dom.value = value;
391 | return this;
392 | }
393 | }
394 |
395 | /**
396 | * UIColor
397 | */
398 | export class UIColor extends UIElement {
399 |
400 | constructor() {
401 |
402 | super("input");
403 |
404 | try {
405 |
406 | this.dom.type = "color";
407 | this.dom.value = "#ffffff";
408 |
409 | } catch (e) {
410 |
411 | console.exception(e);
412 | }
413 | }
414 |
415 | getValue() {
416 |
417 | return this.dom.value;
418 | }
419 |
420 | getHexValue() {
421 |
422 | return parseInt(this.dom.value.substr(1), 16);
423 | }
424 |
425 | setValue(value) {
426 |
427 | this.dom.value = value;
428 | return this;
429 | }
430 |
431 | setHexValue(hex) {
432 |
433 | this.dom.value = "#" + ("000000" + hex.toString(16)).slice(-6);
434 | return this;
435 | }
436 | }
437 |
438 | /**
439 | * UINumber
440 | * @param {number} value
441 | * @param {number} step
442 | * @param {number} min
443 | * @param {number} max
444 | * @param {number} precision
445 | */
446 | export class UINumber extends UIElement {
447 |
448 | constructor(value, step, min, max, precision) {
449 |
450 | super("input");
451 |
452 | this.dom.type = "number";
453 | this.dom.step = step || 1;
454 | this.dom.onkeydown = (e) => {
455 |
456 | e.stopPropagation();
457 | };
458 | this.value = value || 0;
459 | this.min = min || -Infinity;
460 | this.max = max || +Infinity;
461 | this.precision = precision || 0;
462 | this.setValue(value);
463 | this.dom.onchange = (e) => {
464 |
465 | this.setValue(this.value);
466 | };
467 | }
468 |
469 | getName() {
470 |
471 | return this.dom.name;
472 | }
473 |
474 | setName(name) {
475 |
476 | this.dom.name = name;
477 | return this;
478 | }
479 |
480 | setPrecision(precision) {
481 |
482 | this.precision = precision;
483 | this.setValue(this.value);
484 | return this;
485 | }
486 |
487 | setRange(min, max) {
488 |
489 | this.min = min;
490 | this.max = max;
491 | this.dom.min = min;
492 | this.dom.max = max;
493 | return this;
494 | }
495 |
496 | setStep(step) {
497 |
498 | this.dom.step = step;
499 | return this;
500 | }
501 |
502 | getValue() {
503 |
504 | return parseFloat(this.dom.value);
505 | }
506 |
507 | setValue(value) {
508 |
509 | if (value !== undefined) {
510 | value = parseFloat(value);
511 |
512 | if (value < this.min)
513 | value = this.min;
514 | if (value > this.max)
515 | value = this.max;
516 |
517 | this.value = value;
518 | this.dom.value = value.toFixed(this.precision);
519 | }
520 | return this;
521 | }
522 | }
523 |
524 | /**
525 | * UIBreak
526 | */
527 | export class UIBreak extends UIElement {
528 |
529 | constructor() {
530 |
531 | super("br");
532 | }
533 | }
534 |
535 | /**
536 | * UIHorizontalRule
537 | */
538 | export class UIHorizontalRule extends UIElement {
539 |
540 | constructor() {
541 |
542 | super("hr");
543 | }
544 | }
545 |
546 | /**
547 | * UIProgress
548 | * @param {*} value
549 | */
550 | export class UIProgress extends UIElement {
551 |
552 | constructor(value) {
553 |
554 | super("progress");
555 |
556 | this.dom.value = value;
557 | }
558 |
559 | setValue(value) {
560 |
561 | this.dom.value = value;
562 | return this;
563 | }
564 | }
565 |
566 | /**
567 | * UITabbedPanel
568 | * @param {string} align (horizontal | vertical)
569 | */
570 | export class UITabbedPanel extends UIDiv {
571 |
572 | constructor(align) {
573 |
574 | super();
575 |
576 | this.align = align || "horizontal";
577 | this.tabs = [];
578 | this.panels = [];
579 | this.selector = new UISpan().setClass("tab-selector");
580 | this.menuDiv = new UIDiv().setClass("menu");
581 | this.tabsDiv = new UIDiv().setClass("tabs");
582 | this.tabsDiv.add(this.selector);
583 | this.panelsDiv = new UIDiv().setClass("panels");
584 | this.selected = "";
585 | this.add(this.menuDiv);
586 | this.add(this.tabsDiv);
587 | this.add(this.panelsDiv);
588 | }
589 |
590 | addMenu(items) {
591 | this.menuDiv.add(items);
592 | }
593 |
594 | addTab(id, label, items) {
595 |
596 | const tab = new UITab(label, this);
597 | tab.setId(id);
598 | tab.setClass("box");
599 | this.tabs.push(tab);
600 | this.tabsDiv.add(tab);
601 |
602 | const panel = new UIDiv();
603 | panel.setId(id);
604 | panel.add(items);
605 | this.panels.push(panel);
606 | this.panelsDiv.add(panel);
607 | this.select(id);
608 | }
609 |
610 | select(id) {
611 |
612 | for (let tab of this.tabs) {
613 | if (tab.dom.id === id) {
614 | tab.addClass("selected");
615 | this.transformSelector(tab);
616 | } else if (tab.dom.id === this.selected) {
617 | tab.removeClass("selected");
618 | }
619 | }
620 |
621 | for (let panel of this.panels) {
622 | if (panel.dom.id === id) {
623 | panel.dom.style.display = "block";
624 | } else if (panel.dom.id === this.selected) {
625 | panel.dom.style.display = "none";
626 | }
627 | }
628 |
629 | this.selected = id;
630 | return this;
631 | }
632 |
633 | setLabel(id, text) {
634 |
635 | for (let tab of this.tabs) {
636 | if (tab.dom.id === id) {
637 | tab.setTitle(text);
638 | break;
639 | }
640 | }
641 | }
642 |
643 | transformSelector(tab) {
644 |
645 | let size;
646 | const rect = tab.getBoundingClientRect();
647 | if (this.align === "horizontal") {
648 | size = rect.width * this.tabs.indexOf(tab);
649 | this.selector.dom.style.transform = `translateX(${size}px)`;
650 | } else {
651 | size = rect.height * this.tabs.indexOf(tab);
652 | this.selector.dom.style.transform = `translateY(${size}px)`;
653 | }
654 | }
655 | }
656 |
657 | /**
658 | * UITab
659 | * @param {string} text
660 | * @param {UITabbedPanel} parent
661 | */
662 | export class UITab extends UIDiv {
663 |
664 | constructor(text, parent) {
665 |
666 | super();
667 | this.button = new UIInput("button");
668 | this.button.dom.title = text;
669 | this.dom.onclick = (e) => {
670 |
671 | parent.select(this.dom.id);
672 | e.preventDefault();
673 | };
674 | this.add(this.button);
675 | }
676 | }
677 |
678 | /**
679 | * UIList
680 | * @param {UIItem} parent
681 | */
682 | export class UIList extends UIElement {
683 |
684 | constructor(parent) {
685 |
686 | super("ul");
687 | this.parent = parent && parent.parent; // LI->UL
688 | this.expanded = false;
689 | }
690 |
691 | expand() {
692 |
693 | this.expanded = true;
694 | this.dom.style.display = "block";
695 | if (this.parent)
696 | this.parent.expand();
697 | return this;
698 | }
699 |
700 | collaps() {
701 |
702 | this.expanded = false;
703 | this.dom.style.display = "none";
704 | return this;
705 | }
706 | }
707 |
708 | /**
709 | * UIItem
710 | * @param {UIList} parent
711 | */
712 | export class UIItem extends UIElement {
713 |
714 | constructor(parent) {
715 |
716 | super("li");
717 | this.parent = parent; // UL
718 | this.selected = false;
719 | }
720 |
721 | add() {
722 | let len = 0;
723 | const box = new UIDiv().setId("item-box");
724 | for (let i = 0; i < arguments.length; i++) {
725 | const argument = arguments[i];
726 | if (argument instanceof UIList) {
727 | super.add(argument);
728 | } else {
729 | box.add(argument);
730 | len++;
731 | }
732 | }
733 | if (len) super.add(box);
734 | return this;
735 | }
736 |
737 | select() {
738 |
739 | this.selected = true;
740 | this.setClass("selected");
741 | return this;
742 | }
743 |
744 | unselect() {
745 |
746 | this.selected = false;
747 | this.removeAttribute("class");
748 | return this;
749 | }
750 | }
751 |
752 | /**
753 | * UIBox
754 | * @param {UIElement} items
755 | */
756 | export class UIBox extends UIElement {
757 |
758 | constructor(items) {
759 |
760 | super("div");
761 | this.setClass("box");
762 | this.add(items);
763 | }
764 | }
--------------------------------------------------------------------------------