1
0
Fork 0
mirror of https://github.com/futurepress/epub.js.git synced 2025-10-02 14:49:16 +02:00
This commit is contained in:
kmacshane 2023-05-15 22:03:08 -07:00 committed by GitHub
commit 184520769d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1024 additions and 21196 deletions

325
espark_reader/reader.css Normal file
View file

@ -0,0 +1,325 @@
body {
margin: 0;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
/*position: absolute;*/
height: 100%;
width: 100%;
/*min-height: 800px;*/
}
#title {
width: 900px;
min-height: 18px;
margin: 10px auto;
text-align: center;
font-size: 16px;
color: #E2E2E2;
font-weight: 400;
}
#title:hover {
color: #777;
}
#viewer {
width: 900px;
height: 600px;
box-shadow: 0 0 4px #ccc;
padding: 10px 10px 0px 10px;
margin: 5px auto;
background: white;
}
#viewer.spreads {
width: 900px;
height: 600px;
box-shadow: 0 0 4px #ccc;
border-radius: 5px;
padding: 0;
position: relative;
margin: 10px auto;
background: white;
top: calc(50vh - 400px);
}
/* Smartphone - Portrait */
@media only screen and (min-width: 320px) and (max-width: 667px) and (orientation: portrait) {
#viewer {
width: 320px;
height: 667px;
}
}
/* Smartphone - Landscape */
@media only screen and (min-width: 320px) and (max-width: 667px) and (orientation: landscape) {
#viewer {
width: 667px;
height: 320px;
}
}
/* Tablet - Portrait and Landscape */
@media only screen and (min-width: 667px) and (max-width: 1024px) {
#viewer {
width: 600px;
height: 600px;
}
}
#viewer.spreads .epub-view>iframe {
background: white;
}
#viewer.scrolled {
overflow: hidden;
width: 800px;
margin: 0 auto;
position: relative;
background: url('ajax-loader.gif') center center no-repeat;
box-shadow: 0 0 4px #ccc;
padding: 20px;
background: white;
}
#viewer.scrolled .epub-view>iframe {
background: white;
}
#prev {
left: 0;
}
#next {
right: 0;
}
#toc {
display: block;
margin: 10px auto;
}
@media (min-width: 1000px) {
/*#viewer.spreads:after {
position: absolute;
width: 1px;
border-right: 1px #000 solid;
height: 90%;
z-index: 1;
left: 50%;
margin-left: -1px;
top: 5%;
opacity: .15;
box-shadow: -2px 0 15px rgba(0, 0, 0, 1);
content: "";
}
#viewer.spreads.single:after {
display: none;
}*/
#prev {
left: 40px;
}
#next {
right: 40px;
}
}
.arrow {
position: fixed;
top: 50%;
margin-top: -32px;
font-size: 64px;
color: #E2E2E2;
font-family: arial, sans-serif;
font-weight: bold;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
text-decoration: none;
}
.navlink {
margin: 14px;
display: block;
text-align: center;
text-decoration: none;
color: #ccc;
}
.arrow:hover,
.navlink:hover {
color: #777;
}
.arrow:active,
.navlink:hover {
color: #000;
}
#book-wrapper {
width: 480px;
height: 640px;
overflow: hidden;
border: 1px solid #ccc;
margin: 28px auto;
background: #fff;
border-radius: 0 5px 5px 0;
position: absolute;
}
#book-viewer {
width: 480px;
height: 660px;
margin: -30px auto;
-moz-box-shadow: inset 10px 0 20px rgba(0, 0, 0, .1);
-webkit-box-shadow: inset 10px 0 20px rgba(0, 0, 0, .1);
box-shadow: inset 10px 0 20px rgba(0, 0, 0, .1);
}
#book-viewer iframe {
padding: 40px 40px;
}
#controls {
position: absolute;
bottom: 16px;
left: 50%;
width: 400px;
margin-left: -200px;
text-align: center;
display: none;
}
#controls>input[type=range] {
width: 400px;
}
#navigation {
width: 400px;
height: 100vh;
position: absolute;
overflow: auto;
top: 0;
left: 0;
background: #777;
-webkit-transition: -webkit-transform .25s ease-out;
-moz-transition: -moz-transform .25s ease-out;
-ms-transition: -moz-transform .25s ease-out;
transition: transform .25s ease-out;
}
#navigation.fixed {
position: fixed;
}
#navigation h1 {
width: 200px;
font-size: 16px;
font-weight: normal;
color: #fff;
margin-bottom: 10px;
}
#navigation h2 {
font-size: 14px;
font-weight: normal;
color: #B0B0B0;
margin-bottom: 20px;
}
#navigation ul {
padding-left: 36px;
margin-left: 0;
margin-top: 12px;
margin-bottom: 12px;
width: 340px;
}
#navigation ul li {
list-style: decimal;
margin-bottom: 10px;
color: #cccddd;
font-size: 12px;
padding-left: 0;
margin-left: 0;
}
#navigation ul li a {
color: #ccc;
text-decoration: none;
}
#navigation ul li a:hover {
color: #fff;
text-decoration: underline;
}
#navigation ul li a.active {
color: #fff;
}
#navigation #cover {
display: block;
margin: 24px auto;
}
#navigation #closer {
position: absolute;
top: 0;
right: 0;
padding: 12px;
color: #cccddd;
width: 24px;
}
#navigation.closed {
-webkit-transform: translate(-400px, 0);
-moz-transform: translate(-400px, 0);
-ms-transform: translate(-400px, 0);
transform: translate(-400px, 0);
}
svg {
display: block;
}
.close-x {
stroke: #cccddd;
fill: transparent;
stroke-linecap: round;
stroke-width: 5;
}
.close-x:hover {
stroke: #fff;
}
#opener {
position: absolute;
top: 0;
left: 0;
padding: 10px;
stroke: #E2E2E2;
fill: #E2E2E2;
}
#opener:hover {
stroke: #777;
fill: #777;
}
#audioPlayer {
margin: 10px auto;
text-align: center;
font-size: 16px;
color: #E2E2E2;
font-weight: 400;
}

