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", "stop": "Stop",
"submit": "Submit" "submit": "Submit"
}, },
"colors": {
"black": "Black",
"gray": "Gray",
"white": "White"
},
"date": { "date": {
"label": { "label": {
"day_one": "Day", "day_one": "Day",
@@ -389,22 +394,30 @@
"label": { "label": {
"advanced": "Advanced", "advanced": "Advanced",
"are_you_sure": "Are you sure?", "are_you_sure": "Are you sure?",
"auto": "Auto",
"both": "Both",
"bottom": "Bottom",
"browse": "Browse", "browse": "Browse",
"client": "Client", "client": "Client",
"close": "Close", "close": "Close",
"copied_clipboard": "Copied to clipboard", "copied_clipboard": "Copied to clipboard",
"default": "Default", "default": "Default",
"desktop": "Desktop",
"disabled": "Disabled", "disabled": "Disabled",
"discord": "Discord", "discord": "Discord",
"display": "Display", "display": "Display",
"filter": "Filter", "filter": "Filter",
"general": "General", "general": "General",
"github": "GitHub", "github": "GitHub",
"hidden": "Hidden",
"horizontal": "Horizontal",
"left": "Left",
"links": "Links", "links": "Links",
"load_in_progress": "Still loading required data…", "load_in_progress": "Still loading required data…",
"loading": "Loading…", "loading": "Loading…",
"logged_in": "Logged in", "logged_in": "Logged in",
"menu": "Menu", "menu": "Menu",
"mobile": "Mobile",
"never": "Never", "never": "Never",
"next": "Next", "next": "Next",
"none": "None", "none": "None",
@@ -413,12 +426,15 @@
"password": "Password", "password": "Password",
"placeholder": "Placeholder", "placeholder": "Placeholder",
"previous": "Previous", "previous": "Previous",
"right": "Right",
"share": "Share", "share": "Share",
"sort": "Sort", "sort": "Sort",
"standard": "Standard",
"started": "Started", "started": "Started",
"type": "Type", "type": "Type",
"unknown": "Unknown", "unknown": "Unknown",
"username": "Username" "username": "Username",
"vertical": "Vertical"
}, },
"language": { "language": {
"label": { "label": {
@@ -453,6 +469,9 @@
"value": "{{count}} $t(global.time.seconds.second, lowercase)" "value": "{{count}} $t(global.time.seconds.second, lowercase)"
} }
}, },
"unit": {
"px": "px"
},
"update": { "update": {
"label": { "label": {
"available": "Update available", "available": "Update available",
@@ -472,7 +491,8 @@
} }
} }
} }
} },
"value": "{{value}}{{unit}}"
}, },
"library": { "library": {
"action": { "action": {
@@ -779,16 +799,51 @@
} }
}, },
"settings": { "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": { "error": {
"label": { "label": {
"failed_to_save_default_settings": "Could not save the default reader settings to the server", "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" "failed_to_save_settings": "Could not save the reader settings to the server"
} }
}, },
"exit_mode": "Open page on exit",
"label": { "label": {
"behaviour": "Behaviour",
"fit_page_to_window": "Fit page to window", "fit_page_to_window": "Fit page to window",
"layout": "Layout",
"limit_reader_width": "Limit reader width",
"load_next_chapter": "Load next chapter at ending", "load_next_chapter": "Load next chapter at ending",
"offset_double_spread": "Offset double spreads", "offset_double_spread": "Offset double spreads",
"page_gap": "Page gap",
"reader_type": "Reader type", "reader_type": "Reader type",
"reader_width": "Reader width", "reader_width": "Reader width",
"reading_direction": "Reading direction", "reading_direction": "Reading direction",
@@ -798,13 +853,20 @@
"skip_dup_chapters": "Skip duplicate chapters", "skip_dup_chapters": "Skip duplicate chapters",
"static_navigation": "Static navigation" "static_navigation": "Static navigation"
}, },
"overlay_mode": "Overlay mode",
"page_scale": { "page_scale": {
"height": "Fit height", "height": "Fit height",
"original": "Original size", "original": "Original size",
"screen": "Fit screen", "screen": "Fit screen",
"stretch": "Stretch small pages", "stretch": "Stretch small pages",
"title": "Scale type",
"width": "Fit width" "width": "Fit width"
}, },
"progress_bar": {
"position": "Progress bar position",
"size": "Progress bar size",
"style": "Progress bar style"
},
"reader_type": { "reader_type": {
"label": { "label": {
"continuous_horizontal": "Continuous horizontal", "continuous_horizontal": "Continuous horizontal",
@@ -818,6 +880,14 @@
"ltr": "Left to right", "ltr": "Left to right",
"rtl": "Right to left" "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": { "title": {
"default_reader_settings": "Default Reader Settings", "default_reader_settings": "Default Reader Settings",
"reader_settings": "Reader Settings" "reader_settings": "Reader Settings"

View File

@@ -57,6 +57,10 @@ const { LibraryDuplicates } = loadable(
lazyLoadFallback, lazyLoadFallback,
); );
const { Appearance } = loadable(() => import('@/modules/settings/screens/Appearance.tsx'), 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') { if (process.env.NODE_ENV !== 'production') {
// Adds messages only in a dev environment // Adds messages only in a dev environment
@@ -114,7 +118,7 @@ const MainApp = () => {
<Route index element={<Settings />} /> <Route index element={<Settings />} />
<Route path="about" element={<About />} /> <Route path="about" element={<About />} />
<Route path="categories" element={<CategorySettings />} /> <Route path="categories" element={<CategorySettings />} />
<Route path="defaultReaderSettings" element={null} /> <Route path="defaultReaderSettings" element={<DefaultReaderSettings />} />
<Route path="librarySettings"> <Route path="librarySettings">
<Route index element={<LibrarySettings />} /> <Route index element={<LibrarySettings />} />
<Route path="duplicates" element={<LibraryDuplicates />} /> <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 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]>; [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U]; }[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'; } from '@apollo/client';
import { OperationVariables, Reference } from '@apollo/client/core'; import { OperationVariables, Reference } from '@apollo/client/core';
import { useEffect, useMemo, useRef, useState } from 'react'; 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 { IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
import { 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 { CHAPTER_META_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts';
import { MetadataMigrationSettings } from '@/modules/migration/Migration.types.ts'; import { MetadataMigrationSettings } from '@/modules/migration/Migration.types.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts';
enum GQLMethod { enum GQLMethod {
QUERY = 'QUERY', 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 - 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 // TODO - correctly update cache after all mutations instead of refetching queries
export class RequestManager { export class RequestManager {
@@ -1599,7 +1581,7 @@ export class RequestManager {
id: cache.identify({ __typename: 'SourceType', id: sourceId }), id: cache.identify({ __typename: 'SourceType', id: sourceId }),
fields: { fields: {
meta(existingMetas, { readField }) { meta(existingMetas, { readField }) {
return updateMetadata(key, existingMetas, readField, () => return updateMetadataList(key, existingMetas, readField, () =>
cache.writeFragment({ cache.writeFragment({
data: data!.setSourceMeta!.meta, data: data!.setSourceMeta!.meta,
fragment: SOURCE_META_FIELDS, fragment: SOURCE_META_FIELDS,
@@ -1637,7 +1619,7 @@ export class RequestManager {
}, },
}, },
update(cache) { update(cache) {
cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', id: sourceId, key }) }); cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', sourceId, key }) });
}, },
...options, ...options,
}, },
@@ -2123,7 +2105,7 @@ export class RequestManager {
id: cache.identify({ __typename: 'MangaType', id: mangaId }), id: cache.identify({ __typename: 'MangaType', id: mangaId }),
fields: { fields: {
meta(existingMetas, { readField }) { meta(existingMetas, { readField }) {
return updateMetadata(key, existingMetas, readField, () => return updateMetadataList(key, existingMetas, readField, () =>
cache.writeFragment({ cache.writeFragment({
data: data!.setMangaMeta!.meta, data: data!.setMangaMeta!.meta,
fragment: MANGA_META_FIELDS, fragment: MANGA_META_FIELDS,
@@ -2161,7 +2143,7 @@ export class RequestManager {
}, },
}, },
update(cache) { update(cache) {
cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', id: mangaId, key }) }); cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId, key }) });
}, },
...options, ...options,
}, },
@@ -2351,7 +2333,7 @@ export class RequestManager {
id: cache.identify({ __typename: 'ChapterType', id: chapterId }), id: cache.identify({ __typename: 'ChapterType', id: chapterId }),
fields: { fields: {
meta(existingMetas, { readField }) { meta(existingMetas, { readField }) {
return updateMetadata(key, existingMetas, readField, () => return updateMetadataList(key, existingMetas, readField, () =>
cache.writeFragment({ cache.writeFragment({
data: data!.setChapterMeta!.meta, data: data!.setChapterMeta!.meta,
fragment: CHAPTER_META_FIELDS, fragment: CHAPTER_META_FIELDS,
@@ -2389,7 +2371,7 @@ export class RequestManager {
}, },
}, },
update(cache) { update(cache) {
cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', id: chapterId, key }) }); cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', chapterId, key }) });
}, },
...options, ...options,
}, },
@@ -2630,7 +2612,7 @@ export class RequestManager {
id: cache.identify({ __typename: 'CategoryType', id: categoryId }), id: cache.identify({ __typename: 'CategoryType', id: categoryId }),
fields: { fields: {
meta(existingMetas, { readField }) { meta(existingMetas, { readField }) {
return updateMetadata(key, existingMetas, readField, () => return updateMetadataList(key, existingMetas, readField, () =>
cache.writeFragment({ cache.writeFragment({
data: data!.setCategoryMeta!.meta, data: data!.setCategoryMeta!.meta,
fragment: CATEGORY_META_FIELDS, fragment: CATEGORY_META_FIELDS,
@@ -2668,7 +2650,7 @@ export class RequestManager {
}, },
}, },
update(cache) { update(cache) {
cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', id: categoryId, key }) }); cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', categoryId, key }) });
}, },
...options, ...options,
}, },

View File

@@ -22,3 +22,20 @@ export type ValueToDisplayData<Value extends string | number> = Record<
icon: ReactNode; 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 Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button'; 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>({ export const ButtonSelect = <Value extends string | number>({
value, value,
values, values,
setValue, setValue,
valueToDisplayData, valueToDisplayData,
}: { isDefaultable,
value: Value; onDefault,
values: Value[]; }: MultiValueButtonProps<Value>) => {
setValue: (value: Value) => void;
valueToDisplayData: ValueToDisplayData<Value>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}> <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) => ( {values.map((displayValue) => (
<Button <Button
key={displayValue} key={displayValue}

View File

@@ -7,28 +7,58 @@
*/ */
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import Button from '@mui/material/Button'; 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>({ export const ValueRotationButton = <Value extends string | number>({
value, value,
values, values,
setValue, setValue,
valueToDisplayData, valueToDisplayData,
}: { isDefaultable,
value: Value; onDefault,
values: Value[]; defaultIcon,
setValue: (value: Value) => void; }: MultiValueButtonProps<Value> & { defaultIcon?: ReactNode }) => {
valueToDisplayData: ValueToDisplayData<Value>;
}) => {
const { t } = useTranslation(); 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 ( return (
<Button <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 }} sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }}
variant="contained" variant="contained"
startIcon={valueToDisplayData[value].icon} 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/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import Box, { BoxProps } from '@mui/material/Box';
import React from 'react'; import React from 'react';
interface IProps { interface IProps extends BoxProps {
children: React.ReactNode; children: React.ReactNode;
index: any; index: any;
currentIndex: any; currentIndex: any;
} }
export function TabPanel(props: IProps) { export function TabPanel(props: IProps) {
const { children, index, currentIndex } = props; const { children, index, currentIndex, ...boxProps } = props;
return ( 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} {currentIndex === index && children}
</div> </Box>
); );
} }

View File

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

View File

@@ -6,10 +6,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 = export const shouldForwardProp =
<TCustomProps extends Record<string, unknown>>(customProps: TupleUnion<keyof TCustomProps>) => <TCustomProps extends Record<string, unknown>>(customProps: TupleUnion<keyof TCustomProps>) =>
(prop: string): boolean => (prop: string): boolean =>

View File

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

View File

@@ -12,10 +12,6 @@ import { ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.
export const APP_METADATA_KEY_PREFIX = 'webUI_'; export const APP_METADATA_KEY_PREFIX = 'webUI_';
const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = { const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
staticNav: undefined,
showPageNumber: undefined,
loadNextOnEnding: undefined,
skipDupChapters: undefined,
migration: undefined, migration: undefined,
deleteChaptersManuallyMarkedRead: undefined, deleteChaptersManuallyMarkedRead: undefined,
deleteChaptersWhileReading: undefined, deleteChaptersWhileReading: undefined,
@@ -60,8 +56,16 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
progressBarPosition: undefined, progressBarPosition: undefined,
readingMode: undefined, readingMode: undefined,
pageScaleMode: undefined, pageScaleMode: undefined,
shouldScalePage: undefined,
shouldOffsetDoubleSpreads: 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); export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
@@ -122,6 +126,11 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
// themes // themes
'customThemes', 'customThemes',
'mangaThumbnailBackdrop', 'mangaThumbnailBackdrop',
// reader
'exitMode',
'customFilter',
'shouldSkipDupChapters',
]; ];
/** /**
@@ -203,7 +212,19 @@ export const METADATA_MIGRATIONS: IMetadataMigration[] = [
}, },
{ {
oldKey: 'scalePage', oldKey: 'scalePage',
newKey: 'shouldScalePage', newKey: 'shouldStretchPage',
},
{
oldKey: 'skipDupChapters',
newKey: 'shouldSkipDupChapters',
},
{
oldKey: 'showPageNumber',
newKey: 'shouldShowPageNumber',
},
{
oldKey: 'staticNav',
newKey: 'isStaticNav',
}, },
], ],
values: [ values: [

View File

@@ -31,14 +31,19 @@ export const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = A
return value as T; 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) { if (!gqlMetadata) {
return undefined; return undefined;
} }
const metadata: Metadata = {}; const metadata: Metadata = {};
gqlMetadata.forEach(({ key, value }) => { gqlMetadata.forEach(({ key, value }) => {
metadata[key] = value; if (filter(key)) {
metadata[key] = value;
}
}); });
return metadata; 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 { CategoryIdInfo } from '@/modules/category/Category.types.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.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) => { export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => {
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys); 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 { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.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 = () => { export const ReaderPageNumber = () => {
const isMobileWidth = MediaQuery.useIsMobileWidth(); const { isDesktop } = ReaderService.useOverlayMode();
const { scrollbarXSize } = useReaderScrollbarContext();
const { readerNavBarWidth } = useNavBarContext(); const { readerNavBarWidth } = useNavBarContext();
const { isMaximized } = useReaderProgressBarContext(); const { isMaximized } = useReaderProgressBarContext();
const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext(); const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext();
const { progressBarType, showPageNumber } = ReaderService.useSettings(); const { progressBarType, shouldShowPageNumber } = ReaderService.useSettings();
const pageName = useMemo(() => getPage(currentPageIndex, pages).name, [currentPageIndex, pages]); const pageName = useMemo(() => getPage(currentPageIndex, pages).name, [currentPageIndex, pages]);
if (!showPageNumber) { if (!shouldShowPageNumber) {
return null; return null;
} }
@@ -35,7 +36,7 @@ export const ReaderPageNumber = () => {
return null; return null;
} }
if (!isMobileWidth && progressBarType === ProgressBarType.STANDARD) { if (isDesktop && progressBarType === ProgressBarType.STANDARD) {
return null; return null;
} }
@@ -49,7 +50,7 @@ export const ReaderPageNumber = () => {
position: 'fixed', position: 'fixed',
left: readerNavBarWidth, left: readerNavBarWidth,
right: 0, right: 0,
bottom: (theme) => theme.spacing(1), bottom: (theme) => `calc(${theme.spacing(1)} + ${scrollbarXSize}px)`,
alignItems: 'center', alignItems: 'center',
transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`, transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`,
}} }}

View File

@@ -35,10 +35,10 @@ export const TapZoneLayout = () => {
}, [tapZoneLayoutElement]), }, [tapZoneLayoutElement]),
); );
const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout, width, height, theme.typography.h3, { const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout.value, width, height, theme.typography.h3, {
vertical: tapZoneInvertMode.vertical, vertical: tapZoneInvertMode.value.vertical,
horizontal: tapZoneInvertMode.horizontal, horizontal: tapZoneInvertMode.value.horizontal,
isRTL: readingDirection === ReadingDirection.RTL, isRTL: readingDirection.value === ReadingDirection.RTL,
}); });
useLayoutEffect(() => { useLayoutEffect(() => {

View File

@@ -7,36 +7,39 @@
*/ */
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { useState } from 'react';
import { BaseReaderOverlayProps, MobileHeaderProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; 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 { ReaderPageNumber } from '@/modules/reader/components/ReaderPageNumber.tsx';
import { StandardReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.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 { ReaderNavBarDesktop } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx';
import { ReaderOverlayHeaderMobile } from '@/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx'; import { ReaderOverlayHeaderMobile } from '@/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx';
import { ReaderBottomBarMobile } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.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) => { export const ReaderOverlay = ({ isVisible }: BaseReaderOverlayProps & MobileHeaderProps) => {
const isTouchDevice = MediaQuery.useIsTouchDevice(); const { isDesktop, isMobile } = ReaderService.useOverlayMode();
const isDesktop = !isTouchDevice; const [areSettingsOpen, setAreSettingsOpen] = useState(false);
const isMobile = isTouchDevice;
return ( return (
<Box sx={{ position: 'absolute', width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}> <Box sx={{ position: 'absolute', width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}>
{isDesktop && ( {isDesktop && (
<> <>
<StandardReaderProgressBar /> <StandardReaderProgressBar />
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => {}} /> <ReaderNavBarDesktop isVisible={isVisible} openSettings={() => setAreSettingsOpen(true)} />
</> </>
)} )}
{isMobile && ( {isMobile && (
<> <>
<ReaderOverlayHeaderMobile isVisible={isVisible} /> <ReaderOverlayHeaderMobile isVisible={isVisible} />
<ReaderBottomBarMobile openSettings={() => {}} isVisible={isVisible} /> <ReaderBottomBarMobile openSettings={() => setAreSettingsOpen(true)} isVisible={isVisible} />
</> </>
)} )}
<ReaderSettings isOpen={areSettingsOpen} close={() => setAreSettingsOpen(false)} />
<ReaderPageNumber /> <ReaderPageNumber />
</Box> </Box>
); );

View File

@@ -18,7 +18,6 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import Drawer from '@mui/material/Drawer'; import Drawer from '@mui/material/Drawer';
import { useGetOptionForDirection } from '@/theme.tsx'; import { useGetOptionForDirection } from '@/theme.tsx';
import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; 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 { ReaderNavContainer } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavContainer.tsx';
import { ReaderNavBarDesktopMetadata } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopMetadata.tsx'; import { ReaderNavBarDesktopMetadata } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopMetadata.tsx';
import { ReaderNavBarDesktopPageNavigation } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopPageNavigation.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 { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.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 { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
const useGetPreviousNavBarStaticValue = (isVisible: boolean, staticNav: boolean) => { const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
const wasNavBarStaticRef = useRef(staticNav); const wasNavBarStaticRef = useRef(isStaticNav);
const wasNavBarStaticPreviousRef = useRef(staticNav); const wasNavBarStaticPreviousRef = useRef(isStaticNav);
const resetWasNavBarStaticValue = wasNavBarStaticPreviousRef.current !== wasNavBarStaticRef.current && !isVisible; const resetWasNavBarStaticValue = wasNavBarStaticPreviousRef.current !== wasNavBarStaticRef.current && !isVisible;
if (resetWasNavBarStaticValue) { if (resetWasNavBarStaticValue) {
wasNavBarStaticRef.current = false; wasNavBarStaticRef.current = false;
} }
const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== staticNav; const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== isStaticNav;
if (didNavBarStaticValueChange) { if (didNavBarStaticValueChange) {
wasNavBarStaticRef.current = wasNavBarStaticPreviousRef.current; wasNavBarStaticRef.current = wasNavBarStaticPreviousRef.current;
wasNavBarStaticPreviousRef.current = staticNav; wasNavBarStaticPreviousRef.current = isStaticNav;
} }
return wasNavBarStaticRef.current; return wasNavBarStaticRef.current;
}; };
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDesktopProps) => { export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDesktopProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setReaderNavBarWidth } = useNavBarContext(); const { setReaderNavBarWidth } = useNavBarContext();
@@ -60,36 +59,33 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext(); const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
const { pages, currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext(); const { pages, currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext();
const handleBack = useBackButton();
const getOptionForDirection = useGetOptionForDirection(); const getOptionForDirection = useGetOptionForDirection();
const updateReaderSettings = createUpdateReaderSettings(manga ?? { id: -1 }, () => const exit = ReaderService.useExit();
makeToast(t('reader.settings.error.label.failed_to_save_settings')), const updateReaderSettings = ReaderService.useCreateUpdateSetting(manga ?? DEFAULT_MANGA);
);
const settings = ReaderService.useSettings(); const settings = ReaderService.useSettings();
const [navBarElement, setNavBarElement] = useState<HTMLDivElement | null>(); const [navBarElement, setNavBarElement] = useState<HTMLDivElement | null>();
useResizeObserver( useResizeObserver(
navBarElement, navBarElement,
useCallback(() => { useCallback(() => {
if (!settings?.staticNav) { if (!settings?.isStaticNav) {
return; return;
} }
setReaderNavBarWidth(navBarElement!.offsetWidth); setReaderNavBarWidth(navBarElement!.offsetWidth);
}, [navBarElement, settings?.staticNav]), }, [navBarElement, settings?.isStaticNav]),
); );
useLayoutEffect(() => () => setReaderNavBarWidth(0), []); useLayoutEffect(() => () => setReaderNavBarWidth(0), []);
const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, settings.staticNav); const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, settings.isStaticNav);
const changedNavBarStaticValue = wasNavBarStatic && isVisible; const changedNavBarStaticValue = wasNavBarStatic && isVisible;
const drawerTransitionDuration = changedNavBarStaticValue ? 0 : undefined; const drawerTransitionDuration = changedNavBarStaticValue ? 0 : undefined;
return ( return (
<Drawer <Drawer
variant={settings.staticNav ? 'permanent' : 'persistent'} variant={settings.isStaticNav ? 'permanent' : 'persistent'}
open={isVisible || settings.staticNav} open={isVisible || settings.isStaticNav}
transitionDuration={drawerTransitionDuration} transitionDuration={drawerTransitionDuration}
PaperProps={{ PaperProps={{
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref), 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={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Tooltip title={t('reader.button.exit')}> <Tooltip title={t('reader.button.exit')}>
<IconButton onClick={handleBack} color="inherit"> <IconButton onClick={exit} color="inherit">
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)} {getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -107,9 +103,9 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
<IconButton <IconButton
onClick={() => { onClick={() => {
setReaderNavBarWidth(0); setReaderNavBarWidth(0);
updateReaderSettings('staticNav', !settings.staticNav); updateReaderSettings('isStaticNav', !settings.isStaticNav);
}} }}
color={settings.staticNav ? 'primary' : 'inherit'} color={settings.isStaticNav ? 'primary' : 'inherit'}
> >
<PushPinIcon /> <PushPinIcon />
</IconButton> </IconButton>
@@ -147,6 +143,8 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
settings={settings} settings={settings}
updateSetting={updateReaderSettings} updateSetting={updateReaderSettings}
openSettings={openSettings} openSettings={openSettings}
isDefaultable
onDefault={(...args) => manga && ReaderService.deleteSetting(manga, ...args)}
/> />
</Stack> </Stack>
</ReaderNavContainer> </ReaderNavContainer>

View File

@@ -9,7 +9,8 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { OffsetDoubleSpreadIcon } from '@/assets/icons/svg/OffsetDoubleSpreadIcon.tsx'; 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 = ({ export const ReaderNavBarDesktopOffsetDoubleSpread = ({
readingMode, readingMode,
@@ -20,7 +21,7 @@ export const ReaderNavBarDesktopOffsetDoubleSpread = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (readingMode !== ReadingMode.DOUBLE_PAGE) { if (!isOffsetDoubleSpreadPagesEditable(readingMode)) {
return null; return null;
} }

View File

@@ -9,65 +9,52 @@
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { useTranslation } from 'react-i18next'; 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 FitScreenIcon from '@mui/icons-material/FitScreen';
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx'; import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx'; import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
import { IReaderSettings, ReaderPageScaleMode } from '@/modules/reader/types/Reader.types.ts'; import {
import { ValueToDisplayData } from '@/modules/core/Core.types'; IReaderSettings,
IReaderSettingsWithDefaultFlag,
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderPageScaleMode> = { ReaderPageScaleMode,
[ReaderPageScaleMode.WIDTH]: { } from '@/modules/reader/types/Reader.types.ts';
title: 'reader.settings.page_scale.width', import {
icon: <ExpandIcon sx={{ transform: 'rotate(90deg)' }} />, PAGE_SCALE_VALUE_TO_DISPLAY_DATA,
}, READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED,
[ReaderPageScaleMode.HEIGHT]: { READER_PAGE_SCALE_MODE_VALUES,
title: 'reader.settings.page_scale.height', } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
icon: <ExpandIcon />, import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
},
[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');
export const ReaderNavBarDesktopPageScale = ({ export const ReaderNavBarDesktopPageScale = ({
pageScaleMode, pageScaleMode,
shouldScalePage, shouldStretchPage,
updateSetting, updateSetting,
}: Pick<IReaderSettings, 'pageScaleMode' | 'shouldScalePage'> & { ...buttonSelectInputProps
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldScalePage'>>( }: Pick<IReaderSettingsWithDefaultFlag, 'pageScaleMode' | 'shouldStretchPage'> &
setting: Setting, Pick<MultiValueButtonDefaultableProps<ReaderPageScaleMode>, 'isDefaultable' | 'onDefault'> & {
value: IReaderSettings[Setting], updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'>>(
) => void; setting: Setting,
}) => { value: IReaderSettings[Setting],
) => void;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const isPageScalingPossible = pageScaleMode !== ReaderPageScaleMode.ORIGINAL;
return ( return (
<Stack sx={{ flexDirection: 'row', gap: 1 }}> <Stack sx={{ flexDirection: 'row', gap: 1 }}>
<ValueRotationButton <ValueRotationButton
value={pageScaleMode} {...buttonSelectInputProps}
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
values={READER_PAGE_SCALE_MODE_VALUES} values={READER_PAGE_SCALE_MODE_VALUES}
setValue={(value) => updateSetting('pageScaleMode', value)} 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')}> <Tooltip title={t('reader.settings.label.scale_page')}>
<CustomIconButton <CustomIconButton
onClick={() => updateSetting('shouldScalePage', !shouldScalePage)} onClick={() => updateSetting('shouldStretchPage', !shouldStretchPage.value)}
sx={{ minWidth: 0 }} sx={{ minWidth: 0 }}
variant="contained" variant="contained"
color={shouldScalePage ? 'secondary' : 'primary'} color={shouldStretchPage.value ? 'secondary' : 'primary'}
> >
<FitScreenIcon /> <FitScreenIcon />
</CustomIconButton> </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 { 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 { 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 { 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'; import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
export const ReaderNavBarDesktopQuickSettings = ({ export const ReaderNavBarDesktopQuickSettings = ({
settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldScalePage, readingDirection }, settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldStretchPage, readingDirection },
updateSetting, updateSetting,
openSettings, openSettings,
}: { isDefaultable,
settings: IReaderSettings; onDefault,
updateSetting: <Setting extends keyof IReaderSettings>(setting: Setting, value: IReaderSettings[Setting]) => void; }: ReaderSettingsTypeProps & Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
} & Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -32,19 +31,26 @@ export const ReaderNavBarDesktopQuickSettings = ({
<ReaderNavBarDesktopReadingMode <ReaderNavBarDesktopReadingMode
readingMode={readingMode} readingMode={readingMode}
setReadingMode={(value) => updateSetting('readingMode', value)} setReadingMode={(value) => updateSetting('readingMode', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('readingMode')}
/> />
<ReaderNavBarDesktopOffsetDoubleSpread <ReaderNavBarDesktopOffsetDoubleSpread
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads} readingMode={readingMode.value}
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads.value}
setShouldOffsetDoubleSpreads={(value) => updateSetting('shouldOffsetDoubleSpreads', value)} setShouldOffsetDoubleSpreads={(value) => updateSetting('shouldOffsetDoubleSpreads', value)}
/> />
<ReaderNavBarDesktopPageScale <ReaderNavBarDesktopPageScale
pageScaleMode={pageScaleMode} pageScaleMode={pageScaleMode}
shouldScalePage={shouldScalePage} shouldStretchPage={shouldStretchPage}
updateSetting={updateSetting} updateSetting={updateSetting}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('pageScaleMode')}
/> />
<ReaderNavBarDesktopReadingDirection <ReaderNavBarDesktopReadingDirection
readingDirection={readingDirection} readingDirection={readingDirection}
setReadingDirection={(value) => updateSetting('readingDirection', value)} setReadingDirection={(value) => updateSetting('readingDirection', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('readingDirection')}
/> />
<Button <Button
onClick={() => openSettings()} onClick={() => openSettings()}

View File

@@ -6,35 +6,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; import {
READING_DIRECTION_VALUES,
const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = { READING_DIRECTION_VALUE_TO_DISPLAY_DATA,
[ReadingDirection.LTR]: { } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
title: 'reader.settings.reading_direction.ltr', import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
icon: <ArrowCircleRightIcon />,
},
[ReadingDirection.RTL]: {
title: 'reader.settings.reading_direction.rtl',
icon: <ArrowCircleLeftIcon />,
},
};
const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
export const ReaderNavBarDesktopReadingDirection = ({ export const ReaderNavBarDesktopReadingDirection = ({
readingDirection, readingDirection,
setReadingDirection, setReadingDirection,
}: Pick<IReaderSettings, 'readingDirection'> & { ...buttonSelectInputProps
setReadingDirection: (readingDirection: ReadingDirection) => void; }: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
}) => ( Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setReadingDirection: (readingDirection: ReadingDirection) => void;
}) => (
<ValueRotationButton <ValueRotationButton
value={readingDirection} {...buttonSelectInputProps}
value={readingDirection.isDefault ? undefined : readingDirection.value}
values={READING_DIRECTION_VALUES} values={READING_DIRECTION_VALUES}
setValue={setReadingDirection} 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/. * 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 { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
import { ValueToDisplayData } from '@/modules/core/Core.types.tsx'; import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; import {
READING_MODE_VALUE_TO_DISPLAY_DATA,
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode> = { READING_MODE_VALUES,
[ReadingMode.SINGLE_PAGE]: { } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
title: 'reader.settings.reader_type.label.single_page', import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
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 ReaderNavBarDesktopReadingMode = ({ export const ReaderNavBarDesktopReadingMode = ({
readingMode, readingMode,
setReadingMode, setReadingMode,
}: Pick<IReaderSettings, 'readingMode'> & { ...buttonSelectInputProps
setReadingMode: (mode: ReadingMode) => void; }: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
}) => ( Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
setReadingMode: (mode: ReadingMode) => void;
}) => (
<ValueRotationButton <ValueRotationButton
value={readingMode} {...buttonSelectInputProps}
value={readingMode.isDefault ? undefined : readingMode.value}
values={READING_MODE_VALUES} values={READING_MODE_VALUES}
setValue={setReadingMode} 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 { ReaderBottomBarMobileProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { MobileReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx'; import { MobileReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx';
import { ReaderChapterList } from '@/modules/reader/components/overlay/navigation/ReaderChapterList.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 { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.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']), shouldForwardProp: shouldForwardProp<ReaderProgressBarContainerProps>(['progressBarPosition']),
})<ReaderProgressBarContainerProps>(({ theme, progressBarPosition }) => ({ })<ReaderProgressBarContainerProps>(({ theme, progressBarPosition }) => ({
position: 'fixed', position: 'fixed',
pointerEvents: 'all',
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, { ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, {
justifyContent: 'flex-end', justifyContent: 'flex-end',
bottom: 0, bottom: 0,

View File

@@ -11,8 +11,8 @@ import Tooltip from '@mui/material/Tooltip';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { CurrentPageSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { CurrentPageSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.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 { 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 = ({ export const ReaderProgressBarCurrentPageSlot = ({
pageName, pageName,

View File

@@ -10,7 +10,8 @@ import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { ReaderProgressBarSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; 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 = ({ export const ReaderProgressBarSlot = ({
pageName, 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 ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight'; import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { ValueToDisplayData } from '@/modules/core/Core.types.ts'; import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/modules/core/Core.types.ts';
import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx'; import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = { 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'); const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
export const ReaderBottomBarMobileReadingDirection = ({ export const ReaderSettingReadingDirection = ({
readingDirection, readingDirection,
setReadingDirection, setReadingDirection,
}: Pick<IReaderSettings, 'readingDirection'> & { ...buttonSelectInputProps
setReadingDirection: (readingDirection: ReadingDirection) => void; }: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
}) => { Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
setReadingDirection: (readingDirection: ReadingDirection) => void;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ButtonSelectInput <ButtonSelectInput
{...buttonSelectInputProps}
label={t('reader.settings.label.reading_direction')} label={t('reader.settings.label.reading_direction')}
value={readingDirection} value={readingDirection.isDefault ? undefined : readingDirection.value}
values={READING_DIRECTION_VALUES} values={READING_DIRECTION_VALUES}
setValue={setReadingDirection} setValue={setReadingDirection}
valueToDisplayData={VALUE_TO_DISPLAY_DATA} 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/. * 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 { 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> = { export const READING_DIRECTION_TO_THEME_DIRECTION: Record<ReadingDirection, Direction> = {
[ReadingDirection.LTR]: 'ltr', [ReadingDirection.LTR]: 'ltr',
[ReadingDirection.RTL]: 'rtl', [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/. * 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 { useNavigate } from 'react-router-dom';
import { Direction, useTheme } from '@mui/material/styles'; import { Direction, useTheme } from '@mui/material/styles';
import { t as translate } from 'i18next';
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
import { Chapters } from '@/modules/chapter/services/Chapters.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 { 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> = { const DIRECTION_TO_INVERTED: Record<Direction, Direction> = {
ltr: 'rtl', ltr: 'rtl',
@@ -25,6 +46,8 @@ const DIRECTION_TO_READING_DIRECTION: Record<Direction, ReadingDirection> = {
rtl: ReadingDirection.RTL, rtl: ReadingDirection.RTL,
}; };
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
export class ReaderService { export class ReaderService {
static useNavigateToChapter(chapter?: TChapterReader): () => void { static useNavigateToChapter(chapter?: TChapterReader): () => void {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -32,21 +55,170 @@ export class ReaderService {
return useCallback(() => chapter && navigate(Chapters.getReaderUrl(chapter), { replace: true }), [chapter]); return useCallback(() => chapter && navigate(Chapters.getReaderUrl(chapter), { replace: true }), [chapter]);
} }
static useSettings(): IReaderSettings { static useSettings(): IReaderSettingsWithDefaultFlag {
const { manga } = useReaderStateMangaContext(); const { manga } = useReaderStateMangaContext();
const defaultReaderSettings = useDefaultReaderSettings(); const defaultReaderSettings = useDefaultReaderSettings();
return useMemo(
() => getReaderSettingsFor(manga ?? { id: -1 }, defaultReaderSettings.settings), return useGetReaderSettingsFor(manga ?? DEFAULT_MANGA, defaultReaderSettings.settings);
[manga, defaultReaderSettings],
);
} }
static useGetThemeDirection(): Direction { static useGetThemeDirection(): Direction {
const { direction } = useTheme(); const { direction } = useTheme();
const { readingDirection } = ReaderService.useSettings(); const { readingDirection } = ReaderService.useSettings();
return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection.value
? direction ? direction
: DIRECTION_TO_INVERTED[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 { useEffect, useMemo } from 'react';
import { requestManager } from '@/lib/requests/RequestManager.ts'; 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 { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/Reader.constants.ts'; import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.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 { import {
AllowedMetadataValueTypes, AllowedMetadataValueTypes,
AppMetadataKeys, AppMetadataKeys,
@@ -25,14 +27,69 @@ import {
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { jsonSaveParse } from '@/lib/HelperFunctions.ts'; import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.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 = ( const convertSettingsToMetadata = (
settings: Partial<IReaderSettings>, settings: Partial<IReaderSettings>,
): Metadata<string, AllowedMetadataValueTypes> => ({ ): Metadata<string, AllowedMetadataValueTypes> => ({
...settings, ...settings,
tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode), 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 = ( const convertMetadataToSettings = (
metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>, metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
@@ -41,27 +98,21 @@ const convertMetadataToSettings = (
tapZoneInvertMode: tapZoneInvertMode:
jsonSaveParse<IReaderSettings['tapZoneInvertMode']>((metadata.tapZoneInvertMode as string) ?? '') ?? jsonSaveParse<IReaderSettings['tapZoneInvertMode']>((metadata.tapZoneInvertMode as string) ?? '') ??
defaultSettings.tapZoneInvertMode, 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>( const getReaderMetadata = (
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>(
type: Extract<MetadataHolderType, 'global' | 'manga'>, type: Extract<MetadataHolderType, 'global' | 'manga'>,
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
defaultSettings: DefaultSettings = DEFAULT_READER_SETTINGS as DefaultSettings, defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): DefaultSettings { ) =>
return convertMetadataToSettings( convertMetadataToSettings(
getMetadataFrom( getMetadataFrom(
type as Parameters<typeof getMetadataFrom>[0], type as Parameters<typeof getMetadataFrom>[0],
metadataHolder as Parameters<typeof getMetadataFrom>[1], metadataHolder as Parameters<typeof getMetadataFrom>[1],
@@ -70,7 +121,28 @@ function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IRead
useEffectFn, useEffectFn,
), ),
defaultSettings as IReaderSettings, 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 = ( const getSettings = (
@@ -82,7 +154,10 @@ const getSettings = (
'manga', 'manga',
{ {
...metaHolder, ...metaHolder,
meta: convertFromGqlMeta(metaHolder.meta), meta: convertFromGqlMeta(
metaHolder.meta,
(key) => !GLOBAL_READER_SETTING_KEYS.includes(extractOriginalKey(key)),
),
}, },
defaultSettings, defaultSettings,
useEffectFn, useEffectFn,
@@ -91,12 +166,12 @@ const getSettings = (
export const getReaderSettingsFor = ( export const getReaderSettingsFor = (
metaHolder: MangaIdInfo & GqlMetaHolder, metaHolder: MangaIdInfo & GqlMetaHolder,
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
): IReaderSettings => getSettings(metaHolder, defaultSettings); ): IReaderSettingsWithDefaultFlag => getSettings(metaHolder, defaultSettings);
export const useGetReaderSettingsFor = ( export const useGetReaderSettingsFor = (
metaHolder: MangaIdInfo & GqlMetaHolder, metaHolder: MangaIdInfo & GqlMetaHolder,
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
): IReaderSettings => { ): IReaderSettingsWithDefaultFlag => {
const settings = getSettings(metaHolder, defaultSettings, useEffect); const settings = getSettings(metaHolder, defaultSettings, useEffect);
return useMemo(() => settings, [metaHolder, defaultSettings]); return useMemo(() => settings, [metaHolder, defaultSettings]);
}; };
@@ -106,6 +181,30 @@ export const useDefaultReaderSettings = (): {
settings: IReaderSettings; settings: IReaderSettings;
loading: boolean; loading: boolean;
request: ReturnType<typeof requestManager.useGetGlobalMeta>; 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 request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true });
const { data, loading } = request; const { data, loading } = request;
@@ -129,13 +228,19 @@ export const updateReaderSettings = async <Setting extends keyof IReaderSettings
manga: Pick<MangaType, 'id'> & GqlMetaHolder, manga: Pick<MangaType, 'id'> & GqlMetaHolder,
setting: Setting, setting: Setting,
value: IReaderSettings[Setting], value: IReaderSettings[Setting],
): Promise<void[]> => isGlobal: boolean = false,
requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); ): 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 = export const createUpdateReaderSettings =
<Settings extends keyof IReaderSettings>( <Settings extends keyof IReaderSettings>(
manga: Pick<MangaType, 'id'> & GqlMetaHolder, manga: Pick<MangaType, 'id'> & GqlMetaHolder,
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'), handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'),
): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) => ): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) =>
(setting, value) => (setting, value, isGlobal) =>
updateReaderSettings(manga, setting, value).catch(handleError); updateReaderSettings(manga, setting, value, isGlobal).catch(handleError);

View File

@@ -8,6 +8,7 @@
import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
export enum ProgressBarType { export enum ProgressBarType {
HIDDEN, HIDDEN,
@@ -39,6 +40,112 @@ export enum ReaderPageScaleMode {
ORIGINAL, 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 { export interface ReaderStateChapters {
chapters: TChapterReader[]; chapters: TChapterReader[];
currentChapter?: TChapterReader | null; currentChapter?: TChapterReader | null;
@@ -47,20 +154,25 @@ export interface ReaderStateChapters {
setReaderStateChapters: React.Dispatch<React.SetStateAction<Omit<ReaderStateChapters, 'setReaderStateChapters'>>>; setReaderStateChapters: React.Dispatch<React.SetStateAction<Omit<ReaderStateChapters, 'setReaderStateChapters'>>>;
} }
export interface IReaderSettings { interface ReaderSettingsTypeBaseProps {
staticNav: boolean; settings: IReaderSettingsWithDefaultFlag;
showPageNumber: boolean; updateSetting: (
loadNextOnEnding: boolean; ...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>
skipDupChapters: boolean; ) => ReturnType<typeof ReaderService.updateSetting>;
readerWidth: number;
tapZoneLayout: TapZoneLayouts;
tapZoneInvertMode: TapZoneInvertMode;
progressBarType: ProgressBarType;
progressBarSize: number;
progressBarPosition: ProgressBarPosition;
pageScaleMode: ReaderPageScaleMode;
shouldScalePage: boolean;
shouldOffsetDoubleSpreads: boolean;
readingDirection: ReadingDirection;
readingMode: ReadingMode;
} }
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; ignoreFilters: boolean;
} }
export type ServerSettings = GetServerSettingsQuery['settings']; export type ServerSettings = Omit<GetServerSettingsQuery['settings'], '__typename'>;