From a7b4ee59fa1da5640690bf938c9fe7268dabf875 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 1 Aug 2026 10:47:30 +0900 Subject: [PATCH 1/2] implement fade animations on BookOsd to match video player --- src/plugins/bookPlayer/BookOsd/BookOsd.scss | 11 ++---- src/plugins/bookPlayer/BookOsd/BookOsd.tsx | 44 +++++++++++++++++---- src/plugins/bookPlayer/plugin.js | 21 +++++----- src/plugins/bookPlayer/style.scss | 8 +--- src/plugins/comicsPlayer/style.scss | 8 +--- src/plugins/pdfPlayer/plugin.js | 2 +- src/plugins/pdfPlayer/style.scss | 8 +--- 7 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/plugins/bookPlayer/BookOsd/BookOsd.scss b/src/plugins/bookPlayer/BookOsd/BookOsd.scss index 6e582dfc15..9394cc4d21 100644 --- a/src/plugins/bookPlayer/BookOsd/BookOsd.scss +++ b/src/plugins/bookPlayer/BookOsd/BookOsd.scss @@ -16,6 +16,9 @@ padding: 0 0.8vw; background-color: var(--jf-palette-AppBar-defaultBg); pointer-events: auto; + transition: all 200ms linear; + // force WebKit to promote the element onto its own compositor layer for opacity on iOS + transform: translateZ(0); } // TODO disable landscape on mobile devices @@ -29,14 +32,6 @@ flex: 1; } -.bookOsdTop { - padding-top: env(safe-area-inset-top); -} - -.bookOsdBottom { - padding-bottom: env(safe-area-inset-bottom); -} - .bookOsdTitle { font-size: 1.1rem; margin-left: 0.35rem; diff --git a/src/plugins/bookPlayer/BookOsd/BookOsd.tsx b/src/plugins/bookPlayer/BookOsd/BookOsd.tsx index 79a9a2b20f..d7427debb4 100644 --- a/src/plugins/bookPlayer/BookOsd/BookOsd.tsx +++ b/src/plugins/bookPlayer/BookOsd/BookOsd.tsx @@ -1,4 +1,4 @@ -import React, { type FC, useCallback, useEffect, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'; import './BookOsd.scss'; import IconButton from '../../../elements/emby-button/IconButton'; @@ -40,10 +40,17 @@ const BookOsd: FC = ({ onToggleFullscreen }) => { const settings = userSettings.getComicsPlayerSettings(item.Id!) as ComicsPlayerSettings; + const timeout = useRef>(); const [direction, setDirection] = useState(settings.langDir === 'rtl'); const [layout, setLayout] = useState(settings.pagesPerView === 2); const [fullscreen, setFullscreen] = useState(false); + const [visible, setVisible] = useState(true); + + const scheduleHide = useCallback(() => { + clearTimeout(timeout.current); + timeout.current = setTimeout(() => setVisible(false), 2000); + }, []); const updateFullscreen = useCallback((state: boolean) => { if (Screenfull.isEnabled && Screenfull.isFullscreen !== state) { @@ -53,10 +60,6 @@ const BookOsd: FC = ({ } }, []); - useEffect(() => { - return () => updateFullscreen(false); - }, [updateFullscreen]); - const onClickDirection = useCallback(() => { onToggleDirection?.(); setDirection(state => !state); @@ -73,14 +76,41 @@ const BookOsd: FC = ({ setFullscreen(state => !state); }, [onToggleFullscreen, updateFullscreen, fullscreen]); + useEffect(() => { + const onPointerMove = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') return; + + scheduleHide(); + setVisible(true); + }; + + const onClick = (event: MouseEvent) => { + if ((event.target as Element | null)?.closest?.('.bookOsdRow')) return; + + scheduleHide(); + setVisible(state => !state); + }; + + scheduleHide(); + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('click', onClick); + + return () => { + clearTimeout(timeout.current); + updateFullscreen(false); + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('click', onClick); + }; + }, [scheduleHide, updateFullscreen]); + return (
-
+
{item.Name}
-
+
diff --git a/src/plugins/bookPlayer/plugin.js b/src/plugins/bookPlayer/plugin.js index 76cc58cf54..1ea5101609 100644 --- a/src/plugins/bookPlayer/plugin.js +++ b/src/plugins/bookPlayer/plugin.js @@ -2,7 +2,6 @@ import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'; import { PluginType } from 'constants/pluginType'; import { ServerConnections } from 'lib/jellyfin-apiclient'; -import browser from 'scripts/browser'; import screenSaverManager from 'scripts/screensavermanager'; import TouchHelper from 'scripts/touchHelper'; @@ -187,26 +186,24 @@ export class BookPlayer { document.addEventListener('keydown', this.onWindowKeyDown); this.rendition?.on('keydown', this.onWindowKeyDown); - - if (browser.safari) { - this.addSwipeGestures(document.querySelector('#bookPlayerContainer')); - } else { - this.rendition?.on('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement)); - } + this.rendition?.on('rendered', (e, i) => this.forwardEvents(i.document)); + this.addSwipeGestures(document.querySelector('#bookPlayerContainer')); } unbindEvents() { 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)); - } - this.touchHelper?.destroy(); } + // ensure certain iframe events are forwarded to the document for BookOsd visibility listeners + forwardEvents(iframe) { + // eslint-disable-next-line compat/compat + iframe.addEventListener('pointermove', (event) => document.dispatchEvent(new PointerEvent(event.type, event))); + iframe.addEventListener('click', (event) => document.dispatchEvent(new MouseEvent(event.type, event))); + } + openTableOfContents() { if (this.loaded) { this.tocElement = new TableOfContents(this); diff --git a/src/plugins/bookPlayer/style.scss b/src/plugins/bookPlayer/style.scss index b1987025ff..caad29df48 100644 --- a/src/plugins/bookPlayer/style.scss +++ b/src/plugins/bookPlayer/style.scss @@ -20,12 +20,8 @@ inset: 0; z-index: 1000; width: 100%; - /* stylelint-disable declaration-block-no-duplicate-properties */ - top: 5vh; - height: 90vh; - top: calc(5vh + env(safe-area-inset-top)); - height: calc(90vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); - /* stylelint-enable declaration-block-no-duplicate-properties */ + top: env(safe-area-inset-top); + height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); } #dialogToc { diff --git a/src/plugins/comicsPlayer/style.scss b/src/plugins/comicsPlayer/style.scss index 2944623e06..8447bd4a19 100644 --- a/src/plugins/comicsPlayer/style.scss +++ b/src/plugins/comicsPlayer/style.scss @@ -3,12 +3,8 @@ .slideshowSwiperContainer { position: relative; - /* stylelint-disable declaration-block-no-duplicate-properties */ - top: 5vh; - height: 90vh; - top: calc(5vh + env(safe-area-inset-top)); - height: calc(90vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); - /* stylelint-enable declaration-block-no-duplicate-properties */ + top: env(safe-area-inset-top); + height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); } .slider-zoom-container { diff --git a/src/plugins/pdfPlayer/plugin.js b/src/plugins/pdfPlayer/plugin.js index 8898d2911d..b0b58a31be 100644 --- a/src/plugins/pdfPlayer/plugin.js +++ b/src/plugins/pdfPlayer/plugin.js @@ -294,7 +294,7 @@ export class PdfPlayer { const devicePixelRatio = window.devicePixelRatio || 1; this.book.getPage(number).then(page => { const original = page.getViewport({ scale: 1 }); - const scale = Math.min((window.innerHeight / original.height * 0.9), (window.innerWidth / original.width)); + const scale = Math.min((window.innerHeight / original.height), (window.innerWidth / original.width)); const viewport = page.getViewport({ scale: scale * devicePixelRatio }); canvas.width = viewport.width; diff --git a/src/plugins/pdfPlayer/style.scss b/src/plugins/pdfPlayer/style.scss index 4f19dead74..015d368d5a 100644 --- a/src/plugins/pdfPlayer/style.scss +++ b/src/plugins/pdfPlayer/style.scss @@ -2,12 +2,8 @@ position: relative; display: grid; place-items: center; - /* stylelint-disable declaration-block-no-duplicate-properties */ - top: 5vh; - height: 90vh; - top: calc(5vh + env(safe-area-inset-top)); - height: calc(90vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); - /* stylelint-enable declaration-block-no-duplicate-properties */ + top: env(safe-area-inset-top); + height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); } #pdfPlayer { From 2080ea998104f144574f30e6337bf1ba73551089 Mon Sep 17 00:00:00 2001 From: dkanada Date: Mon, 3 Aug 2026 12:00:31 +0900 Subject: [PATCH 2/2] improve BookOsd hide behavior and fix swipe gestures --- src/plugins/bookPlayer/BookOsd/BookOsd.tsx | 5 +++-- src/plugins/bookPlayer/plugin.js | 13 ++++++++++++- src/plugins/comicsPlayer/style.scss | 2 ++ src/plugins/pdfPlayer/style.scss | 2 ++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/plugins/bookPlayer/BookOsd/BookOsd.tsx b/src/plugins/bookPlayer/BookOsd/BookOsd.tsx index d7427debb4..9804871509 100644 --- a/src/plugins/bookPlayer/BookOsd/BookOsd.tsx +++ b/src/plugins/bookPlayer/BookOsd/BookOsd.tsx @@ -85,9 +85,10 @@ const BookOsd: FC = ({ }; const onClick = (event: MouseEvent) => { - if ((event.target as Element | null)?.closest?.('.bookOsdRow')) return; - + // apply this before the BookOsd check so IconButton clicks will reset the timer scheduleHide(); + + if ((event.target as Element | null)?.closest?.('.bookOsdRow')) return; setVisible(state => !state); }; diff --git a/src/plugins/bookPlayer/plugin.js b/src/plugins/bookPlayer/plugin.js index 1ea5101609..0eddb3e8e0 100644 --- a/src/plugins/bookPlayer/plugin.js +++ b/src/plugins/bookPlayer/plugin.js @@ -2,6 +2,7 @@ import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'; import { PluginType } from 'constants/pluginType'; import { ServerConnections } from 'lib/jellyfin-apiclient'; +import browser from 'scripts/browser'; import screenSaverManager from 'scripts/screensavermanager'; import TouchHelper from 'scripts/touchHelper'; @@ -187,13 +188,23 @@ export class BookPlayer { document.addEventListener('keydown', this.onWindowKeyDown); this.rendition?.on('keydown', this.onWindowKeyDown); this.rendition?.on('rendered', (e, i) => this.forwardEvents(i.document)); - this.addSwipeGestures(document.querySelector('#bookPlayerContainer')); + + if (browser.safari) { + this.addSwipeGestures(document.querySelector('#bookPlayerContainer')); + } else { + this.rendition?.on('rendered', (e, i) => this.addSwipeGestures(i.document.documentElement)); + } } unbindEvents() { 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)); + } + this.touchHelper?.destroy(); } diff --git a/src/plugins/comicsPlayer/style.scss b/src/plugins/comicsPlayer/style.scss index 8447bd4a19..2b1be017cd 100644 --- a/src/plugins/comicsPlayer/style.scss +++ b/src/plugins/comicsPlayer/style.scss @@ -3,6 +3,8 @@ .slideshowSwiperContainer { position: relative; + /* stylelint-disable-next-line declaration-block-no-duplicate-properties */ + height: 100%; top: env(safe-area-inset-top); height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); } diff --git a/src/plugins/pdfPlayer/style.scss b/src/plugins/pdfPlayer/style.scss index 015d368d5a..0b2f940ef8 100644 --- a/src/plugins/pdfPlayer/style.scss +++ b/src/plugins/pdfPlayer/style.scss @@ -2,6 +2,8 @@ position: relative; display: grid; place-items: center; + /* stylelint-disable-next-line declaration-block-no-duplicate-properties */ + height: 100%; top: env(safe-area-inset-top); height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }