mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-10-26 16:45:18 +00:00
migrate book OSD to react component
This commit is contained in:
1 parent
a9f6c0aad9
commit
b312d928ac
8 files changed
+181
-127
No files matched your search
Vendored
+6
@@ -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;
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user