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;
|
||||
}
|
||||
|
||||
function getPosition(positionTo: Element, options: Options, dlg: HTMLElement) {
|
||||
export function getPosition(positionTo: Element, options: Options, dlg: HTMLElement) {
|
||||
const windowSize = dom.getWindowSize();
|
||||
const windowHeight = windowSize.innerHeight;
|
||||
const windowWidth = windowSize.innerWidth;
|
||||
|
@ -387,5 +387,6 @@ export function show(options: Options) {
|
|||
}
|
||||
|
||||
export default {
|
||||
show: show
|
||||
show,
|
||||
getPosition,
|
||||
};
|
||||
|
|
|
@ -5,58 +5,251 @@ import keyboardnavigation from '../../scripts/keyboardNavigation';
|
|||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||
import ServerConnections from '../../components/ServerConnections';
|
||||
import Screenfull from 'screenfull';
|
||||
import TableOfContents from './tableOfContents';
|
||||
import { translateHtml } from '../../lib/globalize';
|
||||
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 { PluginType } from '../../types/plugin.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 html from './template.html';
|
||||
import './style.scss';
|
||||
|
||||
const THEMES = {
|
||||
'dark': { 'body': { 'color': '#d8dadc', 'background': '#000', 'font-size': 'medium' } },
|
||||
'sepia': { 'body': { 'color': '#d8a262', 'background': '#000', 'font-size': 'medium' } },
|
||||
'light': { 'body': { 'color': '#000', 'background': '#fff', 'font-size': 'medium' } }
|
||||
const ColorSchemes = {
|
||||
'dark': {
|
||||
'color': '#d8dadc',
|
||||
'background': '#202124',
|
||||
},
|
||||
'black': {
|
||||
'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 {
|
||||
#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() {
|
||||
this.name = 'Book Player';
|
||||
this.type = PluginType.MediaPlayer;
|
||||
this.id = 'bookplayer';
|
||||
this.priority = 1;
|
||||
if (!userSettings.theme() || userSettings.theme() === 'dark') {
|
||||
this.theme = 'dark';
|
||||
} else {
|
||||
this.theme = 'light';
|
||||
}
|
||||
this.fontSize = 'medium';
|
||||
|
||||
this.#displayConfig = {
|
||||
bodyCss: {},
|
||||
colorScheme: ((userSettings.theme()||'dark') === 'dark') ? 'dark' : 'light',
|
||||
};
|
||||
|
||||
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.next = this.next.bind(this);
|
||||
this.gotoPositionAsSlider = this.gotoPositionAsSlider.bind(this);
|
||||
this.onWindowKeyDown = this.onWindowKeyDown.bind(this);
|
||||
this.onWindowWheel = this.onWindowWheel.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.cancellationToken = false;
|
||||
this.loaded = false;
|
||||
|
||||
loading.show();
|
||||
const elem = this.createMediaElement();
|
||||
return this.setCurrentSrc(elem, options);
|
||||
this.#cacheStore = await caches?.open('epubPlayer');
|
||||
this.#loadDisplayConfig();
|
||||
|
||||
const elem = await this.createMediaElement(options);
|
||||
await this.setCurrentSrc(elem, options);
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
@ -68,15 +261,9 @@ export class BookPlayer {
|
|||
|
||||
Events.trigger(this, 'stopped', [stopInfo]);
|
||||
|
||||
const elem = this.mediaElement;
|
||||
const tocElement = this.tocElement;
|
||||
const rendition = this.rendition;
|
||||
|
||||
if (elem) {
|
||||
dialogHelper.close(elem);
|
||||
this.mediaElement = null;
|
||||
}
|
||||
|
||||
if (tocElement) {
|
||||
tocElement.destroy();
|
||||
this.tocElement = null;
|
||||
|
@ -89,10 +276,19 @@ export class BookPlayer {
|
|||
// hide loader in case player was not fully loaded yet
|
||||
loading.hide();
|
||||
this.cancellationToken = true;
|
||||
|
||||
this.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() {
|
||||
|
@ -130,6 +326,15 @@ export class BookPlayer {
|
|||
return true;
|
||||
}
|
||||
|
||||
onWindowWheel(e) {
|
||||
if (e.deltaY < 0) {
|
||||
this.previous();
|
||||
}
|
||||
else if(e.deltaY > 0) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
onWindowKeyDown(e) {
|
||||
// Skip modified keys
|
||||
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
|
||||
|
@ -150,16 +355,6 @@ export class BookPlayer {
|
|||
e.preventDefault();
|
||||
this.previous();
|
||||
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() {
|
||||
const elem = this.mediaElement;
|
||||
|
||||
elem.addEventListener('close', this.onDialogClosed, { once: true });
|
||||
elem.querySelector('#btnBookplayerExit').addEventListener('click', this.onDialogClosed, { once: true });
|
||||
elem.querySelector('#btnBookplayerToc').addEventListener('click', this.openTableOfContents);
|
||||
elem.querySelector('#btnBookplayerFullscreen').addEventListener('click', this.toggleFullscreen);
|
||||
elem.querySelector('#btnBookplayerRotateTheme').addEventListener('click', this.rotateTheme);
|
||||
elem.querySelector('#btnBookplayerIncreaseFontSize').addEventListener('click', this.increaseFontSize);
|
||||
elem.querySelector('#btnBookplayerDecreaseFontSize').addEventListener('click', this.decreaseFontSize);
|
||||
elem.querySelector('#btnBookplayerPrev')?.addEventListener('click', this.previous);
|
||||
elem.querySelector('#btnBookplayerNext')?.addEventListener('click', this.next);
|
||||
this.#epubDialog.addEventListener('close', this.onDialogClosed, { once: true });
|
||||
this.#epubDialog.querySelector('.headerBackButton').addEventListener('click', this.onDialogClosed, { once: true });
|
||||
this.#epubDialog.querySelector('.headerTocButton').addEventListener('click', this.openTableOfContents);
|
||||
this.#epubDialog.querySelector('.headerFullscreenButton').addEventListener('click', this.toggleFullscreen);
|
||||
this.#epubDialog.querySelector('.headerTextformatButton').addEventListener('click', this.openDisplayConfig);
|
||||
this.#epubDialog.querySelector('.headerSearchButton').addEventListener('click', this.openSearch);
|
||||
this.#epubDialog.querySelector('.footerPrevButton').addEventListener('click', this.previous);
|
||||
this.#epubDialog.querySelector('.footerNextButton').addEventListener('click', this.next);
|
||||
this.#epubDialog.querySelector('.epubPositionSlider').addEventListener('change', this.gotoPositionAsSlider);
|
||||
this.#epubDialog.querySelector('.epubPositionSlider').getBubbleHtml = this.getBubbleHtml;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.bindMediaElementEvents();
|
||||
|
||||
document.addEventListener('keydown', this.onWindowKeyDown);
|
||||
// document.addEventListener('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) {
|
||||
const player = document.getElementById('bookPlayerContainer');
|
||||
this.addSwipeGestures(player);
|
||||
this.addSwipeGestures(this.#mediaElement);
|
||||
} else {
|
||||
this.rendition?.on('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
|
||||
}
|
||||
}
|
||||
|
||||
unbindMediaElementEvents() {
|
||||
const elem = this.mediaElement;
|
||||
|
||||
elem.removeEventListener('close', this.onDialogClosed);
|
||||
elem.querySelector('#btnBookplayerExit').removeEventListener('click', this.onDialogClosed);
|
||||
elem.querySelector('#btnBookplayerToc').removeEventListener('click', this.openTableOfContents);
|
||||
elem.querySelector('#btnBookplayerFullscreen').removeEventListener('click', this.toggleFullscreen);
|
||||
elem.querySelector('#btnBookplayerRotateTheme').removeEventListener('click', this.rotateTheme);
|
||||
elem.querySelector('#btnBookplayerIncreaseFontSize').removeEventListener('click', this.increaseFontSize);
|
||||
elem.querySelector('#btnBookplayerDecreaseFontSize').removeEventListener('click', this.decreaseFontSize);
|
||||
elem.querySelector('#btnBookplayerPrev')?.removeEventListener('click', this.previous);
|
||||
elem.querySelector('#btnBookplayerNext')?.removeEventListener('click', this.next);
|
||||
this.#epubDialog.removeEventListener('close', this.onDialogClosed, { once: true });
|
||||
this.#epubDialog.querySelector('.headerBackButton').removeEventListener('click', this.onDialogClosed, { once: true });
|
||||
this.#epubDialog.querySelector('.headerTocButton').removeEventListener('click', this.openTableOfContents);
|
||||
this.#epubDialog.querySelector('.headerFullscreenButton').removeEventListener('click', this.toggleFullscreen);
|
||||
this.#epubDialog.querySelector('.headerTextformatButton').removeEventListener('click', this.openDisplayConfig);
|
||||
this.#epubDialog.querySelector('.headerSearchButton').removeEventListener('click', this.openSearch);
|
||||
this.#epubDialog.querySelector('.footerPrevButton').removeEventListener('click', this.previous);
|
||||
this.#epubDialog.querySelector('.footerNextButton').removeEventListener('click', this.next);
|
||||
this.#epubDialog.querySelector('.epubPositionSlider').removeEventListener('change', this.gotoPositionAsSlider);
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
if (this.mediaElement) {
|
||||
if (this.#mediaElement) {
|
||||
this.unbindMediaElementEvents();
|
||||
}
|
||||
|
||||
document.removeEventListener('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) {
|
||||
this.rendition?.off('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
|
||||
|
@ -230,46 +425,202 @@ export class BookPlayer {
|
|||
this.touchHelper?.destroy();
|
||||
}
|
||||
|
||||
openTableOfContents() {
|
||||
async openTableOfContents(e) {
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
e?.preventDefault();
|
||||
if (this.rendition) {
|
||||
|
@ -284,34 +635,79 @@ export class BookPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
createMediaElement() {
|
||||
let elem = this.mediaElement;
|
||||
if (elem) {
|
||||
return elem;
|
||||
gotoPositionAsSlider(e) {
|
||||
console.log(e);
|
||||
const input = e.target;
|
||||
if (this.rendition) {
|
||||
this.rendition.display(input.value/100);
|
||||
}
|
||||
|
||||
elem = document.getElementById('bookPlayer');
|
||||
if (!elem) {
|
||||
elem = dialogHelper.createDialog({
|
||||
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;
|
||||
return elem;
|
||||
}
|
||||
|
||||
setCurrentSrc(elem, options) {
|
||||
getBubbleHtml(value) {
|
||||
const cfi = this.rendition.book.locations.cfiFromPercentage(value/100);
|
||||
return this.#getChapterFromCfi(cfi).label;
|
||||
}
|
||||
|
||||
async createMediaElement(options) {
|
||||
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');
|
||||
}
|
||||
|
||||
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];
|
||||
this.item = item;
|
||||
this.streamInfo = {
|
||||
|
@ -327,63 +723,68 @@ export class BookPlayer {
|
|||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
if (!Screenfull.isEnabled) {
|
||||
document.getElementById('btnBookplayerFullscreen').display = 'none';
|
||||
this.#epubDialog.querySelector('.headerFullscreenButton').display = 'none';
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
import('epubjs').then(({ default: epubjs }) => {
|
||||
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
|
||||
const book = epubjs(downloadHref, { openAs: 'epub' });
|
||||
this.#epubDialog.querySelector('.pageTitle').textContent = item.Name;
|
||||
const epubBlobUrl = await this.#fetchEpub(apiClient.getItemDownloadUrl(item.Id));
|
||||
const positionSlider = this.#epubDialog.querySelector('.epubPositionSlider');
|
||||
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.
|
||||
// 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);
|
||||
this.#epubDialog.querySelector('.epubMediaStatusText').textContent = globalize.translate('BookStatusProcessing');
|
||||
|
||||
const rendition = book.renderTo('bookPlayerContainer', {
|
||||
width: '100%',
|
||||
height: renderHeight,
|
||||
// TODO: Add option for scrolled-doc
|
||||
flow: 'paginated'
|
||||
});
|
||||
|
||||
this.currentSrc = downloadHref;
|
||||
this.rendition = rendition;
|
||||
|
||||
rendition.themes.register('default', THEMES[this.theme]);
|
||||
rendition.themes.select('default');
|
||||
|
||||
return rendition.display().then(() => {
|
||||
const epubElem = document.querySelector('.epub-container');
|
||||
epubElem.style.opacity = '0';
|
||||
|
||||
this.bindEvents();
|
||||
|
||||
return this.rendition.book.locations.generate(1024).then(async () => {
|
||||
if (this.cancellationToken) reject();
|
||||
|
||||
const percentageTicks = options.startPositionTicks / 10000000;
|
||||
if (percentageTicks !== 0.0) {
|
||||
const resumeLocation = book.locations.cfiFromPercentage(percentageTicks);
|
||||
await rendition.display(resumeLocation);
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
epubElem.style.opacity = '';
|
||||
rendition.on('relocated', (locations) => {
|
||||
this.progress = book.locations.percentageFromCfi(locations.start.cfi);
|
||||
Events.trigger(this, 'pause');
|
||||
});
|
||||
|
||||
loading.hide();
|
||||
return resolve();
|
||||
});
|
||||
}, () => {
|
||||
console.error('failed to display epub');
|
||||
return reject();
|
||||
});
|
||||
});
|
||||
const book = new EpubJS.Book(epubBlobUrl, {
|
||||
openAs: 'epub',
|
||||
});
|
||||
|
||||
const rendition = book.renderTo(this.#mediaElement, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flow: 'paginated',
|
||||
});
|
||||
|
||||
this.currentSrc = epubBlobUrl;
|
||||
this.rendition = rendition;
|
||||
|
||||
this.#applyDisplayConfig();
|
||||
|
||||
await rendition.display();
|
||||
|
||||
const epubElem = document.querySelector('.epub-container');
|
||||
epubElem.style.opacity = '0';
|
||||
|
||||
this.bindEvents();
|
||||
|
||||
await this.rendition.book.locations.generate();
|
||||
if (this.cancellationToken) throw new Error;
|
||||
|
||||
const percentageTicks = options.startPositionTicks / 10000000;
|
||||
if (percentageTicks !== 0.0) {
|
||||
const resumeLocation = book.locations.cfiFromPercentage(percentageTicks);
|
||||
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;
|
||||
epubElem.style.opacity = '';
|
||||
rendition.on('relocated', (locations) => {
|
||||
if(this.progress != locations.start.percentage) {
|
||||
this.progress = locations.start.percentage;
|
||||
Events.trigger(this, 'pause');
|
||||
}
|
||||
positionSlider.value = locations.start.percentage * 100;
|
||||
positionText.textContent = percentToString(locations.start.percentage);
|
||||
});
|
||||
|
||||
this.#epubDialog.querySelector('.epubMediaStatus').classList.add('hide');
|
||||
this.#epubDialog.querySelector('.footerPrevButton').disabled=false;
|
||||
this.#epubDialog.querySelector('.footerNextButton').disabled=false;
|
||||
this.#epubDialog.querySelector('.epubPositionSlider').disabled=false;
|
||||
}
|
||||
|
||||
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 {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
|
||||
.epubPlayerContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
z-index: 1002;
|
||||
width: 100%;
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
.epubPlayerWrapper {
|
||||
aspect-ratio: 1.6;
|
||||
position: relative;
|
||||
|
||||
.epubPlayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bookPlayerContainer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.epubPlayerContainer-onTop {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#btnBookplayerToc {
|
||||
float: left;
|
||||
margin-left: 2vw;
|
||||
}
|
||||
.epubPlayerFooter {
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
|
||||
#btnBookplayerExit {
|
||||
float: right;
|
||||
margin-right: 2vw;
|
||||
}
|
||||
|
||||
.bookplayerErrorMsg {
|
||||
.epubPositionText {
|
||||
width: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#btnBookplayerPrev,
|
||||
#btnBookplayerNext {
|
||||
margin: 0.5vh 0.5vh;
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#dialogToc {
|
||||
background-color: white;
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
max-height: 80%;
|
||||
max-width: 60%;
|
||||
padding-right: 50px;
|
||||
padding-bottom: 15px;
|
||||
.epubMediaStatus {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
.bookplayerButtonIcon {
|
||||
color: black;
|
||||
.epubMediaStatusWrapper {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
margin-bottom: 5px;
|
||||
|
||||
list-style-type: none;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
|
||||
ul {
|
||||
padding-left: 1.5rem;
|
||||
|
||||
li {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
color: #00a4dc;
|
||||
text-decoration: none;
|
||||
}
|
||||
.animate {
|
||||
animation: spin 4s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.actionsheetMenuItemIcon {
|
||||
&.indent-1 {
|
||||
padding-right: .5em !important;
|
||||
}
|
||||
|
||||
&.indent-2 {
|
||||
padding-right: 1em !important;
|
||||
}
|
||||
|
||||
&.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">
|
||||
<button is="paper-icon-button-light" id="btnBookplayerToc" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||
<span class="material-icons bookplayerButtonIcon toc" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" id="btnBookplayerPrev" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||
<span class="material-icons bookplayerButtonIcon navigate_before" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" id="btnBookplayerNext" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||
<span class="material-icons bookplayerButtonIcon navigate_next" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" id="btnBookplayerExit" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||
<span class="material-icons bookplayerButtonIcon close" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" id="btnBookplayerRotateTheme" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||
<span class="material-icons bookplayerButtonIcon remove_red_eye" 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>
|
||||
<div class="epubPlayerHeader w-100 skinHeader-withBackground">
|
||||
<div class="flex align-items-center headerTop">
|
||||
<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>
|
||||
<h3 class="pageTitle" aria-hidden="true"></h3>
|
||||
</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 is="paper-icon-button-light" class="headerSearchButton headerButton headerButtonRight paper-icon-button-light" title="${Search}">
|
||||
<span class="material-icons search" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" class="headerTextformatButton headerButton headerButtonRight paper-icon-button-light" title="${LabelFont}">
|
||||
<span class="material-icons text_format" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" class="headerTocButton headerButton headerButtonRight paper-icon-button-light" title="${Toc}">
|
||||
<span class="material-icons toc" aria-hidden="true"></span>
|
||||
</button>
|
||||
</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}",
|
||||
"BirthLocation": "Birth location",
|
||||
"BirthPlaceValue": "Birth place: {0}",
|
||||
"BookPlayerDisplayPreferences": "Display Preferences",
|
||||
"LabelLineHeight": "Line height",
|
||||
"Toc": "Table of Contents",
|
||||
"BookStatusFetching": "Downloading",
|
||||
"BookStatusProcessing": "Processing",
|
||||
"BookPlayerDisplayUnset": "Keep default",
|
||||
"Blacklist": "Blacklist",
|
||||
"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}.",
|
||||
|
|
|
@ -12,6 +12,12 @@
|
|||
"Backdrops": "배경",
|
||||
"BirthDateValue": "출생: {0}",
|
||||
"BirthPlaceValue": "출생지: {0}",
|
||||
"BookPlayerDisplayPreferences": "표시 설정",
|
||||
"LabelLineHeight": "줄 간격",
|
||||
"Toc": "목차",
|
||||
"BookStatusFetching": "다운로드 중",
|
||||
"BookStatusProcessing": "처리 중",
|
||||
"BookPlayerDisplayUnset": "설정 안 함",
|
||||
"MessageBrowsePluginCatalog": "사용 가능한 플러그인을 보려면 플러그인 카탈로그를 참고하십시오.",
|
||||
"ButtonAddScheduledTaskTrigger": "트리거 추가",
|
||||
"ButtonAddServer": "서버 추가",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue