diff --git a/public/locales/en.json b/public/locales/en.json index 47ac57ce..67280fb5 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -344,6 +344,11 @@ "stop": "Stop", "submit": "Submit" }, + "colors": { + "black": "Black", + "gray": "Gray", + "white": "White" + }, "date": { "label": { "day_one": "Day", @@ -389,22 +394,30 @@ "label": { "advanced": "Advanced", "are_you_sure": "Are you sure?", + "auto": "Auto", + "both": "Both", + "bottom": "Bottom", "browse": "Browse", "client": "Client", "close": "Close", "copied_clipboard": "Copied to clipboard", "default": "Default", + "desktop": "Desktop", "disabled": "Disabled", "discord": "Discord", "display": "Display", "filter": "Filter", "general": "General", "github": "GitHub", + "hidden": "Hidden", + "horizontal": "Horizontal", + "left": "Left", "links": "Links", "load_in_progress": "Still loading required data…", "loading": "Loading…", "logged_in": "Logged in", "menu": "Menu", + "mobile": "Mobile", "never": "Never", "next": "Next", "none": "None", @@ -413,12 +426,15 @@ "password": "Password", "placeholder": "Placeholder", "previous": "Previous", + "right": "Right", "share": "Share", "sort": "Sort", + "standard": "Standard", "started": "Started", "type": "Type", "unknown": "Unknown", - "username": "Username" + "username": "Username", + "vertical": "Vertical" }, "language": { "label": { @@ -453,6 +469,9 @@ "value": "{{count}} $t(global.time.seconds.second, lowercase)" } }, + "unit": { + "px": "px" + }, "update": { "label": { "available": "Update available", @@ -472,7 +491,8 @@ } } } - } + }, + "value": "{{value}}{{unit}}" }, "library": { "action": { @@ -779,16 +799,51 @@ } }, "settings": { + "background_color": "Background color", + "custom_filter": { + "brightness": { + "title": "Custom brightness", + "value": "$t(reader.settings.custom_filter.brightness.title)" + }, + "contrast": { + "title": "Custom contrast", + "value": "$t(reader.settings.custom_filter.contrast.title)" + }, + "grayscale": "Grayscale", + "hue": { + "title": "Custom hue", + "value": "$t(reader.settings.custom_filter.hue.title)" + }, + "invert": "Invert", + "rgba": { + "alpha": "Alpha", + "blue": "Blue", + "green": "Green", + "red": "Red", + "title": "Custom color filters" + }, + "saturate": { + "title": "Custom saturate", + "value": "$t(reader.settings.custom_filter.saturate.title)" + }, + "sepia": "Sepia", + "title": "Custom filter" + }, "error": { "label": { "failed_to_save_default_settings": "Could not save the default reader settings to the server", "failed_to_save_settings": "Could not save the reader settings to the server" } }, + "exit_mode": "Open page on exit", "label": { + "behaviour": "Behaviour", "fit_page_to_window": "Fit page to window", + "layout": "Layout", + "limit_reader_width": "Limit reader width", "load_next_chapter": "Load next chapter at ending", "offset_double_spread": "Offset double spreads", + "page_gap": "Page gap", "reader_type": "Reader type", "reader_width": "Reader width", "reading_direction": "Reading direction", @@ -798,13 +853,20 @@ "skip_dup_chapters": "Skip duplicate chapters", "static_navigation": "Static navigation" }, + "overlay_mode": "Overlay mode", "page_scale": { "height": "Fit height", "original": "Original size", "screen": "Fit screen", "stretch": "Stretch small pages", + "title": "Scale type", "width": "Fit width" }, + "progress_bar": { + "position": "Progress bar position", + "size": "Progress bar size", + "style": "Progress bar style" + }, "reader_type": { "label": { "continuous_horizontal": "Continuous horizontal", @@ -818,6 +880,14 @@ "ltr": "Left to right", "rtl": "Right to left" }, + "tap_zones": { + "edge": "Edge", + "invert": "Invert tap zones", + "kindle": "Kindle", + "l_shape": "L-Shape", + "right_left": "Right and left", + "title": "Tap zones" + }, "title": { "default_reader_settings": "Default Reader Settings", "reader_settings": "Reader Settings" diff --git a/src/App.tsx b/src/App.tsx index 1650d2f9..25818d61 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -57,6 +57,10 @@ const { LibraryDuplicates } = loadable( lazyLoadFallback, ); const { Appearance } = loadable(() => import('@/modules/settings/screens/Appearance.tsx'), lazyLoadFallback); +const { DefaultReaderSettings } = loadable( + () => import('@/modules/reader/screens/DefaultReaderSettings.tsx'), + lazyLoadFallback, +); if (process.env.NODE_ENV !== 'production') { // Adds messages only in a dev environment @@ -114,7 +118,7 @@ const MainApp = () => { } /> } /> } /> - + } /> } /> } /> diff --git a/src/UtilTypes.d.ts b/src/UtilTypes.d.ts index 72f5b2d5..a40f511b 100644 --- a/src/UtilTypes.d.ts +++ b/src/UtilTypes.d.ts @@ -26,6 +26,10 @@ type PropertiesNever = { [key in keyof T]?: never }; type OmitFirst = T extends [any, ...infer R] ? R : never; -type TupleUnion = { +type TupleUnion = { [S in U]: Exclude extends never ? [...R, S] : TupleUnion, [...R, S]>; }[U]; + +type TransformRecordToWithDefaultFlag> = { + [K in keyof T]: { value: T[K]; isDefault: boolean }; +}; diff --git a/src/assets/icons/svg/HiddenProgressBarIcon.tsx b/src/assets/icons/svg/HiddenProgressBarIcon.tsx new file mode 100644 index 00000000..12edef14 --- /dev/null +++ b/src/assets/icons/svg/HiddenProgressBarIcon.tsx @@ -0,0 +1,24 @@ +/* + * 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 SvgIcon from '@mui/material/SvgIcon'; + +export const HiddenProgressBarIcon = () => ( + + + + + +); diff --git a/src/assets/icons/svg/StandardProgressBarIcon.tsx b/src/assets/icons/svg/StandardProgressBarIcon.tsx new file mode 100644 index 00000000..b1783421 --- /dev/null +++ b/src/assets/icons/svg/StandardProgressBarIcon.tsx @@ -0,0 +1,24 @@ +/* + * 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 SvgIcon from '@mui/material/SvgIcon'; + +export const StandardProgressBarIcon = () => ( + + + + + +); diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 791c5350..e0c13e85 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -27,7 +27,6 @@ import { } from '@apollo/client'; import { OperationVariables, Reference } from '@apollo/client/core'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { ReadFieldFunction } from '@apollo/client/cache/core/types/common'; import { IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts'; import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; import { @@ -327,6 +326,7 @@ import { SOURCE_META_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts'; import { CHAPTER_META_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts'; import { MetadataMigrationSettings } from '@/modules/migration/Migration.types.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; +import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts'; enum GQLMethod { QUERY = 'QUERY', @@ -427,24 +427,6 @@ export const SPECIAL_ED_SOURCES = { ], }; -const updateMetadata = ( - key: string, - existingMetas: Reference[] | undefined, - readField: ReadFieldFunction, - createMetaRef: () => Reference | undefined, -): (Reference | undefined)[] | undefined => { - if (!existingMetas) { - return existingMetas; - } - - const exists = existingMetas.some((metaRef: Reference) => readField('key', metaRef) === key); - if (exists) { - return existingMetas; - } - - return [...existingMetas, createMetaRef()]; -}; - // TODO - extract logic to reduce the size of this file... grew waaaaaaaaaaaaay too big peepoFat // TODO - correctly update cache after all mutations instead of refetching queries export class RequestManager { @@ -1599,7 +1581,7 @@ export class RequestManager { id: cache.identify({ __typename: 'SourceType', id: sourceId }), fields: { meta(existingMetas, { readField }) { - return updateMetadata(key, existingMetas, readField, () => + return updateMetadataList(key, existingMetas, readField, () => cache.writeFragment({ data: data!.setSourceMeta!.meta, fragment: SOURCE_META_FIELDS, @@ -1637,7 +1619,7 @@ export class RequestManager { }, }, update(cache) { - cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', id: sourceId, key }) }); + cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', sourceId, key }) }); }, ...options, }, @@ -2123,7 +2105,7 @@ export class RequestManager { id: cache.identify({ __typename: 'MangaType', id: mangaId }), fields: { meta(existingMetas, { readField }) { - return updateMetadata(key, existingMetas, readField, () => + return updateMetadataList(key, existingMetas, readField, () => cache.writeFragment({ data: data!.setMangaMeta!.meta, fragment: MANGA_META_FIELDS, @@ -2161,7 +2143,7 @@ export class RequestManager { }, }, update(cache) { - cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', id: mangaId, key }) }); + cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId, key }) }); }, ...options, }, @@ -2351,7 +2333,7 @@ export class RequestManager { id: cache.identify({ __typename: 'ChapterType', id: chapterId }), fields: { meta(existingMetas, { readField }) { - return updateMetadata(key, existingMetas, readField, () => + return updateMetadataList(key, existingMetas, readField, () => cache.writeFragment({ data: data!.setChapterMeta!.meta, fragment: CHAPTER_META_FIELDS, @@ -2389,7 +2371,7 @@ export class RequestManager { }, }, update(cache) { - cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', id: chapterId, key }) }); + cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', chapterId, key }) }); }, ...options, }, @@ -2630,7 +2612,7 @@ export class RequestManager { id: cache.identify({ __typename: 'CategoryType', id: categoryId }), fields: { meta(existingMetas, { readField }) { - return updateMetadata(key, existingMetas, readField, () => + return updateMetadataList(key, existingMetas, readField, () => cache.writeFragment({ data: data!.setCategoryMeta!.meta, fragment: CATEGORY_META_FIELDS, @@ -2668,7 +2650,7 @@ export class RequestManager { }, }, update(cache) { - cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', id: categoryId, key }) }); + cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', categoryId, key }) }); }, ...options, }, diff --git a/src/modules/core/Core.types.ts b/src/modules/core/Core.types.ts index 26b1bb64..01c1baac 100644 --- a/src/modules/core/Core.types.ts +++ b/src/modules/core/Core.types.ts @@ -22,3 +22,20 @@ export type ValueToDisplayData = Record< icon: ReactNode; } >; + +export interface MultiValueButtonBaseProps { + value: Value; + values: Value[]; + setValue: (value: Value) => void; + valueToDisplayData: ValueToDisplayData; +} + +export interface MultiValueButtonDefaultableProps + extends OptionalProperty, 'value'> { + isDefaultable?: boolean; + onDefault?: () => void; +} + +export type MultiValueButtonProps = + | (MultiValueButtonBaseProps & PropertiesNever>) + | MultiValueButtonDefaultableProps; diff --git a/src/modules/core/components/buttons/ButtonSelect.tsx b/src/modules/core/components/buttons/ButtonSelect.tsx index 60148c1f..92cb46ef 100644 --- a/src/modules/core/components/buttons/ButtonSelect.tsx +++ b/src/modules/core/components/buttons/ButtonSelect.tsx @@ -9,23 +9,25 @@ import Stack from '@mui/material/Stack'; import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; -import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { MultiValueButtonProps } from '@/modules/core/Core.types.ts'; export const ButtonSelect = ({ value, values, setValue, valueToDisplayData, -}: { - value: Value; - values: Value[]; - setValue: (value: Value) => void; - valueToDisplayData: ValueToDisplayData; -}) => { + isDefaultable, + onDefault, +}: MultiValueButtonProps) => { const { t } = useTranslation(); return ( + {isDefaultable && ( + + {t('global.label.default')} + + )} {values.map((displayValue) => ( ({ value, values, setValue, valueToDisplayData, -}: { - value: Value; - values: Value[]; - setValue: (value: Value) => void; - valueToDisplayData: ValueToDisplayData; -}) => { + isDefaultable, + onDefault, + defaultIcon, +}: MultiValueButtonProps & { defaultIcon?: ReactNode }) => { const { t } = useTranslation(); - const indexOfValue = useMemo(() => values.indexOf(value), [value, values]); + const isDefault = value === undefined; + const indexOfValue = useMemo(() => { + if (isDefault) { + return -1; + } + + return values.indexOf(value); + }, [value, values]); + + if (isDefault) { + return ( + setValue(values[0])} + sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }} + variant="contained" + startIcon={defaultIcon} + size="large" + > + {t('global.label.default')} + + ); + } return ( setValue(values[(indexOfValue + 1) % values.length])} + onClick={() => { + const nextValueIndex = (indexOfValue + 1) % values.length; + const wasLastValue = nextValueIndex === 0; + + const isDefaultNextValue = isDefaultable && wasLastValue; + if (isDefaultNextValue) { + onDefault?.(); + return; + } + + setValue(values[(indexOfValue + 1) % values.length]); + }} sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }} variant="contained" startIcon={valueToDisplayData[value].icon} diff --git a/src/modules/core/components/inputs/SliderInput.tsx b/src/modules/core/components/inputs/SliderInput.tsx new file mode 100644 index 00000000..c06bd1fd --- /dev/null +++ b/src/modules/core/components/inputs/SliderInput.tsx @@ -0,0 +1,37 @@ +/* + * 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 Slider, { SliderProps } from '@mui/material/Slider'; +import Typography, { TypographyProps } from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; + +export const SliderInput = ({ + label, + value, + slotProps, +}: { + label: string; + value: number | string; + slotProps?: { + label?: TypographyProps; + value?: TypographyProps; + slider?: SliderProps; + }; +}) => ( + + + + {label} + + + {value} + + + + +); diff --git a/src/modules/core/components/tabs/TabPanel.tsx b/src/modules/core/components/tabs/TabPanel.tsx index 784f1bec..870338c8 100644 --- a/src/modules/core/components/tabs/TabPanel.tsx +++ b/src/modules/core/components/tabs/TabPanel.tsx @@ -6,20 +6,21 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import Box, { BoxProps } from '@mui/material/Box'; import React from 'react'; -interface IProps { +interface IProps extends BoxProps { children: React.ReactNode; index: any; currentIndex: any; } export function TabPanel(props: IProps) { - const { children, index, currentIndex } = props; + const { children, index, currentIndex, ...boxProps } = props; return ( - + {currentIndex === index && children} - + ); } diff --git a/src/modules/core/components/tabs/TabsMenu.tsx b/src/modules/core/components/tabs/TabsMenu.tsx index b5f7c8f0..316d092e 100644 --- a/src/modules/core/components/tabs/TabsMenu.tsx +++ b/src/modules/core/components/tabs/TabsMenu.tsx @@ -35,7 +35,7 @@ export const TabsMenu = forwardRef( indicatorColor="primary" textColor="primary" variant="scrollable" - scrollButtons + scrollButtons="auto" allowScrollButtonsMobile {...props} > diff --git a/src/modules/core/utils/ShouldForwardProp.ts b/src/modules/core/utils/ShouldForwardProp.ts index 47e791e0..e4d9a73d 100644 --- a/src/modules/core/utils/ShouldForwardProp.ts +++ b/src/modules/core/utils/ShouldForwardProp.ts @@ -6,10 +6,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -type TupleUnion = { - [S in U]: Exclude extends never ? [...R, S] : TupleUnion, [...R, S]>; -}[U]; - export const shouldForwardProp = >(customProps: TupleUnion) => (prop: string): boolean => diff --git a/src/modules/library/screens/Library.tsx b/src/modules/library/screens/Library.tsx index 4e2c54be..79fb98be 100644 --- a/src/modules/library/screens/Library.tsx +++ b/src/modules/library/screens/Library.tsx @@ -272,7 +272,7 @@ export function Library() { handleTabChange(newTab)}> {tabs.map((tab) => ( diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index d281685b..6d724838 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -12,10 +12,6 @@ import { ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader. export const APP_METADATA_KEY_PREFIX = 'webUI_'; const APP_METADATA_OBJECT: Record = { - staticNav: undefined, - showPageNumber: undefined, - loadNextOnEnding: undefined, - skipDupChapters: undefined, migration: undefined, deleteChaptersManuallyMarkedRead: undefined, deleteChaptersWhileReading: undefined, @@ -60,8 +56,16 @@ const APP_METADATA_OBJECT: Record = { progressBarPosition: undefined, readingMode: undefined, pageScaleMode: undefined, - shouldScalePage: undefined, shouldOffsetDoubleSpreads: undefined, + exitMode: undefined, + customFilter: undefined, + shouldSkipDupChapters: undefined, + isStaticNav: undefined, + overlayMode: undefined, + shouldStretchPage: undefined, + shouldShowPageNumber: undefined, + backgroundColor: undefined, + pageGap: undefined, }; export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT); @@ -122,6 +126,11 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [ // themes 'customThemes', 'mangaThumbnailBackdrop', + + // reader + 'exitMode', + 'customFilter', + 'shouldSkipDupChapters', ]; /** @@ -203,7 +212,19 @@ export const METADATA_MIGRATIONS: IMetadataMigration[] = [ }, { oldKey: 'scalePage', - newKey: 'shouldScalePage', + newKey: 'shouldStretchPage', + }, + { + oldKey: 'skipDupChapters', + newKey: 'shouldSkipDupChapters', + }, + { + oldKey: 'showPageNumber', + newKey: 'shouldShowPageNumber', + }, + { + oldKey: 'staticNav', + newKey: 'isStaticNav', }, ], values: [ diff --git a/src/modules/metadata/services/MetadataConverter.ts b/src/modules/metadata/services/MetadataConverter.ts index 80f0609c..4f905f2b 100644 --- a/src/modules/metadata/services/MetadataConverter.ts +++ b/src/modules/metadata/services/MetadataConverter.ts @@ -31,14 +31,19 @@ export const convertValueFromMetadata = { +export const convertFromGqlMeta = ( + gqlMetadata?: MetaType[], + filter: (key: string) => boolean = () => true, +): Metadata | undefined => { if (!gqlMetadata) { return undefined; } const metadata: Metadata = {}; gqlMetadata.forEach(({ key, value }) => { - metadata[key] = value; + if (filter(key)) { + metadata[key] = value; + } }); return metadata; diff --git a/src/modules/metadata/services/MetadataReader.ts b/src/modules/metadata/services/MetadataReader.ts index 56f6135d..4d909673 100644 --- a/src/modules/metadata/services/MetadataReader.ts +++ b/src/modules/metadata/services/MetadataReader.ts @@ -42,7 +42,7 @@ import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts'; import { CategoryIdInfo } from '@/modules/category/Category.types.ts'; import { SourceType } from '@/lib/graphql/generated/graphql.ts'; -export const extractOriginalKey = (key: string): string => key.split('_').slice(-1)[0]; +export const extractOriginalKey = (key: string) => key.split('_').slice(-1)[0]; export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => { const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys); diff --git a/src/modules/reader/components/ReaderPageNumber.tsx b/src/modules/reader/components/ReaderPageNumber.tsx index 7bcb3583..25138a0b 100644 --- a/src/modules/reader/components/ReaderPageNumber.tsx +++ b/src/modules/reader/components/ReaderPageNumber.tsx @@ -16,18 +16,19 @@ import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/Rea import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx'; -import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; +import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; export const ReaderPageNumber = () => { - const isMobileWidth = MediaQuery.useIsMobileWidth(); + const { isDesktop } = ReaderService.useOverlayMode(); + const { scrollbarXSize } = useReaderScrollbarContext(); const { readerNavBarWidth } = useNavBarContext(); const { isMaximized } = useReaderProgressBarContext(); const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext(); - const { progressBarType, showPageNumber } = ReaderService.useSettings(); + const { progressBarType, shouldShowPageNumber } = ReaderService.useSettings(); const pageName = useMemo(() => getPage(currentPageIndex, pages).name, [currentPageIndex, pages]); - if (!showPageNumber) { + if (!shouldShowPageNumber) { return null; } @@ -35,7 +36,7 @@ export const ReaderPageNumber = () => { return null; } - if (!isMobileWidth && progressBarType === ProgressBarType.STANDARD) { + if (isDesktop && progressBarType === ProgressBarType.STANDARD) { return null; } @@ -49,7 +50,7 @@ export const ReaderPageNumber = () => { position: 'fixed', left: readerNavBarWidth, right: 0, - bottom: (theme) => theme.spacing(1), + bottom: (theme) => `calc(${theme.spacing(1)} + ${scrollbarXSize}px)`, alignItems: 'center', transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`, }} diff --git a/src/modules/reader/components/TapZoneLayout.tsx b/src/modules/reader/components/TapZoneLayout.tsx index ff089e48..fb0c3530 100644 --- a/src/modules/reader/components/TapZoneLayout.tsx +++ b/src/modules/reader/components/TapZoneLayout.tsx @@ -35,10 +35,10 @@ export const TapZoneLayout = () => { }, [tapZoneLayoutElement]), ); - const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout, width, height, theme.typography.h3, { - vertical: tapZoneInvertMode.vertical, - horizontal: tapZoneInvertMode.horizontal, - isRTL: readingDirection === ReadingDirection.RTL, + const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout.value, width, height, theme.typography.h3, { + vertical: tapZoneInvertMode.value.vertical, + horizontal: tapZoneInvertMode.value.horizontal, + isRTL: readingDirection.value === ReadingDirection.RTL, }); useLayoutEffect(() => { diff --git a/src/modules/reader/components/overlay/ReaderOverlay.tsx b/src/modules/reader/components/overlay/ReaderOverlay.tsx index c019f92e..85f567c4 100644 --- a/src/modules/reader/components/overlay/ReaderOverlay.tsx +++ b/src/modules/reader/components/overlay/ReaderOverlay.tsx @@ -7,36 +7,39 @@ */ import Box from '@mui/material/Box'; +import { useState } from 'react'; import { BaseReaderOverlayProps, MobileHeaderProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; +import { ReaderSettings } from '@/modules/reader/components/settings/ReaderSettings.tsx'; import { ReaderPageNumber } from '@/modules/reader/components/ReaderPageNumber.tsx'; import { StandardReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx'; import { ReaderNavBarDesktop } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx'; import { ReaderOverlayHeaderMobile } from '@/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx'; import { ReaderBottomBarMobile } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx'; -import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; export const ReaderOverlay = ({ isVisible }: BaseReaderOverlayProps & MobileHeaderProps) => { - const isTouchDevice = MediaQuery.useIsTouchDevice(); + const { isDesktop, isMobile } = ReaderService.useOverlayMode(); - const isDesktop = !isTouchDevice; - const isMobile = isTouchDevice; + const [areSettingsOpen, setAreSettingsOpen] = useState(false); return ( {isDesktop && ( <> - {}} /> + setAreSettingsOpen(true)} /> > )} {isMobile && ( <> - {}} isVisible={isVisible} /> + setAreSettingsOpen(true)} isVisible={isVisible} /> > )} + setAreSettingsOpen(false)} /> + ); diff --git a/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx b/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx index 9a84f1df..2a7b88fe 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx @@ -18,7 +18,6 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import Drawer from '@mui/material/Drawer'; import { useGetOptionForDirection } from '@/theme.tsx'; import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; -import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; import { ReaderNavContainer } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavContainer.tsx'; import { ReaderNavBarDesktopMetadata } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopMetadata.tsx'; import { ReaderNavBarDesktopPageNavigation } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopPageNavigation.tsx'; @@ -28,31 +27,31 @@ import { ReaderNavBarDesktopActions } from '@/modules/reader/components/overlay/ import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; -import { createUpdateReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; -import { makeToast } from '@/modules/core/utils/Toast.ts'; import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; +import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; -const useGetPreviousNavBarStaticValue = (isVisible: boolean, staticNav: boolean) => { - const wasNavBarStaticRef = useRef(staticNav); - const wasNavBarStaticPreviousRef = useRef(staticNav); +const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => { + const wasNavBarStaticRef = useRef(isStaticNav); + const wasNavBarStaticPreviousRef = useRef(isStaticNav); const resetWasNavBarStaticValue = wasNavBarStaticPreviousRef.current !== wasNavBarStaticRef.current && !isVisible; if (resetWasNavBarStaticValue) { wasNavBarStaticRef.current = false; } - const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== staticNav; + const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== isStaticNav; if (didNavBarStaticValueChange) { wasNavBarStaticRef.current = wasNavBarStaticPreviousRef.current; - wasNavBarStaticPreviousRef.current = staticNav; + wasNavBarStaticPreviousRef.current = isStaticNav; } return wasNavBarStaticRef.current; }; +const DEFAULT_MANGA: MangaIdInfo = { id: -1 }; export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDesktopProps) => { const { t } = useTranslation(); const { setReaderNavBarWidth } = useNavBarContext(); @@ -60,36 +59,33 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext(); const { pages, currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext(); - const handleBack = useBackButton(); const getOptionForDirection = useGetOptionForDirection(); - const updateReaderSettings = createUpdateReaderSettings(manga ?? { id: -1 }, () => - makeToast(t('reader.settings.error.label.failed_to_save_settings')), - ); - + const exit = ReaderService.useExit(); + const updateReaderSettings = ReaderService.useCreateUpdateSetting(manga ?? DEFAULT_MANGA); const settings = ReaderService.useSettings(); const [navBarElement, setNavBarElement] = useState(); useResizeObserver( navBarElement, useCallback(() => { - if (!settings?.staticNav) { + if (!settings?.isStaticNav) { return; } setReaderNavBarWidth(navBarElement!.offsetWidth); - }, [navBarElement, settings?.staticNav]), + }, [navBarElement, settings?.isStaticNav]), ); useLayoutEffect(() => () => setReaderNavBarWidth(0), []); - const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, settings.staticNav); + const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, settings.isStaticNav); const changedNavBarStaticValue = wasNavBarStatic && isVisible; const drawerTransitionDuration = changedNavBarStaticValue ? 0 : undefined; return ( setNavBarElement(ref), @@ -99,7 +95,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes - + {getOptionForDirection(, )} @@ -107,9 +103,9 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes { setReaderNavBarWidth(0); - updateReaderSettings('staticNav', !settings.staticNav); + updateReaderSettings('isStaticNav', !settings.isStaticNav); }} - color={settings.staticNav ? 'primary' : 'inherit'} + color={settings.isStaticNav ? 'primary' : 'inherit'} > @@ -147,6 +143,8 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes settings={settings} updateSetting={updateReaderSettings} openSettings={openSettings} + isDefaultable + onDefault={(...args) => manga && ReaderService.deleteSetting(manga, ...args)} /> diff --git a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx index 2455f273..b6a1cf7a 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx @@ -9,7 +9,8 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import { OffsetDoubleSpreadIcon } from '@/assets/icons/svg/OffsetDoubleSpreadIcon.tsx'; -import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; export const ReaderNavBarDesktopOffsetDoubleSpread = ({ readingMode, @@ -20,7 +21,7 @@ export const ReaderNavBarDesktopOffsetDoubleSpread = ({ }) => { const { t } = useTranslation(); - if (readingMode !== ReadingMode.DOUBLE_PAGE) { + if (!isOffsetDoubleSpreadPagesEditable(readingMode)) { return null; } diff --git a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopPageScale.tsx b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopPageScale.tsx index 141f49e1..9469f310 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopPageScale.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopPageScale.tsx @@ -9,65 +9,52 @@ import Stack from '@mui/material/Stack'; import Tooltip from '@mui/material/Tooltip'; import { useTranslation } from 'react-i18next'; -import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap'; -import ExpandIcon from '@mui/icons-material/Expand'; -import CropOriginalIcon from '@mui/icons-material/CropOriginal'; import FitScreenIcon from '@mui/icons-material/FitScreen'; import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx'; import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx'; -import { IReaderSettings, ReaderPageScaleMode } from '@/modules/reader/types/Reader.types.ts'; -import { ValueToDisplayData } from '@/modules/core/Core.types'; - -const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { - [ReaderPageScaleMode.WIDTH]: { - title: 'reader.settings.page_scale.width', - icon: , - }, - [ReaderPageScaleMode.HEIGHT]: { - title: 'reader.settings.page_scale.height', - icon: , - }, - [ReaderPageScaleMode.SCREEN]: { - title: 'reader.settings.page_scale.screen', - icon: , - }, - [ReaderPageScaleMode.ORIGINAL]: { - title: 'reader.settings.page_scale.original', - icon: , - }, -}; - -const READER_PAGE_SCALE_MODE_VALUES = Object.values(ReaderPageScaleMode).filter((value) => typeof value === 'number'); +import { + IReaderSettings, + IReaderSettingsWithDefaultFlag, + ReaderPageScaleMode, +} from '@/modules/reader/types/Reader.types.ts'; +import { + PAGE_SCALE_VALUE_TO_DISPLAY_DATA, + READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED, + READER_PAGE_SCALE_MODE_VALUES, +} from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts'; export const ReaderNavBarDesktopPageScale = ({ pageScaleMode, - shouldScalePage, + shouldStretchPage, updateSetting, -}: Pick & { - updateSetting: >( - setting: Setting, - value: IReaderSettings[Setting], - ) => void; -}) => { + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + updateSetting: >( + setting: Setting, + value: IReaderSettings[Setting], + ) => void; + }) => { const { t } = useTranslation(); - const isPageScalingPossible = pageScaleMode !== ReaderPageScaleMode.ORIGINAL; - return ( updateSetting('pageScaleMode', value)} - valueToDisplayData={VALUE_TO_DISPLAY_DATA} + valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA} + defaultIcon={PAGE_SCALE_VALUE_TO_DISPLAY_DATA[pageScaleMode.value].icon} /> - {isPageScalingPossible && ( + {READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode.value] && ( updateSetting('shouldScalePage', !shouldScalePage)} + onClick={() => updateSetting('shouldStretchPage', !shouldStretchPage.value)} sx={{ minWidth: 0 }} variant="contained" - color={shouldScalePage ? 'secondary' : 'primary'} + color={shouldStretchPage.value ? 'secondary' : 'primary'} > diff --git a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopQuickSettings.tsx b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopQuickSettings.tsx index 55906849..33f96656 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopQuickSettings.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopQuickSettings.tsx @@ -14,17 +14,16 @@ import { ReaderNavBarDesktopPageScale } from '@/modules/reader/components/overla import { ReaderNavBarDesktopReadingMode } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx'; import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx'; import { ReaderNavBarDesktopReadingDirection } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx'; -import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts'; import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; export const ReaderNavBarDesktopQuickSettings = ({ - settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldScalePage, readingDirection }, + settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldStretchPage, readingDirection }, updateSetting, openSettings, -}: { - settings: IReaderSettings; - updateSetting: (setting: Setting, value: IReaderSettings[Setting]) => void; -} & Pick) => { + isDefaultable, + onDefault, +}: ReaderSettingsTypeProps & Pick) => { const { t } = useTranslation(); return ( @@ -32,19 +31,26 @@ export const ReaderNavBarDesktopQuickSettings = ({ updateSetting('readingMode', value)} + isDefaultable={isDefaultable} + onDefault={() => onDefault?.('readingMode')} /> updateSetting('shouldOffsetDoubleSpreads', value)} /> onDefault?.('pageScaleMode')} /> updateSetting('readingDirection', value)} + isDefaultable={isDefaultable} + onDefault={() => onDefault?.('readingDirection')} /> openSettings()} diff --git a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx index 4803222e..85e98619 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx @@ -6,35 +6,28 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft'; -import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight'; import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx'; -import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; -import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; - -const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { - [ReadingDirection.LTR]: { - title: 'reader.settings.reading_direction.ltr', - icon: , - }, - [ReadingDirection.RTL]: { - title: 'reader.settings.reading_direction.rtl', - icon: , - }, -}; - -const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number'); +import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { + READING_DIRECTION_VALUES, + READING_DIRECTION_VALUE_TO_DISPLAY_DATA, +} from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts'; export const ReaderNavBarDesktopReadingDirection = ({ readingDirection, setReadingDirection, -}: Pick & { - setReadingDirection: (readingDirection: ReadingDirection) => void; -}) => ( + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + setReadingDirection: (readingDirection: ReadingDirection) => void; + }) => ( ); diff --git a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx index a53fd089..e84d8b6e 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx @@ -6,45 +6,28 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx'; -import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx'; -import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx'; -import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx'; import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx'; -import { ValueToDisplayData } from '@/modules/core/Core.types.tsx'; -import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; - -const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { - [ReadingMode.SINGLE_PAGE]: { - title: 'reader.settings.reader_type.label.single_page', - icon: , - }, - [ReadingMode.DOUBLE_PAGE]: { - title: 'reader.settings.reader_type.label.double_page', - icon: , - }, - [ReadingMode.CONTINUOUS_VERTICAL]: { - title: 'reader.settings.reader_type.label.continuous_vertical', - icon: , - }, - [ReadingMode.CONTINUOUS_HORIZONTAL]: { - title: 'reader.settings.reader_type.label.continuous_horizontal', - icon: , - }, -}; - -const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number'); +import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { + READING_MODE_VALUE_TO_DISPLAY_DATA, + READING_MODE_VALUES, +} from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts'; export const ReaderNavBarDesktopReadingMode = ({ readingMode, setReadingMode, -}: Pick & { - setReadingMode: (mode: ReadingMode) => void; -}) => ( + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + setReadingMode: (mode: ReadingMode) => void; + }) => ( ); diff --git a/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx b/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx index a8fc53e8..c98d90c0 100644 --- a/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx +++ b/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx @@ -22,7 +22,7 @@ import { useLayoutEffect } from 'react'; import { ReaderBottomBarMobileProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; import { MobileReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx'; import { ReaderChapterList } from '@/modules/reader/components/overlay/navigation/ReaderChapterList.tsx'; -import { ReaderBottomBarMobileQuickSettings } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx'; +import { ReaderBottomBarMobileQuickSettings } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx'; import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; diff --git a/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx b/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx new file mode 100644 index 00000000..05677c86 --- /dev/null +++ b/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx @@ -0,0 +1,42 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { ReaderSettingReadingMode } from '@/modules/reader/components/settings/layout/ReaderSettingReadingMode.tsx'; +import { ReaderSettingReadingDirection } from '@/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; +import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; + +const DEFAULT_MANGA: MangaIdInfo = { id: -1 }; +export const ReaderBottomBarMobileQuickSettings = () => { + const { manga } = useReaderStateMangaContext(); + const { readingMode, readingDirection } = ReaderService.useSettings(); + const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? DEFAULT_MANGA); + + if (!manga) { + return null; + } + + return ( + + ReaderService.updateSetting(manga, 'readingMode', value)} + isDefaultable + onDefault={() => deleteSetting('readingMode')} + /> + ReaderService.updateSetting(manga, 'readingDirection', value)} + isDefaultable + onDefault={() => deleteSetting('readingDirection')} + /> + + ); +}; diff --git a/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx b/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx deleted file mode 100644 index 30634956..00000000 --- a/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 Stack from '@mui/material/Stack'; -import { useTranslation } from 'react-i18next'; -import { ReaderBottomBarMobileReadingMode } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingMode.tsx'; -import { ReaderBottomBarMobileReadingDirection } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingDirection.tsx'; -import { createUpdateReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; -import { makeToast } from '@/modules/core/utils/Toast.ts'; -import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; -import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; -import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; - -export const ReaderBottomBarMobileQuickSettings = () => { - const { t } = useTranslation(); - - const { manga } = useReaderStateMangaContext(); - const { readingMode, readingDirection } = ReaderService.useSettings(); - - const updateReaderSettings = createUpdateReaderSettings< - keyof Pick - >(manga ?? { id: -1 }, () => makeToast(t('reader.settings.error.label.failed_to_save_settings'))); - - if (!manga) { - return null; - } - - return ( - - updateReaderSettings('readingMode', value)} - /> - updateReaderSettings('readingDirection', value)} - /> - - ); -}; diff --git a/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingMode.tsx b/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingMode.tsx deleted file mode 100644 index b785e5dc..00000000 --- a/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingMode.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { useTranslation } from 'react-i18next'; -import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; -import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx'; -import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx'; -import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx'; -import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx'; -import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; -import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; - -const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { - [ReadingMode.SINGLE_PAGE]: { - title: 'reader.settings.reader_type.label.single_page', - icon: , - }, - [ReadingMode.DOUBLE_PAGE]: { - title: 'reader.settings.reader_type.label.double_page', - icon: , - }, - [ReadingMode.CONTINUOUS_VERTICAL]: { - title: 'reader.settings.reader_type.label.continuous_vertical', - icon: , - }, - [ReadingMode.CONTINUOUS_HORIZONTAL]: { - title: 'reader.settings.reader_type.label.continuous_horizontal', - icon: , - }, -}; - -const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number'); - -export const ReaderBottomBarMobileReadingMode = ({ - readingMode, - setReadingMode, -}: Pick & { - setReadingMode: (mode: ReadingMode) => void; -}) => { - const { t } = useTranslation(); - - return ( - - ); -}; diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx index 1fa7388f..5de4cfb2 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx @@ -18,6 +18,7 @@ export const ReaderProgressBarContainer = styled(Stack, { shouldForwardProp: shouldForwardProp(['progressBarPosition']), })(({ theme, progressBarPosition }) => ({ position: 'fixed', + pointerEvents: 'all', ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, { justifyContent: 'flex-end', bottom: 0, diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx index adefac32..f2124039 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx @@ -11,8 +11,8 @@ import Tooltip from '@mui/material/Tooltip'; import { ReactNode } from 'react'; import { CurrentPageSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; -import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderProgressBar.constants.ts'; import { getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; +import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; export const ReaderProgressBarCurrentPageSlot = ({ pageName, diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx index 2e9fbde7..f896336b 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx @@ -10,7 +10,8 @@ import Box from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import { ReactNode } from 'react'; import { ReaderProgressBarSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; -import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderProgressBar.constants.ts'; + +import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; export const ReaderProgressBarSlot = ({ pageName, diff --git a/src/modules/reader/components/settings/ReaderSettings.tsx b/src/modules/reader/components/settings/ReaderSettings.tsx new file mode 100644 index 00000000..9df11a6a --- /dev/null +++ b/src/modules/reader/components/settings/ReaderSettings.tsx @@ -0,0 +1,51 @@ +/* + * 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 Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; + +import { useState } from 'react'; +import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { ReaderSettingsTabs } from '@/modules/reader/components/settings/ReaderSettingsTabs.tsx'; +import { ReaderSettingTab } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettings = ({ isOpen, close }: { isOpen: boolean; close: () => void }) => { + const { manga } = useReaderStateMangaContext(); + const settings = ReaderService.useSettings(); + + const [activeTab, setActiveTab] = useState(0); + + if (!manga) { + return null; + } + + if (!isOpen) { + return null; + } + + return ( + + + ReaderService.updateSetting(manga, ...args)} + deleteSetting={(...args) => ReaderService.deleteSetting(manga, ...args)} + /> + + + ); +}; diff --git a/src/modules/reader/components/settings/ReaderSettingsTabs.tsx b/src/modules/reader/components/settings/ReaderSettingsTabs.tsx new file mode 100644 index 00000000..c1bd1e7a --- /dev/null +++ b/src/modules/reader/components/settings/ReaderSettingsTabs.tsx @@ -0,0 +1,159 @@ +/* + * 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 Tab from '@mui/material/Tab'; +import { useTranslation } from 'react-i18next'; +import { TabsMenu } from '@/modules/core/components/tabs/TabsMenu.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; +import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts'; +import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; +import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { READER_SETTING_TABS, ReaderSettingTab } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { TabPanel } from '@/modules/core/components/tabs/TabPanel.tsx'; +import { ReaderLayoutSettings } from '@/modules/reader/components/settings/layout/ReaderLayoutSettings.tsx'; +import { ReaderGeneralSettings } from '@/modules/reader/components/settings/general/ReaderGeneralSettings.tsx'; +import { ReaderFilterSettings } from '@/modules/reader/components/settings/filters/ReaderFilterSettings.tsx'; +import { ReaderBehaviourSettings } from '@/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx'; + +interface BaseProps { + activeTab: number; + setActiveTab: (tab: number) => void; + settings: IReaderSettingsWithDefaultFlag; + updateSetting: (...args: OmitFirst>) => void; +} + +interface DefaultSettingsProps extends BaseProps { + areDefaultSettings: true; +} + +interface MangaSettingsProps extends BaseProps { + deleteSetting: (setting: keyof IReaderSettings) => void; +} + +type Props = + | (DefaultSettingsProps & PropertiesNever>) + | (PropertiesNever> & MangaSettingsProps); + +export const ReaderSettingsTabs = ({ + activeTab, + setActiveTab, + areDefaultSettings, + settings, + updateSetting, + deleteSetting, +}: Props) => { + const { t } = useTranslation(); + const { setShowPreview } = useReaderTapZoneContext(); + const isTouchDevice = MediaQuery.useIsTouchDevice(); + + const { mode: overlayMode } = ReaderService.useOverlayMode(); + + return ( + <> + setActiveTab(newTab)} + sx={{ + ...applyStyles(!!areDefaultSettings, { zIndex: 2 }), + ...applyStyles(!areDefaultSettings, { + backgroundColor: 'background.paper', + backgroundImage: 'var(--Paper-overlay)', + }), + }} + > + {Object.values(READER_SETTING_TABS).map(({ id, label, supportsTouchDevices }) => { + if (!supportsTouchDevices && isTouchDevice) { + return null; + } + + return ( + + ); + })} + + + {Object.values(READER_SETTING_TABS).map(({ id, supportsTouchDevices }) => { + if (!supportsTouchDevices && isTouchDevice) { + return null; + } + + switch (id as ReaderSettingTab) { + case ReaderSettingTab.LAYOUT: + return ( + + updateSetting(...args)} + setShowPreview={setShowPreview!} + // @ts-expect-error - TS2322: Type boolean is not assignable to type true + isDefaultable={!areDefaultSettings} + onDefault={(...args) => deleteSetting?.(...args)} + /> + + ); + case ReaderSettingTab.GENERAL: + return ( + + updateSetting(...args)} + // @ts-expect-error - TS2322: Type boolean is not assignable to type true + isDefaultable={!areDefaultSettings} + onDefault={(...args) => deleteSetting?.(...args)} + /> + + ); + case ReaderSettingTab.FILTER: + return ( + + updateSetting(...args)} + /> + + ); + case ReaderSettingTab.BEHAVIOUR: + return ( + + updateSetting(...args)} + /> + + ); + default: + throw new Error(`Unexpected "ReaderSettingTab" (${id})`); + } + })} + + > + ); +}; diff --git a/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx b/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx new file mode 100644 index 00000000..4739f25f --- /dev/null +++ b/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx @@ -0,0 +1,48 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { ReaderSettingExitMode } from '@/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx'; +import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; + +export const ReaderBehaviourSettings = ({ + settings, + updateSetting, +}: { + settings: IReaderSettingsWithDefaultFlag; + updateSetting: ( + ...args: OmitFirst> + ) => ReturnType; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting('exitMode', value)} + /> + updateSetting('shouldSkipDupChapters', checked)} + /> + {isOffsetDoubleSpreadPagesEditable(settings.readingMode.value) && ( + updateSetting('shouldOffsetDoubleSpreads', checked)} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx b/src/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx new file mode 100644 index 00000000..a7222ba9 --- /dev/null +++ b/src/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx @@ -0,0 +1,44 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { IReaderSettings, ReaderExitMode } from '@/modules/reader/types/Reader.types.ts'; +import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ReaderExitMode.PREVIOUS]: { + title: 'global.label.previous', + icon: null, + }, + [ReaderExitMode.MANGA]: { + title: 'manga.title_one', + icon: null, + }, +}; + +const READER_EXIT_MODE_VALUES = Object.values(ReaderExitMode).filter((value) => typeof value === 'number'); + +export const ReaderSettingExitMode = ({ + exitMode, + setExitMode, +}: Pick & { + setExitMode: (mode: ReaderExitMode) => void; +}) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderFilterSettings.tsx b/src/modules/reader/components/settings/filters/ReaderFilterSettings.tsx new file mode 100644 index 00000000..e5197a88 --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderFilterSettings.tsx @@ -0,0 +1,115 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { ReaderSettingBrightness } from '@/modules/reader/components/settings/filters/ReaderSettingBrightness.tsx'; +import { ReaderSettingContrast } from '@/modules/reader/components/settings/filters/ReaderSettingContrast.tsx'; +import { ReaderSettingSaturate } from '@/modules/reader/components/settings/filters/ReaderSettingSaturate.tsx'; +import { ReaderSettingHue } from '@/modules/reader/components/settings/filters/ReaderSettingHue.tsx'; +import { ReaderSettingRGBA } from '@/modules/reader/components/settings/filters/ReaderSettingRGBA.tsx'; +import { ReaderSettingSepia } from '@/modules/reader/components/settings/filters/ReaderSettingSepia.tsx'; +import { ReaderSettingGrayscale } from '@/modules/reader/components/settings/filters/ReaderSettingGrayscale.tsx'; +import { ReaderSettingInvert } from '@/modules/reader/components/settings/filters/ReaderSettingInvert.tsx'; +import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts'; + +export const ReaderFilterSettings = ({ settings: { customFilter }, updateSetting }: ReaderSettingsTypeProps) => ( + + + updateSetting( + 'customFilter', + { + ...customFilter, + [key]: value, + }, + commit, + ) + } + /> + + updateSetting( + 'customFilter', + { + ...customFilter, + [key]: value, + }, + commit, + ) + } + /> + + updateSetting( + 'customFilter', + { + ...customFilter, + [key]: value, + }, + commit, + ) + } + /> + + updateSetting( + 'customFilter', + { + ...customFilter, + [key]: value, + }, + commit, + ) + } + /> + + updateSetting( + 'customFilter', + { + ...customFilter, + [key]: value, + }, + commit, + ) + } + /> + + updateSetting('customFilter', { + ...customFilter, + sepia: value, + }) + } + /> + + updateSetting('customFilter', { + ...customFilter, + grayscale: value, + }) + } + /> + + updateSetting('customFilter', { + ...customFilter, + invert: value, + }) + } + /> + +); diff --git a/src/modules/reader/components/settings/filters/ReaderSettingBrightness.tsx b/src/modules/reader/components/settings/filters/ReaderSettingBrightness.tsx new file mode 100644 index 00000000..85239cfc --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingBrightness.tsx @@ -0,0 +1,59 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; + +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingBrightness = ({ + brightness, + updateSetting, +}: Pick & { + updateSetting: ( + filter: Filter, + value: IReaderSettings['customFilter'][Filter], + commit: boolean, + ) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting('brightness', { ...brightness, enabled: checked }, true)} + /> + {brightness.enabled && ( + { + updateSetting('brightness', { ...brightness, value: value as number }, false); + }, + onChangeCommitted: (_, value) => { + updateSetting('brightness', { ...brightness, value: value as number }, true); + }, + }, + }} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingContrast.tsx b/src/modules/reader/components/settings/filters/ReaderSettingContrast.tsx new file mode 100644 index 00000000..24d1cd47 --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingContrast.tsx @@ -0,0 +1,58 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingContrast = ({ + contrast, + updateSetting, +}: Pick & { + updateSetting: ( + filter: Filter, + value: IReaderSettings['customFilter'][Filter], + commit: boolean, + ) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting('contrast', { ...contrast, enabled: checked }, true)} + /> + {contrast.enabled && ( + { + updateSetting('contrast', { ...contrast, value: newValue as number }, false); + }, + onChangeCommitted: (_, newValue) => { + updateSetting('contrast', { ...contrast, value: newValue as number }, true); + }, + }, + }} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingGrayscale.tsx b/src/modules/reader/components/settings/filters/ReaderSettingGrayscale.tsx new file mode 100644 index 00000000..10b757ab --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingGrayscale.tsx @@ -0,0 +1,31 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; + +export const ReaderSettingGrayscale = ({ + grayscale, + updateSetting, +}: Pick & { + updateSetting: (grayscale: boolean) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting(checked)} + /> + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingHue.tsx b/src/modules/reader/components/settings/filters/ReaderSettingHue.tsx new file mode 100644 index 00000000..6b450d1d --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingHue.tsx @@ -0,0 +1,58 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingHue = ({ + hue, + updateSetting, +}: Pick & { + updateSetting: ( + filter: Filter, + value: IReaderSettings['customFilter'][Filter], + commit: boolean, + ) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting('hue', { ...hue, enabled: checked }, true)} + /> + {hue.enabled && ( + { + updateSetting('hue', { ...hue, value: newValue as number }, false); + }, + onChangeCommitted: (_, newValue) => { + updateSetting('hue', { ...hue, value: newValue as number }, true); + }, + }, + }} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingInvert.tsx b/src/modules/reader/components/settings/filters/ReaderSettingInvert.tsx new file mode 100644 index 00000000..7ed71e31 --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingInvert.tsx @@ -0,0 +1,31 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; + +export const ReaderSettingInvert = ({ + invert, + updateSetting, +}: Pick & { + updateSetting: (invert: boolean) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting(checked)} + /> + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingRGBA.tsx b/src/modules/reader/components/settings/filters/ReaderSettingRGBA.tsx new file mode 100644 index 00000000..ce801add --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingRGBA.tsx @@ -0,0 +1,89 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { TranslationKey } from '@/Base.types.ts'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; + +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +type RGBAType = keyof IReaderSettings['customFilter']['rgba']['value']; + +const RGBA_TYPE_TO_TRANSLATION_KEY: Record = { + red: 'reader.settings.custom_filter.rgba.red', + green: 'reader.settings.custom_filter.rgba.green', + blue: 'reader.settings.custom_filter.rgba.blue', + alpha: 'reader.settings.custom_filter.rgba.alpha', +}; + +const RGBA_TYPE_TO_MAX_VALUE: Record = { + red: 255, + green: 255, + blue: 255, + alpha: 100, +}; + +export const ReaderSettingRGBA = ({ + rgba, + updateSetting, +}: Pick & { + updateSetting: ( + filter: Filter, + value: IReaderSettings['customFilter'][Filter], + commit: boolean, + ) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting('rgba', { ...rgba, enabled: checked }, true)} + /> + {rgba.enabled && ( + + {Object.entries(rgba.value).map(([key, value]) => ( + { + updateSetting( + 'rgba', + { ...rgba, value: { ...rgba.value, [key]: newValue } }, + false, + ); + }, + onChangeCommitted: (_, newValue) => { + updateSetting( + 'rgba', + { ...rgba, value: { ...rgba.value, [key]: newValue } }, + true, + ); + }, + }, + }} + /> + ))} + + )} + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingSaturate.tsx b/src/modules/reader/components/settings/filters/ReaderSettingSaturate.tsx new file mode 100644 index 00000000..b5db85dc --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingSaturate.tsx @@ -0,0 +1,58 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingSaturate = ({ + saturate, + updateSetting, +}: Pick & { + updateSetting: ( + filter: Filter, + value: IReaderSettings['customFilter'][Filter], + commit: boolean, + ) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting('saturate', { ...saturate, enabled: checked }, true)} + /> + {saturate.enabled && ( + { + updateSetting('saturate', { ...saturate, value: value as number }, false); + }, + onChangeCommitted: (_, value) => { + updateSetting('saturate', { ...saturate, value: value as number }, true); + }, + }, + }} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/filters/ReaderSettingSepia.tsx b/src/modules/reader/components/settings/filters/ReaderSettingSepia.tsx new file mode 100644 index 00000000..209bb85a --- /dev/null +++ b/src/modules/reader/components/settings/filters/ReaderSettingSepia.tsx @@ -0,0 +1,31 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; + +export const ReaderSettingSepia = ({ + sepia, + updateSetting, +}: Pick & { + updateSetting: (sepia: boolean) => void; +}) => { + const { t } = useTranslation(); + + return ( + + updateSetting(checked)} + /> + + ); +}; diff --git a/src/modules/reader/components/settings/general/ReaderGeneralSettings.tsx b/src/modules/reader/components/settings/general/ReaderGeneralSettings.tsx new file mode 100644 index 00000000..ba7d6f4c --- /dev/null +++ b/src/modules/reader/components/settings/general/ReaderGeneralSettings.tsx @@ -0,0 +1,66 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { ReaderSettingProgressBarType } from '@/modules/reader/components/settings/general/ReaderSettingProgressBarType.tsx'; +import { ReaderSettingProgressBarSize } from '@/modules/reader/components/settings/general/ReaderSettingProgressBarSize.tsx'; +import { ReaderSettingProgressBarPosition } from '@/modules/reader/components/settings/general/ReaderSettingProgressBarPosition.tsx'; +import { + IReaderSettings, + ProgressBarType, + ReaderOverlayMode, + ReaderSettingsTypeProps, +} from '@/modules/reader/types/Reader.types.ts'; +import { ReaderSettingOverlayMode } from '@/modules/reader/components/settings/general/ReaderSettingOverlayMode.tsx'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { ReaderSettingBackgroundColor } from '@/modules/reader/components/settings/general/ReaderSettingBackgroundColor.tsx'; + +export const ReaderGeneralSettings = ({ + overlayMode, + settings, + updateSetting, +}: Pick & ReaderSettingsTypeProps) => { + const { t } = useTranslation(); + + return ( + + updateSetting('overlayMode', value)} + /> + updateSetting('backgroundColor', value)} + /> + updateSetting('progressBarType', value)} + /> + updateSetting('progressBarSize', ...args)} + /> + updateSetting('progressBarPosition', value)} + /> + {(settings.progressBarType === ProgressBarType.HIDDEN || overlayMode === ReaderOverlayMode.MOBILE) && ( + updateSetting('shouldShowPageNumber', checked)} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/general/ReaderSettingBackgroundColor.tsx b/src/modules/reader/components/settings/general/ReaderSettingBackgroundColor.tsx new file mode 100644 index 00000000..a2e77cc6 --- /dev/null +++ b/src/modules/reader/components/settings/general/ReaderSettingBackgroundColor.tsx @@ -0,0 +1,54 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; +import { IReaderSettingsWithDefaultFlag, ReaderBackgroundColor } from '@/modules/reader/types/Reader.types.ts'; + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ReaderBackgroundColor.THEME]: { + title: 'settings.appearance.theme.title', + icon: null, + }, + [ReaderBackgroundColor.BLACK]: { + title: 'global.colors.black', + icon: null, + }, + [ReaderBackgroundColor.GRAY]: { + title: 'global.colors.gray', + icon: null, + }, + [ReaderBackgroundColor.WHITE]: { + title: 'global.colors.white', + icon: null, + }, +}; + +const READER_BACKGROUND_COLOR_VALUES = Object.values(ReaderBackgroundColor).filter( + (value) => typeof value === 'number', +); + +export const ReaderSettingBackgroundColor = ({ + backgroundColor, + updateSetting, +}: Pick & { + updateSetting: (color: ReaderBackgroundColor) => void; +}) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/general/ReaderSettingOverlayMode.tsx b/src/modules/reader/components/settings/general/ReaderSettingOverlayMode.tsx new file mode 100644 index 00000000..2eec3611 --- /dev/null +++ b/src/modules/reader/components/settings/general/ReaderSettingOverlayMode.tsx @@ -0,0 +1,51 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone'; +import ComputerIcon from '@mui/icons-material/Computer'; +import AutoModeIcon from '@mui/icons-material/AutoMode'; +import { IReaderSettings, ReaderOverlayMode } from '@/modules/reader/types/Reader.types.ts'; +import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ReaderOverlayMode.AUTO]: { + title: 'global.label.auto', + icon: , + }, + [ReaderOverlayMode.DESKTOP]: { + title: 'global.label.desktop', + icon: , + }, + [ReaderOverlayMode.MOBILE]: { + title: 'global.label.mobile', + icon: , + }, +}; + +const READING_MODE_VALUES = Object.values(ReaderOverlayMode).filter((value) => typeof value === 'number'); + +export const ReaderSettingOverlayMode = ({ + overlayMode, + setOverlayMode, +}: Pick & { + setOverlayMode: (mode: ReaderOverlayMode) => void; +}) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/general/ReaderSettingProgressBarPosition.tsx b/src/modules/reader/components/settings/general/ReaderSettingProgressBarPosition.tsx new file mode 100644 index 00000000..1504be35 --- /dev/null +++ b/src/modules/reader/components/settings/general/ReaderSettingProgressBarPosition.tsx @@ -0,0 +1,55 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import { IReaderSettings, ProgressBarPosition, ReaderOverlayMode } from '@/modules/reader/types/Reader.types.ts'; +import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ProgressBarPosition.BOTTOM]: { + title: 'global.label.bottom', + icon: , + }, + [ProgressBarPosition.LEFT]: { + title: 'global.label.left', + icon: , + }, + [ProgressBarPosition.RIGHT]: { + title: 'global.label.right', + icon: , + }, +}; + +const PROGRESS_BAR_POSITION_VALUES = Object.values(ProgressBarPosition).filter((value) => typeof value === 'number'); + +export const ReaderSettingProgressBarPosition = ({ + overlayMode, + progressBarPosition, + setProgressBarPosition, +}: Pick & { + setProgressBarPosition: (position: ProgressBarPosition) => void; +}) => { + const { t } = useTranslation(); + + if (overlayMode !== ReaderOverlayMode.DESKTOP) { + return null; + } + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/general/ReaderSettingProgressBarSize.tsx b/src/modules/reader/components/settings/general/ReaderSettingProgressBarSize.tsx new file mode 100644 index 00000000..4ac9f8de --- /dev/null +++ b/src/modules/reader/components/settings/general/ReaderSettingProgressBarSize.tsx @@ -0,0 +1,50 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { IReaderSettings, ProgressBarType, ReaderOverlayMode } from '@/modules/reader/types/Reader.types.ts'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingProgressBarSize = ({ + overlayMode, + progressBarType, + progressBarSize, + setProgressBarSize, +}: Pick & { + setProgressBarSize: (size: number, commit: boolean) => void; +}) => { + const { t } = useTranslation(); + const isChangeable = overlayMode === ReaderOverlayMode.DESKTOP && progressBarType === ProgressBarType.STANDARD; + + if (!isChangeable) { + return null; + } + + return ( + { + setProgressBarSize(value as number, false); + }, + onChangeCommitted: (_, value) => { + setProgressBarSize(value as number, true); + }, + }, + }} + /> + ); +}; diff --git a/src/modules/reader/components/settings/general/ReaderSettingProgressBarType.tsx b/src/modules/reader/components/settings/general/ReaderSettingProgressBarType.tsx new file mode 100644 index 00000000..7f57bda6 --- /dev/null +++ b/src/modules/reader/components/settings/general/ReaderSettingProgressBarType.tsx @@ -0,0 +1,51 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { IReaderSettings, ProgressBarType, ReaderOverlayMode } from '@/modules/reader/types/Reader.types.ts'; +import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { HiddenProgressBarIcon } from '@/assets/icons/svg/HiddenProgressBarIcon.tsx'; +import { StandardProgressBarIcon } from '@/assets/icons/svg/StandardProgressBarIcon.tsx'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ProgressBarType.HIDDEN]: { + title: 'global.label.hidden', + icon: , + }, + [ProgressBarType.STANDARD]: { + title: 'global.label.standard', + icon: , + }, +}; + +const PROGRESS_BAR_TYPE_VALUES = Object.values(ProgressBarType).filter((value) => typeof value === 'number'); + +export const ReaderSettingProgressBarType = ({ + overlayMode, + progressBarType, + setProgressBarType, +}: Pick & { + setProgressBarType: (progressBarType: ProgressBarType) => void; +}) => { + const { t } = useTranslation(); + + if (overlayMode !== ReaderOverlayMode.DESKTOP) { + return null; + } + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/layout/ReaderLayoutSettings.tsx b/src/modules/reader/components/settings/layout/ReaderLayoutSettings.tsx new file mode 100644 index 00000000..09720857 --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderLayoutSettings.tsx @@ -0,0 +1,90 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { ContextType } from 'react'; +import { ReaderSettingReadingMode } from '@/modules/reader/components/settings/layout/ReaderSettingReadingMode.tsx'; +import { ReaderSettingReadingDirection } from '@/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx'; +import { ReaderSettingTapZoneLayout } from '@/modules/reader/components/settings/layout/ReaderSettingTapZoneLayout.tsx'; +import { ReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; +import { ReaderSettingTapZoneInvertMode } from '@/modules/reader/components/settings/layout/ReaderSettingTapZoneInvertMode.tsx'; +import { ReaderSettingPageScaleMode } from '@/modules/reader/components/settings/layout/ReaderSettingPageScaleMode.tsx'; +import { ReaderSettingStretchPage } from '@/modules/reader/components/settings/layout/ReaderSettingStretchPage.tsx'; +import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderSettingPageGap } from '@/modules/reader/components/settings/layout/ReaderSettingPageGap.tsx'; +import { ReaderSettingWidth } from '@/modules/reader/components/settings/layout/ReaderSettingWidth.tsx'; + +export const ReaderLayoutSettings = ({ + setShowPreview, + settings, + updateSetting, + isDefaultable, + onDefault, +}: ReaderSettingsTypeProps & { + setShowPreview: ContextType['setShowPreview']; +}) => ( + + updateSetting('readingMode', value)} + isDefaultable={isDefaultable} + onDefault={() => onDefault?.('readingMode')} + /> + updateSetting('pageGap', ...args)} + /> + updateSetting('readingDirection', value)} + isDefaultable={isDefaultable} + onDefault={() => onDefault?.('readingDirection')} + /> + { + setShowPreview(true); + updateSetting('tapZoneLayout', value); + }} + isDefaultable={isDefaultable} + onDefault={() => { + setShowPreview(true); + onDefault?.('tapZoneLayout'); + }} + /> + { + setShowPreview(true); + updateSetting('tapZoneInvertMode', value); + }} + isDefaultable={isDefaultable} + onDefault={() => { + setShowPreview(true); + onDefault?.('tapZoneInvertMode'); + }} + /> + updateSetting('pageScaleMode', value)} + isDefaultable={isDefaultable} + onDefault={() => onDefault?.('pageScaleMode')} + /> + updateSetting('shouldStretchPage', value)} + /> + updateSetting(...args)} + /> + +); diff --git a/src/modules/reader/components/settings/layout/ReaderSettingPageGap.tsx b/src/modules/reader/components/settings/layout/ReaderSettingPageGap.tsx new file mode 100644 index 00000000..00c7cbc2 --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderSettingPageGap.tsx @@ -0,0 +1,49 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingPageGap = ({ + pageGap, + readingMode, + updateSetting, +}: Pick & { + updateSetting: (gap: number, commit: boolean) => void; +}) => { + const { t } = useTranslation(); + + return ( + + {[ReadingMode.CONTINUOUS_HORIZONTAL, ReadingMode.CONTINUOUS_VERTICAL].includes(readingMode.value) && ( + { + updateSetting(value as number, false); + }, + onChangeCommitted: (_, value) => { + updateSetting(value as number, true); + }, + }, + }} + /> + )} + + ); +}; diff --git a/src/modules/reader/components/settings/layout/ReaderSettingPageScaleMode.tsx b/src/modules/reader/components/settings/layout/ReaderSettingPageScaleMode.tsx new file mode 100644 index 00000000..089d3840 --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderSettingPageScaleMode.tsx @@ -0,0 +1,42 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { + IReaderSettings, + IReaderSettingsWithDefaultFlag, + ReadingDirection, +} from '@/modules/reader/types/Reader.types.ts'; +import { + PAGE_SCALE_VALUE_TO_DISPLAY_DATA, + READER_PAGE_SCALE_MODE_VALUES, +} from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; +import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts'; + +export const ReaderSettingPageScaleMode = ({ + pageScaleMode, + setPageScaleMode, + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + setPageScaleMode: (mode: IReaderSettings['pageScaleMode']) => void; + }) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingDirection.tsx b/src/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx similarity index 67% rename from src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingDirection.tsx rename to src/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx index 8c840ff4..e9e96fc6 100644 --- a/src/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingDirection.tsx +++ b/src/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx @@ -9,8 +9,8 @@ import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft'; import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight'; import { useTranslation } from 'react-i18next'; -import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; -import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/modules/core/Core.types.ts'; import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { @@ -26,18 +26,21 @@ const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number'); -export const ReaderBottomBarMobileReadingDirection = ({ +export const ReaderSettingReadingDirection = ({ readingDirection, setReadingDirection, -}: Pick & { - setReadingDirection: (readingDirection: ReadingDirection) => void; -}) => { + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + setReadingDirection: (readingDirection: ReadingDirection) => void; + }) => { const { t } = useTranslation(); return ( & + Pick, 'isDefaultable' | 'onDefault'> & { + setReadingMode: (mode: ReadingMode) => void; + }) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/layout/ReaderSettingStretchPage.tsx b/src/modules/reader/components/settings/layout/ReaderSettingStretchPage.tsx new file mode 100644 index 00000000..1584e84c --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderSettingStretchPage.tsx @@ -0,0 +1,38 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; + +export const ReaderSettingStretchPage = ({ + pageScaleMode, + shouldStretchPage, + setShouldStretchPage, +}: Pick & { + setShouldStretchPage: (mode: IReaderSettings['shouldStretchPage']) => void; +}) => { + const { t } = useTranslation(); + + if (!READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode]) { + return null; + } + + return ( + + setShouldStretchPage(!shouldStretchPage)} + variant={shouldStretchPage ? 'contained' : 'outlined'} + > + {t('reader.settings.page_scale.stretch')} + + + ); +}; diff --git a/src/modules/reader/components/settings/layout/ReaderSettingTapZoneInvertMode.tsx b/src/modules/reader/components/settings/layout/ReaderSettingTapZoneInvertMode.tsx new file mode 100644 index 00000000..135d0148 --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderSettingTapZoneInvertMode.tsx @@ -0,0 +1,102 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { + IReaderSettings, + IReaderSettingsWithDefaultFlag, + ReadingDirection, +} from '@/modules/reader/types/Reader.types.ts'; +import { TapZoneInvertMode } from '@/modules/reader/types/TapZoneLayout.types.ts'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; + +enum TapZonesInvertOption { + NONE, + HORIZONTAL, + VERTICAL, + BOTH, +} + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [TapZonesInvertOption.NONE]: { + title: 'global.label.none', + icon: null, + }, + [TapZonesInvertOption.HORIZONTAL]: { + title: 'global.label.horizontal', + icon: null, + }, + [TapZonesInvertOption.VERTICAL]: { + title: 'global.label.vertical', + icon: null, + }, + [TapZonesInvertOption.BOTH]: { + title: 'global.label.both', + icon: null, + }, +}; + +const TAP_ZONES_INVERT_OPTION_VALUES = Object.values(TapZonesInvertOption).filter((value) => typeof value === 'number'); + +const TAP_ZONES_INVERT_OPTION_TO_SETTING: Record = { + [TapZonesInvertOption.NONE]: { + vertical: false, + horizontal: false, + }, + [TapZonesInvertOption.HORIZONTAL]: { + vertical: false, + horizontal: true, + }, + [TapZonesInvertOption.VERTICAL]: { + vertical: true, + horizontal: false, + }, + [TapZonesInvertOption.BOTH]: { + vertical: true, + horizontal: true, + }, +}; + +const convertTapZoneInvertModeToOption = ({ vertical, horizontal }: TapZoneInvertMode): TapZonesInvertOption => { + if (vertical && horizontal) { + return TapZonesInvertOption.BOTH; + } + + if (vertical) { + return TapZonesInvertOption.VERTICAL; + } + + if (horizontal) { + return TapZonesInvertOption.HORIZONTAL; + } + + return TapZonesInvertOption.NONE; +}; + +export const ReaderSettingTapZoneInvertMode = ({ + tapZoneInvertMode, + setTapZoneInvertMode, + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + setTapZoneInvertMode: (invert: IReaderSettings['tapZoneInvertMode']) => void; + }) => { + const { t } = useTranslation(); + + return ( + setTapZoneInvertMode(TAP_ZONES_INVERT_OPTION_TO_SETTING[value])} + valueToDisplayData={VALUE_TO_DISPLAY_DATA} + /> + ); +}; diff --git a/src/modules/reader/components/settings/layout/ReaderSettingTapZoneLayout.tsx b/src/modules/reader/components/settings/layout/ReaderSettingTapZoneLayout.tsx new file mode 100644 index 00000000..e748f6ad --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderSettingTapZoneLayout.tsx @@ -0,0 +1,56 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; +import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; + +const VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [TapZoneLayouts.EDGE]: { + title: 'reader.settings.tap_zones.edge', + icon: null, + }, + [TapZoneLayouts.KINDLE]: { + title: 'reader.settings.tap_zones.kindle', + icon: null, + }, + [TapZoneLayouts.L_SHAPE]: { + title: 'reader.settings.tap_zones.l_shape', + icon: null, + }, + [TapZoneLayouts.RIGHT_LEFT]: { + title: 'reader.settings.tap_zones.right_left', + icon: null, + }, +}; + +const READER_TAP_ZONE_LAYOUT_VALUES = Object.values(TapZoneLayouts).filter((value) => typeof value === 'number'); + +export const ReaderSettingTapZoneLayout = ({ + tapZoneLayout, + setTapZoneLayout, + ...buttonSelectInputProps +}: Pick & + Pick, 'isDefaultable' | 'onDefault'> & { + setTapZoneLayout: (layout: TapZoneLayouts) => void; + }) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/src/modules/reader/components/settings/layout/ReaderSettingWidth.tsx b/src/modules/reader/components/settings/layout/ReaderSettingWidth.tsx new file mode 100644 index 00000000..75905ee3 --- /dev/null +++ b/src/modules/reader/components/settings/layout/ReaderSettingWidth.tsx @@ -0,0 +1,61 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx'; +import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; + +export const ReaderSettingWidth = ({ + readerWidth, + pageScaleMode, + updateSetting, +}: Pick & { + updateSetting: (...args: OmitFirst>) => void; +}) => { + const { t } = useTranslation(); + + if (!isReaderWidthEditable(pageScaleMode)) { + return null; + } + + return ( + + updateSetting('readerWidth', { ...readerWidth, enabled: checked }, true)} + /> + {readerWidth.enabled && ( + { + updateSetting('readerWidth', { ...readerWidth, value: value as number }, false); + }, + onChangeCommitted: (_, value) => { + updateSetting('readerWidth', { ...readerWidth, value: value as number }, true); + }, + }, + }} + /> + )} + + ); +}; diff --git a/src/modules/reader/constants/Reader.constants.ts b/src/modules/reader/constants/Reader.constants.ts deleted file mode 100644 index af1e7ebe..00000000 --- a/src/modules/reader/constants/Reader.constants.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { - IReaderSettings, - ProgressBarPosition, - ProgressBarType, - ReaderPageScaleMode, - ReadingDirection, - ReadingMode, -} from '@/modules/reader/types/Reader.types.ts'; -import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; - -export const DEFAULT_READER_SETTINGS: IReaderSettings = { - staticNav: false, - showPageNumber: true, - loadNextOnEnding: false, - skipDupChapters: true, - readerWidth: 50, - tapZoneLayout: TapZoneLayouts.RIGHT_LEFT, - tapZoneInvertMode: { vertical: false, horizontal: false }, - progressBarType: ProgressBarType.STANDARD, - progressBarSize: 4, - progressBarPosition: ProgressBarPosition.BOTTOM, - pageScaleMode: ReaderPageScaleMode.ORIGINAL, - shouldScalePage: false, - shouldOffsetDoubleSpreads: false, - readingDirection: ReadingDirection.LTR, - readingMode: ReadingMode.SINGLE_PAGE, -}; diff --git a/src/modules/reader/constants/ReaderProgressBar.constants.ts b/src/modules/reader/constants/ReaderProgressBar.constants.ts deleted file mode 100644 index dedb392a..00000000 --- a/src/modules/reader/constants/ReaderProgressBar.constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { TooltipProps } from '@mui/material/Tooltip'; -import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts'; - -export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record = { - [ProgressBarPosition.BOTTOM]: 'top', - [ProgressBarPosition.LEFT]: 'right', - [ProgressBarPosition.RIGHT]: 'left', -}; diff --git a/src/modules/reader/constants/ReaderSettings.constants.tsx b/src/modules/reader/constants/ReaderSettings.constants.tsx index 567d10b3..d66ea572 100644 --- a/src/modules/reader/constants/ReaderSettings.constants.tsx +++ b/src/modules/reader/constants/ReaderSettings.constants.tsx @@ -6,10 +6,206 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft'; +import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight'; +import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap'; +import ExpandIcon from '@mui/icons-material/Expand'; +import CropOriginalIcon from '@mui/icons-material/CropOriginal'; +import { TooltipProps } from '@mui/material/Tooltip'; import { Direction } from '@mui/material/styles'; -import { ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; +import { + IReaderSettings, + IReaderSettingsGlobal, + ProgressBarPosition, + ProgressBarType, + ReaderBackgroundColor, + ReaderExitMode, + ReaderOverlayMode, + ReaderPageScaleMode, + ReadingDirection, + ReadingMode, +} from '@/modules/reader/types/Reader.types.ts'; +import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx'; +import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx'; +import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx'; +import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx'; +import { TranslationKey } from '@/Base.types.ts'; +import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; export const READING_DIRECTION_TO_THEME_DIRECTION: Record = { [ReadingDirection.LTR]: 'ltr', [ReadingDirection.RTL]: 'rtl', }; + +export const GLOBAL_READER_SETTING_OBJECT: Record = { + overlayMode: undefined, + exitMode: undefined, + customFilter: undefined, + shouldSkipDupChapters: undefined, + progressBarType: undefined, + progressBarSize: undefined, + progressBarPosition: undefined, + shouldShowPageNumber: undefined, + isStaticNav: undefined, + backgroundColor: undefined, +}; + +export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT); + +export const DEFAULT_READER_SETTINGS: IReaderSettings = { + readerWidth: { value: 50, enabled: false }, + overlayMode: ReaderOverlayMode.AUTO, + tapZoneLayout: TapZoneLayouts.RIGHT_LEFT, + tapZoneInvertMode: { vertical: false, horizontal: false }, + progressBarType: ProgressBarType.STANDARD, + progressBarSize: 4, + progressBarPosition: ProgressBarPosition.BOTTOM, + pageScaleMode: ReaderPageScaleMode.ORIGINAL, + shouldStretchPage: false, + shouldOffsetDoubleSpreads: false, + shouldSkipDupChapters: true, + shouldShowPageNumber: true, + isStaticNav: false, + readingDirection: ReadingDirection.LTR, + readingMode: ReadingMode.SINGLE_PAGE, + exitMode: ReaderExitMode.PREVIOUS, + backgroundColor: ReaderBackgroundColor.THEME, + customFilter: { + brightness: { + value: 100, + enabled: false, + }, + contrast: { + value: 100, + enabled: false, + }, + saturate: { + value: 100, + enabled: false, + }, + hue: { + value: 0, + enabled: false, + }, + rgba: { + value: { + red: 0, + green: 0, + blue: 0, + alpha: 0, + }, + enabled: false, + }, + sepia: false, + grayscale: false, + invert: false, + }, + pageGap: 5, +}; + +export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record = { + [ProgressBarPosition.BOTTOM]: 'top', + [ProgressBarPosition.LEFT]: 'right', + [ProgressBarPosition.RIGHT]: 'left', +}; + +export const READING_DIRECTION_VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ReadingDirection.LTR]: { + title: 'reader.settings.reading_direction.ltr', + icon: , + }, + [ReadingDirection.RTL]: { + title: 'reader.settings.reading_direction.rtl', + icon: , + }, +}; + +export const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number'); + +export const PAGE_SCALE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ReaderPageScaleMode.WIDTH]: { + title: 'reader.settings.page_scale.width', + icon: , + }, + [ReaderPageScaleMode.HEIGHT]: { + title: 'reader.settings.page_scale.height', + icon: , + }, + [ReaderPageScaleMode.SCREEN]: { + title: 'reader.settings.page_scale.screen', + icon: , + }, + [ReaderPageScaleMode.ORIGINAL]: { + title: 'reader.settings.page_scale.original', + icon: , + }, +}; + +export const READER_PAGE_SCALE_MODE_VALUES = Object.values(ReaderPageScaleMode).filter( + (value) => typeof value === 'number', +); + +export const READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED: Record = { + [ReaderPageScaleMode.WIDTH]: true, + [ReaderPageScaleMode.HEIGHT]: true, + [ReaderPageScaleMode.SCREEN]: true, + [ReaderPageScaleMode.ORIGINAL]: false, +}; + +export const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData = { + [ReadingMode.SINGLE_PAGE]: { + title: 'reader.settings.reader_type.label.single_page', + icon: , + }, + [ReadingMode.DOUBLE_PAGE]: { + title: 'reader.settings.reader_type.label.double_page', + icon: , + }, + [ReadingMode.CONTINUOUS_VERTICAL]: { + title: 'reader.settings.reader_type.label.continuous_vertical', + icon: , + }, + [ReadingMode.CONTINUOUS_HORIZONTAL]: { + title: 'reader.settings.reader_type.label.continuous_horizontal', + icon: , + }, +}; +export const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number'); + +export enum ReaderSettingTab { + LAYOUT, + GENERAL, + FILTER, + BEHAVIOUR, +} + +export const READER_SETTING_TABS: Record< + ReaderSettingTab, + { + id: ReaderSettingTab; + label: TranslationKey; + supportsTouchDevices: boolean; + } +> = { + [ReaderSettingTab.LAYOUT]: { + id: ReaderSettingTab.LAYOUT, + label: 'reader.settings.label.layout', + supportsTouchDevices: true, + }, + [ReaderSettingTab.GENERAL]: { + id: ReaderSettingTab.GENERAL, + label: 'global.label.general', + supportsTouchDevices: true, + }, + [ReaderSettingTab.FILTER]: { + id: ReaderSettingTab.FILTER, + label: 'reader.settings.custom_filter.title', + supportsTouchDevices: true, + }, + [ReaderSettingTab.BEHAVIOUR]: { + id: ReaderSettingTab.BEHAVIOUR, + label: 'reader.settings.label.behaviour', + supportsTouchDevices: true, + }, +}; diff --git a/src/modules/reader/screens/DefaultReaderSettings.tsx b/src/modules/reader/screens/DefaultReaderSettings.tsx new file mode 100644 index 00000000..6f0097b3 --- /dev/null +++ b/src/modules/reader/screens/DefaultReaderSettings.tsx @@ -0,0 +1,72 @@ +/* + * 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 { useTranslation } from 'react-i18next'; +import { useLayoutEffect, useState } from 'react'; +import { useDefaultReaderSettingsWithDefaultFlag } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; +import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; +import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { ReaderSettingsTabs } from '@/modules/reader/components/settings/ReaderSettingsTabs.tsx'; + +export const DefaultReaderSettings = () => { + const { t } = useTranslation(); + const { setTitle, setAction } = useNavBarContext(); + + useLayoutEffect(() => { + setTitle(t('reader.settings.title.default_reader_settings')); + setAction(null); + + return () => { + setTitle(''); + setAction(null); + }; + }, [t]); + + const [activeTab, setActiveTab] = useState(0); + + const { + settings, + request: { loading, error, refetch }, + } = useDefaultReaderSettingsWithDefaultFlag(); + + const updateSetting = ( + key: Setting, + value: IReaderSettings[Setting], + commit?: boolean, + ) => { + ReaderService.updateSetting({ id: -1 }, key, value, commit, true); + }; + + if (loading) { + return ; + } + + if (error) { + return ( + refetch().catch(defaultPromiseErrorHandler('DefaultReaderSettings::refetch'))} + /> + ); + } + + return ( + updateSetting(setting, value, commit)} + /> + ); +}; diff --git a/src/modules/reader/services/ReaderService.ts b/src/modules/reader/services/ReaderService.ts index d9ca542d..51f62a01 100644 --- a/src/modules/reader/services/ReaderService.ts +++ b/src/modules/reader/services/ReaderService.ts @@ -6,14 +6,35 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Direction, useTheme } from '@mui/material/styles'; +import { t as translate } from 'i18next'; import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; import { Chapters } from '@/modules/chapter/services/Chapters.ts'; -import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { + IReaderSettings, + IReaderSettingsWithDefaultFlag, + ReaderExitMode, + ReaderOverlayMode, + ReadingDirection, +} 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'; +import { + updateReaderSettings, + useDefaultReaderSettings, + useGetReaderSettingsFor, +} from '@/modules/reader/services/ReaderSettingsMetadata.ts'; +import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; +import { getMetadataKey } from '@/modules/metadata/services/MetadataReader.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { MANGA_META_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts'; +import { makeToast } from '@/modules/core/utils/Toast.ts'; +import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; +import { GLOBAL_METADATA } from '@/lib/graphql/fragments/Fragments.ts'; +import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts'; +import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; +import { GLOBAL_READER_SETTING_KEYS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; const DIRECTION_TO_INVERTED: Record = { ltr: 'rtl', @@ -25,6 +46,8 @@ const DIRECTION_TO_READING_DIRECTION: Record = { rtl: ReadingDirection.RTL, }; +const DEFAULT_MANGA: MangaIdInfo = { id: -1 }; + export class ReaderService { static useNavigateToChapter(chapter?: TChapterReader): () => void { const navigate = useNavigate(); @@ -32,21 +55,170 @@ export class ReaderService { return useCallback(() => chapter && navigate(Chapters.getReaderUrl(chapter), { replace: true }), [chapter]); } - static useSettings(): IReaderSettings { + static useSettings(): IReaderSettingsWithDefaultFlag { const { manga } = useReaderStateMangaContext(); const defaultReaderSettings = useDefaultReaderSettings(); - return useMemo( - () => getReaderSettingsFor(manga ?? { id: -1 }, defaultReaderSettings.settings), - [manga, defaultReaderSettings], - ); + + return useGetReaderSettingsFor(manga ?? DEFAULT_MANGA, defaultReaderSettings.settings); } static useGetThemeDirection(): Direction { const { direction } = useTheme(); const { readingDirection } = ReaderService.useSettings(); - return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection + return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection.value ? direction : DIRECTION_TO_INVERTED[direction]; } + + /** + * Writes the change immediately to the cache and sends a mutation in case "commit" is true. + */ + static updateSetting( + manga: MangaIdInfo, + setting: Setting, + value: IReaderSettings[Setting], + commit: boolean = true, + isGlobal: boolean = false, + ): void { + if (!manga) { + return; + } + + const key = getMetadataKey(setting); + + const { cache } = requestManager.graphQLClient.client; + + const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting); + if (isGlobalSetting) { + const reference = cache.writeFragment({ + fragment: GLOBAL_METADATA, + data: { + __typename: 'GlobalMetaType', + key, + value: JSON.stringify(value), + }, + }); + cache.modify({ + fields: { + metas(existingMetas, { readField }) { + return { + ...existingMetas, + nodes: updateMetadataList(key, existingMetas?.nodes, readField, () => reference), + }; + }, + }, + }); + } else { + const reference = cache.writeFragment({ + fragment: MANGA_META_FIELDS, + data: { + __typename: 'MangaMetaType', + mangaId: manga.id, + key, + value: JSON.stringify(value), + }, + }); + cache.modify({ + id: cache.identify({ __typename: 'MangaType', id: manga.id }), + fields: { + meta(existingMetas, { readField }) { + return updateMetadataList(key, existingMetas, readField, () => reference); + }, + }, + }); + } + + if (commit) { + updateReaderSettings(manga, setting, value, isGlobal).catch(() => + makeToast(translate('reader.settings.error.label.failed_to_save_settings'), 'error'), + ); + } + } + + static deleteSetting( + manga: MangaIdInfo, + setting: Setting, + isGlobal: boolean = false, + ): void { + if (!manga) { + return; + } + + const key = getMetadataKey(setting); + + const { cache } = requestManager.graphQLClient.client; + + const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting); + if (isGlobalSetting) { + cache.evict({ id: cache.identify({ __typename: 'GlobalMetaType', key }) }); + } else { + cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId: manga.id, key }) }); + } + + const deleteSetting = isGlobalSetting + ? () => requestManager.deleteGlobalMeta(key).response + : () => requestManager.deleteMangaMeta(manga.id, key).response; + deleteSetting().catch(() => + makeToast(translate('reader.settings.error.label.failed_to_save_settings'), 'error'), + ); + } + + static useCreateUpdateSetting( + manga: MangaIdInfo, + ): ( + ...args: OmitFirst>> + ) => ReturnType> { + return useCallback((...args) => this.updateSetting(manga, ...args), [manga]); + } + + static useCreateDeleteSetting( + manga: MangaIdInfo, + ): ( + ...args: OmitFirst>> + ) => ReturnType> { + return useCallback((...args) => this.deleteSetting(manga, ...args), [manga]); + } + + static useOverlayMode(): { mode: ReaderOverlayMode; isDesktop: boolean; isMobile: boolean } { + const isTouchDevice = MediaQuery.useIsTouchDevice(); + const { overlayMode } = this.useSettings(); + + const isAutoModeSelected = overlayMode === ReaderOverlayMode.AUTO; + const isDesktopModeSelected = overlayMode === ReaderOverlayMode.DESKTOP; + const isMobileModeSelected = overlayMode === ReaderOverlayMode.MOBILE; + + const isDesktop = isDesktopModeSelected || (isAutoModeSelected && !isTouchDevice); + const isMobile = isMobileModeSelected || (isAutoModeSelected && isTouchDevice); + + return { + mode: isDesktop ? ReaderOverlayMode.DESKTOP : ReaderOverlayMode.MOBILE, + isDesktop, + isMobile, + }; + } + + static useExit(): () => void { + const { manga } = useReaderStateMangaContext(); + const { exitMode } = this.useSettings(); + const handleBack = useBackButton(); + const navigate = useNavigate(); + + const openMangaPage = useCallback(() => { + if (!manga) { + return () => {}; + } + + return navigate(`/manga/${manga.id}`); + }, [manga]); + + switch (exitMode) { + case ReaderExitMode.PREVIOUS: + return handleBack; + case ReaderExitMode.MANGA: + return openMangaPage; + default: + throw new Error(`Unexpected "exitMode" (${exitMode})`); + } + } } diff --git a/src/modules/reader/services/ReaderSettingsMetadata.ts b/src/modules/reader/services/ReaderSettingsMetadata.ts index 5f07d98b..9d6d7723 100644 --- a/src/modules/reader/services/ReaderSettingsMetadata.ts +++ b/src/modules/reader/services/ReaderSettingsMetadata.ts @@ -8,12 +8,14 @@ import { useEffect, useMemo } from 'react'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts'; +import { + requestUpdateMangaMetadata, + requestUpdateServerMetadata, +} from '@/modules/metadata/services/MetadataUpdater.ts'; import { MangaType } from '@/lib/graphql/generated/graphql.ts'; -import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/Reader.constants.ts'; -import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts'; import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts'; -import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts'; +import { extractOriginalKey, getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts'; import { AllowedMetadataValueTypes, AppMetadataKeys, @@ -25,14 +27,69 @@ import { import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { jsonSaveParse } from '@/lib/HelperFunctions.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; +import { + DEFAULT_READER_SETTINGS, + GLOBAL_READER_SETTING_KEYS, +} from '@/modules/reader/constants/ReaderSettings.constants.tsx'; +import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts'; +import { APP_METADATA_KEY_PREFIX } from '@/modules/metadata/Metadata.constants.ts'; + +const convertToSettingsWithDefaultFlag = ( + type: Extract, + settings: IReaderSettings, + metadataHolder: MetadataHolder, +): IReaderSettingsWithDefaultFlag => { + const activeDevice = getActiveDevice(); + const istDefaultDevice = activeDevice === DEFAULT_DEVICE; + const existingSettings = Object.keys(metadataHolder.meta ?? {}) + // settings that are not for the active device need to be filtered out, otherwise, they mess up the "isDefault" flag + .filter((metaKey) => { + // the default device is not added as a prefix to the key, thus, there should only be the app prefix for these reader settings + if (istDefaultDevice) { + return metaKey.match(/_/g)?.length === 1; + } + + return metaKey.startsWith(`${APP_METADATA_KEY_PREFIX}_${activeDevice}_`); + }) + .map((metaKey) => extractOriginalKey(metaKey)); + const settingsWithDefault = Object.fromEntries( + (Object.entries(settings) as [keyof IReaderSettings, IReaderSettings[keyof IReaderSettings]][]).map( + ([key, value]) => { + const isGlobalSetting = GLOBAL_READER_SETTING_KEYS.includes(key); + if (isGlobalSetting) { + return [key, value]; + } + + const isDefaultSetting = type === 'manga' && !existingSettings.includes(key); + return [ + key, + { + value, + isDefault: isDefaultSetting, + }, + ]; + }, + ), + ) as IReaderSettingsWithDefaultFlag; + + return settingsWithDefault; +}; const convertSettingsToMetadata = ( settings: Partial, ): Metadata => ({ ...settings, tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode), + customFilter: JSON.stringify(settings.customFilter), + readerWidth: JSON.stringify(settings.readerWidth), }); +export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag( + 'global', + DEFAULT_READER_SETTINGS, + { meta: convertSettingsToMetadata(DEFAULT_READER_SETTINGS) as Metadata }, +); + const convertMetadataToSettings = ( metadata: Partial>, defaultSettings: IReaderSettings, @@ -41,27 +98,21 @@ const convertMetadataToSettings = ( tapZoneInvertMode: jsonSaveParse((metadata.tapZoneInvertMode as string) ?? '') ?? defaultSettings.tapZoneInvertMode, + customFilter: + jsonSaveParse((metadata.customFilter as string) ?? '') ?? + defaultSettings.customFilter, + readerWidth: + jsonSaveParse((metadata.readerWidth as string) ?? '') ?? + defaultSettings.readerWidth, }); -function getReaderSettingsWithDefaultValueFallback( - type: 'global', - metadataHolder: MetadataHolder, - defaultSettings?: DefaultSettings, - useEffectFn?: typeof useEffect, -): DefaultSettings; -function getReaderSettingsWithDefaultValueFallback( - type: 'manga', - metadataHolder: MangaIdInfo & MetadataHolder, - defaultSettings?: DefaultSettings, - useEffectFn?: typeof useEffect, -): DefaultSettings; -function getReaderSettingsWithDefaultValueFallback( +const getReaderMetadata = ( type: Extract, metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, - defaultSettings: DefaultSettings = DEFAULT_READER_SETTINGS as DefaultSettings, + defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS, useEffectFn?: typeof useEffect, -): DefaultSettings { - return convertMetadataToSettings( +) => + convertMetadataToSettings( getMetadataFrom( type as Parameters[0], metadataHolder as Parameters[1], @@ -70,7 +121,28 @@ function getReaderSettingsWithDefaultValueFallback, + metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, + defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS, + useEffectFn?: typeof useEffect, +): IReaderSettingsWithDefaultFlag { + const settings = getReaderMetadata(type, metadataHolder, defaultSettings, useEffectFn); + return convertToSettingsWithDefaultFlag(type, settings, metadataHolder); } const getSettings = ( @@ -82,7 +154,10 @@ const getSettings = ( 'manga', { ...metaHolder, - meta: convertFromGqlMeta(metaHolder.meta), + meta: convertFromGqlMeta( + metaHolder.meta, + (key) => !GLOBAL_READER_SETTING_KEYS.includes(extractOriginalKey(key)), + ), }, defaultSettings, useEffectFn, @@ -91,12 +166,12 @@ const getSettings = ( export const getReaderSettingsFor = ( metaHolder: MangaIdInfo & GqlMetaHolder, defaultSettings: IReaderSettings, -): IReaderSettings => getSettings(metaHolder, defaultSettings); +): IReaderSettingsWithDefaultFlag => getSettings(metaHolder, defaultSettings); export const useGetReaderSettingsFor = ( metaHolder: MangaIdInfo & GqlMetaHolder, defaultSettings: IReaderSettings, -): IReaderSettings => { +): IReaderSettingsWithDefaultFlag => { const settings = getSettings(metaHolder, defaultSettings, useEffect); return useMemo(() => settings, [metaHolder, defaultSettings]); }; @@ -106,6 +181,30 @@ export const useDefaultReaderSettings = (): { settings: IReaderSettings; loading: boolean; request: ReturnType; +} => { + const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true }); + const { data, loading } = request; + const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]); + const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]); + const tmpSettings = getReaderMetadata('global', metaHolder, undefined, useEffect); + const settings = useMemo(() => tmpSettings, [metaHolder]); + + return useMemo( + () => ({ + metadata, + settings, + loading, + request, + }), + [metadata, settings, loading, request], + ); +}; + +export const useDefaultReaderSettingsWithDefaultFlag = (): { + metadata?: Metadata; + settings: IReaderSettingsWithDefaultFlag; + loading: boolean; + request: ReturnType; } => { const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true }); const { data, loading } = request; @@ -129,13 +228,19 @@ export const updateReaderSettings = async & GqlMetaHolder, setting: Setting, value: IReaderSettings[Setting], -): Promise => - requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); + isGlobal: boolean = false, +): Promise => { + const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting); + if (isGlobalSetting) { + return requestUpdateServerMetadata([[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); + } + return requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); +}; export const createUpdateReaderSettings = ( manga: Pick & GqlMetaHolder, handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'), ): ((...args: OmitFirst>>) => Promise) => - (setting, value) => - updateReaderSettings(manga, setting, value).catch(handleError); + (setting, value, isGlobal) => + updateReaderSettings(manga, setting, value, isGlobal).catch(handleError); diff --git a/src/modules/reader/types/Reader.types.ts b/src/modules/reader/types/Reader.types.ts index e9f9d730..7d72d611 100644 --- a/src/modules/reader/types/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -8,6 +8,7 @@ import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; export enum ProgressBarType { HIDDEN, @@ -39,6 +40,112 @@ export enum ReaderPageScaleMode { ORIGINAL, } +export enum ReaderOverlayMode { + AUTO, + DESKTOP, + MOBILE, +} + +export enum ReaderExitMode { + PREVIOUS, + MANGA, +} + +export enum ReaderBackgroundColor { + THEME, + BLACK, + GRAY, + WHITE, +} + +export interface ReaderFilterRGBA { + red: number; + green: number; + blue: number; + alpha: number; +} + +export interface ReaderCustomFilter { + brightness: { + /** + * percentage + */ + value: number; + enabled: boolean; + }; + contrast: { + /** + * percentage + */ + value: number; + enabled: boolean; + }; + saturate: { + /** + * percentage + */ + value: number; + enabled: boolean; + }; + hue: { + /** + * degree + */ + value: number; + enabled: boolean; + }; + rgba: { + value: ReaderFilterRGBA; + enabled: boolean; + }; + sepia: boolean; + grayscale: boolean; + invert: boolean; +} + +export interface IReaderSettingsGlobal { + overlayMode: ReaderOverlayMode; + exitMode: ReaderExitMode; + customFilter: ReaderCustomFilter; + shouldSkipDupChapters: boolean; + progressBarType: ProgressBarType; + /** + * pixel + */ + progressBarSize: number; + progressBarPosition: ProgressBarPosition; + shouldShowPageNumber: boolean; + isStaticNav: boolean; + backgroundColor: ReaderBackgroundColor; +} + +export interface IReaderSettingsManga { + tapZoneLayout: TapZoneLayouts; + tapZoneInvertMode: TapZoneInvertMode; + pageScaleMode: ReaderPageScaleMode; + shouldStretchPage: boolean; + shouldOffsetDoubleSpreads: boolean; + readingDirection: ReadingDirection; + readingMode: ReadingMode; + /** + * pixel + */ + pageGap: number; + readerWidth: { + /** + * percentage + */ + value: number; + enabled: boolean; + }; +} + +export interface IReaderSettings extends IReaderSettingsGlobal, IReaderSettingsManga {} + +export interface IReaderSettingsWithDefaultFlag + extends IReaderSettingsGlobal, + TransformRecordToWithDefaultFlag {} + export interface ReaderStateChapters { chapters: TChapterReader[]; currentChapter?: TChapterReader | null; @@ -47,20 +154,25 @@ export interface ReaderStateChapters { setReaderStateChapters: React.Dispatch>>; } -export interface IReaderSettings { - staticNav: boolean; - showPageNumber: boolean; - loadNextOnEnding: boolean; - skipDupChapters: boolean; - readerWidth: number; - tapZoneLayout: TapZoneLayouts; - tapZoneInvertMode: TapZoneInvertMode; - progressBarType: ProgressBarType; - progressBarSize: number; - progressBarPosition: ProgressBarPosition; - pageScaleMode: ReaderPageScaleMode; - shouldScalePage: boolean; - shouldOffsetDoubleSpreads: boolean; - readingDirection: ReadingDirection; - readingMode: ReadingMode; +interface ReaderSettingsTypeBaseProps { + settings: IReaderSettingsWithDefaultFlag; + updateSetting: ( + ...args: OmitFirst> + ) => ReturnType; } + +export interface ReaderSettingsDefaultableProps { + isDefaultable: boolean; + onDefault: (setting: keyof IReaderSettings) => void; +} + +interface ReaderSettingsTypeDefaultableProps extends ReaderSettingsTypeBaseProps, ReaderSettingsDefaultableProps { + isDefaultable: true; +} + +interface ReaderSettingsTypeNonDefaultableProps extends ReaderSettingsTypeBaseProps {} + +export type ReaderSettingsTypeProps = + | ReaderSettingsTypeDefaultableProps + | (PropertiesNever> & + ReaderSettingsTypeNonDefaultableProps); diff --git a/src/modules/reader/utils/ReaderSettings.utils.tsx b/src/modules/reader/utils/ReaderSettings.utils.tsx new file mode 100644 index 00000000..cd62b46d --- /dev/null +++ b/src/modules/reader/utils/ReaderSettings.utils.tsx @@ -0,0 +1,12 @@ +/* + * 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 { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; + +export const isOffsetDoubleSpreadPagesEditable = (readingMode: IReaderSettings['readingMode']): boolean => + readingMode === ReadingMode.DOUBLE_PAGE; diff --git a/src/modules/settings/Settings.types.ts b/src/modules/settings/Settings.types.ts index dabefe16..91570814 100644 --- a/src/modules/settings/Settings.types.ts +++ b/src/modules/settings/Settings.types.ts @@ -33,4 +33,4 @@ export interface ISearchSettings { ignoreFilters: boolean; } -export type ServerSettings = GetServerSettingsQuery['settings']; +export type ServerSettings = Omit;