38
espark_reader/reader.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EPUB.js Input Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js"></script>
<script src="../dist/epub.js"></script>
<script src="../howler.js/src/howler.core.js"></script>
<script src="reader.js"></script>
<link rel="stylesheet" type="text/css" href="reader.css">
</head>
<body>
<div id="title">
<input type="file" id="input">
<div id="audioplayer"><button id="playButton" onclick="playPause()">Play</button><button id="resetButton"
onclick="resetAudioToStart()">Reset</button></div>
</div>
<div id="viewer"></div>
<a id="prev" href="#prev" class="arrow"></a>
<a id="next" href="#next" class="arrow"></a>
<script>
init();
</script>
</body>
</html>

186
espark_reader/reader.js Normal file
View file

@ -0,0 +1,186 @@
var book = ePub();
var rendition;
var inputElement;
var audioClips = [];
var currentWord = 0;
var isPlaying = false;
function init() {
inputElement = document.getElementById("input");
inputElement.addEventListener('change', function (e) {
var file = e.target.files[0];
if (window.FileReader) {
var reader = new FileReader();
reader.onload = openBook;
reader.readAsArrayBuffer(file);
}
});
}
function openBook(e) {
var bookData = e.target.result;
var title = document.getElementById("title");
var next = document.getElementById("next");
var prev = document.getElementById("prev");
book.open(bookData, "binary");
/* console.log("opened book")
console.log(book) */
var rendition = book.renderTo("viewer", {
//manager: "continuous",
flow: "paginated",
width: "100%",
height: "100%",
snap: true
});
rendition.display();
var keyListener = function (e) {
// Left Key
if ((e.keyCode || e.which) == 37) {
rendition.prev();
}
// Right Key
if ((e.keyCode || e.which) == 39) {
rendition.next();
}
};
rendition.on("keyup", keyListener);
rendition.on("relocated", function (location) {
console.log(location);
initAudio();
});
next.addEventListener("click", function (e) {
rendition.next();
e.preventDefault();
}, false);
prev.addEventListener("click", function (e) {
rendition.prev();
e.preventDefault();
}, false);
document.addEventListener("keyup", keyListener, false);
}
function initAudio() {
var iframeList = document.getElementsByTagName("iframe");
audioClips = [];
for (let iframe of iframeList) {
var iframeDoc = iframe.contentDocument;
//get audio information
iframeDoc.querySelectorAll('par').forEach(par => {
var text = par.getElementsByTagName("text")[0]; //should only be one
var audio = par.getElementsByTagName("audio")[0]; //should only be one
var textId = text.getAttribute("src").split("#")[1];//text source format is page.xhtml#word
var textRef = iframeDoc.getElementById(textId);
var audioSrc = audio.getAttribute("src");
const contentType = "audio/mp3";
/*var sound = new Howl({
src: [`data:${contentType};base64,${audioSrc}`]
});*/
audioClips.push({
textId: textId,
text: textRef,
audio: audio,
clipBegin: parseFloat(audio.getAttribute("clipbegin")),
clipEnd: parseFloat(audio.getAttribute("clipend"))
//duration: 1000 * (parseFloat(audio.getAttribute("clipend")) - parseFloat(audio.getAttribute("clipbegin")))
})
});
//add next clip's start to the duration to account for pauses
for (let i = 0; i < audioClips.length - 1; i++) {
audioClips[i].duration = 1000 * (audioClips[i + 1].clipBegin - audioClips[i].clipBegin);
}
if (audioClips.length > 0) {
document.getElementById("audioplayer").style.visibility = "visible";
resetAudioToStart();
}
else {
document.getElementById("audioplayer").style.visibility = "hidden";
}
}
}
/**
* This is where the audio play/pause function should load and play everything on the page.
* Needs to loop through all available iframes because there may be 1-2 pages loaded separately on the page.
* I believe the audio tags will be in order per epub spec. Could look at play start and check if that's not the case.
* To play audio and go through highlighting the text and stopping at the right point, need to look at the clipBegin/clipEnd properties
* You could go through each audio tag and play/stop for each one but I think that'd cause some choppy behavior. Probably would be smoother to get the start/end for the page,
* and then track the time so that you can track the word for highlighting and pausing.
*
*
* Something to consider: will tracking the pages like this get messed up if the device rotates and only shows one page?
*/
function playPause() {
if (audioClips.length > 0) {
if (isPlaying) {
pauseAudio();
}
else {
playAudio();
}
}
}
/**
* The audio is the same for the whole book, need to play/pause the same audio tag
*/
function playAudio() {
isPlaying = true;
highlightWord();
audioClips[0].audio.play();
document.getElementById("playButton").textContent = "Pause";
}
function pauseAudio() {
isPlaying = false;
audioClips[0].audio.pause();
document.getElementById("playButton").textContent = "Play";
}
function resetAudioToStart() {
if (currentWord > 0)
audioClips[currentWord - 1].text.setAttribute("style", "");
currentWord = 0;
pauseAudio();
audioClips[0].audio.currentTime = audioClips[currentWord].clipBegin;
}
function resetAudioToWord() {
audioClips[0].audio.currentTime = audioClips[currentWord].clipBegin;
}
/**
* To highlight text with audio, search for the span with the id that matches the #id in the text src wrapping the audio. Each par tag has a text with source and the audio.
* Add a css highlight class to the span, then remove when moving to the next word.
*/
function highlightWord() {
//don't increment words if the audio is ended
if (currentWord >= audioClips.length) {
resetAudioToStart();
return;
}
if (!isPlaying) {
return;
}
/**
* note: using class add/remove doesn't work with iframes because the css is separate
*/
//highlight current word
if (currentWord > 0)
audioClips[currentWord - 1].text.setAttribute("style", "");
audioClips[currentWord].text.setAttribute("style", "background-color:yellow;");
//setup timer for when word is done being read
const myTimeout = setTimeout(highlightWord, audioClips[currentWord].duration);
//increment word index
currentWord++;
}

