Reader settings

This commit is contained in:
schroda
2024-11-04 18:00:35 +01:00
parent 9c57ace973
commit a607f7f368
70 changed files with 2813 additions and 425 deletions

View File

@@ -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"

View File

@@ -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 = () => {
<Route index element={<Settings />} />
<Route path="about" element={<About />} />
<Route path="categories" element={<CategorySettings />} />
<Route path="defaultReaderSettings" element={null} />
<Route path="defaultReaderSettings" element={<DefaultReaderSettings />} />
<Route path="librarySettings">
<Route index element={<LibrarySettings />} />
<Route path="duplicates" element={<LibraryDuplicates />} />

6
src/UtilTypes.d.ts vendored
View File

@@ -26,6 +26,10 @@ type PropertiesNever<T> = { [key in keyof T]?: never };
type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
type TupleUnion<U extends string, R extends any[] = []> = {
type TupleUnion<U extends string | number | symbol, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];
type TransformRecordToWithDefaultFlag<T extends Record<string, any>> = {
[K in keyof T]: { value: T[K]; isDefault: boolean };
};

View File

@@ -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 = () => (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 15h16"
/>
</svg>
</SvgIcon>
);

View File

@@ -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 = () => (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 15h16M7 15v-5m10 5v-5M4 9h16"
/>
</svg>
</SvgIcon>
);

View File

@@ -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,
},

View File

@@ -22,3 +22,20 @@ export type ValueToDisplayData<Value extends string | number> = Record<
icon: ReactNode;
}
>;
export interface MultiValueButtonBaseProps<Value extends string | number> {
value: Value;
values: Value[];
setValue: (value: Value) => void;
valueToDisplayData: ValueToDisplayData<Value>;
}
export interface MultiValueButtonDefaultableProps<Value extends string | number>
extends OptionalProperty<MultiValueButtonBaseProps<Value>, 'value'> {
isDefaultable?: boolean;
onDefault?: () => void;
}
export type MultiValueButtonProps<Value extends string | number> =
| (MultiValueButtonBaseProps<Value> & PropertiesNever<MultiValueButtonDefaultableProps<Value>>)
| MultiValueButtonDefaultableProps<Value>;

View File

@@ -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 extends string | number>({
value,
values,
setValue,
valueToDisplayData,
}: {
value: Value;
values: Value[];
setValue: (value: Value) => void;
valueToDisplayData: ValueToDisplayData<Value>;
}) => {
isDefaultable,
onDefault,
}: MultiValueButtonProps<Value>) => {
const { t } = useTranslation();
return (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
{isDefaultable && (
<Button key="default" onClick={onDefault} variant={value === undefined ? 'contained' : 'outlined'}>
{t('global.label.default')}
</Button>
)}
{values.map((displayValue) => (
<Button
key={displayValue}

View File

@@ -7,28 +7,58 @@
*/
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import { ReactNode, useMemo } from 'react';
import Button from '@mui/material/Button';
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
import { MultiValueButtonProps } from '@/modules/core/Core.types.ts';
export const ValueRotationButton = <Value extends string | number>({
value,
values,
setValue,
valueToDisplayData,
}: {
value: Value;
values: Value[];
setValue: (value: Value) => void;
valueToDisplayData: ValueToDisplayData<Value>;
}) => {
isDefaultable,
onDefault,
defaultIcon,
}: MultiValueButtonProps<Value> & { 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 (
<Button
onClick={() => setValue(values[0])}
sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }}
variant="contained"
startIcon={defaultIcon}
size="large"
>
{t('global.label.default')}
</Button>
);
}
return (
<Button
onClick={() => 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}

View File

@@ -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;
};
}) => (
<Stack sx={{ flexDirection: 'row', gap: 1, alignItems: 'center' }}>
<Stack sx={{ flexBasis: '25%' }}>
<Typography {...slotProps?.label} sx={{ ...slotProps?.label?.sx }}>
{label}
</Typography>
<Typography {...slotProps?.value} sx={{ ...slotProps?.value?.sx }}>
{value}
</Typography>
</Stack>
<Slider {...slotProps?.slider} sx={{ flexBasis: '75%', ...slotProps?.slider?.sx }} />
</Stack>
);

View File

@@ -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 (
<div role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
<Box {...boxProps} role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
{currentIndex === index && children}
</div>
</Box>
);
}

View File

@@ -35,7 +35,7 @@ export const TabsMenu = forwardRef(
indicatorColor="primary"
textColor="primary"
variant="scrollable"
scrollButtons
scrollButtons="auto"
allowScrollButtonsMobile
{...props}
>

View File

@@ -6,10 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
type TupleUnion<U extends string | number | symbol, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];
export const shouldForwardProp =
<TCustomProps extends Record<string, unknown>>(customProps: TupleUnion<keyof TCustomProps>) =>
(prop: string): boolean =>

View File

@@ -272,7 +272,7 @@ export function Library() {
<TabsMenu value={activeTab.order} onChange={(e, newTab) => handleTabChange(newTab)}>
{tabs.map((tab) => (
<Tab
sx={{ display: 'flex' }}
sx={{ flexGrow: 1, maxWidth: 'unset' }}
key={tab.id}
label={
<TitleWithSizeTag>

View File

@@ -12,10 +12,6 @@ import { ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.
export const APP_METADATA_KEY_PREFIX = 'webUI_';
const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
staticNav: undefined,
showPageNumber: undefined,
loadNextOnEnding: undefined,
skipDupChapters: undefined,
migration: undefined,
deleteChaptersManuallyMarkedRead: undefined,
deleteChaptersWhileReading: undefined,
@@ -60,8 +56,16 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
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: [

View File

@@ -31,14 +31,19 @@ export const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = A
return value as T;
};
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
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;

View File

@@ -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);

View File

@@ -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`,
}}

View File

@@ -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(() => {

View File

@@ -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 (
<Box sx={{ position: 'absolute', width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}>
{isDesktop && (
<>
<StandardReaderProgressBar />
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => {}} />
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => setAreSettingsOpen(true)} />
</>
)}
{isMobile && (
<>
<ReaderOverlayHeaderMobile isVisible={isVisible} />
<ReaderBottomBarMobile openSettings={() => {}} isVisible={isVisible} />
<ReaderBottomBarMobile openSettings={() => setAreSettingsOpen(true)} isVisible={isVisible} />
</>
)}
<ReaderSettings isOpen={areSettingsOpen} close={() => setAreSettingsOpen(false)} />
<ReaderPageNumber />
</Box>
);

View File

@@ -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<HTMLDivElement | null>();
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 (
<Drawer
variant={settings.staticNav ? 'permanent' : 'persistent'}
open={isVisible || settings.staticNav}
variant={settings.isStaticNav ? 'permanent' : 'persistent'}
open={isVisible || settings.isStaticNav}
transitionDuration={drawerTransitionDuration}
PaperProps={{
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref),
@@ -99,7 +95,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Tooltip title={t('reader.button.exit')}>
<IconButton onClick={handleBack} color="inherit">
<IconButton onClick={exit} color="inherit">
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
</IconButton>
</Tooltip>
@@ -107,9 +103,9 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
<IconButton
onClick={() => {
setReaderNavBarWidth(0);
updateReaderSettings('staticNav', !settings.staticNav);
updateReaderSettings('isStaticNav', !settings.isStaticNav);
}}
color={settings.staticNav ? 'primary' : 'inherit'}
color={settings.isStaticNav ? 'primary' : 'inherit'}
>
<PushPinIcon />
</IconButton>
@@ -147,6 +143,8 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
settings={settings}
updateSetting={updateReaderSettings}
openSettings={openSettings}
isDefaultable
onDefault={(...args) => manga && ReaderService.deleteSetting(manga, ...args)}
/>
</Stack>
</ReaderNavContainer>

View File

@@ -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;
}

View File

@@ -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> = {
[ReaderPageScaleMode.WIDTH]: {
title: 'reader.settings.page_scale.width',
icon: <ExpandIcon sx={{ transform: 'rotate(90deg)' }} />,
},
[ReaderPageScaleMode.HEIGHT]: {
title: 'reader.settings.page_scale.height',
icon: <ExpandIcon />,
},
[ReaderPageScaleMode.SCREEN]: {
title: 'reader.settings.page_scale.screen',
icon: <ZoomOutMapIcon />,
},
[ReaderPageScaleMode.ORIGINAL]: {
title: 'reader.settings.page_scale.original',
icon: <CropOriginalIcon />,
},
};
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<IReaderSettings, 'pageScaleMode' | 'shouldScalePage'> & {
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldScalePage'>>(
setting: Setting,
value: IReaderSettings[Setting],
) => void;
}) => {
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'pageScaleMode' | 'shouldStretchPage'> &
Pick<MultiValueButtonDefaultableProps<ReaderPageScaleMode>, 'isDefaultable' | 'onDefault'> & {
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'>>(
setting: Setting,
value: IReaderSettings[Setting],
) => void;
}) => {
const { t } = useTranslation();
const isPageScalingPossible = pageScaleMode !== ReaderPageScaleMode.ORIGINAL;
return (
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
<ValueRotationButton
value={pageScaleMode}
{...buttonSelectInputProps}
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
values={READER_PAGE_SCALE_MODE_VALUES}
setValue={(value) => 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] && (
<Tooltip title={t('reader.settings.label.scale_page')}>
<CustomIconButton
onClick={() => updateSetting('shouldScalePage', !shouldScalePage)}
onClick={() => updateSetting('shouldStretchPage', !shouldStretchPage.value)}
sx={{ minWidth: 0 }}
variant="contained"
color={shouldScalePage ? 'secondary' : 'primary'}
color={shouldStretchPage.value ? 'secondary' : 'primary'}
>
<FitScreenIcon />
</CustomIconButton>

View File

@@ -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 extends keyof IReaderSettings>(setting: Setting, value: IReaderSettings[Setting]) => void;
} & Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
isDefaultable,
onDefault,
}: ReaderSettingsTypeProps & Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
const { t } = useTranslation();
return (
@@ -32,19 +31,26 @@ export const ReaderNavBarDesktopQuickSettings = ({
<ReaderNavBarDesktopReadingMode
readingMode={readingMode}
setReadingMode={(value) => updateSetting('readingMode', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('readingMode')}
/>
<ReaderNavBarDesktopOffsetDoubleSpread
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads}
readingMode={readingMode.value}
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads.value}
setShouldOffsetDoubleSpreads={(value) => updateSetting('shouldOffsetDoubleSpreads', value)}
/>
<ReaderNavBarDesktopPageScale
pageScaleMode={pageScaleMode}
shouldScalePage={shouldScalePage}
shouldStretchPage={shouldStretchPage}
updateSetting={updateSetting}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('pageScaleMode')}
/>
<ReaderNavBarDesktopReadingDirection
readingDirection={readingDirection}
setReadingDirection={(value) => updateSetting('readingDirection', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('readingDirection')}
/>
<Button
onClick={() => openSettings()}

View File

@@ -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> = {
[ReadingDirection.LTR]: {
title: 'reader.settings.reading_direction.ltr',
icon: <ArrowCircleRightIcon />,
},
[ReadingDirection.RTL]: {
title: 'reader.settings.reading_direction.rtl',
icon: <ArrowCircleLeftIcon />,
},
};
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<IReaderSettings, 'readingDirection'> & {
setReadingDirection: (readingDirection: ReadingDirection) => void;
}) => (
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setReadingDirection: (readingDirection: ReadingDirection) => void;
}) => (
<ValueRotationButton
value={readingDirection}
{...buttonSelectInputProps}
value={readingDirection.isDefault ? undefined : readingDirection.value}
values={READING_DIRECTION_VALUES}
setValue={setReadingDirection}
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
valueToDisplayData={READING_DIRECTION_VALUE_TO_DISPLAY_DATA}
defaultIcon={READING_DIRECTION_VALUE_TO_DISPLAY_DATA[readingDirection.value].icon}
/>
);

View File

@@ -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> = {
[ReadingMode.SINGLE_PAGE]: {
title: 'reader.settings.reader_type.label.single_page',
icon: <SinglePageIcon />,
},
[ReadingMode.DOUBLE_PAGE]: {
title: 'reader.settings.reader_type.label.double_page',
icon: <DoublePageIcon />,
},
[ReadingMode.CONTINUOUS_VERTICAL]: {
title: 'reader.settings.reader_type.label.continuous_vertical',
icon: <ContinuousVerticalPageIcon />,
},
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
title: 'reader.settings.reader_type.label.continuous_horizontal',
icon: <ContinuousHorizontalPageIcon />,
},
};
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<IReaderSettings, 'readingMode'> & {
setReadingMode: (mode: ReadingMode) => void;
}) => (
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
setReadingMode: (mode: ReadingMode) => void;
}) => (
<ValueRotationButton
value={readingMode}
{...buttonSelectInputProps}
value={readingMode.isDefault ? undefined : readingMode.value}
values={READING_MODE_VALUES}
setValue={setReadingMode}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
defaultIcon={READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].icon}
/>
);

View File

@@ -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';

View File

@@ -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 (
<Stack sx={{ gap: 2 }}>
<ReaderSettingReadingMode
readingMode={readingMode}
setReadingMode={(value) => ReaderService.updateSetting(manga, 'readingMode', value)}
isDefaultable
onDefault={() => deleteSetting('readingMode')}
/>
<ReaderSettingReadingDirection
readingDirection={readingDirection}
setReadingDirection={(value) => ReaderService.updateSetting(manga, 'readingDirection', value)}
isDefaultable
onDefault={() => deleteSetting('readingDirection')}
/>
</Stack>
);
};

View File

@@ -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<IReaderSettings, 'readingMode' | 'readingDirection'>
>(manga ?? { id: -1 }, () => makeToast(t('reader.settings.error.label.failed_to_save_settings')));
if (!manga) {
return null;
}
return (
<Stack sx={{ gap: 2 }}>
<ReaderBottomBarMobileReadingMode
readingMode={readingMode}
setReadingMode={(value) => updateReaderSettings('readingMode', value)}
/>
<ReaderBottomBarMobileReadingDirection
readingDirection={readingDirection}
setReadingDirection={(value) => updateReaderSettings('readingDirection', value)}
/>
</Stack>
);
};

View File

@@ -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> = {
[ReadingMode.SINGLE_PAGE]: {
title: 'reader.settings.reader_type.label.single_page',
icon: <SinglePageIcon />,
},
[ReadingMode.DOUBLE_PAGE]: {
title: 'reader.settings.reader_type.label.double_page',
icon: <DoublePageIcon />,
},
[ReadingMode.CONTINUOUS_VERTICAL]: {
title: 'reader.settings.reader_type.label.continuous_vertical',
icon: <ContinuousVerticalPageIcon />,
},
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
title: 'reader.settings.reader_type.label.continuous_horizontal',
icon: <ContinuousHorizontalPageIcon />,
},
};
const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
export const ReaderBottomBarMobileReadingMode = ({
readingMode,
setReadingMode,
}: Pick<IReaderSettings, 'readingMode'> & {
setReadingMode: (mode: ReadingMode) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
label={t('reader.settings.label.reading_mode')}
value={readingMode}
values={READING_MODE_VALUES}
setValue={setReadingMode}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -18,6 +18,7 @@ export const ReaderProgressBarContainer = styled(Stack, {
shouldForwardProp: shouldForwardProp<ReaderProgressBarContainerProps>(['progressBarPosition']),
})<ReaderProgressBarContainerProps>(({ theme, progressBarPosition }) => ({
position: 'fixed',
pointerEvents: 'all',
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, {
justifyContent: 'flex-end',
bottom: 0,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 (
<Dialog
open={isOpen}
maxWidth="md"
fullWidth
onClose={close}
hideBackdrop={activeTab === ReaderSettingTab.FILTER}
>
<DialogContent sx={{ p: 0 }}>
<ReaderSettingsTabs
activeTab={activeTab}
setActiveTab={setActiveTab}
settings={settings}
updateSetting={(...args) => ReaderService.updateSetting(manga, ...args)}
deleteSetting={(...args) => ReaderService.deleteSetting(manga, ...args)}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -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<Parameters<typeof ReaderService.updateSetting>>) => void;
}
interface DefaultSettingsProps extends BaseProps {
areDefaultSettings: true;
}
interface MangaSettingsProps extends BaseProps {
deleteSetting: (setting: keyof IReaderSettings) => void;
}
type Props =
| (DefaultSettingsProps & PropertiesNever<Omit<MangaSettingsProps, keyof BaseProps>>)
| (PropertiesNever<Omit<DefaultSettingsProps, keyof BaseProps>> & 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 (
<>
<TabsMenu
value={activeTab}
onChange={(_, newTab) => 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 (
<Tab
key={id}
value={id}
label={t(label)}
sx={{ flexGrow: 1, maxWidth: 'unset', textTransform: 'none' }}
/>
);
})}
</TabsMenu>
<Box sx={{ p: areDefaultSettings ? undefined : 2, overflowX: 'hidden' }}>
{Object.values(READER_SETTING_TABS).map(({ id, supportsTouchDevices }) => {
if (!supportsTouchDevices && isTouchDevice) {
return null;
}
switch (id as ReaderSettingTab) {
case ReaderSettingTab.LAYOUT:
return (
<TabPanel key={id} index={id} currentIndex={activeTab}>
<ReaderLayoutSettings
settings={settings}
updateSetting={(...args) => updateSetting(...args)}
setShowPreview={setShowPreview!}
// @ts-expect-error - TS2322: Type boolean is not assignable to type true
isDefaultable={!areDefaultSettings}
onDefault={(...args) => deleteSetting?.(...args)}
/>
</TabPanel>
);
case ReaderSettingTab.GENERAL:
return (
<TabPanel
key={id}
index={id}
currentIndex={activeTab}
sx={{ p: areDefaultSettings ? 2 : undefined }}
>
<ReaderGeneralSettings
overlayMode={overlayMode}
settings={settings}
updateSetting={(...args) => updateSetting(...args)}
// @ts-expect-error - TS2322: Type boolean is not assignable to type true
isDefaultable={!areDefaultSettings}
onDefault={(...args) => deleteSetting?.(...args)}
/>
</TabPanel>
);
case ReaderSettingTab.FILTER:
return (
<TabPanel
key={id}
index={id}
currentIndex={activeTab}
sx={{ p: areDefaultSettings ? 2 : undefined }}
>
<ReaderFilterSettings
settings={settings}
updateSetting={(...args) => updateSetting(...args)}
/>
</TabPanel>
);
case ReaderSettingTab.BEHAVIOUR:
return (
<TabPanel
key={id}
index={id}
currentIndex={activeTab}
sx={{ p: areDefaultSettings ? 2 : undefined }}
>
<ReaderBehaviourSettings
settings={settings}
updateSetting={(...args) => updateSetting(...args)}
/>
</TabPanel>
);
default:
throw new Error(`Unexpected "ReaderSettingTab" (${id})`);
}
})}
</Box>
</>
);
};

View File

@@ -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<Parameters<typeof ReaderService.updateSetting>>
) => ReturnType<typeof ReaderService.updateSetting>;
}) => {
const { t } = useTranslation();
return (
<Stack sx={{ gap: 2 }}>
<ReaderSettingExitMode
exitMode={settings.exitMode}
setExitMode={(value) => updateSetting('exitMode', value)}
/>
<CheckboxInput
label={t('reader.settings.label.skip_dup_chapters')}
checked={settings.shouldSkipDupChapters}
onChange={(_, checked) => updateSetting('shouldSkipDupChapters', checked)}
/>
{isOffsetDoubleSpreadPagesEditable(settings.readingMode.value) && (
<CheckboxInput
label={t('reader.settings.label.offset_double_spread')}
checked={settings.shouldOffsetDoubleSpreads.value}
onChange={(_, checked) => updateSetting('shouldOffsetDoubleSpreads', checked)}
/>
)}
</Stack>
);
};

View File

@@ -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> = {
[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<IReaderSettings, 'exitMode'> & {
setExitMode: (mode: ReaderExitMode) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
label={t('reader.settings.exit_mode')}
value={exitMode}
values={READER_EXIT_MODE_VALUES}
setValue={setExitMode}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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) => (
<Stack sx={{ gap: 2 }}>
<ReaderSettingBrightness
brightness={customFilter.brightness}
updateSetting={(key, value, commit) =>
updateSetting(
'customFilter',
{
...customFilter,
[key]: value,
},
commit,
)
}
/>
<ReaderSettingContrast
contrast={customFilter.contrast}
updateSetting={(key, value, commit) =>
updateSetting(
'customFilter',
{
...customFilter,
[key]: value,
},
commit,
)
}
/>
<ReaderSettingSaturate
saturate={customFilter.saturate}
updateSetting={(key, value, commit) =>
updateSetting(
'customFilter',
{
...customFilter,
[key]: value,
},
commit,
)
}
/>
<ReaderSettingHue
hue={customFilter.hue}
updateSetting={(key, value, commit) =>
updateSetting(
'customFilter',
{
...customFilter,
[key]: value,
},
commit,
)
}
/>
<ReaderSettingRGBA
rgba={customFilter.rgba}
updateSetting={(key, value, commit) =>
updateSetting(
'customFilter',
{
...customFilter,
[key]: value,
},
commit,
)
}
/>
<ReaderSettingSepia
sepia={customFilter.sepia}
updateSetting={(value) =>
updateSetting('customFilter', {
...customFilter,
sepia: value,
})
}
/>
<ReaderSettingGrayscale
grayscale={customFilter.grayscale}
updateSetting={(value) =>
updateSetting('customFilter', {
...customFilter,
grayscale: value,
})
}
/>
<ReaderSettingInvert
invert={customFilter.invert}
updateSetting={(value) =>
updateSetting('customFilter', {
...customFilter,
invert: value,
})
}
/>
</Stack>
);

View File

@@ -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<IReaderSettings['customFilter'], 'brightness'> & {
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
filter: Filter,
value: IReaderSettings['customFilter'][Filter],
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.brightness.title')}
checked={brightness.enabled}
onChange={(_, checked) => updateSetting('brightness', { ...brightness, enabled: checked }, true)}
/>
{brightness.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.brightness.value')}
value={brightness.value}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.brightness.value,
value: brightness.value,
step: 1,
min: 5,
max: 200,
onChange: (_, value) => {
updateSetting('brightness', { ...brightness, value: value as number }, false);
},
onChangeCommitted: (_, value) => {
updateSetting('brightness', { ...brightness, value: value as number }, true);
},
},
}}
/>
)}
</Stack>
);
};

View File

@@ -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<IReaderSettings['customFilter'], 'contrast'> & {
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
filter: Filter,
value: IReaderSettings['customFilter'][Filter],
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.contrast.title')}
checked={contrast.enabled}
onChange={(_, checked) => updateSetting('contrast', { ...contrast, enabled: checked }, true)}
/>
{contrast.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.contrast.value')}
value={contrast.value}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.contrast.value,
value: contrast.value,
step: 1,
min: 5,
max: 200,
onChange: (_, newValue) => {
updateSetting('contrast', { ...contrast, value: newValue as number }, false);
},
onChangeCommitted: (_, newValue) => {
updateSetting('contrast', { ...contrast, value: newValue as number }, true);
},
},
}}
/>
)}
</Stack>
);
};

View File

@@ -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<IReaderSettings['customFilter'], 'grayscale'> & {
updateSetting: (grayscale: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.grayscale')}
checked={grayscale}
onChange={(_, checked) => updateSetting(checked)}
/>
</Stack>
);
};

View File

@@ -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<IReaderSettings['customFilter'], 'hue'> & {
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
filter: Filter,
value: IReaderSettings['customFilter'][Filter],
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.hue.title')}
checked={hue.enabled}
onChange={(_, checked) => updateSetting('hue', { ...hue, enabled: checked }, true)}
/>
{hue.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.hue.value')}
value={hue.value}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.hue.value,
value: hue.value,
step: 1,
min: 0,
max: 200,
onChange: (_, newValue) => {
updateSetting('hue', { ...hue, value: newValue as number }, false);
},
onChangeCommitted: (_, newValue) => {
updateSetting('hue', { ...hue, value: newValue as number }, true);
},
},
}}
/>
)}
</Stack>
);
};

View File

@@ -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<IReaderSettings['customFilter'], 'invert'> & {
updateSetting: (invert: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.invert')}
checked={invert}
onChange={(_, checked) => updateSetting(checked)}
/>
</Stack>
);
};

View File

@@ -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<RGBAType, TranslationKey> = {
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<RGBAType, number> = {
red: 255,
green: 255,
blue: 255,
alpha: 100,
};
export const ReaderSettingRGBA = ({
rgba,
updateSetting,
}: Pick<IReaderSettings['customFilter'], 'rgba'> & {
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
filter: Filter,
value: IReaderSettings['customFilter'][Filter],
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.rgba.title')}
checked={rgba.enabled}
onChange={(_, checked) => updateSetting('rgba', { ...rgba, enabled: checked }, true)}
/>
{rgba.enabled && (
<Stack sx={{ gap: 1 }}>
{Object.entries(rgba.value).map(([key, value]) => (
<SliderInput
key={key}
label={t(RGBA_TYPE_TO_TRANSLATION_KEY[key as RGBAType])}
value={value}
slotProps={{
slider: {
value,
defaultValue: DEFAULT_READER_SETTINGS.customFilter.rgba.value[key as RGBAType],
step: 1,
min: 0,
max: RGBA_TYPE_TO_MAX_VALUE[key as RGBAType],
onChange: (_, newValue) => {
updateSetting(
'rgba',
{ ...rgba, value: { ...rgba.value, [key]: newValue } },
false,
);
},
onChangeCommitted: (_, newValue) => {
updateSetting(
'rgba',
{ ...rgba, value: { ...rgba.value, [key]: newValue } },
true,
);
},
},
}}
/>
))}
</Stack>
)}
</Stack>
);
};

View File

@@ -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<IReaderSettings['customFilter'], 'saturate'> & {
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
filter: Filter,
value: IReaderSettings['customFilter'][Filter],
commit: boolean,
) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.saturate.title')}
checked={saturate.enabled}
onChange={(_, checked) => updateSetting('saturate', { ...saturate, enabled: checked }, true)}
/>
{saturate.enabled && (
<SliderInput
label={t('reader.settings.custom_filter.saturate.value')}
value={saturate.value}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.saturate.value,
value: saturate.value,
step: 1,
min: 0,
max: 200,
onChange: (_, value) => {
updateSetting('saturate', { ...saturate, value: value as number }, false);
},
onChangeCommitted: (_, value) => {
updateSetting('saturate', { ...saturate, value: value as number }, true);
},
},
}}
/>
)}
</Stack>
);
};

View File

@@ -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<IReaderSettings['customFilter'], 'sepia'> & {
updateSetting: (sepia: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
<CheckboxInput
label={t('reader.settings.custom_filter.sepia')}
checked={sepia}
onChange={(_, checked) => updateSetting(checked)}
/>
</Stack>
);
};

View File

@@ -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<IReaderSettings, 'overlayMode'> & ReaderSettingsTypeProps) => {
const { t } = useTranslation();
return (
<Stack sx={{ gap: 2 }}>
<ReaderSettingOverlayMode
overlayMode={settings.overlayMode}
setOverlayMode={(value) => updateSetting('overlayMode', value)}
/>
<ReaderSettingBackgroundColor
backgroundColor={settings.backgroundColor}
updateSetting={(value) => updateSetting('backgroundColor', value)}
/>
<ReaderSettingProgressBarType
overlayMode={overlayMode}
progressBarType={settings.progressBarType}
setProgressBarType={(value) => updateSetting('progressBarType', value)}
/>
<ReaderSettingProgressBarSize
overlayMode={overlayMode}
progressBarType={settings.progressBarType}
progressBarSize={settings.progressBarSize}
setProgressBarSize={(...args) => updateSetting('progressBarSize', ...args)}
/>
<ReaderSettingProgressBarPosition
overlayMode={overlayMode}
progressBarPosition={settings.progressBarPosition}
setProgressBarPosition={(value) => updateSetting('progressBarPosition', value)}
/>
{(settings.progressBarType === ProgressBarType.HIDDEN || overlayMode === ReaderOverlayMode.MOBILE) && (
<CheckboxInput
label={t('reader.settings.label.show_page_number')}
checked={settings.shouldShowPageNumber}
onChange={(_, checked) => updateSetting('shouldShowPageNumber', checked)}
/>
)}
</Stack>
);
};

View File

@@ -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> = {
[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<IReaderSettingsWithDefaultFlag, 'backgroundColor'> & {
updateSetting: (color: ReaderBackgroundColor) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
label={t('reader.settings.background_color')}
value={backgroundColor}
values={READER_BACKGROUND_COLOR_VALUES}
setValue={updateSetting}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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> = {
[ReaderOverlayMode.AUTO]: {
title: 'global.label.auto',
icon: <AutoModeIcon />,
},
[ReaderOverlayMode.DESKTOP]: {
title: 'global.label.desktop',
icon: <ComputerIcon />,
},
[ReaderOverlayMode.MOBILE]: {
title: 'global.label.mobile',
icon: <PhoneIphoneIcon />,
},
};
const READING_MODE_VALUES = Object.values(ReaderOverlayMode).filter((value) => typeof value === 'number');
export const ReaderSettingOverlayMode = ({
overlayMode,
setOverlayMode,
}: Pick<IReaderSettings, 'overlayMode'> & {
setOverlayMode: (mode: ReaderOverlayMode) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
label={t('reader.settings.overlay_mode')}
value={overlayMode}
values={READING_MODE_VALUES}
setValue={setOverlayMode}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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> = {
[ProgressBarPosition.BOTTOM]: {
title: 'global.label.bottom',
icon: <ArrowBackIosNewIcon sx={{ transform: 'rotate(90deg)' }} />,
},
[ProgressBarPosition.LEFT]: {
title: 'global.label.left',
icon: <ArrowForwardIosIcon />,
},
[ProgressBarPosition.RIGHT]: {
title: 'global.label.right',
icon: <ArrowBackIosNewIcon />,
},
};
const PROGRESS_BAR_POSITION_VALUES = Object.values(ProgressBarPosition).filter((value) => typeof value === 'number');
export const ReaderSettingProgressBarPosition = ({
overlayMode,
progressBarPosition,
setProgressBarPosition,
}: Pick<IReaderSettings, 'progressBarPosition' | 'overlayMode'> & {
setProgressBarPosition: (position: ProgressBarPosition) => void;
}) => {
const { t } = useTranslation();
if (overlayMode !== ReaderOverlayMode.DESKTOP) {
return null;
}
return (
<ButtonSelectInput
label={t('reader.settings.progress_bar.position')}
value={progressBarPosition}
values={PROGRESS_BAR_POSITION_VALUES}
setValue={setProgressBarPosition}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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<IReaderSettings, 'progressBarType' | 'progressBarSize' | 'overlayMode'> & {
setProgressBarSize: (size: number, commit: boolean) => void;
}) => {
const { t } = useTranslation();
const isChangeable = overlayMode === ReaderOverlayMode.DESKTOP && progressBarType === ProgressBarType.STANDARD;
if (!isChangeable) {
return null;
}
return (
<SliderInput
label={t('reader.settings.progress_bar.size')}
value={t('global.value', { value: progressBarSize, unit: t('global.unit.px') })}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.progressBarSize,
value: progressBarSize,
step: 1,
min: 2,
max: 20,
onChange: (_, value) => {
setProgressBarSize(value as number, false);
},
onChangeCommitted: (_, value) => {
setProgressBarSize(value as number, true);
},
},
}}
/>
);
};

View File

@@ -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> = {
[ProgressBarType.HIDDEN]: {
title: 'global.label.hidden',
icon: <HiddenProgressBarIcon />,
},
[ProgressBarType.STANDARD]: {
title: 'global.label.standard',
icon: <StandardProgressBarIcon />,
},
};
const PROGRESS_BAR_TYPE_VALUES = Object.values(ProgressBarType).filter((value) => typeof value === 'number');
export const ReaderSettingProgressBarType = ({
overlayMode,
progressBarType,
setProgressBarType,
}: Pick<IReaderSettings, 'progressBarType' | 'overlayMode'> & {
setProgressBarType: (progressBarType: ProgressBarType) => void;
}) => {
const { t } = useTranslation();
if (overlayMode !== ReaderOverlayMode.DESKTOP) {
return null;
}
return (
<ButtonSelectInput
label={t('reader.settings.progress_bar.style')}
value={progressBarType}
values={PROGRESS_BAR_TYPE_VALUES}
setValue={setProgressBarType}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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<typeof ReaderTapZoneContext>['setShowPreview'];
}) => (
<Stack sx={{ gap: 2 }}>
<ReaderSettingReadingMode
readingMode={settings.readingMode}
setReadingMode={(value) => updateSetting('readingMode', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('readingMode')}
/>
<ReaderSettingPageGap
pageGap={settings.pageGap}
readingMode={settings.readingMode}
updateSetting={(...args) => updateSetting('pageGap', ...args)}
/>
<ReaderSettingReadingDirection
readingDirection={settings.readingDirection}
setReadingDirection={(value) => updateSetting('readingDirection', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('readingDirection')}
/>
<ReaderSettingTapZoneLayout
tapZoneLayout={settings.tapZoneLayout}
setTapZoneLayout={(value) => {
setShowPreview(true);
updateSetting('tapZoneLayout', value);
}}
isDefaultable={isDefaultable}
onDefault={() => {
setShowPreview(true);
onDefault?.('tapZoneLayout');
}}
/>
<ReaderSettingTapZoneInvertMode
tapZoneInvertMode={settings.tapZoneInvertMode}
setTapZoneInvertMode={(value) => {
setShowPreview(true);
updateSetting('tapZoneInvertMode', value);
}}
isDefaultable={isDefaultable}
onDefault={() => {
setShowPreview(true);
onDefault?.('tapZoneInvertMode');
}}
/>
<ReaderSettingPageScaleMode
pageScaleMode={settings.pageScaleMode}
setPageScaleMode={(value) => updateSetting('pageScaleMode', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('pageScaleMode')}
/>
<ReaderSettingStretchPage
pageScaleMode={settings.pageScaleMode.value}
shouldStretchPage={settings.shouldStretchPage.value}
setShouldStretchPage={(value) => updateSetting('shouldStretchPage', value)}
/>
<ReaderSettingWidth
readerWidth={settings.readerWidth.value}
pageScaleMode={settings.pageScaleMode.value}
updateSetting={(...args) => updateSetting(...args)}
/>
</Stack>
);

View File

@@ -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<IReaderSettingsWithDefaultFlag, 'pageGap' | 'readingMode'> & {
updateSetting: (gap: number, commit: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<Stack>
{[ReadingMode.CONTINUOUS_HORIZONTAL, ReadingMode.CONTINUOUS_VERTICAL].includes(readingMode.value) && (
<SliderInput
label={t('reader.settings.label.page_gap')}
value={t('global.value', { value: pageGap.value, unit: t('global.unit.px') })}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.readerWidth.value,
value: pageGap.value,
step: 1,
min: 0,
max: 20,
onChange: (_, value) => {
updateSetting(value as number, false);
},
onChangeCommitted: (_, value) => {
updateSetting(value as number, true);
},
},
}}
/>
)}
</Stack>
);
};

View File

@@ -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<IReaderSettingsWithDefaultFlag, 'pageScaleMode'> &
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setPageScaleMode: (mode: IReaderSettings['pageScaleMode']) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
{...buttonSelectInputProps}
label={t('reader.settings.page_scale.title')}
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
values={READER_PAGE_SCALE_MODE_VALUES}
setValue={setPageScaleMode}
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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<ReadingDirection> = {
@@ -26,18 +26,21 @@ const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
export const ReaderBottomBarMobileReadingDirection = ({
export const ReaderSettingReadingDirection = ({
readingDirection,
setReadingDirection,
}: Pick<IReaderSettings, 'readingDirection'> & {
setReadingDirection: (readingDirection: ReadingDirection) => void;
}) => {
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setReadingDirection: (readingDirection: ReadingDirection) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
{...buttonSelectInputProps}
label={t('reader.settings.label.reading_direction')}
value={readingDirection}
value={readingDirection.isDefault ? undefined : readingDirection.value}
values={READING_DIRECTION_VALUES}
setValue={setReadingDirection}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}

View File

@@ -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 { 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 { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
export const ReaderSettingReadingMode = ({
readingMode,
setReadingMode,
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
setReadingMode: (mode: ReadingMode) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
{...buttonSelectInputProps}
label={t('reader.settings.label.reading_mode')}
value={readingMode.isDefault ? undefined : readingMode.value}
values={READING_MODE_VALUES}
setValue={setReadingMode}
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'> & {
setShouldStretchPage: (mode: IReaderSettings['shouldStretchPage']) => void;
}) => {
const { t } = useTranslation();
if (!READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode]) {
return null;
}
return (
<Box>
<Button
onClick={() => setShouldStretchPage(!shouldStretchPage)}
variant={shouldStretchPage ? 'contained' : 'outlined'}
>
{t('reader.settings.page_scale.stretch')}
</Button>
</Box>
);
};

View File

@@ -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> = {
[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, TapZoneInvertMode> = {
[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<IReaderSettingsWithDefaultFlag, 'tapZoneInvertMode'> &
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setTapZoneInvertMode: (invert: IReaderSettings['tapZoneInvertMode']) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
{...buttonSelectInputProps}
label={t('reader.settings.tap_zones.invert')}
value={tapZoneInvertMode.isDefault ? undefined : convertTapZoneInvertModeToOption(tapZoneInvertMode.value)}
values={TAP_ZONES_INVERT_OPTION_VALUES}
setValue={(value) => setTapZoneInvertMode(TAP_ZONES_INVERT_OPTION_TO_SETTING[value])}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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> = {
[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<IReaderSettingsWithDefaultFlag, 'tapZoneLayout'> &
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setTapZoneLayout: (layout: TapZoneLayouts) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
{...buttonSelectInputProps}
label={t('reader.settings.tap_zones.title')}
value={tapZoneLayout.isDefault ? undefined : tapZoneLayout.value}
values={READER_TAP_ZONE_LAYOUT_VALUES}
setValue={setTapZoneLayout}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -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<IReaderSettings, 'readerWidth' | 'pageScaleMode'> & {
updateSetting: (...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>) => void;
}) => {
const { t } = useTranslation();
if (!isReaderWidthEditable(pageScaleMode)) {
return null;
}
return (
<Stack>
<CheckboxInput
label={t('reader.settings.label.limit_reader_width')}
checked={readerWidth.enabled}
onChange={(_, checked) => updateSetting('readerWidth', { ...readerWidth, enabled: checked }, true)}
/>
{readerWidth.enabled && (
<SliderInput
label={t('reader.settings.label.reader_width')}
value={t('global.value', { value: readerWidth.value, unit: '%' })}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.readerWidth.value,
value: readerWidth.value,
step: 1,
min: 10,
max: 100,
onChange: (_, value) => {
updateSetting('readerWidth', { ...readerWidth, value: value as number }, false);
},
onChangeCommitted: (_, value) => {
updateSetting('readerWidth', { ...readerWidth, value: value as number }, true);
},
},
}}
/>
)}
</Stack>
);
};

View File

@@ -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,
};

View File

@@ -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, TooltipProps['placement']> = {
[ProgressBarPosition.BOTTOM]: 'top',
[ProgressBarPosition.LEFT]: 'right',
[ProgressBarPosition.RIGHT]: 'left',
};

View File

@@ -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, Direction> = {
[ReadingDirection.LTR]: 'ltr',
[ReadingDirection.RTL]: 'rtl',
};
export const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefined> = {
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, TooltipProps['placement']> = {
[ProgressBarPosition.BOTTOM]: 'top',
[ProgressBarPosition.LEFT]: 'right',
[ProgressBarPosition.RIGHT]: 'left',
};
export const READING_DIRECTION_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
[ReadingDirection.LTR]: {
title: 'reader.settings.reading_direction.ltr',
icon: <ArrowCircleRightIcon />,
},
[ReadingDirection.RTL]: {
title: 'reader.settings.reading_direction.rtl',
icon: <ArrowCircleLeftIcon />,
},
};
export const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
export const PAGE_SCALE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderPageScaleMode> = {
[ReaderPageScaleMode.WIDTH]: {
title: 'reader.settings.page_scale.width',
icon: <ExpandIcon sx={{ transform: 'rotate(90deg)' }} />,
},
[ReaderPageScaleMode.HEIGHT]: {
title: 'reader.settings.page_scale.height',
icon: <ExpandIcon />,
},
[ReaderPageScaleMode.SCREEN]: {
title: 'reader.settings.page_scale.screen',
icon: <ZoomOutMapIcon />,
},
[ReaderPageScaleMode.ORIGINAL]: {
title: 'reader.settings.page_scale.original',
icon: <CropOriginalIcon />,
},
};
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, boolean> = {
[ReaderPageScaleMode.WIDTH]: true,
[ReaderPageScaleMode.HEIGHT]: true,
[ReaderPageScaleMode.SCREEN]: true,
[ReaderPageScaleMode.ORIGINAL]: false,
};
export const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode> = {
[ReadingMode.SINGLE_PAGE]: {
title: 'reader.settings.reader_type.label.single_page',
icon: <SinglePageIcon />,
},
[ReadingMode.DOUBLE_PAGE]: {
title: 'reader.settings.reader_type.label.double_page',
icon: <DoublePageIcon />,
},
[ReadingMode.CONTINUOUS_VERTICAL]: {
title: 'reader.settings.reader_type.label.continuous_vertical',
icon: <ContinuousVerticalPageIcon />,
},
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
title: 'reader.settings.reader_type.label.continuous_horizontal',
icon: <ContinuousHorizontalPageIcon />,
},
};
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,
},
};

View File

@@ -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 = <Setting extends keyof IReaderSettings>(
key: Setting,
value: IReaderSettings[Setting],
commit?: boolean,
) => {
ReaderService.updateSetting({ id: -1 }, key, value, commit, true);
};
if (loading) {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={error.message}
retry={() => refetch().catch(defaultPromiseErrorHandler('DefaultReaderSettings::refetch'))}
/>
);
}
return (
<ReaderSettingsTabs
activeTab={activeTab}
setActiveTab={setActiveTab}
areDefaultSettings
settings={settings}
updateSetting={(setting, value, commit) => updateSetting(setting, value, commit)}
/>
);
};

View File

@@ -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<Direction, Direction> = {
ltr: 'rtl',
@@ -25,6 +46,8 @@ const DIRECTION_TO_READING_DIRECTION: Record<Direction, ReadingDirection> = {
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<Setting extends keyof IReaderSettings>(
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<Setting extends keyof IReaderSettings>(
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<Setting extends keyof IReaderSettings>(
manga: MangaIdInfo,
): (
...args: OmitFirst<Parameters<typeof this.updateSetting<Setting>>>
) => ReturnType<typeof this.updateSetting<Setting>> {
return useCallback((...args) => this.updateSetting<Setting>(manga, ...args), [manga]);
}
static useCreateDeleteSetting<Setting extends keyof IReaderSettings>(
manga: MangaIdInfo,
): (
...args: OmitFirst<Parameters<typeof this.deleteSetting<Setting>>>
) => ReturnType<typeof this.deleteSetting<Setting>> {
return useCallback((...args) => this.deleteSetting<Setting>(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})`);
}
}
}

View File

@@ -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<MetadataHolderType, 'global' | 'manga'>,
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<IReaderSettings>,
): Metadata<string, AllowedMetadataValueTypes> => ({
...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<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
defaultSettings: IReaderSettings,
@@ -41,27 +98,21 @@ const convertMetadataToSettings = (
tapZoneInvertMode:
jsonSaveParse<IReaderSettings['tapZoneInvertMode']>((metadata.tapZoneInvertMode as string) ?? '') ??
defaultSettings.tapZoneInvertMode,
customFilter:
jsonSaveParse<IReaderSettings['customFilter']>((metadata.customFilter as string) ?? '') ??
defaultSettings.customFilter,
readerWidth:
jsonSaveParse<IReaderSettings['readerWidth']>((metadata.readerWidth as string) ?? '') ??
defaultSettings.readerWidth,
});
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
type: 'global',
metadataHolder: MetadataHolder,
defaultSettings?: DefaultSettings,
useEffectFn?: typeof useEffect,
): DefaultSettings;
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
type: 'manga',
metadataHolder: MangaIdInfo & MetadataHolder,
defaultSettings?: DefaultSettings,
useEffectFn?: typeof useEffect,
): DefaultSettings;
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
const getReaderMetadata = (
type: Extract<MetadataHolderType, 'global' | 'manga'>,
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<typeof getMetadataFrom>[0],
metadataHolder as Parameters<typeof getMetadataFrom>[1],
@@ -70,7 +121,28 @@ function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IRead
useEffectFn,
),
defaultSettings as IReaderSettings,
) as DefaultSettings;
);
function getReaderSettingsWithDefaultValueFallback(
type: 'global',
metadataHolder: MetadataHolder,
defaultSettings?: IReaderSettings,
useEffectFn?: typeof useEffect,
): IReaderSettingsWithDefaultFlag;
function getReaderSettingsWithDefaultValueFallback(
type: 'manga',
metadataHolder: MangaIdInfo & MetadataHolder,
defaultSettings?: IReaderSettings,
useEffectFn?: typeof useEffect,
): IReaderSettingsWithDefaultFlag;
function getReaderSettingsWithDefaultValueFallback(
type: Extract<MetadataHolderType, 'global' | 'manga'>,
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<typeof requestManager.useGetGlobalMeta>;
} => {
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<typeof requestManager.useGetGlobalMeta>;
} => {
const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true });
const { data, loading } = request;
@@ -129,13 +228,19 @@ export const updateReaderSettings = async <Setting extends keyof IReaderSettings
manga: Pick<MangaType, 'id'> & GqlMetaHolder,
setting: Setting,
value: IReaderSettings[Setting],
): Promise<void[]> =>
requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]);
isGlobal: boolean = false,
): Promise<void[]> => {
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 =
<Settings extends keyof IReaderSettings>(
manga: Pick<MangaType, 'id'> & GqlMetaHolder,
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'),
): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) =>
(setting, value) =>
updateReaderSettings(manga, setting, value).catch(handleError);
(setting, value, isGlobal) =>
updateReaderSettings(manga, setting, value, isGlobal).catch(handleError);

View File

@@ -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<IReaderSettingsManga> {}
export interface ReaderStateChapters {
chapters: TChapterReader[];
currentChapter?: TChapterReader | null;
@@ -47,20 +154,25 @@ export interface ReaderStateChapters {
setReaderStateChapters: React.Dispatch<React.SetStateAction<Omit<ReaderStateChapters, 'setReaderStateChapters'>>>;
}
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<Parameters<typeof ReaderService.updateSetting>>
) => ReturnType<typeof ReaderService.updateSetting>;
}
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<Omit<ReaderSettingsTypeDefaultableProps, keyof ReaderSettingsTypeBaseProps>> &
ReaderSettingsTypeNonDefaultableProps);

View File

@@ -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;

View File

@@ -33,4 +33,4 @@ export interface ISearchSettings {
ignoreFilters: boolean;
}
export type ServerSettings = GetServerSettingsQuery['settings'];
export type ServerSettings = Omit<GetServerSettingsQuery['settings'], '__typename'>;