add basic pdf reader

This commit is contained in:
dkanada 2020-09-01 22:55:08 +09:00
parent 4d04d54104
commit 108ebc58a6
11 changed files with 400 additions and 69 deletions

View file

@ -62,6 +62,7 @@
"core-js": "^3.6.5",
"date-fns": "^2.16.0",
"epubjs": "^0.3.85",
"pdfjs-dist": "^2.4.456",
"fast-text-encoding": "^1.0.3",
"flv.js": "^1.5.0",
"headroom.js": "^0.11.0",
@ -323,6 +324,7 @@
"src/libraries/scroller.js",
"src/plugins/backdropScreensaver/plugin.js",
"src/plugins/bookPlayer/plugin.js",
"src/plugins/pdfPlayer/plugin.js",
"src/plugins/bookPlayer/tableOfContents.js",
"src/plugins/chromecastPlayer/chromecastHelper.js",
"src/plugins/photoPlayer/plugin.js",

View file

@ -101,6 +101,11 @@ _define('epubjs', function () {
return epubjs;
});
var pdfjs = require('pdfjs-dist/build/pdf');
_define('pdfjs', function () {
return pdfjs;
});
// page.js
const page = require('page');
_define('page', function() {

View file

@ -172,7 +172,6 @@ function supportsCue() {
function onAppVisible() {
if (isHidden) {
isHidden = false;
console.debug('triggering app resume event');
events.trigger(appHost, 'resume');
}
}
@ -180,7 +179,6 @@ function onAppVisible() {
function onAppHidden() {
if (!isHidden) {
isHidden = true;
console.debug('app is hidden');
}
}

View file

@ -32,6 +32,7 @@
"plugins/bookPlayer/plugin",
"plugins/youtubePlayer/plugin",
"plugins/backdropScreensaver/plugin",
"plugins/pdfPlayer/plugin",
"plugins/logoScreensaver/plugin",
"plugins/sessionPlayer/plugin",
"plugins/chromecastPlayer/plugin"

View file

@ -20,11 +20,13 @@ export class BookPlayer {
this.onDialogClosed = this.onDialogClosed.bind(this);
this.openTableOfContents = this.openTableOfContents.bind(this);
this.onWindowKeyUp = this.onWindowKeyUp.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
}
play(options) {
this._progress = 0;
this._loaded = false;
this.progress = 0;
this.cancellationToken = false;
this.loaded = false;
loading.show();
const elem = this.createMediaElement();
@ -34,35 +36,35 @@ export class BookPlayer {
stop() {
this.unbindEvents();
const elem = this._mediaElement;
const tocElement = this._tocElement;
const rendition = this._rendition;
const elem = this.mediaElement;
const tocElement = this.tocElement;
const rendition = this.rendition;
if (elem) {
dialogHelper.close(elem);
this._mediaElement = null;
this.mediaElement = null;
}
if (tocElement) {
tocElement.destroy();
this._tocElement = null;
this.tocElement = null;
}
if (rendition) {
rendition.destroy();
}
// Hide loader in case player was not fully loaded yet
// hide loader in case player was not fully loaded yet
loading.hide();
this._cancellationToken.shouldCancel = true;
this.cancellationToken = true;
}
currentItem() {
return this._currentItem;
return this.item;
}
currentTime() {
return this._progress * 1000;
return this.progress * 1000;
}
duration() {
@ -94,12 +96,10 @@ export class BookPlayer {
onWindowKeyUp(e) {
const key = keyboardnavigation.getKeyName(e);
// TODO: depending on the event this can be the document or the rendition itself
const rendition = this._rendition || this;
const rendition = this.rendition;
const book = rendition.book;
if (this._loaded === false) return;
if (!this.loaded) return;
switch (key) {
case 'l':
case 'ArrowRight':
@ -112,9 +112,9 @@ export class BookPlayer {
book.package.metadata.direction === 'rtl' ? rendition.next() : rendition.prev();
break;
case 'Escape':
if (this._tocElement) {
if (this.tocElement) {
// Close table of contents on ESC if it is open
this._tocElement.destroy();
this.tocElement.destroy();
} else {
// Otherwise stop the entire book player
this.stop();
@ -124,17 +124,12 @@ export class BookPlayer {
}
onTouchStart(e) {
// TODO: depending on the event this can be the document or the rendition itself
const rendition = this._rendition || this;
const rendition = this.rendition;
const book = rendition.book;
// check that the event is from the book or the document
if (!book || this._loaded === false) return;
// epubjs stores pages off the screen or something for preloading
// get the modulus of the touch event to account for the increased width
if (!e.touches || e.touches.length === 0) return;
if (!this.loaded || !e.touches || e.touches.length === 0) return;
const touch = e.touches[0].clientX % dom.getWindowSize().innerWidth;
if (touch < dom.getWindowSize().innerWidth / 2) {
book.package.metadata.direction === 'rtl' ? rendition.next() : rendition.prev();
@ -148,7 +143,7 @@ export class BookPlayer {
}
bindMediaElementEvents() {
const elem = this._mediaElement;
const elem = this.mediaElement;
elem.addEventListener('close', this.onDialogClosed, {once: true});
elem.querySelector('.btnBookplayerExit').addEventListener('click', this.onDialogClosed, {once: true});
@ -161,13 +156,13 @@ export class BookPlayer {
document.addEventListener('keyup', this.onWindowKeyUp);
document.addEventListener('touchstart', this.onTouchStart);
// FIXME: I don't really get why document keyup event is not triggered when epub is in focus
this._rendition.on('keyup', this.onWindowKeyUp);
this._rendition.on('touchstart', this.onTouchStart);
// FIXME: document keyup event is not triggered when epub is in focus
this.rendition.on('keyup', this.onWindowKeyUp);
this.rendition.on('touchstart', this.onTouchStart);
}
unbindMediaElementEvents() {
const elem = this._mediaElement;
const elem = this.mediaElement;
elem.removeEventListener('close', this.onDialogClosed);
elem.querySelector('.btnBookplayerExit').removeEventListener('click', this.onDialogClosed);
@ -175,27 +170,27 @@ export class BookPlayer {
}
unbindEvents() {
if (this._mediaElement) {
if (this.mediaElement) {
this.unbindMediaElementEvents();
}
document.removeEventListener('keyup', this.onWindowKeyUp);
document.removeEventListener('touchstart', this.onTouchStart);
if (this._rendition) {
this._rendition.off('keyup', this.onWindowKeyUp);
this._rendition.off('touchstart', this.onTouchStart);
if (this.rendition) {
this.rendition.off('keyup', this.onWindowKeyUp);
this.rendition.off('touchstart', this.onTouchStart);
}
}
openTableOfContents() {
if (this._loaded) {
this._tocElement = new TableOfContents(this);
if (this.loaded) {
this.tocElement = new TableOfContents(this);
}
}
createMediaElement() {
let elem = this._mediaElement;
let elem = this.mediaElement;
if (elem) {
return elem;
}
@ -211,8 +206,6 @@ export class BookPlayer {
removeOnClose: true
});
elem.id = 'bookPlayer';
let html = '';
html += '<div class="topRightActionButtons">';
html += '<button is="paper-icon-button-light" class="autoSize bookplayerButton btnBookplayerExit hide-mouse-idle-tv" tabindex="-1"><i class="material-icons bookplayerButtonIcon close"></i></button>';
@ -221,19 +214,19 @@ export class BookPlayer {
html += '<button is="paper-icon-button-light" class="autoSize bookplayerButton btnBookplayerToc hide-mouse-idle-tv" tabindex="-1"><i class="material-icons bookplayerButtonIcon toc"></i></button>';
html += '</div>';
elem.id = 'bookPlayer';
elem.innerHTML = html;
dialogHelper.open(elem);
}
this._mediaElement = elem;
this.mediaElement = elem;
return elem;
}
setCurrentSrc(elem, options) {
const item = options.items[0];
this._currentItem = item;
this.item = item;
this.streamInfo = {
started: true,
ended: false,
@ -249,15 +242,10 @@ export class BookPlayer {
import('epubjs').then(({default: epubjs}) => {
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
const book = epubjs(downloadHref, {openAs: 'epub'});
const rendition = book.renderTo(elem, {width: '100%', height: '97%'});
const rendition = book.renderTo(elem, {width: '100%', height: '96%'});
this._currentSrc = downloadHref;
this._rendition = rendition;
const cancellationToken = {
shouldCancel: false
};
this._cancellationToken = cancellationToken;
this.currentSrc = downloadHref;
this.rendition = rendition;
return rendition.display().then(() => {
const epubElem = document.querySelector('.epub-container');
@ -265,10 +253,8 @@ export class BookPlayer {
this.bindEvents();
return this._rendition.book.locations.generate(1024).then(async () => {
if (cancellationToken.shouldCancel) {
return reject();
}
return this.rendition.book.locations.generate(1024).then(async () => {
if (this.cancellationToken) reject();
const percentageTicks = options.startPositionTicks / 10000000;
if (percentageTicks !== 0.0) {
@ -276,15 +262,14 @@ export class BookPlayer {
await rendition.display(resumeLocation);
}
this._loaded = true;
this.loaded = true;
epubElem.style.display = 'block';
rendition.on('relocated', (locations) => {
this._progress = book.locations.percentageFromCfi(locations.start.cfi);
this.progress = book.locations.percentageFromCfi(locations.start.cfi);
events.trigger(this, 'timeupdate');
});
loading.hide();
return resolve();
});
}, () => {
@ -300,7 +285,7 @@ export class BookPlayer {
}
canPlayItem(item) {
if (item.Path && (item.Path.endsWith('epub'))) {
if (item.Path && item.Path.endsWith('epub')) {
return true;
}

View file

@ -2,8 +2,8 @@ import dialogHelper from 'dialogHelper';
export default class TableOfContents {
constructor(bookPlayer) {
this._bookPlayer = bookPlayer;
this._rendition = bookPlayer._rendition;
this.bookPlayer = bookPlayer;
this.rendition = bookPlayer.rendition;
this.onDialogClosed = this.onDialogClosed.bind(this);
@ -11,24 +11,24 @@ export default class TableOfContents {
}
destroy() {
const elem = this._elem;
const elem = this.elem;
if (elem) {
this.unbindEvents();
dialogHelper.close(elem);
}
this._bookPlayer._tocElement = null;
this.bookPlayer.tocElement = null;
}
bindEvents() {
const elem = this._elem;
const elem = this.elem;
elem.addEventListener('close', this.onDialogClosed, {once: true});
elem.querySelector('.btnBookplayerTocClose').addEventListener('click', this.onDialogClosed, {once: true});
}
unbindEvents() {
const elem = this._elem;
const elem = this.elem;
elem.removeEventListener('close', this.onDialogClosed);
elem.querySelector('.btnBookplayerTocClose').removeEventListener('click', this.onDialogClosed);
@ -52,7 +52,7 @@ export default class TableOfContents {
}
createMediaElement() {
const rendition = this._rendition;
const rendition = this.rendition;
const elem = dialogHelper.createDialog({
size: 'small',
@ -68,7 +68,8 @@ export default class TableOfContents {
tocHtml += '<ul class="toc">';
rendition.book.navigation.forEach((chapter) => {
tocHtml += '<li>';
// Remove '../' from href
// remove parent directory reference from href to fix certain books
const link = chapter.href.startsWith('../') ? chapter.href.substr(3) : chapter.href;
tocHtml += `<a href="${rendition.book.path.directory + link}">${chapter.label}</a>`;
tocHtml += '</li>';
@ -83,7 +84,7 @@ export default class TableOfContents {
this.destroy();
});
this._elem = elem;
this.elem = elem;
this.bindEvents();
dialogHelper.open(elem);

View file

@ -0,0 +1,307 @@
import connectionManager from 'connectionManager';
import loading from 'loading';
import keyboardnavigation from 'keyboardnavigation';
import dialogHelper from 'dialogHelper';
import dom from 'dom';
import appRouter from 'appRouter';
import events from 'events';
import 'css!./style';
import 'material-icons';
import 'paper-icon-button-light';
export class PdfPlayer {
constructor() {
this.name = 'PDF Player';
this.type = 'mediaplayer';
this.id = 'pdfplayer';
this.priority = 1;
this.onDialogClosed = this.onDialogClosed.bind(this);
this.onWindowKeyUp = this.onWindowKeyUp.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
}
play(options) {
this.progress = 0;
this.loaded = false;
this.cancellationToken = false;
this.pages = {};
loading.show();
let elem = this.createMediaElement();
return this.setCurrentSrc(elem, options);
}
stop() {
this.unbindEvents();
let elem = this.mediaElement;
if (elem) {
dialogHelper.close(elem);
this.mediaElement = null;
}
// hide loading animation
loading.hide();
// cancel page render
this.cancellationToken = true;
}
currentItem() {
return this.item;
}
currentTime() {
return this.progress;
}
duration() {
return this.book ? this.book.numPages : 0;
}
volume() {
return 100;
}
isMuted() {
return false;
}
paused() {
return false;
}
seekable() {
return true;
}
onWindowKeyUp(e) {
let key = keyboardnavigation.getKeyName(e);
if (!this.loaded) return;
switch (key) {
case 'l':
case 'ArrowRight':
case 'Right':
this.next();
break;
case 'j':
case 'ArrowLeft':
case 'Left':
this.previous();
break;
case 'Escape':
this.stop();
break;
}
}
onTouchStart(e) {
if (!this.loaded || !e.touches || e.touches.length === 0) return;
if (e.touches[0].clientX < dom.getWindowSize().innerWidth / 2) {
this.previous();
} else {
this.next();
}
}
onDialogClosed() {
this.stop();
}
bindMediaElementEvents() {
let elem = this.mediaElement;
elem.addEventListener('close', this.onDialogClosed, {once: true});
elem.querySelector('.btnExit').addEventListener('click', this.onDialogClosed, {once: true});
}
bindEvents() {
this.bindMediaElementEvents();
document.addEventListener('keyup', this.onWindowKeyUp);
document.addEventListener('touchstart', this.onTouchStart);
}
unbindMediaElementEvents() {
let elem = this.mediaElement;
elem.removeEventListener('close', this.onDialogClosed);
elem.querySelector('.btnExit').removeEventListener('click', this.onDialogClosed);
}
unbindEvents() {
if (this.mediaElement) {
this.unbindMediaElementEvents();
}
document.removeEventListener('keyup', this.onWindowKeyUp);
document.removeEventListener('touchstart', this.onTouchStart);
}
createMediaElement() {
let elem = this.mediaElement;
if (elem) {
return elem;
}
elem = document.getElementById('pdfPlayer');
if (!elem) {
elem = dialogHelper.createDialog({
exitAnimationDuration: 400,
size: 'fullscreen',
autoFocus: false,
scrollY: false,
exitAnimation: 'fadeout',
removeOnClose: true
});
let html = '';
html += '<canvas id="canvas"></canvas>';
html += '<div class="actionButtons">';
html += '<button is="paper-icon-button-light" class="autoSize btnExit" tabindex="-1"><i class="material-icons actionButtonIcon close"></i></button>';
html += '</div>';
elem.id = 'pdfPlayer';
elem.innerHTML = html;
dialogHelper.open(elem);
}
this.mediaElement = elem;
return elem;
}
setCurrentSrc(elem, options) {
let item = options.items[0];
this.item = item;
this.streamInfo = {
started: true,
ended: false,
mediaSource: {
Id: item.Id
}
};
let serverId = item.ServerId;
let apiClient = connectionManager.getApiClient(serverId);
return new Promise((resolve, reject) => {
import('pdfjs').then(({default: pdfjs}) => {
let downloadHref = apiClient.getItemDownloadUrl(item.Id);
this.bindEvents();
pdfjs.GlobalWorkerOptions.workerSrc = appRouter.baseUrl() + '/libraries/pdf.worker.js';
let downloadTask = pdfjs.getDocument(downloadHref);
downloadTask.promise.then(book => {
if (this.cancellationToken) return;
this.book = book;
this.loaded = true;
const percentageTicks = options.startPositionTicks / 10000;
if (percentageTicks !== 0) {
this.loadPage(percentageTicks);
this.progress = percentageTicks;
} else {
this.loadPage(1);
}
return resolve();
});
});
});
}
next() {
if (this.progress === this.duration() - 1) return;
this.loadPage(this.progress + 2);
this.progress = this.progress + 1;
}
previous() {
if (this.progress === 0) return;
this.loadPage(this.progress);
this.progress = this.progress - 1;
}
replaceCanvas(canvas) {
const old = document.getElementById('canvas');
canvas.id = 'canvas';
old.parentNode.replaceChild(canvas, old);
}
loadPage(number) {
const prefix = 'page';
const pad = 2;
// generate list of cached pages by padding the requested page on both sides
let pages = [prefix + number];
for (let i = 1; i <= pad; i++) {
if (number - i > 0) pages.push(prefix + (number - i));
if (number + i < this.duration()) pages.push(prefix + (number + i));
}
// load any missing pages in the cache
for (let page of pages) {
if (!this.pages[page]) {
this.pages[page] = document.createElement('canvas');
this.renderPage(this.pages[page], parseInt(page.substr(4)));
}
}
// show the requested page
this.replaceCanvas(this.pages[prefix + number], number);
// delete all pages outside the cache area
for (let page in this.pages) {
if (!pages.includes(page)) {
delete this.pages[page];
}
}
}
renderPage(canvas, number) {
this.book.getPage(number).then(page => {
events.trigger(this, 'timeupdate');
const original = page.getViewport({ scale: 1 });
const context = canvas.getContext('2d');
const widthRatio = dom.getWindowSize().innerWidth / original.width;
const heightRatio = dom.getWindowSize().innerHeight / original.height;
const scale = Math.min(heightRatio, widthRatio);
const viewport = page.getViewport({ scale: scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
var renderContext = {
canvasContext: context,
viewport: viewport
};
let renderTask = page.render(renderContext);
renderTask.promise.then(() => {
loading.hide();
});
});
}
canPlayMediaType(mediaType) {
return (mediaType || '').toLowerCase() === 'book';
}
canPlayItem(item) {
if (item.Path && item.Path.endsWith('pdf')) {
return true;
}
return false;
}
}
export default PdfPlayer;

View file

@ -0,0 +1,25 @@
#pdfPlayer {
position: relative;
height: 100%;
width: 100%;
overflow: none;
z-index: 100;
background: #fff;
}
#canvas {
display: block;
margin: auto;
}
.actionButtons {
right: 0.5vh;
top: 0.5vh;
z-index: 1002;
position: absolute;
}
.actionButtonIcon {
color: black;
opacity: 0.7;
}

View file

@ -503,6 +503,7 @@ function initClient() {
'flvjs',
'jstree',
'epubjs',
'pdfjs',
'jQuery',
'hlsjs',
'howler',

View file

@ -12,7 +12,8 @@ const Assets = [
'libass-wasm/dist/js/subtitles-octopus-worker.wasm',
'libass-wasm/dist/js/subtitles-octopus-worker-legacy.js',
'libass-wasm/dist/js/subtitles-octopus-worker-legacy.data',
'libass-wasm/dist/js/subtitles-octopus-worker-legacy.js.mem'
'libass-wasm/dist/js/subtitles-octopus-worker-legacy.js.mem',
'pdfjs-dist/build/pdf.worker.js'
];
const LibarchiveWasm = [

View file

@ -8222,6 +8222,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
pdfjs-dist@^2.4.456:
version "2.4.456"
resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz#0eaad2906cda866bbb393e79a0e5b4e68bd75520"
integrity sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"