From 51fb660c7b783933578a37187c4bcb93f29c15f7 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:02:44 +0200 Subject: [PATCH] Reader tap zones --- public/locales/en.json | 3 + src/modules/core/contexts/AppContext.tsx | 5 +- src/modules/manga/Manga.types.ts | 2 +- src/modules/metadata/Metadata.constants.ts | 3 + .../reader/components/TapZoneLayout.tsx | 64 ++++++ .../{ => constants}/Reader.constants.ts | 6 +- .../constants/ReaderTapZone.constants.ts | 136 ++++++++++++ .../reader/contexts/ReaderContextProvider.tsx | 14 ++ .../reader/contexts/ReaderTapZoneContext.tsx | 21 ++ .../contexts/ReaderTapZoneContextProvider.tsx | 18 ++ .../state/ReaderStateContextProvider.tsx | 14 ++ .../state/ReaderStateMangaContext.tsx | 22 ++ .../state/ReaderStateMangaContextProvider.tsx | 18 ++ src/modules/reader/services/ReaderService.ts | 21 ++ .../reader/services/ReaderSettingsMetadata.ts | 48 ++++- .../reader/services/ReaderTapZoneService.ts | 196 ++++++++++++++++++ .../reader/{ => types}/Reader.types.ts | 10 + .../reader/types/TapZoneLayout.types.ts | 35 ++++ 18 files changed, 623 insertions(+), 13 deletions(-) create mode 100644 src/modules/reader/components/TapZoneLayout.tsx rename src/modules/reader/{ => constants}/Reader.constants.ts (62%) create mode 100644 src/modules/reader/constants/ReaderTapZone.constants.ts create mode 100644 src/modules/reader/contexts/ReaderContextProvider.tsx create mode 100644 src/modules/reader/contexts/ReaderTapZoneContext.tsx create mode 100644 src/modules/reader/contexts/ReaderTapZoneContextProvider.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStateContextProvider.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStateMangaContext.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx create mode 100644 src/modules/reader/services/ReaderService.ts create mode 100644 src/modules/reader/services/ReaderTapZoneService.ts rename src/modules/reader/{ => types}/Reader.types.ts (66%) create mode 100644 src/modules/reader/types/TapZoneLayout.types.ts diff --git a/public/locales/en.json b/public/locales/en.json index bd9df935..17b0e27f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -404,12 +404,15 @@ "load_in_progress": "Still loading required data…", "loading": "Loading…", "logged_in": "Logged in", + "menu": "Menu", "never": "Never", + "next": "Next", "none": "None", "open_in_browser": "Open in browser", "other": "Other", "password": "Password", "placeholder": "Placeholder", + "previous": "Previous", "sort": "Sort", "started": "Started", "type": "Type", diff --git a/src/modules/core/contexts/AppContext.tsx b/src/modules/core/contexts/AppContext.tsx index 019a183e..30ebbed1 100644 --- a/src/modules/core/contexts/AppContext.tsx +++ b/src/modules/core/contexts/AppContext.tsx @@ -26,6 +26,7 @@ import { ActiveDeviceContextProvider } from '@/modules/device/contexts/DeviceCon import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { AppThemes, getTheme } from '@/modules/theme/services/AppThemes.ts'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; +import { ReaderContextProvider } from '@/modules/reader/contexts/ReaderContextProvider.tsx'; interface Props { children: React.ReactNode; @@ -94,7 +95,9 @@ export const AppContext: React.FC = ({ children }) => { - {children} + + {children} + diff --git a/src/modules/manga/Manga.types.ts b/src/modules/manga/Manga.types.ts index 5f4414d7..59430f9b 100644 --- a/src/modules/manga/Manga.types.ts +++ b/src/modules/manga/Manga.types.ts @@ -12,7 +12,7 @@ import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSe import { useManageMangaLibraryState } from '@/modules/manga/hooks/useManageMangaLibraryState.tsx'; import { ChapterType, MangaReaderFieldsFragment, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts'; import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx'; -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; import { GridLayout } from '@/modules/core/Core.types.ts'; export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate'; diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index c56c0da0..c80e20c4 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -54,6 +54,9 @@ const APP_METADATA_OBJECT: Record = { hasStatus: undefined, customThemes: undefined, mangaThumbnailBackdrop: undefined, + tapZoneLayout: undefined, + tapZoneInvertMode: undefined, + readingDirection: undefined, }; export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT); diff --git a/src/modules/reader/components/TapZoneLayout.tsx b/src/modules/reader/components/TapZoneLayout.tsx new file mode 100644 index 00000000..ff089e48 --- /dev/null +++ b/src/modules/reader/components/TapZoneLayout.tsx @@ -0,0 +1,64 @@ +/* + * 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/. + */ + +import Box from '@mui/material/Box'; +import { useCallback, useLayoutEffect, useState } from 'react'; +import { useTheme } from '@mui/material/styles'; +import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; +import { ReaderTapZoneService } from '@/modules/reader/services/ReaderTapZoneService.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; + +const CANVAS_ID = 'reader-tap-zone-layout-canvas'; + +export const TapZoneLayout = () => { + const theme = useTheme(); + const { readerNavBarWidth } = useNavBarContext(); + const { showPreview } = useReaderTapZoneContext(); + const { tapZoneLayout, readingDirection, tapZoneInvertMode } = ReaderService.useSettings(); + + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const [tapZoneLayoutElement, setTapZoneLayoutElement] = useState(null); + useResizeObserver( + tapZoneLayoutElement, + useCallback(() => { + setWidth(tapZoneLayoutElement?.clientWidth ?? 0); + setHeight(tapZoneLayoutElement?.clientHeight ?? 0); + }, [tapZoneLayoutElement]), + ); + + const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout, width, height, theme.typography.h3, { + vertical: tapZoneInvertMode.vertical, + horizontal: tapZoneInvertMode.horizontal, + isRTL: readingDirection === ReadingDirection.RTL, + }); + + useLayoutEffect(() => { + const canvasContainerElement = document.getElementById(CANVAS_ID); + canvasContainerElement?.replaceChildren(canvas); + }, [canvas]); + + return ( + + {showPreview &&
} + + ); +}; diff --git a/src/modules/reader/Reader.constants.ts b/src/modules/reader/constants/Reader.constants.ts similarity index 62% rename from src/modules/reader/Reader.constants.ts rename to src/modules/reader/constants/Reader.constants.ts index 77b197cf..ee02054e 100644 --- a/src/modules/reader/Reader.constants.ts +++ b/src/modules/reader/constants/Reader.constants.ts @@ -6,7 +6,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; +import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; export const DEFAULT_READER_SETTINGS: IReaderSettings = { staticNav: false, @@ -17,4 +18,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = { scalePage: false, offsetFirstPage: false, readerWidth: 50, + tapZoneLayout: TapZoneLayouts.RIGHT_LEFT, + tapZoneInvertMode: { vertical: false, horizontal: false }, + readingDirection: ReadingDirection.LTR, }; diff --git a/src/modules/reader/constants/ReaderTapZone.constants.ts b/src/modules/reader/constants/ReaderTapZone.constants.ts new file mode 100644 index 00000000..362bc1dd --- /dev/null +++ b/src/modules/reader/constants/ReaderTapZone.constants.ts @@ -0,0 +1,136 @@ +/* + * 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/. + */ + +import { TapZoneLayouts, TapZoneRegion, TapZoneRegionType } from '@/modules/reader/types/TapZoneLayout.types.ts'; +import { TranslationKey } from '@/Base.types.ts'; + +export const READER_TAP_ZONE_LAYOUTS: Record = { + /** + * +---+---+---+ + * | N | N | N | P: Previous + * +---+---+---+ + * | N | M | N | M: Menu + * +---+---+---+ + * | N | P | N | N: Next + * +---+---+---+ + */ + [TapZoneLayouts.EDGE]: [ + { + rect: [0, 0, 100, 33.33], + type: TapZoneRegionType.NEXT, + }, + { + rect: [0, 33.33, 33.33, 66.66], + type: TapZoneRegionType.NEXT, + }, + { + rect: [66.66, 33.33, 33.33, 66.66], + type: TapZoneRegionType.NEXT, + }, + { + rect: [33.33, 33.33, 33.33, 33.33], + type: TapZoneRegionType.MENU, + }, + { + rect: [33.33, 66.66, 33.33, 33.33], + type: TapZoneRegionType.PREVIOUS, + }, + ], + /** + * +---+---+---+ + * | M | M | M | P: Previous + * +---+---+---+ + * | P | N | N | M: Menu + * +---+---+---+ + * | P | N | N | N: Next + * +---+---+---+ + */ + [TapZoneLayouts.KINDLE]: [ + { + rect: [0, 0, 100, 33.33], + type: TapZoneRegionType.MENU, + }, + { + rect: [0, 33.33, 33.33, 66.66], + type: TapZoneRegionType.PREVIOUS, + }, + { + rect: [33.33, 33.33, 66.66, 66.66], + type: TapZoneRegionType.NEXT, + }, + ], + /** + * +---+---+---+ + * | P | P | P | P: Previous + * +---+---+---+ + * | P | M | N | M: Menu + * +---+---+---+ + * | N | N | N | N: Next + * +---+---+---+ + */ + [TapZoneLayouts.L_SHAPE]: [ + { + rect: [0, 0, 100, 33.33], + type: TapZoneRegionType.PREVIOUS, + }, + { + rect: [0, 33.33, 33.33, 33.33], + type: TapZoneRegionType.PREVIOUS, + }, + { + rect: [33.33, 33.33, 33.33, 33.33], + type: TapZoneRegionType.MENU, + }, + { + rect: [66.66, 33.33, 33.33, 33.33], + type: TapZoneRegionType.NEXT, + }, + { + rect: [0, 66.66, 100, 33.33], + type: TapZoneRegionType.NEXT, + }, + ], + /** + * +---+---+---+ + * | P | M | N | P: Previous + * +---+---+---+ + * | P | M | N | M: Menu + * +---+---+---+ + * | P | M | N | N: Next + * +---+---+---+ + */ + [TapZoneLayouts.RIGHT_LEFT]: [ + { + rect: [0, 0, 33.33, 100], + type: TapZoneRegionType.PREVIOUS, + }, + { + rect: [33.33, 0, 33.33, 100], + type: TapZoneRegionType.MENU, + }, + { + rect: [66.66, 0, 33.33, 100], + type: TapZoneRegionType.NEXT, + }, + ], +}; + +export const TAP_ZONE_REGION_TYPE_DATA: Record = { + [TapZoneRegionType.PREVIOUS]: { + text: 'global.label.previous', + color: 'rgba(255, 114, 118, .5)', + }, + [TapZoneRegionType.NEXT]: { + text: 'global.label.next', + color: 'rgba(144, 238, 144, .5)', + }, + [TapZoneRegionType.MENU]: { + text: 'global.label.menu', + color: 'rgba(0, 0, 0, .5)', + }, +}; diff --git a/src/modules/reader/contexts/ReaderContextProvider.tsx b/src/modules/reader/contexts/ReaderContextProvider.tsx new file mode 100644 index 00000000..2d146bf6 --- /dev/null +++ b/src/modules/reader/contexts/ReaderContextProvider.tsx @@ -0,0 +1,14 @@ +/* + * 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/. + */ + +import { ReactNode } from 'react'; +import { ReaderTapZoneContextProvider } from '@/modules/reader/contexts/ReaderTapZoneContextProvider.tsx'; + +export const ReaderContextProvider = ({ children }: { children?: ReactNode }) => ( + {children} +); diff --git a/src/modules/reader/contexts/ReaderTapZoneContext.tsx b/src/modules/reader/contexts/ReaderTapZoneContext.tsx new file mode 100644 index 00000000..1b6b9930 --- /dev/null +++ b/src/modules/reader/contexts/ReaderTapZoneContext.tsx @@ -0,0 +1,21 @@ +/* + * 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/. + */ + +import { createContext, useContext } from 'react'; + +type TReaderTapZoneContext = { + showPreview: boolean; + setShowPreview: (showPreview: boolean) => void; +}; + +export const ReaderTapZoneContext = createContext({ + showPreview: false, + setShowPreview: () => undefined, +}); + +export const useReaderTapZoneContext = () => useContext(ReaderTapZoneContext); diff --git a/src/modules/reader/contexts/ReaderTapZoneContextProvider.tsx b/src/modules/reader/contexts/ReaderTapZoneContextProvider.tsx new file mode 100644 index 00000000..280233f5 --- /dev/null +++ b/src/modules/reader/contexts/ReaderTapZoneContextProvider.tsx @@ -0,0 +1,18 @@ +/* + * 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/. + */ + +import { ReactNode, useMemo, useState } from 'react'; +import { ReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; + +export const ReaderTapZoneContextProvider = ({ children }: { children: ReactNode }) => { + const [showPreview, setShowPreview] = useState(false); + + const value = useMemo(() => ({ showPreview, setShowPreview }), [showPreview]); + + return {children}; +}; diff --git a/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx new file mode 100644 index 00000000..4cb3bd83 --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx @@ -0,0 +1,14 @@ +/* + * 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/. + */ + +import { ReactNode } from 'react'; +import { ReaderStateMangaContextProvider } from '@/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx'; + +export const ReaderStateContextProvider = ({ children }: { children: ReactNode }) => ( + {children} +); diff --git a/src/modules/reader/contexts/state/ReaderStateMangaContext.tsx b/src/modules/reader/contexts/state/ReaderStateMangaContext.tsx new file mode 100644 index 00000000..55912465 --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStateMangaContext.tsx @@ -0,0 +1,22 @@ +/* + * 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/. + */ + +import { createContext, useContext } from 'react'; +import { TMangaReader } from '@/modules/manga/Manga.types.ts'; + +type TReaderStateMangaContext = { + manga: TMangaReader | undefined; + setManga: (manga: TMangaReader | undefined) => void; +}; + +export const ReaderStateMangaContext = createContext({ + manga: undefined, + setManga: () => undefined, +}); + +export const useReaderStateMangaContext = () => useContext(ReaderStateMangaContext); diff --git a/src/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx new file mode 100644 index 00000000..17cd8769 --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx @@ -0,0 +1,18 @@ +/* + * 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/. + */ + +import { ContextType, ReactNode, useMemo, useState } from 'react'; +import { ReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; + +export const ReaderStateMangaContextProvider = ({ children }: { children: ReactNode }) => { + const [manga, setManga] = useState['manga']>(); + + const value = useMemo(() => ({ manga, setManga }), [manga]); + + return {children}; +}; diff --git a/src/modules/reader/services/ReaderService.ts b/src/modules/reader/services/ReaderService.ts new file mode 100644 index 00000000..dac12493 --- /dev/null +++ b/src/modules/reader/services/ReaderService.ts @@ -0,0 +1,21 @@ +/* + * 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/. + */ + +import { useMemo } from 'react'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; +import { getReaderSettingsFor, useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; + +export class ReaderService { + static useSettings(): IReaderSettings { + const { manga } = useReaderStateMangaContext(); + const { settings } = useDefaultReaderSettings(); + + return useMemo(() => getReaderSettingsFor(manga ?? { id: -1 }, settings), [manga, settings]); + } +} diff --git a/src/modules/reader/services/ReaderSettingsMetadata.ts b/src/modules/reader/services/ReaderSettingsMetadata.ts index 211ca26f..1b616073 100644 --- a/src/modules/reader/services/ReaderSettingsMetadata.ts +++ b/src/modules/reader/services/ReaderSettingsMetadata.ts @@ -8,13 +8,38 @@ import { useEffect, useMemo } from 'react'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.ts'; -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/Reader.constants.ts'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts'; import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts'; -import { GqlMetaHolder, Metadata, MetadataHolder, MetadataHolderType } from '@/modules/metadata/Metadata.types.ts'; +import { + AllowedMetadataValueTypes, + AppMetadataKeys, + GqlMetaHolder, + Metadata, + MetadataHolder, + MetadataHolderType, +} from '@/modules/metadata/Metadata.types.ts'; +import { jsonSaveParse } from '@/lib/HelperFunctions.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; +const convertSettingsToMetadata = ( + settings: Partial, +): Metadata => ({ + ...settings, + tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode), +}); + +const convertMetadataToSettings = ( + metadata: Partial>, + defaultSettings: IReaderSettings, +): IReaderSettings => ({ + ...(metadata as unknown as IReaderSettings), + tapZoneInvertMode: + jsonSaveParse((metadata.tapZoneInvertMode as string) ?? '') ?? + defaultSettings.tapZoneInvertMode, +}); + function getReaderSettingsWithDefaultValueFallback( type: 'global', metadataHolder: MetadataHolder, @@ -33,13 +58,16 @@ function getReaderSettingsWithDefaultValueFallback[0], - metadataHolder as Parameters[1], - defaultSettings, - true, - useEffectFn, - ); + return convertMetadataToSettings( + getMetadataFrom( + type as Parameters[0], + metadataHolder as Parameters[1], + convertSettingsToMetadata(defaultSettings), + true, + useEffectFn, + ), + defaultSettings as IReaderSettings, + ) as DefaultSettings; } const getSettings = ( diff --git a/src/modules/reader/services/ReaderTapZoneService.ts b/src/modules/reader/services/ReaderTapZoneService.ts new file mode 100644 index 00000000..fba97974 --- /dev/null +++ b/src/modules/reader/services/ReaderTapZoneService.ts @@ -0,0 +1,196 @@ +/* + * 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/. + */ + +import i18next, { t as translate } from 'i18next'; +import { CSSProperties } from 'react'; +import { + ReaderTapZoneRect, + TapZoneInvertMode, + TapZoneLayouts, + TapZoneRegion, + TapZoneRegionType, +} from '@/modules/reader/types/TapZoneLayout.types.ts'; +import { + READER_TAP_ZONE_LAYOUTS, + TAP_ZONE_REGION_TYPE_DATA, +} from '@/modules/reader/constants/ReaderTapZone.constants.ts'; + +interface InvertMode extends TapZoneInvertMode { + isRTL: boolean; +} + +export class ReaderTapZoneService { + private static layout: TapZoneLayouts | null = null; + + private static canvas: HTMLCanvasElement | null = null; + + private static width: number | null = null; + + private static height: number | null = null; + + private static language: string | null = null; + + private static invertMode: InvertMode | null = null; + + private static regions: TapZoneRegion[] | null = null; + + private static fontStyle: CSSProperties | null = null; + + private static invertVertical = ([x, y, width, height]: ReaderTapZoneRect): ReaderTapZoneRect => [ + x, + 100 - (y + height), + width, + height, + ]; + + private static invertHorizontal = ([x, y, width, height]: ReaderTapZoneRect): ReaderTapZoneRect => [ + 100 - (x + width), + y, + width, + height, + ]; + + private static invertRect(rect: ReaderTapZoneRect, { vertical, horizontal, isRTL }: InvertMode): ReaderTapZoneRect { + const rectInvertedVertical = vertical ? this.invertVertical(rect) : rect; + const rectInvertedHorizontal = horizontal ? this.invertHorizontal(rectInvertedVertical) : rectInvertedVertical; + return isRTL ? this.invertHorizontal(rectInvertedHorizontal) : rectInvertedHorizontal; + } + + private static getRegions(layout: TapZoneLayouts, invertMode: InvertMode): TapZoneRegion[] { + const regions = READER_TAP_ZONE_LAYOUTS[layout]; + + return regions.map(({ type, rect }) => ({ type, rect: this.invertRect(rect, invertMode) })); + } + + private static isCanvasReusable( + layout: TapZoneLayouts, + canvasWidth: number, + canvasHeight: number, + fontStyle: CSSProperties, + invertMode: InvertMode, + ): boolean { + const isSameLayout = this.layout === layout; + const isSameWidth = this.width === canvasWidth; + const isSameHeight = this.height === canvasHeight; + const doesCanvasExist = !!this.canvas; + const isSameLanguage = this.language === i18next.language; + const isSameInvertMode = + this.invertMode?.vertical === invertMode.vertical && + this.invertMode.horizontal === invertMode.horizontal && + this.invertMode.isRTL === invertMode.isRTL; + const isSameFontStyle = this.fontStyle === fontStyle; + + return ( + doesCanvasExist && + isSameWidth && + isSameHeight && + isSameLayout && + isSameLanguage && + isSameInvertMode && + isSameFontStyle + ); + } + + private static drawCanvas( + context: CanvasRenderingContext2D, + canvasWidth: number, + canvasHeight: number, + fontStyle: CSSProperties, + regions: TapZoneRegion[], + ): void { + regions.forEach(({ type, rect: [rectX, rectY, rectWidth, rectHeight] }) => { + const { text: translationKey, color } = TAP_ZONE_REGION_TYPE_DATA[type]; + const text = translate(translationKey); + + const calcActualValue = (value: number, size: number) => (value / 100) * size; + const x = calcActualValue(rectX, canvasWidth); + const y = calcActualValue(rectY, canvasHeight); + const width = calcActualValue(rectWidth, canvasWidth); + const height = calcActualValue(rectHeight, canvasHeight); + + const calcRectCenter = (pos1: number, pos2: number) => (pos1 + pos2) * 0.5 + pos1 * 0.5; + const rectCenterX = calcRectCenter(x, width); + const rectCenterY = calcRectCenter(y, height); + + context.beginPath(); + + context.rect(x, y, width, height); + context.fillStyle = color; + context.strokeStyle = color; + context.lineWidth = 0.1; + + context.stroke(); + context.fill(); + + context.strokeStyle = 'black'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.font = `${fontStyle.fontSize} ${fontStyle.fontFamily}`; + + context.strokeStyle = 'black'; + context.lineWidth = 3; + context.strokeText(text, rectCenterX, rectCenterY); + + context.lineWidth = 1; + context.fillStyle = 'white'; + context.fillText(text, rectCenterX, rectCenterY); + }); + } + + static getOrCreateCanvas( + layout: TapZoneLayouts, + canvasWidth: number, + canvasHeight: number, + fontStyle: CSSProperties, + invertMode: InvertMode, + ): HTMLCanvasElement { + if (this.isCanvasReusable(layout, canvasWidth, canvasHeight, fontStyle, invertMode)) { + return this.canvas!; + } + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d')!; + + context.canvas.width = canvasWidth; + context.canvas.height = canvasHeight; + + const regions = this.getRegions(layout, invertMode); + this.drawCanvas(context, canvasWidth, canvasHeight, fontStyle, regions); + + this.layout = layout; + this.canvas = canvas; + this.width = canvasWidth; + this.height = canvasHeight; + this.language = i18next.language; + this.invertMode = invertMode; + this.regions = regions; + this.fontStyle = fontStyle; + + return canvas; + } + + private static doesRegionContainPos(region: TapZoneRegion, x: number, y: number): boolean { + const [rectX, rectY, width, height] = region.rect; + + return x >= rectX && x <= rectX + width && y >= rectY && y <= rectY + height; + } + + static getAction(x: number, y: number): TapZoneRegionType { + const xPercentage = (x / this.width!) * 100; + const yPercentage = (y / this.height!) * 100; + const region = this.regions?.find((regionToCheck) => + this.doesRegionContainPos(regionToCheck, xPercentage, yPercentage), + ); + + if (region) { + return region.type; + } + + return TapZoneRegionType.MENU; + } +} diff --git a/src/modules/reader/Reader.types.ts b/src/modules/reader/types/Reader.types.ts similarity index 66% rename from src/modules/reader/Reader.types.ts rename to src/modules/reader/types/Reader.types.ts index 14967bac..63b22075 100644 --- a/src/modules/reader/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -6,6 +6,13 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; + +export enum ReadingDirection { + LTR, + RTL, +} + export interface IReaderSettings { staticNav: boolean; showPageNumber: boolean; @@ -15,4 +22,7 @@ export interface IReaderSettings { scalePage: boolean; offsetFirstPage: boolean; readerWidth: number; + tapZoneLayout: TapZoneLayouts; + tapZoneInvertMode: TapZoneInvertMode; + readingDirection: ReadingDirection; } diff --git a/src/modules/reader/types/TapZoneLayout.types.ts b/src/modules/reader/types/TapZoneLayout.types.ts new file mode 100644 index 00000000..e0a714b5 --- /dev/null +++ b/src/modules/reader/types/TapZoneLayout.types.ts @@ -0,0 +1,35 @@ +/* + * 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/. + */ + +export enum TapZoneLayouts { + EDGE, + KINDLE, + L_SHAPE, + RIGHT_LEFT, +} + +export enum TapZoneRegionType { + MENU, + PREVIOUS, + NEXT, +} + +/** + * percentage based values + */ +export type ReaderTapZoneRect = [X: number, Y: number, Width: number, Height: number]; + +export interface TapZoneRegion { + rect: ReaderTapZoneRect; + type: TapZoneRegionType; +} + +export interface TapZoneInvertMode { + vertical: boolean; + horizontal: boolean; +}