3
espark_reader/view.css Normal file
View file

@ -0,0 +1,3 @@
.highlight {
background: yellow;
}

View file

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -11,6 +12,7 @@
<link rel="stylesheet" type="text/css" href="examples.css">
</head>
<body>
<div id="title"><input type="file" id="input"></div>
<div id="viewer" class="spreads"></div>
@ -43,7 +45,7 @@
rendition = book.renderTo("viewer", {
width: "100%",
height: 600
height: "100%"
});
rendition.display();
@ -88,4 +90,5 @@
</script>
</body>
</html>

20921
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -59,6 +59,7 @@
"@xmldom/xmldom": "^0.7.5",
"core-js": "^3.18.3",
"event-emitter": "^0.3.5",
"howler": "^2.2.3",
"jszip": "^3.7.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",

View file

@ -53,7 +53,7 @@ class Packaging {
this.coverPath = this.findCoverPath(packageDocument);
this.spineNodeIndex = indexOfElementNode(spineNode);
//KEM: spine creation/reading
this.spine = this.parseSpine(spineNode, this.manifest);
this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument);

View file

@ -176,6 +176,7 @@ class Rendition {
// If manager is a string, try to load from imported managers
if (typeof manager === "string" && manager === "default") {
viewManager = DefaultViewManager;
} else if (typeof manager === "string" && manager === "continuous") {
viewManager = ContinuousViewManager;
} else {
@ -331,7 +332,7 @@ class Rendition {
if (this.book.locations.length() && isFloat(target)) {
target = this.book.locations.cfiFromPercentage(parseFloat(target));
}
//KEM: Section display
section = this.book.spine.get(target);
if (!section) {

View file

@ -40,6 +40,7 @@ class Resources {
this.replacementUrls = [];
this.html = [];
//KEM: assets and urls are where the urls in items from the manifest are replaced by "blobs"
this.assets = [];
this.css = [];
@ -110,7 +111,7 @@ class Resources {
createUrl(url) {
var parsedUrl = new Url(url);
var mimeType = mime.lookup(parsedUrl.filename);
//KEM: based on the printout of the parsedUrl here, the url is correct, but the smil file is referring to it differently. May need to modify the replacement to include the ../
if (this.settings.archive) {
return this.settings.archive.createUrl(url, { "base64": (this.settings.replacements === "base64") });
} else {
@ -257,12 +258,16 @@ class Resources {
*/
relativeTo(absolute, resolver) {
resolver = resolver || this.settings.resolver;
// Get Urls relative to current sections
return this.urls.
map(function (href) {
var resolved = resolver(href);
var relative = new Path(absolute).relative(resolved);
//KEM: hardcoding to fix audio links for testing
//KEM: these are the links that are searched for in the content
if (relative.includes("audio")) {
relative = "../" + relative;
}
return relative;
}.bind(this));
}
@ -300,6 +305,12 @@ class Resources {
} else {
relUrls = this.urls;
}
//KEM: this seems to be where the audio urls are going wrong, but it could be that there's a problem with the original file.
//KEM: The smil files have relative paths to the resources with an extra folder jump like ../, and the regular pages don't do that
//KEM: so there may need to be some kind of check to see where the files actually are before replacing them?
/**
* Goes through the content and replaces any instances of relUrls with this.replacementUrls
*/
return substitute(content, relUrls, this.replacementUrls);
}

View file

@ -24,7 +24,7 @@ class Section {
this.canonical = item.canonical;
this.next = item.next;
this.prev = item.prev;
this.overlay = item.overlay;
this.cfiBase = item.cfiBase;
if (hooks) {
@ -38,6 +38,7 @@ class Section {
this.document = undefined;
this.contents = undefined;
this.output = undefined;
this.mediaOverlay = undefined;
}
/**
@ -50,25 +51,56 @@ class Section {
var loading = new defer();
var loaded = loading.promise;
//KEM: this is where the file is loaded and turned into xml
//KEM: add in a load to the smil file and append it?
//KEM: try to load overlay
if (this.overlay) {
if (this.contents) {
loading.resolve(this.contents);
} else {
request(this.url)
.then(function(xml){
// var directory = new Url(this.url).directory;
request(this.overlay.url).then(function (overlayXml) {
var div = document.createElement("div");
div.classList.add("audioContainer");
//overlay is returning as a string? possibly because xml instead of xhtml
var start = overlayXml.search("<smil");
var xmlStr = overlayXml.substring(start);
div.insertAdjacentHTML('beforeend', xmlStr);
this.mediaOverlay = div;
return request(this.url).then(function (xml) {
this.document = xml;
this.contents = xml.documentElement;
this.contents.appendChild(div);
return this.hooks.content.trigger(this.document, this);
}.bind(this))
.then(function(){
}.bind(this)).then(function () {
loading.resolve(this.contents);
}.bind(this))
.catch(function(error){
}.bind(this)).catch(function (error) {
loading.reject(error);
});
}
.bind(this)).catch(function (error) {
loading.reject(error);
});
}
}
else {
if (this.contents) {
loading.resolve(this.contents);
} else {
request(this.url).then(function (xml) {
// var directory = new Url(this.url).directory;
this.document = xml;
this.contents = xml.documentElement;
return this.hooks.content.trigger(this.document, this);
}.bind(this)).then(function () {
loading.resolve(this.contents);
}.bind(this)).catch(function (error) {
loading.reject(error);
});
}
}
return loaded;
}
@ -90,7 +122,7 @@ class Section {
var rendering = new defer();
var rendered = rendering.promise;
this.output; // TODO: better way to return this from hooks?
//console.log("rendering section");
this.load(_request).
then(function (contents) {
var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
@ -299,6 +331,7 @@ class Section {
this.document = undefined;
this.contents = undefined;
this.output = undefined;
this.mediaOverlay = undefined;
}
destroy() {

View file

@ -45,7 +45,7 @@ class Spine {
this.spineNodeIndex = _package.spineNodeIndex;
this.baseUrl = _package.baseUrl || _package.basePath || "";
this.length = this.items.length;
//KEM: unpacking opf file
this.items.forEach((item, index) => {
var manifestItem = this.manifest[item.idref];
var spineItem;
@ -59,10 +59,16 @@ class Spine {
}
if (manifestItem) {
//KEM: This is where spine items get the URL from the manifest, this is where to connect the manifest media-overlay and get SMIL URL
item.href = manifestItem.href;
item.url = resolver(item.href, true);
item.canonical = canonical(item.href);
//KEM: added overlay so can load later in section
item.overlay = this.manifest[manifestItem.overlay];
if (item.overlay) {
item.overlay.url = resolver(item.overlay.href, true);
item.overlay.canonical = canonical(item.overlay.href);
}
if (manifestItem.properties.length) {
item.properties.push.apply(item.properties, manifestItem.properties);
}
@ -100,7 +106,7 @@ class Spine {
}
}
//KEM: section creation
spineItem = new Section(item, this.hooks);
this.append(spineItem);

85
src/test.html Normal file
View file

@ -0,0 +1,85 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en-US">
<head>
<title>Me and My Cat</title>
<link href="CSS/default.css" rel="stylesheet" type="text/css" />
<link href="CSS/css_1.css" rel="stylesheet" type="text/css" />
<meta name="viewport" content="width=1400, height=1685" />
</head>
<body epub:type="frontmatter" lang="en-US" xml:lang="en-US">
<div class="pg1">
<section epub:type="titlepage">
<img src="images/1.jpg" class="img" alt="written " />
<p id="para1">
<span id="p1s1"><span id="written">written</span></span>
<span id="Me"></span>
<span id="and"></span>
<span id="My"></span>
<span id="Cat"></span>
<span id="p1s2"><span id="by">by</span></span> <br />
<span id="p1s4"><span id="Michael">Michael</span></span>
<span id="p1s5"><span id="Dahl">Dahl</span></span> <br />
<span id="p1s6"><span id="art">art</span></span>
<span id="p1s7"><span id="by1">by</span></span> <br />
<span id="p1s9"><span id="Zoe">Zoe</span></span>
<span id="p1s10"><span id="Persico">Persico</span></span> <br />
</p>
</section>
</div>
</body>
<smil xmlns="http://www.w3.org/ns/SMIL" xmlns:epub="http://www.idpf.org/2007/ops" version="3.0">
<body>
<par id="id0">
<text src="../P1.xhtml#Me">
</text>
<audio clipBegin="4.15" clipEnd="4.479" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id1">
<text src="../P1.xhtml#and">
</text>
<audio clipBegin="4.479" clipEnd="4.674" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id2">
<text src="../P1.xhtml#My">
</text>
<audio clipBegin="4.674" clipEnd="4.895" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id3">
<text src="../P1.xhtml#Cat">
</text>
<audio clipBegin="4.895" clipEnd="5.31" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id4">
<text src="../P1.xhtml#by">
</text>
<audio clipBegin="5.76" clipEnd="5.919" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id5">
<text src="../P1.xhtml#Michael">
</text>
<audio clipBegin="5.919" clipEnd="6.37" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id6">
<text src="../P1.xhtml#Dahl">
</text>
<audio clipBegin="6.371" clipEnd="6.778" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
</body>
</smil>
</html>

View file

@ -79,7 +79,6 @@ export function replaceLinks(contents, fn) {
if (!links.length) {
return;
}
var base = qs(contents.ownerDocument, "base");
var location = base ? base.getAttribute("href") : undefined;
var replaceLink = function (link) {