mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge a998a9881d
into 7d84185d0e
This commit is contained in:
commit
4137b4e564
9 changed files with 754 additions and 373 deletions
|
@ -86,7 +86,7 @@ function getOffsets(elems: Element[]): Offset[] {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPosition(positionTo: Element, options: Options, dlg: HTMLElement) {
|
export function getPosition(positionTo: Element, options: Options, dlg: HTMLElement) {
|
||||||
const windowSize = dom.getWindowSize();
|
const windowSize = dom.getWindowSize();
|
||||||
const windowHeight = windowSize.innerHeight;
|
const windowHeight = windowSize.innerHeight;
|
||||||
const windowWidth = windowSize.innerWidth;
|
const windowWidth = windowSize.innerWidth;
|
||||||
|
@ -387,5 +387,6 @@ export function show(options: Options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
show: show
|
show,
|
||||||
|
getPosition,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,58 +5,251 @@ import keyboardnavigation from '../../scripts/keyboardNavigation';
|
||||||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||||
import ServerConnections from '../../components/ServerConnections';
|
import ServerConnections from '../../components/ServerConnections';
|
||||||
import Screenfull from 'screenfull';
|
import Screenfull from 'screenfull';
|
||||||
import TableOfContents from './tableOfContents';
|
|
||||||
import { translateHtml } from '../../lib/globalize';
|
import { translateHtml } from '../../lib/globalize';
|
||||||
import browser from 'scripts/browser';
|
import browser from 'scripts/browser';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import { currentSettings as userSettings } from '../../scripts/settings/userSettings';
|
||||||
import TouchHelper from 'scripts/touchHelper';
|
import TouchHelper from 'scripts/touchHelper';
|
||||||
import { PluginType } from '../../types/plugin.ts';
|
import { PluginType } from '../../types/plugin.ts';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from '../../utils/events.ts';
|
||||||
|
import globalize from '../../lib/globalize';
|
||||||
|
import * as EpubJS from 'epubjs';
|
||||||
|
import actionSheet from '../../components/actionSheet/actionSheet';
|
||||||
|
|
||||||
import '../../elements/emby-button/paper-icon-button-light';
|
import '../../elements/emby-button/paper-icon-button-light';
|
||||||
|
|
||||||
import html from './template.html';
|
const ColorSchemes = {
|
||||||
import './style.scss';
|
'dark': {
|
||||||
|
'color': '#d8dadc',
|
||||||
const THEMES = {
|
'background': '#202124',
|
||||||
'dark': { 'body': { 'color': '#d8dadc', 'background': '#000', 'font-size': 'medium' } },
|
},
|
||||||
'sepia': { 'body': { 'color': '#d8a262', 'background': '#000', 'font-size': 'medium' } },
|
'black': {
|
||||||
'light': { 'body': { 'color': '#000', 'background': '#fff', 'font-size': 'medium' } }
|
'color': '#d8dadc',
|
||||||
|
'background': '#000',
|
||||||
|
},
|
||||||
|
'sepia': {
|
||||||
|
'color': '#d8a262',
|
||||||
|
'background': '#202124',
|
||||||
|
},
|
||||||
|
'light': {
|
||||||
|
'color': '#000',
|
||||||
|
'background': '#fff',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const THEME_ORDER = ['dark', 'sepia', 'light'];
|
|
||||||
const FONT_SIZES = ['x-small', 'small', 'medium', 'large', 'x-large'];
|
/**
|
||||||
|
* Get Cfi from href
|
||||||
|
* @param {EpubJS.Book} book
|
||||||
|
* @param {string} href
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getCfiFromHref(book, href) {
|
||||||
|
const [_, id] = href.split('#');
|
||||||
|
const section = book.spine.get(href);
|
||||||
|
return section?.cfiFromRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten chapters
|
||||||
|
* @param {EpubJS.NavItem[]} chapters
|
||||||
|
* @param {EpubJS.NavItem} [parent]
|
||||||
|
* @param {number} [depth]
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
function flattenChapters(chapters, parent, depth) {
|
||||||
|
return [].concat.apply([], chapters.map((chapter) => {
|
||||||
|
chapter.parent = parent;
|
||||||
|
chapter.depth = ~~depth;
|
||||||
|
return [].concat.apply([chapter], flattenChapters(chapter.subitems, chapter, chapter.depth+1));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert float to percent string
|
||||||
|
* @param {number} percent
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function percentToString(percent) {
|
||||||
|
return `${(percent * 100).toFixed(2).replace(/(\d)[\.0]+$/, '$1')}%`;
|
||||||
|
}
|
||||||
|
|
||||||
export class BookPlayer {
|
export class BookPlayer {
|
||||||
|
#epubDialog;
|
||||||
|
#mediaElement;
|
||||||
|
#cacheStore;
|
||||||
|
#flattenedToc;
|
||||||
|
#displayConfig;
|
||||||
|
#displayConfigItems = {
|
||||||
|
colorScheme: {
|
||||||
|
label: globalize.translate('LabelTheme'),
|
||||||
|
type: 'select',
|
||||||
|
handler: e => {
|
||||||
|
this.#displayConfig.colorScheme = e.target.value;
|
||||||
|
this.#applyDisplayConfig();
|
||||||
|
this.#saveDisplayConfig();
|
||||||
|
},
|
||||||
|
default: () => this.#displayConfig.colorScheme,
|
||||||
|
values: Object.keys(ColorSchemes),
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
label: globalize.translate('LabelFont'),
|
||||||
|
type: 'select',
|
||||||
|
handler: e => this.#displayConfigCssSimpleHandler('font-family', e.target.value),
|
||||||
|
default: () => this.#displayConfigCssSimpleHandler('font-family'),
|
||||||
|
values: {
|
||||||
|
'unset': globalize.translate('BookPlayerDisplayUnset'),
|
||||||
|
'serif': 'serif',
|
||||||
|
'sans-serif': 'sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
label: globalize.translate('LabelTextSize'),
|
||||||
|
type: 'select',
|
||||||
|
handler: e => this.#displayConfigCssSimpleHandler('font-size', e.target.value),
|
||||||
|
default: () => this.#displayConfigCssSimpleHandler('font-size'),
|
||||||
|
values: {
|
||||||
|
'unset': globalize.translate('BookPlayerDisplayUnset'),
|
||||||
|
'x-small': globalize.translate('Smaller'),
|
||||||
|
'small': globalize.translate('Small'),
|
||||||
|
'medium': globalize.translate('Normal'),
|
||||||
|
'large': globalize.translate('Large'),
|
||||||
|
'x-large': globalize.translate('Larger'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lineHeight: {
|
||||||
|
label: globalize.translate('LabelLineHeight'),
|
||||||
|
type: 'select',
|
||||||
|
handler: e => this.#displayConfigCssSimpleHandler('line-height', e.target.value),
|
||||||
|
default: () => this.#displayConfigCssSimpleHandler('line-height'),
|
||||||
|
values: {
|
||||||
|
'unset': globalize.translate('BookPlayerDisplayUnset'),
|
||||||
|
'2.025em': '2.025em',
|
||||||
|
'2.3625em': '2.3625em',
|
||||||
|
'2.7em': '2.7em',
|
||||||
|
'3.0375em': '3.0375em',
|
||||||
|
'3.375em': '3.375em',
|
||||||
|
'3.7125em': '3.7125em',
|
||||||
|
'4.05em': '4.05em',
|
||||||
|
'4.725em': '4.725em',
|
||||||
|
'5.4em': '5.4em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#loadDisplayConfig() {
|
||||||
|
try {
|
||||||
|
const loadedConfig = JSON.parse(userSettings.get('bookplayer-displayconfig', false));
|
||||||
|
for(const key in this.#displayConfig) {
|
||||||
|
if(loadedConfig[key] === undefined) continue;
|
||||||
|
if((typeof loadedConfig[key]) !== (typeof this.#displayConfig[key])) continue;
|
||||||
|
this.#displayConfig[key] = loadedConfig[key];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Event}
|
||||||
|
*/
|
||||||
|
#saveDisplayConfig() {
|
||||||
|
userSettings.set('bookplayer-displayconfig', JSON.stringify(this.#displayConfig), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#applyDisplayConfig() {
|
||||||
|
const theme = {
|
||||||
|
'body[style]': {...ColorSchemes[this.#displayConfig.colorScheme], ...this.#displayConfig.bodyCss},
|
||||||
|
};
|
||||||
|
this.rendition.themes.register('default', theme);
|
||||||
|
this.rendition.themes.select('default');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name Name of css property
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
#displayConfigCssSimpleHandler(name, value) {
|
||||||
|
if(value === undefined) {
|
||||||
|
return this.#displayConfig.bodyCss[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value) {
|
||||||
|
this.#displayConfig.bodyCss[name] = value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.#displayConfig.bodyCss[name] = 'unset';
|
||||||
|
}
|
||||||
|
this.#applyDisplayConfig();
|
||||||
|
this.#saveDisplayConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EpubJS.EpubCFI} cfi
|
||||||
|
* @returns {EpubJS.NavItem}
|
||||||
|
*/
|
||||||
|
#getChapterFromCfi(cfi) {
|
||||||
|
let i;
|
||||||
|
for(i=0;i<this.#flattenedToc.length && EpubJS.EpubCFI.prototype.compare(cfi, this.#flattenedToc[i].cfi) > 0;i++);;
|
||||||
|
return this.#flattenedToc[i-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} query
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async #getSearchResult(query) {
|
||||||
|
const {book} = this.rendition;
|
||||||
|
const resultsPerSpine = await Promise.all(book.spine.spineItems.map(item => item.load(book.load.bind(book)).then(item.find.bind(item, query)).finally(item.unload.bind(item))));
|
||||||
|
/** @type {Object.<string,{cfi:EpubJS.EpubCFI,excerpt:string,chapter:EpubJS.NavItem}>[]} */
|
||||||
|
const flattenedResults = [];
|
||||||
|
for(const results of resultsPerSpine) {
|
||||||
|
if(!results.length) continue;
|
||||||
|
const currentChapter = this.#getChapterFromCfi(results[0].cfi);
|
||||||
|
for(const result of results) {
|
||||||
|
flattenedResults.push(Object.assign({chapter: currentChapter}, result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flattenedResults;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.name = 'Book Player';
|
this.name = 'Book Player';
|
||||||
this.type = PluginType.MediaPlayer;
|
this.type = PluginType.MediaPlayer;
|
||||||
this.id = 'bookplayer';
|
this.id = 'bookplayer';
|
||||||
this.priority = 1;
|
this.priority = 1;
|
||||||
if (!userSettings.theme() || userSettings.theme() === 'dark') {
|
|
||||||
this.theme = 'dark';
|
this.#displayConfig = {
|
||||||
} else {
|
bodyCss: {},
|
||||||
this.theme = 'light';
|
colorScheme: ((userSettings.theme()||'dark') === 'dark') ? 'dark' : 'light',
|
||||||
}
|
};
|
||||||
this.fontSize = 'medium';
|
|
||||||
this.onDialogClosed = this.onDialogClosed.bind(this);
|
this.onDialogClosed = this.onDialogClosed.bind(this);
|
||||||
this.openTableOfContents = this.openTableOfContents.bind(this);
|
|
||||||
this.rotateTheme = this.rotateTheme.bind(this);
|
|
||||||
this.increaseFontSize = this.increaseFontSize.bind(this);
|
|
||||||
this.decreaseFontSize = this.decreaseFontSize.bind(this);
|
|
||||||
this.previous = this.previous.bind(this);
|
this.previous = this.previous.bind(this);
|
||||||
this.next = this.next.bind(this);
|
this.next = this.next.bind(this);
|
||||||
|
this.gotoPositionAsSlider = this.gotoPositionAsSlider.bind(this);
|
||||||
this.onWindowKeyDown = this.onWindowKeyDown.bind(this);
|
this.onWindowKeyDown = this.onWindowKeyDown.bind(this);
|
||||||
|
this.onWindowWheel = this.onWindowWheel.bind(this);
|
||||||
this.addSwipeGestures = this.addSwipeGestures.bind(this);
|
this.addSwipeGestures = this.addSwipeGestures.bind(this);
|
||||||
|
this.getBubbleHtml = this.getBubbleHtml.bind(this);
|
||||||
|
this.openTableOfContents = this.openTableOfContents.bind(this);
|
||||||
|
this.openDisplayConfig = this.openDisplayConfig.bind(this);
|
||||||
|
this.openSearch = this.openSearch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
play(options) {
|
async play(options) {
|
||||||
|
window._bookPlayer = this;
|
||||||
this.progress = 0;
|
this.progress = 0;
|
||||||
this.cancellationToken = false;
|
this.cancellationToken = false;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
|
||||||
loading.show();
|
loading.show();
|
||||||
const elem = this.createMediaElement();
|
this.#cacheStore = await caches?.open('epubPlayer');
|
||||||
return this.setCurrentSrc(elem, options);
|
this.#loadDisplayConfig();
|
||||||
|
|
||||||
|
const elem = await this.createMediaElement(options);
|
||||||
|
await this.setCurrentSrc(elem, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -68,15 +261,9 @@ export class BookPlayer {
|
||||||
|
|
||||||
Events.trigger(this, 'stopped', [stopInfo]);
|
Events.trigger(this, 'stopped', [stopInfo]);
|
||||||
|
|
||||||
const elem = this.mediaElement;
|
|
||||||
const tocElement = this.tocElement;
|
const tocElement = this.tocElement;
|
||||||
const rendition = this.rendition;
|
const rendition = this.rendition;
|
||||||
|
|
||||||
if (elem) {
|
|
||||||
dialogHelper.close(elem);
|
|
||||||
this.mediaElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tocElement) {
|
if (tocElement) {
|
||||||
tocElement.destroy();
|
tocElement.destroy();
|
||||||
this.tocElement = null;
|
this.tocElement = null;
|
||||||
|
@ -89,10 +276,19 @@ export class BookPlayer {
|
||||||
// hide loader in case player was not fully loaded yet
|
// hide loader in case player was not fully loaded yet
|
||||||
loading.hide();
|
loading.hide();
|
||||||
this.cancellationToken = true;
|
this.cancellationToken = true;
|
||||||
|
|
||||||
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
// Nothing to do here
|
document.body.classList.remove('hide-scroll');
|
||||||
|
|
||||||
|
const dlg = this.#epubDialog;
|
||||||
|
if (dlg) {
|
||||||
|
this.#epubDialog = null;
|
||||||
|
|
||||||
|
dlg.parentNode.removeChild(dlg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentItem() {
|
currentItem() {
|
||||||
|
@ -130,6 +326,15 @@ export class BookPlayer {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onWindowWheel(e) {
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
this.previous();
|
||||||
|
}
|
||||||
|
else if(e.deltaY > 0) {
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onWindowKeyDown(e) {
|
onWindowKeyDown(e) {
|
||||||
// Skip modified keys
|
// Skip modified keys
|
||||||
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
|
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
|
||||||
|
@ -150,16 +355,6 @@ export class BookPlayer {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.previous();
|
this.previous();
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.tocElement) {
|
|
||||||
// Close table of contents on ESC if it is open
|
|
||||||
this.tocElement.destroy();
|
|
||||||
} else {
|
|
||||||
// Otherwise stop the entire book player
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,54 +369,54 @@ export class BookPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
bindMediaElementEvents() {
|
bindMediaElementEvents() {
|
||||||
const elem = this.mediaElement;
|
this.#epubDialog.addEventListener('close', this.onDialogClosed, { once: true });
|
||||||
|
this.#epubDialog.querySelector('.headerBackButton').addEventListener('click', this.onDialogClosed, { once: true });
|
||||||
elem.addEventListener('close', this.onDialogClosed, { once: true });
|
this.#epubDialog.querySelector('.headerTocButton').addEventListener('click', this.openTableOfContents);
|
||||||
elem.querySelector('#btnBookplayerExit').addEventListener('click', this.onDialogClosed, { once: true });
|
this.#epubDialog.querySelector('.headerFullscreenButton').addEventListener('click', this.toggleFullscreen);
|
||||||
elem.querySelector('#btnBookplayerToc').addEventListener('click', this.openTableOfContents);
|
this.#epubDialog.querySelector('.headerTextformatButton').addEventListener('click', this.openDisplayConfig);
|
||||||
elem.querySelector('#btnBookplayerFullscreen').addEventListener('click', this.toggleFullscreen);
|
this.#epubDialog.querySelector('.headerSearchButton').addEventListener('click', this.openSearch);
|
||||||
elem.querySelector('#btnBookplayerRotateTheme').addEventListener('click', this.rotateTheme);
|
this.#epubDialog.querySelector('.footerPrevButton').addEventListener('click', this.previous);
|
||||||
elem.querySelector('#btnBookplayerIncreaseFontSize').addEventListener('click', this.increaseFontSize);
|
this.#epubDialog.querySelector('.footerNextButton').addEventListener('click', this.next);
|
||||||
elem.querySelector('#btnBookplayerDecreaseFontSize').addEventListener('click', this.decreaseFontSize);
|
this.#epubDialog.querySelector('.epubPositionSlider').addEventListener('change', this.gotoPositionAsSlider);
|
||||||
elem.querySelector('#btnBookplayerPrev')?.addEventListener('click', this.previous);
|
this.#epubDialog.querySelector('.epubPositionSlider').getBubbleHtml = this.getBubbleHtml;
|
||||||
elem.querySelector('#btnBookplayerNext')?.addEventListener('click', this.next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
this.bindMediaElementEvents();
|
this.bindMediaElementEvents();
|
||||||
|
|
||||||
document.addEventListener('keydown', this.onWindowKeyDown);
|
// document.addEventListener('keydown', this.onWindowKeyDown);
|
||||||
this.rendition?.on('keydown', this.onWindowKeyDown);
|
this.rendition?.on('keydown', this.onWindowKeyDown);
|
||||||
|
// document.addEventListener('wheel', this.onWindowWheel);
|
||||||
|
this.rendition?.on('rendered', (e, i) => i.document.addEventListener('wheel', this.onWindowWheel));
|
||||||
|
|
||||||
if (browser.safari) {
|
if (browser.safari) {
|
||||||
const player = document.getElementById('bookPlayerContainer');
|
this.addSwipeGestures(this.#mediaElement);
|
||||||
this.addSwipeGestures(player);
|
|
||||||
} else {
|
} else {
|
||||||
this.rendition?.on('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
|
this.rendition?.on('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unbindMediaElementEvents() {
|
unbindMediaElementEvents() {
|
||||||
const elem = this.mediaElement;
|
this.#epubDialog.removeEventListener('close', this.onDialogClosed, { once: true });
|
||||||
|
this.#epubDialog.querySelector('.headerBackButton').removeEventListener('click', this.onDialogClosed, { once: true });
|
||||||
elem.removeEventListener('close', this.onDialogClosed);
|
this.#epubDialog.querySelector('.headerTocButton').removeEventListener('click', this.openTableOfContents);
|
||||||
elem.querySelector('#btnBookplayerExit').removeEventListener('click', this.onDialogClosed);
|
this.#epubDialog.querySelector('.headerFullscreenButton').removeEventListener('click', this.toggleFullscreen);
|
||||||
elem.querySelector('#btnBookplayerToc').removeEventListener('click', this.openTableOfContents);
|
this.#epubDialog.querySelector('.headerTextformatButton').removeEventListener('click', this.openDisplayConfig);
|
||||||
elem.querySelector('#btnBookplayerFullscreen').removeEventListener('click', this.toggleFullscreen);
|
this.#epubDialog.querySelector('.headerSearchButton').removeEventListener('click', this.openSearch);
|
||||||
elem.querySelector('#btnBookplayerRotateTheme').removeEventListener('click', this.rotateTheme);
|
this.#epubDialog.querySelector('.footerPrevButton').removeEventListener('click', this.previous);
|
||||||
elem.querySelector('#btnBookplayerIncreaseFontSize').removeEventListener('click', this.increaseFontSize);
|
this.#epubDialog.querySelector('.footerNextButton').removeEventListener('click', this.next);
|
||||||
elem.querySelector('#btnBookplayerDecreaseFontSize').removeEventListener('click', this.decreaseFontSize);
|
this.#epubDialog.querySelector('.epubPositionSlider').removeEventListener('change', this.gotoPositionAsSlider);
|
||||||
elem.querySelector('#btnBookplayerPrev')?.removeEventListener('click', this.previous);
|
|
||||||
elem.querySelector('#btnBookplayerNext')?.removeEventListener('click', this.next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unbindEvents() {
|
unbindEvents() {
|
||||||
if (this.mediaElement) {
|
if (this.#mediaElement) {
|
||||||
this.unbindMediaElementEvents();
|
this.unbindMediaElementEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener('keydown', this.onWindowKeyDown);
|
document.removeEventListener('keydown', this.onWindowKeyDown);
|
||||||
this.rendition?.off('keydown', this.onWindowKeyDown);
|
this.rendition?.off('keydown', this.onWindowKeyDown);
|
||||||
|
// document.removeEventListener('wheel', this.onWindowWheel);
|
||||||
|
this.rendition?.off('rendered', (e, i) => i.document.addEventListener('wheel', this.onWindowWheel));
|
||||||
|
|
||||||
if (!browser.safari) {
|
if (!browser.safari) {
|
||||||
this.rendition?.off('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
|
this.rendition?.off('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
|
||||||
|
@ -230,46 +425,202 @@ export class BookPlayer {
|
||||||
this.touchHelper?.destroy();
|
this.touchHelper?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
openTableOfContents() {
|
async openTableOfContents(e) {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.tocElement = new TableOfContents(this);
|
const currentChapter = this.#getChapterFromCfi(this.rendition.location.start.cfi) || {id: null};
|
||||||
|
const {book} = this.rendition;
|
||||||
|
const menuOptions = {
|
||||||
|
title: globalize.translate('Toc'),
|
||||||
|
items: this.#flattenedToc.map(chapter => ({
|
||||||
|
id: `${book.path.directory}${chapter.href.startsWith('../') ? chapter.href.slice(3) : chapter.href}`,
|
||||||
|
name: chapter.label.replace(/^\s+|\s+$/g,''),
|
||||||
|
icon: (
|
||||||
|
currentChapter.id === chapter.id ? 'chevron_right'
|
||||||
|
: ''
|
||||||
|
) + (
|
||||||
|
chapter.depth > 0 ? ` indent-${Math.min(9, chapter.depth)}`
|
||||||
|
: ''
|
||||||
|
),
|
||||||
|
asideText: percentToString(book.locations.percentageFromCfi(chapter.cfi)),
|
||||||
|
})),
|
||||||
|
positionTo: e.target,
|
||||||
|
resolveOnClick: true,
|
||||||
|
border: true
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const id = await actionSheet.show(menuOptions);
|
||||||
|
this.rendition.display(book.path.relative(id));
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openSearch(e) {
|
||||||
|
let inputTimeout = null;
|
||||||
|
const displayConfigDlg = dialogHelper.createDialog({
|
||||||
|
exitAnimationDuration: 200,
|
||||||
|
size: 'epub300',
|
||||||
|
autoFocus: false,
|
||||||
|
scrollY: false,
|
||||||
|
exitAnimation: 'fadeout',
|
||||||
|
removeOnClose: true
|
||||||
|
});
|
||||||
|
displayConfigDlg.innerHTML = translateHtml(await import('./search.html'));
|
||||||
|
|
||||||
|
const inputElem = displayConfigDlg.querySelector('input[type="search"]');
|
||||||
|
const resultContainer = displayConfigDlg.querySelector('.actionSheetScroller');
|
||||||
|
const annotations = [];
|
||||||
|
const removeAnnotations = () => {
|
||||||
|
while(annotations.length) {
|
||||||
|
const annotation = annotations.pop();
|
||||||
|
this.rendition.annotations.remove(annotation.cfiRange, 'highlight');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onSearch = async () => {
|
||||||
|
removeAnnotations();
|
||||||
|
let currentChapter = null;
|
||||||
|
const results = (await this.#getSearchResult(inputElem.value)).map(row => {
|
||||||
|
const {cfi} = row;
|
||||||
|
annotations.push(this.rendition.annotations.highlight(cfi));
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.setAttribute('is', 'emby-button');
|
||||||
|
button.setAttribute('type', 'button');
|
||||||
|
button.setAttribute('data-cfi', row.cfi);
|
||||||
|
button.addEventListener('click', e=>this.rendition.display(cfi));
|
||||||
|
button.classList.add(
|
||||||
|
'listItem',
|
||||||
|
'listItem-button',
|
||||||
|
'actionSheetMenuItem',
|
||||||
|
'listItem-border',
|
||||||
|
'emby-button',
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.classList.add(
|
||||||
|
'listItemBody',
|
||||||
|
'actionsheetListItemBody',
|
||||||
|
);
|
||||||
|
|
||||||
|
if(row.chapter !== currentChapter) {
|
||||||
|
currentChapter = row.chapter;
|
||||||
|
const text = document.createElement('div');
|
||||||
|
text.classList.add(
|
||||||
|
'listItemBodyText',
|
||||||
|
'actionSheetItemText',
|
||||||
|
);
|
||||||
|
text.textContent = currentChapter.label;
|
||||||
|
body.appendChild(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const text = document.createElement('div');
|
||||||
|
text.classList.add(
|
||||||
|
'listItemBodyText',
|
||||||
|
'secondary',
|
||||||
|
);
|
||||||
|
text.textContent = row.excerpt;
|
||||||
|
body.appendChild(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.appendChild(body);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
});
|
||||||
|
resultContainer.replaceChildren(...results);
|
||||||
|
};
|
||||||
|
|
||||||
|
inputElem.addEventListener('input', () => {
|
||||||
|
if(inputTimeout) {
|
||||||
|
clearTimeout(inputTimeout);
|
||||||
|
inputTimeout = null;
|
||||||
|
}
|
||||||
|
inputTimeout = setTimeout(onSearch, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
displayConfigDlg.addEventListener('close', () => {
|
||||||
|
removeAnnotations();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogHelper.open(displayConfigDlg);
|
||||||
|
|
||||||
|
const pos = actionSheet.getPosition(e.target, {}, displayConfigDlg);
|
||||||
|
displayConfigDlg.style.position = 'fixed';
|
||||||
|
displayConfigDlg.style.margin = '0';
|
||||||
|
displayConfigDlg.style.left = pos.left + 'px';
|
||||||
|
displayConfigDlg.style.top = pos.top + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
async openDisplayConfig(e) {
|
||||||
|
const displayConfigDlg = dialogHelper.createDialog({
|
||||||
|
exitAnimationDuration: 200,
|
||||||
|
size: 'epub300',
|
||||||
|
autoFocus: false,
|
||||||
|
scrollY: false,
|
||||||
|
exitAnimation: 'fadeout',
|
||||||
|
removeOnClose: true
|
||||||
|
});
|
||||||
|
displayConfigDlg.innerHTML = translateHtml(await import('./textformat.html'));
|
||||||
|
displayConfigDlg.querySelector('.btnClose').addEventListener('click', e=>dialogHelper.close(displayConfigDlg));
|
||||||
|
|
||||||
|
const form = displayConfigDlg.querySelector('.editEpubDisplaySettingsForm');
|
||||||
|
for(const key in this.#displayConfigItems) {
|
||||||
|
const item = this.#displayConfigItems[key];
|
||||||
|
switch(item.type) {
|
||||||
|
case 'select':{
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const select = document.createElement('select');
|
||||||
|
container.classList.add('selectContainer');
|
||||||
|
select.setAttribute('label', item.label);
|
||||||
|
select.setAttribute('is', 'emby-select');
|
||||||
|
/** @type {Object.<string, string>} */
|
||||||
|
const values = (list=>{
|
||||||
|
if(typeof list !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if(list instanceof Array) {
|
||||||
|
return list.reduce((obj, curr) => {
|
||||||
|
obj[curr] = curr;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
})(item.values);
|
||||||
|
|
||||||
|
for(const value in values) {
|
||||||
|
const label = values[value];
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.setAttribute('value', value);
|
||||||
|
option.textContent = label;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', item.handler);
|
||||||
|
|
||||||
|
let defaultValue = item.default;
|
||||||
|
if(typeof defaultValue === 'function') {
|
||||||
|
defaultValue = defaultValue();
|
||||||
|
}
|
||||||
|
if(typeof defaultValue === 'string') {
|
||||||
|
select.value = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(select);
|
||||||
|
form.appendChild(container);
|
||||||
|
}break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogHelper.open(displayConfigDlg);
|
||||||
|
}
|
||||||
|
|
||||||
toggleFullscreen() {
|
toggleFullscreen() {
|
||||||
if (Screenfull.isEnabled) {
|
if (Screenfull.isEnabled) {
|
||||||
const icon = document.querySelector('#btnBookplayerFullscreen .material-icons');
|
|
||||||
icon.classList.remove(Screenfull.isFullscreen ? 'fullscreen_exit' : 'fullscreen');
|
|
||||||
icon.classList.add(Screenfull.isFullscreen ? 'fullscreen' : 'fullscreen_exit');
|
|
||||||
Screenfull.toggle();
|
Screenfull.toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rotateTheme() {
|
|
||||||
if (this.loaded) {
|
|
||||||
const newTheme = THEME_ORDER[(THEME_ORDER.indexOf(this.theme) + 1) % THEME_ORDER.length];
|
|
||||||
this.rendition.themes.register('default', THEMES[newTheme]);
|
|
||||||
this.rendition.themes.update('default');
|
|
||||||
this.theme = newTheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
increaseFontSize() {
|
|
||||||
if (this.loaded && this.fontSize !== FONT_SIZES[FONT_SIZES.length - 1]) {
|
|
||||||
const newFontSize = FONT_SIZES[(FONT_SIZES.indexOf(this.fontSize) + 1)];
|
|
||||||
this.rendition.themes.fontSize(newFontSize);
|
|
||||||
this.fontSize = newFontSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decreaseFontSize() {
|
|
||||||
if (this.loaded && this.fontSize !== FONT_SIZES[0]) {
|
|
||||||
const newFontSize = FONT_SIZES[(FONT_SIZES.indexOf(this.fontSize) - 1)];
|
|
||||||
this.rendition.themes.fontSize(newFontSize);
|
|
||||||
this.fontSize = newFontSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previous(e) {
|
previous(e) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if (this.rendition) {
|
if (this.rendition) {
|
||||||
|
@ -284,34 +635,79 @@ export class BookPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createMediaElement() {
|
gotoPositionAsSlider(e) {
|
||||||
let elem = this.mediaElement;
|
console.log(e);
|
||||||
if (elem) {
|
const input = e.target;
|
||||||
return elem;
|
if (this.rendition) {
|
||||||
|
this.rendition.display(input.value/100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elem = document.getElementById('bookPlayer');
|
getBubbleHtml(value) {
|
||||||
if (!elem) {
|
const cfi = this.rendition.book.locations.cfiFromPercentage(value/100);
|
||||||
elem = dialogHelper.createDialog({
|
return this.#getChapterFromCfi(cfi).label;
|
||||||
exitAnimationDuration: 400,
|
|
||||||
size: 'fullscreen',
|
|
||||||
autoFocus: false,
|
|
||||||
scrollY: false,
|
|
||||||
exitAnimation: 'fadeout',
|
|
||||||
removeOnClose: true
|
|
||||||
});
|
|
||||||
|
|
||||||
elem.id = 'bookPlayer';
|
|
||||||
elem.innerHTML = translateHtml(html);
|
|
||||||
|
|
||||||
dialogHelper.open(elem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mediaElement = elem;
|
async createMediaElement(options) {
|
||||||
return elem;
|
const dlg = document.querySelector('.epubPlayerContainer');
|
||||||
|
|
||||||
|
if (!dlg) {
|
||||||
|
await import('./style.scss');
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
const playerDlg = document.createElement('div');
|
||||||
|
playerDlg.setAttribute('dir', 'ltr');
|
||||||
|
|
||||||
|
playerDlg.classList.add('epubPlayerContainer');
|
||||||
|
|
||||||
|
if (options.fullscreen) {
|
||||||
|
playerDlg.classList.add('epubPlayerContainer-onTop');
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentSrc(elem, options) {
|
playerDlg.innerHTML = translateHtml(await import('./template.html'));
|
||||||
|
|
||||||
|
document.body.insertBefore(playerDlg, document.body.firstChild);
|
||||||
|
this.#epubDialog = playerDlg;
|
||||||
|
this.#mediaElement = playerDlg.querySelector('.epubPlayer');
|
||||||
|
|
||||||
|
if (options.fullscreen) {
|
||||||
|
// At this point, we must hide the scrollbar placeholder, so it's not being displayed while the item is being loaded
|
||||||
|
document.body.classList.add('hide-scroll');
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
this.#epubDialog.querySelector('.epubMediaStatusText').textContent = globalize.translate('BookStatusFetching');
|
||||||
|
this.#epubDialog.querySelector('.epubMediaStatus').classList.remove('hide');
|
||||||
|
|
||||||
|
return playerDlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dlg;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #fetchEpub(url) {
|
||||||
|
const epubRequest = new Request(url);
|
||||||
|
const epubResponse = await (async () => {
|
||||||
|
const cacheResponse = await this.#cacheStore?.match(epubRequest);
|
||||||
|
const cacheLastModified = cacheResponse?.headers.get('last-modified');
|
||||||
|
const originRequest = epubRequest.clone();
|
||||||
|
if(cacheLastModified) {
|
||||||
|
originRequest.headers.set('if-modified-since', cacheLastModified);
|
||||||
|
}
|
||||||
|
const originResponse = await fetch(originRequest);
|
||||||
|
if(originResponse.status === 304) {
|
||||||
|
return cacheResponse;
|
||||||
|
}
|
||||||
|
if(originResponse.status >= 200 && originResponse.status < 300) {
|
||||||
|
this.#cacheStore?.put(epubRequest, originResponse.clone());
|
||||||
|
return originResponse;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Origin returned unexpected response code ${originResponse.status}`);
|
||||||
|
})()
|
||||||
|
return URL.createObjectURL(await epubResponse.blob());
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCurrentSrc(elem, options) {
|
||||||
const item = options.items[0];
|
const item = options.items[0];
|
||||||
this.item = item;
|
this.item = item;
|
||||||
this.streamInfo = {
|
this.streamInfo = {
|
||||||
|
@ -327,40 +723,40 @@ export class BookPlayer {
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
const apiClient = ServerConnections.getApiClient(serverId);
|
||||||
|
|
||||||
if (!Screenfull.isEnabled) {
|
if (!Screenfull.isEnabled) {
|
||||||
document.getElementById('btnBookplayerFullscreen').display = 'none';
|
this.#epubDialog.querySelector('.headerFullscreenButton').display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
this.#epubDialog.querySelector('.pageTitle').textContent = item.Name;
|
||||||
import('epubjs').then(({ default: epubjs }) => {
|
const epubBlobUrl = await this.#fetchEpub(apiClient.getItemDownloadUrl(item.Id));
|
||||||
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
|
const positionSlider = this.#epubDialog.querySelector('.epubPositionSlider');
|
||||||
const book = epubjs(downloadHref, { openAs: 'epub' });
|
const positionText = this.#epubDialog.querySelector('.epubPositionText');
|
||||||
|
|
||||||
// We need to calculate the height of the window beforehand because using 100% is not accurate when the dialog is opening.
|
this.#epubDialog.querySelector('.epubMediaStatusText').textContent = globalize.translate('BookStatusProcessing');
|
||||||
// In addition we don't render to the full height so that we have space for the top buttons.
|
|
||||||
const clientHeight = document.body.clientHeight;
|
|
||||||
const renderHeight = clientHeight - (clientHeight * 0.0425);
|
|
||||||
|
|
||||||
const rendition = book.renderTo('bookPlayerContainer', {
|
const book = new EpubJS.Book(epubBlobUrl, {
|
||||||
width: '100%',
|
openAs: 'epub',
|
||||||
height: renderHeight,
|
|
||||||
// TODO: Add option for scrolled-doc
|
|
||||||
flow: 'paginated'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentSrc = downloadHref;
|
const rendition = book.renderTo(this.#mediaElement, {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
flow: 'paginated',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSrc = epubBlobUrl;
|
||||||
this.rendition = rendition;
|
this.rendition = rendition;
|
||||||
|
|
||||||
rendition.themes.register('default', THEMES[this.theme]);
|
this.#applyDisplayConfig();
|
||||||
rendition.themes.select('default');
|
|
||||||
|
await rendition.display();
|
||||||
|
|
||||||
return rendition.display().then(() => {
|
|
||||||
const epubElem = document.querySelector('.epub-container');
|
const epubElem = document.querySelector('.epub-container');
|
||||||
epubElem.style.opacity = '0';
|
epubElem.style.opacity = '0';
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
|
||||||
return this.rendition.book.locations.generate(1024).then(async () => {
|
await this.rendition.book.locations.generate();
|
||||||
if (this.cancellationToken) reject();
|
if (this.cancellationToken) throw new Error;
|
||||||
|
|
||||||
const percentageTicks = options.startPositionTicks / 10000000;
|
const percentageTicks = options.startPositionTicks / 10000000;
|
||||||
if (percentageTicks !== 0.0) {
|
if (percentageTicks !== 0.0) {
|
||||||
|
@ -368,22 +764,27 @@ export class BookPlayer {
|
||||||
await rendition.display(resumeLocation);
|
await rendition.display(resumeLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#flattenedToc = flattenChapters(book.navigation.toc).map(x=>{
|
||||||
|
x.label = x.label.replace(/^\s+|\s+$/g,'');
|
||||||
|
x.cfi = getCfiFromHref(book, x.href);
|
||||||
|
return x;
|
||||||
|
}).filter(x=>x.cfi).sort((a,b) => EpubJS.EpubCFI.prototype.compare(a.cfi,b.cfi));
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
epubElem.style.opacity = '';
|
epubElem.style.opacity = '';
|
||||||
rendition.on('relocated', (locations) => {
|
rendition.on('relocated', (locations) => {
|
||||||
this.progress = book.locations.percentageFromCfi(locations.start.cfi);
|
if(this.progress != locations.start.percentage) {
|
||||||
|
this.progress = locations.start.percentage;
|
||||||
Events.trigger(this, 'pause');
|
Events.trigger(this, 'pause');
|
||||||
|
}
|
||||||
|
positionSlider.value = locations.start.percentage * 100;
|
||||||
|
positionText.textContent = percentToString(locations.start.percentage);
|
||||||
});
|
});
|
||||||
|
|
||||||
loading.hide();
|
this.#epubDialog.querySelector('.epubMediaStatus').classList.add('hide');
|
||||||
return resolve();
|
this.#epubDialog.querySelector('.footerPrevButton').disabled=false;
|
||||||
});
|
this.#epubDialog.querySelector('.footerNextButton').disabled=false;
|
||||||
}, () => {
|
this.#epubDialog.querySelector('.epubPositionSlider').disabled=false;
|
||||||
console.error('failed to display epub');
|
|
||||||
return reject();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canPlayMediaType(mediaType) {
|
canPlayMediaType(mediaType) {
|
||||||
|
|
6
src/plugins/bookPlayer/search.html
Normal file
6
src/plugins/bookPlayer/search.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="actionSheetContent">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<input class="emby-input" type="search" placeholder="${Search}" autocomplete="off" maxlength="40">
|
||||||
|
</div>
|
||||||
|
<div class="actionSheetScroller flex-grow scrollY"></div>
|
||||||
|
</div>
|
|
@ -1,82 +1,113 @@
|
||||||
#bookPlayer {
|
.epubPlayerContainer {
|
||||||
position: relative;
|
position: fixed;
|
||||||
height: 100%;
|
top: 0;
|
||||||
width: 100%;
|
bottom: 0;
|
||||||
overflow: auto;
|
left: 0;
|
||||||
z-index: 100;
|
right: 0;
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: #202124;
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
|
||||||
.topButtons {
|
.epubPlayerWrapper {
|
||||||
z-index: 1002;
|
aspect-ratio: 1.6;
|
||||||
width: 100%;
|
position: relative;
|
||||||
color: #000;
|
|
||||||
opacity: 0.7;
|
.epubPlayer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookPlayerContainer {
|
.epubPlayerContainer-onTop {
|
||||||
flex-grow: 1;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#btnBookplayerToc {
|
.epubPlayerFooter {
|
||||||
float: left;
|
padding: 1em;
|
||||||
margin-left: 2vw;
|
box-sizing: border-box;
|
||||||
}
|
|
||||||
|
|
||||||
#btnBookplayerExit {
|
.epubPositionText {
|
||||||
float: right;
|
width: 3em;
|
||||||
margin-right: 2vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookplayerErrorMsg {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#btnBookplayerPrev,
|
@keyframes spin {
|
||||||
#btnBookplayerNext {
|
100% {
|
||||||
margin: 0.5vh 0.5vh;
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#dialogToc {
|
.epubMediaStatus {
|
||||||
background-color: white;
|
position: relative;
|
||||||
height: fit-content;
|
pointer-events: none;
|
||||||
width: fit-content;
|
|
||||||
max-height: 80%;
|
|
||||||
max-width: 60%;
|
|
||||||
padding-right: 50px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
|
|
||||||
.bookplayerButtonIcon {
|
.epubMediaStatusWrapper {
|
||||||
color: black;
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc li {
|
.animate {
|
||||||
margin-bottom: 5px;
|
animation: spin 4s linear infinite;
|
||||||
|
|
||||||
list-style-type: none;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link {
|
.actionsheetMenuItemIcon {
|
||||||
color: #000;
|
&.indent-1 {
|
||||||
text-decoration: none;
|
padding-right: .5em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:active,
|
&.indent-2 {
|
||||||
a:hover {
|
padding-right: 1em !important;
|
||||||
color: #00a4dc;
|
}
|
||||||
text-decoration: none;
|
|
||||||
|
&.indent-3 {
|
||||||
|
padding-right: 1.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indent-4 {
|
||||||
|
padding-right: 2em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indent-5 {
|
||||||
|
padding-right: 2.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indent-6 {
|
||||||
|
padding-right: 3em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indent-7 {
|
||||||
|
padding-right: 3.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indent-8 {
|
||||||
|
padding-right: 4em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indent-9 {
|
||||||
|
padding-right: 4.5em !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-epub300 {
|
||||||
|
width: 30em;
|
||||||
|
height: 40em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editEpubDisplaySettingsForm {
|
||||||
|
padding-top: 2em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
import escapeHTML from 'escape-html';
|
|
||||||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
|
||||||
|
|
||||||
export default class TableOfContents {
|
|
||||||
constructor(bookPlayer) {
|
|
||||||
this.bookPlayer = bookPlayer;
|
|
||||||
this.rendition = bookPlayer.rendition;
|
|
||||||
|
|
||||||
this.onDialogClosed = this.onDialogClosed.bind(this);
|
|
||||||
|
|
||||||
this.createMediaElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
const elem = this.elem;
|
|
||||||
if (elem) {
|
|
||||||
this.unbindEvents();
|
|
||||||
dialogHelper.close(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bookPlayer.tocElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
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;
|
|
||||||
|
|
||||||
elem.removeEventListener('close', this.onDialogClosed);
|
|
||||||
elem.querySelector('.btnBookplayerTocClose').removeEventListener('click', this.onDialogClosed);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDialogClosed() {
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceLinks(contents, f) {
|
|
||||||
const links = contents.querySelectorAll('a[href]');
|
|
||||||
|
|
||||||
links.forEach((link) => {
|
|
||||||
const href = link.getAttribute('href');
|
|
||||||
|
|
||||||
link.onclick = () => {
|
|
||||||
f(href);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
chapterTocItem(book, chapter) {
|
|
||||||
let itemHtml = '<li>';
|
|
||||||
|
|
||||||
// remove parent directory reference from href to fix certain books
|
|
||||||
const link = chapter.href.startsWith('../') ? chapter.href.slice(3) : chapter.href;
|
|
||||||
itemHtml += `<a href="${escapeHTML(book.path.directory + link)}">${escapeHTML(chapter.label)}</a>`;
|
|
||||||
|
|
||||||
if (chapter.subitems?.length) {
|
|
||||||
const subHtml = chapter.subitems
|
|
||||||
.map((nestedChapter) => this.chapterTocItem(book, nestedChapter))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
itemHtml += `<ul>${subHtml}</ul>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemHtml += '</li>';
|
|
||||||
return itemHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMediaElement() {
|
|
||||||
const rendition = this.rendition;
|
|
||||||
|
|
||||||
const elem = dialogHelper.createDialog({
|
|
||||||
size: 'small',
|
|
||||||
autoFocus: false,
|
|
||||||
removeOnClose: true
|
|
||||||
});
|
|
||||||
|
|
||||||
elem.id = 'dialogToc';
|
|
||||||
|
|
||||||
let tocHtml = '<div class="topRightActionButtons">';
|
|
||||||
tocHtml += '<button is="paper-icon-button-light" class="autoSize bookplayerButton btnBookplayerTocClose hide-mouse-idle-tv" tabindex="-1"><span class="material-icons bookplayerButtonIcon close" aria-hidden="true"></span></button>';
|
|
||||||
tocHtml += '</div>';
|
|
||||||
tocHtml += '<ul class="toc">';
|
|
||||||
rendition.book.navigation.forEach((chapter) => {
|
|
||||||
tocHtml += this.chapterTocItem(rendition.book, chapter);
|
|
||||||
});
|
|
||||||
|
|
||||||
tocHtml += '</ul>';
|
|
||||||
elem.innerHTML = tocHtml;
|
|
||||||
|
|
||||||
this.replaceLinks(elem, (href) => {
|
|
||||||
const relative = rendition.book.path.relative(href);
|
|
||||||
rendition.display(relative);
|
|
||||||
this.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.elem = elem;
|
|
||||||
|
|
||||||
this.bindEvents();
|
|
||||||
dialogHelper.open(elem);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +1,52 @@
|
||||||
<div class="topButtons">
|
<div class="epubPlayerHeader w-100 skinHeader-withBackground">
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerToc" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<div class="flex align-items-center headerTop">
|
||||||
<span class="material-icons bookplayerButtonIcon toc" aria-hidden="true"></span>
|
<div class="headerLeft">
|
||||||
|
<button is="paper-icon-button-light" class="headerBackButton headerButton headerButtonLeft paper-icon-button-light" title="${Previous}">
|
||||||
|
<span class="material-icons arrow_back" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerPrev" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<h3 class="pageTitle" aria-hidden="true"></h3>
|
||||||
<span class="material-icons bookplayerButtonIcon navigate_before" aria-hidden="true"></span>
|
</div>
|
||||||
|
<div class="headerRight">
|
||||||
|
<button is="paper-icon-button-light" class="headerFullscreenButton headerButton headerButtonRight paper-icon-button-light" title="${Fullscreen}">
|
||||||
|
<span class="material-icons fullscreen" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerNext" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<button is="paper-icon-button-light" class="headerSearchButton headerButton headerButtonRight paper-icon-button-light" title="${Search}">
|
||||||
<span class="material-icons bookplayerButtonIcon navigate_next" aria-hidden="true"></span>
|
<span class="material-icons search" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerExit" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<button is="paper-icon-button-light" class="headerTextformatButton headerButton headerButtonRight paper-icon-button-light" title="${LabelFont}">
|
||||||
<span class="material-icons bookplayerButtonIcon close" aria-hidden="true"></span>
|
<span class="material-icons text_format" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerRotateTheme" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<button is="paper-icon-button-light" class="headerTocButton headerButton headerButtonRight paper-icon-button-light" title="${Toc}">
|
||||||
<span class="material-icons bookplayerButtonIcon remove_red_eye" aria-hidden="true"></span>
|
<span class="material-icons toc" aria-hidden="true"></span>
|
||||||
</button>
|
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerDecreaseFontSize" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
|
||||||
<span class="material-icons bookplayerButtonIcon text_decrease" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerIncreaseFontSize" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
|
||||||
<span class="material-icons bookplayerButtonIcon text_increase" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerFullscreen" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
|
||||||
<span class="material-icons bookplayerButtonIcon fullscreen" aria-hidden="true"></span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="bookPlayerContainer" class="bookPlayerContainer"></div>
|
<div class="epubPlayerWrapper flex-grow">
|
||||||
|
<div class="epubPlayer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="epubPlayerFooter flex align-items-center flex-direction-column w-100">
|
||||||
|
<div class="epubMediaStatus w-100 hide">
|
||||||
|
<div class="epubMediaStatusWrapper flex justify-content-center w-100">
|
||||||
|
<div class="flex align-items-center">
|
||||||
|
<span class="material-icons animate autorenew" aria-hidden="true"></span>
|
||||||
|
<span class="epubMediaStatusText"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex align-items-center w-100">
|
||||||
|
<div class="sliderContainer flex-grow" style="margin: .5em 0 .25em;">
|
||||||
|
<div class="sliderMarkerContainer"></div>
|
||||||
|
<input type="range" step=".01" min="0" max="100" value="0" is="emby-slider" class="epubPositionSlider" data-slider-keep-progress="true" disabled>
|
||||||
|
</div>
|
||||||
|
<button is="paper-icon-button-light" class="footerPrevButton headerButton headerButtonRight paper-icon-button-light" title="${Previous}" disabled>
|
||||||
|
<span class="material-icons keyboard_arrow_left" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div class="epubPositionText"></div>
|
||||||
|
<button is="paper-icon-button-light" class="footerNextButton headerButton headerButtonRight paper-icon-button-light" title="${Next}" disabled>
|
||||||
|
<span class="material-icons keyboard_arrow_right" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
13
src/plugins/bookPlayer/textformat.html
Normal file
13
src/plugins/bookPlayer/textformat.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<div class="formDialogHeader">
|
||||||
|
<h3 class="formDialogHeaderTitle flex-grow">${BookPlayerDisplayPreferences}</h3>
|
||||||
|
<div class="dialogHeader flex align-items-center justify-content-center">
|
||||||
|
<button is="paper-icon-button-light" class="btnClose autoSize" tabindex="-1" title="${ButtonClose}">
|
||||||
|
<span class="material-icons close" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formDialogContent">
|
||||||
|
<form class="editEpubDisplaySettingsForm dialogContentInner dialog-content-centered">
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -80,6 +80,12 @@
|
||||||
"BirthDateValue": "Born: {0}",
|
"BirthDateValue": "Born: {0}",
|
||||||
"BirthLocation": "Birth location",
|
"BirthLocation": "Birth location",
|
||||||
"BirthPlaceValue": "Birth place: {0}",
|
"BirthPlaceValue": "Birth place: {0}",
|
||||||
|
"BookPlayerDisplayPreferences": "Display Preferences",
|
||||||
|
"LabelLineHeight": "Line height",
|
||||||
|
"Toc": "Table of Contents",
|
||||||
|
"BookStatusFetching": "Downloading",
|
||||||
|
"BookStatusProcessing": "Processing",
|
||||||
|
"BookPlayerDisplayUnset": "Keep default",
|
||||||
"Blacklist": "Blacklist",
|
"Blacklist": "Blacklist",
|
||||||
"BlockContentWithTagsHelp": "Hide media with at least one of the specified tags.",
|
"BlockContentWithTagsHelp": "Hide media with at least one of the specified tags.",
|
||||||
"BookLibraryHelp": "Audio and text books are supported. Review the {0} book naming guide {1}.",
|
"BookLibraryHelp": "Audio and text books are supported. Review the {0} book naming guide {1}.",
|
||||||
|
|
|
@ -12,6 +12,12 @@
|
||||||
"Backdrops": "배경",
|
"Backdrops": "배경",
|
||||||
"BirthDateValue": "출생: {0}",
|
"BirthDateValue": "출생: {0}",
|
||||||
"BirthPlaceValue": "출생지: {0}",
|
"BirthPlaceValue": "출생지: {0}",
|
||||||
|
"BookPlayerDisplayPreferences": "표시 설정",
|
||||||
|
"LabelLineHeight": "줄 간격",
|
||||||
|
"Toc": "목차",
|
||||||
|
"BookStatusFetching": "다운로드 중",
|
||||||
|
"BookStatusProcessing": "처리 중",
|
||||||
|
"BookPlayerDisplayUnset": "설정 안 함",
|
||||||
"MessageBrowsePluginCatalog": "사용 가능한 플러그인을 보려면 플러그인 카탈로그를 참고하십시오.",
|
"MessageBrowsePluginCatalog": "사용 가능한 플러그인을 보려면 플러그인 카탈로그를 참고하십시오.",
|
||||||
"ButtonAddScheduledTaskTrigger": "트리거 추가",
|
"ButtonAddScheduledTaskTrigger": "트리거 추가",
|
||||||
"ButtonAddServer": "서버 추가",
|
"ButtonAddServer": "서버 추가",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue