2024-11-06 13:00:36 +01:00
/ *
* Copyright ( C ) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at https : //mozilla.org/MPL/2.0/.
* /
2024-12-09 15:07:52 +01:00
import { Direction , Theme } from '@mui/material/styles' ;
2024-12-22 00:42:33 +01:00
import { ComponentProps , ReactNode } from 'react' ;
2024-11-06 13:00:36 +01:00
import {
IReaderSettings ,
PageInViewportType ,
ReaderPageScaleMode ,
2024-12-03 18:00:46 +01:00
ReaderTransitionPageMode ,
2024-12-07 03:43:18 +01:00
ReadingDirection ,
2024-11-06 13:00:36 +01:00
ReadingMode ,
} from '@/modules/reader/types/Reader.types.ts' ;
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts' ;
2024-12-16 16:26:32 +01:00
import {
isContinuousReadingMode ,
isContinuousVerticalReadingMode ,
isReaderWidthEditable ,
} from '@/modules/reader/utils/ReaderSettings.utils.tsx' ;
2024-11-06 13:00:36 +01:00
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts' ;
import { requestManager } from '@/lib/requests/RequestManager.ts' ;
import { ReaderPage } from '@/modules/reader/components/viewer/ReaderPage.tsx' ;
import { reverseString } from '@/util/Strings.ts' ;
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx' ;
2024-12-09 15:07:52 +01:00
import { DirectionOffset } from '@/Base.types.ts' ;
2024-12-11 13:59:50 +01:00
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts' ;
2024-12-09 15:07:52 +01:00
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx' ;
2024-11-06 13:00:36 +01:00
type CSSObject = ReturnType < Theme [ 'applyStyles' ] > ;
const getPageWidth = (
pageScaleMode : IReaderSettings [ 'pageScaleMode' ] ,
isDoublePage? : boolean ,
readerWidth? : IReaderSettings [ 'readerWidth' ] ,
2024-12-16 21:13:07 +01:00
shouldStretchPage? : IReaderSettings [ 'shouldStretchPage' ] ,
2024-11-06 13:00:36 +01:00
isImage? : boolean ,
) : string = > {
2024-12-16 21:13:07 +01:00
// only return 50% in case pages should get stretched, otherwise, pages might unintentionally shrink in size (e.g. 2 pages with different dimensions, the bigger page will shrink due to taking up more than 50%)
if ( isImage && isDoublePage && shouldStretchPage ) {
2024-11-06 13:00:36 +01:00
return '50%' ;
}
// the image wrapper gets applied the reader width limit and therefore the images can take up 100%
if ( ! isImage && readerWidth ? . enabled && isReaderWidthEditable ( pageScaleMode ) ) {
return ` ${ readerWidth . value } % ` ;
}
return '100%' ;
} ;
export const getImagePlaceholderStyling = (
readingMode : IReaderSettings [ 'readingMode' ] ,
shouldStretchPage : IReaderSettings [ 'shouldStretchPage' ] ,
pageScaleMode : IReaderSettings [ 'pageScaleMode' ] ,
readerWidth : IReaderSettings [ 'readerWidth' ] ,
scrollbarXSize : number ,
scrollbarYSize : number ,
isDoublePage? : boolean ,
isTabletWidth? : boolean ,
) : CSSObject = > {
const DEFAULT_SINGLE_PAGE_WIDTH = isTabletWidth ? '100vw' : '40vw' ;
const DEFAULT_SINGLE_PAGE_HEIGHT = isTabletWidth ? '100vh' : '85vh' ;
const READING_MODE_TO_PLACEHOLDER_SIZE : Record < ReadingMode , { minWidth : string ; minHeight : string } > = {
[ ReadingMode . SINGLE_PAGE ] : { minWidth : DEFAULT_SINGLE_PAGE_WIDTH , minHeight : DEFAULT_SINGLE_PAGE_HEIGHT } ,
[ ReadingMode . DOUBLE_PAGE ] : { minWidth : isTabletWidth ? '50vw' : '35vw' , minHeight : DEFAULT_SINGLE_PAGE_HEIGHT } ,
[ ReadingMode . CONTINUOUS_VERTICAL ] : {
minWidth : ` calc( ${ DEFAULT_SINGLE_PAGE_WIDTH } - ${ scrollbarYSize } px) ` ,
minHeight : '100vh' ,
} ,
[ ReadingMode . CONTINUOUS_HORIZONTAL ] : {
minWidth : DEFAULT_SINGLE_PAGE_WIDTH ,
minHeight : ` calc( ${ DEFAULT_SINGLE_PAGE_HEIGHT } - ${ scrollbarXSize } px) ` ,
} ,
2024-12-16 16:26:32 +01:00
[ ReadingMode . WEBTOON ] : {
minWidth : ` calc( ${ DEFAULT_SINGLE_PAGE_WIDTH } - ${ scrollbarYSize } px) ` ,
minHeight : '100vh' ,
} ,
2024-11-06 13:00:36 +01:00
} ;
const defaultStyling = READING_MODE_TO_PLACEHOLDER_SIZE [ readingMode ] ;
const minWidthForStretch = isDoublePage ? '50%' : '100%' ;
switch ( pageScaleMode ) {
case ReaderPageScaleMode . WIDTH :
return {
. . . defaultStyling ,
. . . applyStyles ( readerWidth . enabled && isReaderWidthEditable ( pageScaleMode ) , {
minWidth : minWidthForStretch ,
} ) ,
. . . applyStyles ( shouldStretchPage , {
minWidth : minWidthForStretch ,
} ) ,
2024-12-16 21:14:18 +01:00
// the SpinnerImage placeholder has a default height of 100%, this caused the placeholder due take up 100% of the readers full scrollable height instead of only 100% of the viewport
. . . applyStyles ( isContinuousVerticalReadingMode ( readingMode ) , {
height : 'unset' ,
} ) ,
2024-11-06 13:00:36 +01:00
} ;
case ReaderPageScaleMode . HEIGHT :
return {
. . . defaultStyling ,
. . . applyStyles ( shouldStretchPage , {
minHeight : ` calc(100vh - ${ scrollbarXSize } px) ` ,
2024-12-16 16:26:32 +01:00
. . . applyStyles ( isContinuousVerticalReadingMode ( readingMode ) , {
2024-11-06 13:00:36 +01:00
minHeight : '100vh' ,
} ) ,
} ) ,
} ;
case ReaderPageScaleMode . SCREEN :
return {
. . . defaultStyling ,
. . . applyStyles ( readerWidth . enabled && isReaderWidthEditable ( pageScaleMode ) , {
minWidth : minWidthForStretch ,
} ) ,
. . . applyStyles ( shouldStretchPage , {
minWidth : minWidthForStretch ,
minHeight : ` calc(100vh - ${ scrollbarXSize } px) ` ,
2024-12-16 16:26:32 +01:00
. . . applyStyles ( isContinuousVerticalReadingMode ( readingMode ) , {
2024-11-06 13:00:36 +01:00
minHeight : '100vh' ,
} ) ,
} ) ,
} ;
case ReaderPageScaleMode . ORIGINAL :
2024-12-22 16:07:55 +01:00
return {
. . . defaultStyling ,
. . . applyStyles ( isContinuousVerticalReadingMode ( readingMode ) , {
height : undefined ,
} ) ,
} ;
2024-11-06 13:00:36 +01:00
default :
throw new Error ( ` Unexpected "PageScaleMode" ( ${ pageScaleMode } ) ` ) ;
}
} ;
export const getImageWidthStyling = (
readingMode : IReaderSettings [ 'readingMode' ] ,
shouldStretchPage : IReaderSettings [ 'shouldStretchPage' ] ,
pageScaleMode : IReaderSettings [ 'pageScaleMode' ] ,
isDoublePage? : boolean ,
readerWidth? : IReaderSettings [ 'readerWidth' ] ,
isImage? : boolean ,
) : CSSObject = > {
2024-12-16 21:13:07 +01:00
const width = getPageWidth ( pageScaleMode , isDoublePage , readerWidth , shouldStretchPage , isImage ) ;
2024-11-06 13:00:36 +01:00
// setting the "width" of the wrapper is required for being able to properly size the image placeholders
const staticReaderWidthForWrapper = applyStyles (
! isImage && ! ! readerWidth ? . enabled && isReaderWidthEditable ( pageScaleMode ) ,
{ width } ,
) ;
const readerWidthStretchForWrapper = applyStyles ( ! isImage && shouldStretchPage , { width : '100%' } ) ;
switch ( pageScaleMode ) {
case ReaderPageScaleMode . WIDTH :
return {
2024-12-17 00:59:18 +01:00
minWidth : 0 ,
. . . applyStyles ( isContinuousReadingMode ( readingMode ) , {
minWidth : 'unset' ,
} ) ,
2024-11-06 13:00:36 +01:00
. . . applyStyles ( shouldStretchPage , {
minWidth : width ,
. . . readerWidthStretchForWrapper ,
} ) ,
. . . staticReaderWidthForWrapper ,
maxWidth : width ,
} ;
case ReaderPageScaleMode . HEIGHT :
return {
2024-12-17 00:59:18 +01:00
minHeight : 0 ,
. . . applyStyles ( isContinuousReadingMode ( readingMode ) , {
minHeight : 'unset' ,
} ) ,
2024-11-06 13:00:36 +01:00
. . . applyStyles ( shouldStretchPage , {
minHeight : '100%' ,
} ) ,
maxHeight : ` 100% ` ,
} ;
case ReaderPageScaleMode . SCREEN :
return {
2024-12-17 00:59:18 +01:00
minWidth : 0 ,
minHeight : 0 ,
. . . applyStyles ( isContinuousReadingMode ( readingMode ) , {
minWidth : 'unset' ,
minHeight : 'unset' ,
} ) ,
2024-11-06 13:00:36 +01:00
. . . applyStyles ( shouldStretchPage , {
minWidth : width ,
minHeight : '100%' ,
. . . applyStyles ( readingMode === ReadingMode . CONTINUOUS_HORIZONTAL , {
minWidth : 'unset' ,
minHeight : '100%' ,
} ) ,
2024-12-16 16:26:32 +01:00
. . . applyStyles ( isContinuousVerticalReadingMode ( readingMode ) , {
2024-11-06 13:00:36 +01:00
minWidth : width ,
minHeight : 'unset' ,
} ) ,
. . . readerWidthStretchForWrapper ,
} ) ,
. . . staticReaderWidthForWrapper ,
maxWidth : width ,
maxHeight : ` 100% ` ,
} ;
case ReaderPageScaleMode . ORIGINAL :
return { } ;
default :
throw new Error ( ` Unexpected "PageScaleMode" ( ${ pageScaleMode } ) ` ) ;
}
} ;
2024-12-17 01:01:58 +01:00
export const getImageMarginStyling = ( doublePage : boolean , objectFitPosition ? : 'left' | 'right' ) : CSSObject = > ( {
m : 'auto' ,
. . . applyStyles ( doublePage , {
// the applied margin is the opposite of the objectFitPosition
. . . applyStyles ( objectFitPosition === 'right' , { ml : 'auto ' } ) ,
. . . applyStyles ( objectFitPosition === 'left' , { mr : 'auto ' } ) ,
2024-12-16 21:13:07 +01:00
} ) ,
} ) ;
2024-11-06 13:00:36 +01:00
export const createSinglePageData = ( url : string , index : number ) : ReaderStatePages [ 'pages' ] [ number ] [ 'primary' ] = > ( {
index ,
alt : ` Page # ${ index + 1 } ` ,
url : ` ${ requestManager . getBaseUrl ( ) } ${ url } ` ,
} ) ;
export const createPageData = ( url : string , index : number ) : ReaderStatePages [ 'pages' ] [ number ] = > ( {
name : ` ${ index + 1 } ` ,
primary : createSinglePageData ( url , index ) ,
} ) ;
export const createPagesData = ( pageUrls : string [ ] ) : ReaderStatePages [ 'pages' ] = > pageUrls . map ( createPageData ) ;
2024-12-22 00:42:33 +01:00
const getPageDownloadPriority = (
currentPageIndex : number ,
pageIndex : number ,
totalPages : number ,
shouldLoad : boolean ,
) : number = > {
if ( ! shouldLoad ) {
return Number . MAX_SAFE_INTEGER ;
}
2024-11-06 13:00:36 +01:00
const distanceToCurrentPage = Math . abs ( pageIndex - currentPageIndex ) ;
const priorityBasedOnDistance = totalPages - distanceToCurrentPage ;
const isPreviousPage = pageIndex < currentPageIndex ;
if ( isPreviousPage ) {
return priorityBasedOnDistance - 1 ;
}
return priorityBasedOnDistance ;
} ;
export const createReaderPage = (
{ primary : { index , alt , url } } : ReaderStatePages [ 'pages' ] [ number ] ,
2024-12-22 00:42:33 +01:00
pagesIndex : number ,
isPrimaryPage : boolean ,
onLoad : ComponentProps < typeof ReaderPage > [ 'onLoad' ] ,
onError : ComponentProps < typeof ReaderPage > [ 'onError' ] ,
2024-11-06 13:00:36 +01:00
shouldLoad : boolean ,
display : boolean ,
currentPageIndex : number ,
totalPages : number ,
2024-12-07 04:47:28 +01:00
retryKeyPrefix? : string ,
2024-11-06 13:00:36 +01:00
position ? : 'left' | 'right' ,
isDoublePage? : boolean ,
2024-12-16 21:14:18 +01:00
marginTop? : number ,
2024-12-22 00:42:33 +01:00
setRef ? : ( pagesIndex : number , ref : HTMLElement | null ) = > void ,
2024-11-06 13:00:36 +01:00
) : ReactNode = > (
< ReaderPage
2024-12-22 00:42:33 +01:00
setRef = { setRef }
pageIndex = { index }
pagesIndex = { pagesIndex }
isPrimaryPage = { isPrimaryPage }
2024-11-06 13:00:36 +01:00
key = { url }
src = { url }
alt = { alt }
display = { display }
2024-12-22 00:42:33 +01:00
priority = { getPageDownloadPriority ( currentPageIndex , index , totalPages , shouldLoad ) }
2024-11-06 13:00:36 +01:00
position = { position }
onLoad = { onLoad }
2024-12-07 04:47:28 +01:00
onError = { onError }
2024-11-06 13:00:36 +01:00
doublePage = { isDoublePage }
shouldLoad = { shouldLoad }
2024-12-07 04:47:28 +01:00
retryKeyPrefix = { retryKeyPrefix }
2024-12-16 21:14:18 +01:00
marginTop = { marginTop }
2024-11-06 13:00:36 +01:00
/ >
) ;
2024-12-07 03:43:18 +01:00
const getIsPageInViewportInfo = (
element : HTMLElement ,
) : {
isLeftInViewport : boolean ;
isRightInViewport : boolean ;
isFillingWidthViewportCompletely : boolean ;
isTopInViewport : boolean ;
isBottomInViewport : boolean ;
isFillingHeightViewportCompletely : boolean ;
} = > {
2024-11-06 13:00:36 +01:00
const { top , bottom , left , right } = element . getBoundingClientRect ( ) ;
// for some reason using "scrollIntoView" to scroll a page into view does not always result in the page to be at top,
// left/right 0px and instead at something like -0.25px
const MIN_VISIBLE_PX = 1 ;
const isLeftInViewport = left >= MIN_VISIBLE_PX && left <= window . innerWidth ;
const isRightInViewport = right >= MIN_VISIBLE_PX && right <= window . innerWidth ;
const isFillingWidthViewportCompletely = left <= MIN_VISIBLE_PX && right >= window . innerWidth ;
const isTopInViewport = top >= MIN_VISIBLE_PX && top <= window . innerHeight ;
const isBottomInViewport = bottom >= MIN_VISIBLE_PX && bottom <= window . innerHeight ;
const isFillingHeightViewportCompletely = top <= MIN_VISIBLE_PX && bottom >= window . innerHeight ;
2024-12-07 03:43:18 +01:00
return {
isLeftInViewport ,
isRightInViewport ,
isFillingWidthViewportCompletely ,
isTopInViewport ,
isBottomInViewport ,
isFillingHeightViewportCompletely ,
} ;
} ;
export const isPageInViewport = ( element : HTMLElement , type : PageInViewportType ) : boolean = > {
const {
isLeftInViewport ,
isRightInViewport ,
isFillingWidthViewportCompletely ,
isTopInViewport ,
isBottomInViewport ,
isFillingHeightViewportCompletely ,
} = getIsPageInViewportInfo ( element ) ;
2024-11-06 13:00:36 +01:00
const isInViewportX = isLeftInViewport || isRightInViewport || isFillingWidthViewportCompletely ;
const isInViewportY = isTopInViewport || isBottomInViewport || isFillingHeightViewportCompletely ;
switch ( type ) {
case PageInViewportType . X :
return isInViewportX ;
case PageInViewportType . Y :
return isInViewportY ;
default :
throw new Error ( ` unexpected "type" ( ${ type } ) ` ) ;
}
} ;
2024-12-07 03:43:18 +01:00
export const isEndOfPageInViewport = (
element : HTMLElement ,
type : PageInViewportType ,
direction : ReadingDirection ,
) : boolean = > {
const { isLeftInViewport , isRightInViewport , isBottomInViewport } = getIsPageInViewportInfo ( element ) ;
switch ( type ) {
case PageInViewportType . X :
return direction === ReadingDirection . LTR ? isRightInViewport : isLeftInViewport ;
case PageInViewportType . Y :
return isBottomInViewport ;
default :
throw new Error ( ` unexpected "type" ( ${ type } ) ` ) ;
}
} ;
2024-11-06 13:00:36 +01:00
export const getDoublePageModePages = (
pageUrls : ReaderStatePages [ 'pageUrls' ] ,
pagesToSpreadState : boolean [ ] ,
shouldOffsetDoubleSpreads : IReaderSettings [ 'shouldOffsetDoubleSpreads' ] ,
direction : ReadingDirection ,
) : ReaderStatePages [ 'pages' ] = > {
const doublePageModePages : ReaderStatePages [ 'pages' ] = [ ] ;
// each spread page has to be counted as 2 pages and all trailing page numbers have to be increased by the count
// of leading spread pages
const pageToActualPageIndex = pagesToSpreadState . map (
( _ , page ) = > page + pagesToSpreadState . slice ( 0 , page ) . filter ( Boolean ) . length ,
) ;
pageUrls . forEach ( ( url , index ) = > {
/ *
| = page separator
+ = double page
_ = double spread
without double spreads :
without double spread offset : | 0 + 1 | 2 + 3 | 4 + 5 | 6 + 7 | 8 |
with double spread offset : | 0 | 1 + 2 | 3 + 4 | 5 + 6 | 7 + 8 |
with double spreads
to handle double spreads :
each double spread has to count as 2 pages , thus , each page number after a double spread has to increase
by the number of leading double spreads
without double spread offset : | 0 + 1 | 2 | _3 / 4 _ | 5 | 6 + 7 | 8 | _9 / 10 _ | 11 | 12 + 13 | 14 |
with double spread offset : | 0 | 1 + 2 | _3 / 4 _ | 5 + 6 | 7 + 8 | _9 / 10 _ | 11 + 12 | 13 + 14 |
thus , to get the second page :
without offset : second page = current page number even ? + 1 : - 1
with offset : second page = current page number even ? - 1 : + 1
the second page has to be ignored in case :
- double spreads are offset , and the current page is the first page
- either the current or second page is a double spread
* /
const normalizedIndex = pageToActualPageIndex [ index ] ;
const isCurrentPageEven = ! ( normalizedIndex % 2 ) ;
const secondPageOffset = ( ( ) = > {
const invert = shouldOffsetDoubleSpreads ? - 1 : 1 ;
const offset = isCurrentPageEven ? 1 : - 1 ;
return offset * invert ;
} ) ( ) ;
const secondPageIndex = index + secondPageOffset ;
const isPrimaryPage = index < secondPageIndex ;
const isFirstPage = index === 0 ;
const isLastPage = index === pageUrls . length - 1 ;
const hasSecondPage = isPrimaryPage && ! isLastPage ;
const isPrimaryPageSpreadPage = pagesToSpreadState [ index ] ;
const isSecondPageSpreadPage = pagesToSpreadState [ secondPageIndex ] ;
const hasSpreadPage = isPrimaryPageSpreadPage || isSecondPageSpreadPage ;
const ignoreSecondPageDueToOffset = isFirstPage && shouldOffsetDoubleSpreads ;
const displaySecondPage = ! hasSpreadPage && ! ignoreSecondPageDueToOffset ;
if ( ! isPrimaryPage && displaySecondPage ) {
const doublePageModePage = doublePageModePages [ doublePageModePages . length - 1 ] ;
doublePageModePages [ doublePageModePages . length - 1 ] = {
. . . doublePageModePage ,
secondary : createSinglePageData ( url , index ) ,
} ;
return ;
}
const SEPARATOR = '-' ;
const pageName = ` ${ index + 1 } ${ hasSecondPage && displaySecondPage ? ` ${ SEPARATOR } ${ secondPageIndex + 1 } ` : '' } ` ;
doublePageModePages . push ( {
name : direction === ReadingDirection . LTR ? pageName : reverseString ( pageName , SEPARATOR ) ,
primary : createSinglePageData ( url , index ) ,
} ) ;
} ) ;
return doublePageModePages ;
} ;
export const isSpreadPage = ( image : HTMLImageElement ) : boolean = > {
const aspectRatio = image . height / image . width ;
return aspectRatio < 1 ;
} ;
const PREVIOUS_IMAGE_LOAD_AMOUNT = 2 ;
export const getPageIndexesToLoad = (
currentPageIndex : number ,
pages : ReaderStatePages [ 'pages' ] ,
previousCurrentPageIndex : number ,
imagePreLoadAmount : number ,
) : number [ ] = > {
const currentPagesIndex = getPage ( currentPageIndex , pages ) . pagesIndex ;
const directionInvert = previousCurrentPageIndex <= currentPageIndex ? 1 : - 1 ;
// load at most PREVIOUS_IMAGE_LOAD_AMOUNT of the previous pages to ensure that you do not have to wait too long
// when going back to the previous pages
const startPagesIndex = Math . max (
0 ,
currentPagesIndex - Math . min ( PREVIOUS_IMAGE_LOAD_AMOUNT , imagePreLoadAmount ) * directionInvert ,
) ;
const endPagesIndex = currentPagesIndex + imagePreLoadAmount * directionInvert ;
const pagesToRenderLength = Math . abs ( endPagesIndex - startPagesIndex ) + 1 ;
return Array ( pagesToRenderLength )
. fill ( 1 )
. map ( ( _ , index ) = > startPagesIndex + index * directionInvert ) ;
} ;
2024-12-03 18:00:46 +01:00
export const isTransitionPageVisible = (
mode : ReaderTransitionPageMode ,
activeMode : ReaderTransitionPageMode ,
readingMode : IReaderSettings [ 'readingMode' ] ,
) : boolean = > [ ReaderTransitionPageMode . BOTH , mode ] . includes ( activeMode ) || isContinuousReadingMode ( readingMode ) ;
2024-12-09 15:07:52 +01:00
export const getScrollIntoViewInlineOption = (
offset : DirectionOffset ,
themeDirection : Direction ,
readingDirection : ReadingDirection ,
) : ScrollIntoViewOptions [ 'inline' ] = > {
const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION [ readingDirection ] ;
if ( themeDirection === 'ltr' ) {
if ( offset === DirectionOffset . PREVIOUS ) {
return getOptionForDirection ( 'start' , 'end' , themeDirectionForReadingDirection ) ;
}
return getOptionForDirection ( 'start' , 'end' , themeDirectionForReadingDirection ) ;
}
if ( offset === DirectionOffset . PREVIOUS ) {
return getOptionForDirection ( 'end' , 'start' , themeDirectionForReadingDirection ) ;
}
return getOptionForDirection ( 'end' , 'start' , themeDirectionForReadingDirection ) ;
} ;