migrate book OSD to react component

This commit is contained in:
dkanada committed 2026-05-28 15:31:41 +09:00
1 parent a9f6c0aad9
commit b312d928ac
8 files changed
+181 -127

No files matched your search

+6
View File
@@ -3,3 +3,9 @@ declare module '*.png' {
const value: any;
export = value;
}
declare module '*.scss' {
// style imports are handled by the bundler
const value: string;
export default value;
}
@@ -0,0 +1,45 @@
.bookOsd {
position: absolute;
inset: 0;
z-index: 1002;
display: flex;
justify-content: space-between;
flex-direction: column;
}
.bookOsdRow {
display: flex;
justify-content: flex-start;
align-items: center;
height: 5vh;
padding: 0 0.8vw;
background-color: var(--jf-palette-AppBar-defaultBg);
}
// TODO disable landscape on mobile devices
@media (orientation: landscape) and (max-width: 60em) {
.bookOsdRow {
height: 10vh;
}
}
.bookOsdTop {
padding-top: env(safe-area-inset-top);
}
.bookOsdBottom {
padding-bottom: env(safe-area-inset-bottom);
}
.bookOsdMargin {
margin-left: auto;
}
.bookOsdTitle {
font-size: 1.1rem;
margin-left: 0.35rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -0,0 +1,72 @@
import React, { type FC, useCallback, useState } from 'react';
import './BookOsd.scss';
import IconButton from '../../../elements/emby-button/IconButton';
interface BookOsdProps {
title: string;
onExit: () => void;
onPrevious: () => void;
onNext: () => void;
onOpenTableOfContents?: () => void;
onRotateTheme?: () => void;
onDecreaseFontSize?: () => void;
onIncreaseFontSize?: () => void;
onToggleFullscreen?: () => void;
}
const BookOsd: FC<BookOsdProps> = ({
title,
onExit,
onPrevious,
onNext,
onOpenTableOfContents,
onRotateTheme,
onDecreaseFontSize,
onIncreaseFontSize,
onToggleFullscreen
}) => {
const [fullscreen, setFullscreen] = useState(false);
const onClickFullscreen = useCallback(() => {
onToggleFullscreen?.();
setFullscreen(state => !state);
}, [onToggleFullscreen]);
return (
<div className='bookOsd'>
<div className='bookOsdRow bookOsdTop'>
<IconButton onClick={onExit} icon='arrow_back' />
<span className='bookOsdTitle'>{title}</span>
</div>
<div className='bookOsdRow bookOsdBottom'>
<IconButton onClick={onPrevious} icon='navigate_before' />
<IconButton onClick={onNext} icon='navigate_next' />
<IconButton
onClick={onOpenTableOfContents}
style={{ display: onOpenTableOfContents ? 'flex' : 'none' }}
icon='toc'
className='bookOsdMargin' />
<IconButton
onClick={onRotateTheme}
style={{ display: onRotateTheme ? 'flex' : 'none' }}
icon='remove_red_eye' />
<IconButton
onClick={onDecreaseFontSize}
style={{ display: onDecreaseFontSize ? 'flex' : 'none' }}
icon='text_decrease' />
<IconButton
onClick={onIncreaseFontSize}
style={{ display: onIncreaseFontSize ? 'flex' : 'none' }}
icon='text_increase' />
<IconButton
onClick={onClickFullscreen}
style={{ display: onToggleFullscreen ? 'flex' : 'none' }}
icon={fullscreen ? 'fullscreen_exit' : 'fullscreen'} />
</div>
</div>
);
};
export default BookOsd;
+25 -69
View File
@@ -6,15 +6,16 @@ import browser from 'scripts/browser';
import TouchHelper from 'scripts/touchHelper';
import { toApi } from 'utils/jellyfin-apiclient/compat';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading';
import keyboardnavigation from '../../scripts/keyboardNavigation';
import dialogHelper from '../../components/dialogHelper/dialogHelper';
import TableOfContents from './tableOfContents';
import BookOsd from './BookOsd/BookOsd';
import { translateHtml } from '../../lib/globalize';
import * as userSettings from '../../scripts/settings/userSettings';
import { PluginType } from '../../types/plugin.ts';
import Events from '../../utils/events.ts';
import { renderComponent } from '../../utils/reactUtils';
import 'material-design-icons-iconfont';
import '../../elements/emby-button/paper-icon-button-light';
@@ -52,7 +53,6 @@ export class BookPlayer {
this.next = this.next.bind(this);
this.onWindowKeyDown = this.onWindowKeyDown.bind(this);
this.addSwipeGestures = this.addSwipeGestures.bind(this);
this.getPlayerHeight = this.getPlayerHeight.bind(this);
this.toggleFullscreen = this.toggleFullscreen.bind(this);
this.fullscreen = false;
}
@@ -63,12 +63,13 @@ export class BookPlayer {
this.loaded = false;
loading.show();
const elem = this.createMediaElement();
const elem = this.createMediaElement(options);
return this.setCurrentSrc(elem, options);
}
stop() {
this.unbindEvents();
this.unmountBookOsd();
const stopInfo = {
src: this.item
@@ -185,55 +186,24 @@ export class BookPlayer {
this.stop();
}
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);
}
bindEvents() {
this.bindMediaElementEvents();
this.mediaElement?.addEventListener('close', this.onDialogClosed, { once: true });
document.addEventListener('keydown', this.onWindowKeyDown);
this.rendition?.on('keydown', this.onWindowKeyDown);
if (browser.safari) {
const player = document.getElementById('bookPlayerContainer');
const player = document.querySelector('.bookOsd');
this.addSwipeGestures(player);
} 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);
}
unbindEvents() {
if (this.mediaElement) {
this.unbindMediaElementEvents();
}
document.removeEventListener('keydown', this.onWindowKeyDown);
this.rendition?.off('keydown', this.onWindowKeyDown);
this.mediaElement?.removeEventListener('close', this.onDialogClosed);
if (!browser.safari) {
this.rendition?.off('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement));
@@ -242,16 +212,6 @@ export class BookPlayer {
this.touchHelper?.destroy();
}
getPlayerHeight() {
if (layoutManager.mobile) {
// we have no method from NativeShell to get the required margin for mobile devices
return this.fullscreen ? 0.9 : 0.94;
}
// desktop needs slightly less space than mobile
return 0.958;
}
openTableOfContents() {
if (this.loaded) {
this.tocElement = new TableOfContents(this);
@@ -259,29 +219,17 @@ export class BookPlayer {
}
toggleFullscreen() {
const icon = document.querySelector('#btnBookplayerFullscreen .material-icons');
const buttons = document.querySelector('.topButtons');
const player = document.querySelector('#bookPlayerContainer');
player.classList.toggle('fullscreen', !this.fullscreen);
if (Screenfull.isEnabled) {
icon.classList.remove(Screenfull.isFullscreen ? 'fullscreen_exit' : 'fullscreen');
icon.classList.add(Screenfull.isFullscreen ? 'fullscreen' : 'fullscreen_exit');
Screenfull.toggle();
} else if (window.NativeShell) {
if (this.fullscreen) {
icon.classList.remove('fullscreen_exit');
icon.classList.add('fullscreen');
buttons.classList.remove('fullscreen');
window.NativeShell.disableFullscreen();
} else {
icon.classList.remove('fullscreen');
icon.classList.add('fullscreen_exit');
buttons.classList.add('fullscreen');
window.NativeShell.enableFullscreen();
}
this.fullscreen ? window.NativeShell.disableFullscreen() : window.NativeShell.enableFullscreen();
}
// needs to be executed with a slight delay to give NativeShell time to process the request
setTimeout(() => this.rendition.resize(document.body.clientWidth, document.body.clientHeight * this.getPlayerHeight()), 200);
setTimeout(() => this.rendition.resize(player.clientWidth, player.clientHeight), 200);
// required for mobile apps without browser fullscreen support
this.fullscreen = !this.fullscreen;
@@ -326,7 +274,7 @@ export class BookPlayer {
}
}
createMediaElement() {
createMediaElement(options) {
let elem = this.mediaElement;
if (elem) {
return elem;
@@ -350,6 +298,18 @@ export class BookPlayer {
}
this.mediaElement = elem;
this.unmountBookOsd = renderComponent(BookOsd, {
title: options.items[0].Name,
onExit: this.onDialogClosed,
onPrevious: this.previous,
onNext: this.next,
onOpenTableOfContents: this.openTableOfContents,
onRotateTheme: this.rotateTheme,
onDecreaseFontSize: this.decreaseFontSize,
onIncreaseFontSize: this.increaseFontSize,
onToggleFullscreen: Screenfull.isEnabled || window.NativeShell ? this.toggleFullscreen : null
}, elem.querySelector('#bookOsdMount'));
return elem;
}
@@ -365,10 +325,6 @@ export class BookPlayer {
}
};
if (!Screenfull.isEnabled) {
document.getElementById('btnBookplayerFullscreen').display = 'none';
}
return new Promise((resolve, reject) => {
import('epubjs').then(({ default: epubjs }) => {
const api = toApi(ServerConnections.getApiClient(item));
@@ -377,7 +333,7 @@ export class BookPlayer {
const rendition = book.renderTo('bookPlayerContainer', {
width: '100%',
height: document.body.clientHeight * this.getPlayerHeight(),
height: '100%',
// TODO: Add option for scrolled-doc
flow: 'paginated'
});
+22 -25
View File
@@ -6,31 +6,29 @@
z-index: 100;
background: #fff;
display: flex;
flex-direction: column;
.bookplayerButton {
margin: 0.5vh;
}
.topButtons {
z-index: 1002;
display: flex;
height: 4.2vh;
padding: 0 2vw;
color: #000;
opacity: 0.7;
}
#btnBookplayerExit {
margin-left: auto;
}
.bookplayerErrorMsg {
text-align: center;
}
}
#bookPlayerContainer {
position: absolute;
inset: 0;
z-index: 1000;
width: 100%;
top: 5vh;
height: 90vh;
}
#bookPlayerContainer.fullscreen {
top: calc(5vh + env(safe-area-inset-top));
height: calc(90vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
}
#dialogToc {
background-color: white;
height: fit-content;
@@ -75,18 +73,17 @@
}
}
// TODO disable landscape on mobile devices
@media (orientation: landscape) and (max-width: 60em) {
#bookPlayerContainer {
top: 10vh;
height: 80vh;
}
}
@media (max-width: 60em) {
#dialogToc {
max-width: 100%;
max-height: 100%;
}
#bookPlayer .topButtons {
height: 6vh;
}
#bookPlayer .topButtons.fullscreen {
height: 10vh;
align-items: flex-end;
}
}
+1 -27
View File
@@ -1,28 +1,2 @@
<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="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>
<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>
</div>
<div id="bookOsdMount"></div>
<div id="bookPlayerContainer" class="bookPlayerContainer"></div>
+1 -1
View File
@@ -507,7 +507,7 @@ a[data-role=button],
color: var(--jf-palette-text-primary, $text-primary);
}
#bookPlayer .topButtons,
#bookPlayer .bookOsdRow,
#comicsPlayer .actionButtonIcon,
#pdfPlayer .actionButtonIcon {
color: var(--jf-palette-text-primary, $text-primary);
+9 -5
View File
@@ -1,6 +1,7 @@
import { type SupportedColorScheme, ThemeProvider, useColorScheme } from '@mui/material/styles';
import { QueryClientProvider } from '@tanstack/react-query';
import React, { type FC, type PropsWithChildren, useEffect } from 'react';
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { ApiProvider } from 'hooks/useApi';
@@ -20,11 +21,14 @@ export const renderComponent = <P extends object> (
element: HTMLElement
) => {
const root = createRoot(element);
root.render(
<RootContext>
<Component {...props} />
</RootContext>
);
flushSync(() => {
root.render(
<RootContext>
<Component {...props} />
</RootContext>
);
});
// NOTE: We need to wrap the unmount in a setTimeout to workaround this issue with nested roots:
// https://github.com/facebook/react/issues/25675