Reader settings
This commit is contained in:
@@ -344,6 +344,11 @@
|
||||
"stop": "Stop",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"colors": {
|
||||
"black": "Black",
|
||||
"gray": "Gray",
|
||||
"white": "White"
|
||||
},
|
||||
"date": {
|
||||
"label": {
|
||||
"day_one": "Day",
|
||||
@@ -389,22 +394,30 @@
|
||||
"label": {
|
||||
"advanced": "Advanced",
|
||||
"are_you_sure": "Are you sure?",
|
||||
"auto": "Auto",
|
||||
"both": "Both",
|
||||
"bottom": "Bottom",
|
||||
"browse": "Browse",
|
||||
"client": "Client",
|
||||
"close": "Close",
|
||||
"copied_clipboard": "Copied to clipboard",
|
||||
"default": "Default",
|
||||
"desktop": "Desktop",
|
||||
"disabled": "Disabled",
|
||||
"discord": "Discord",
|
||||
"display": "Display",
|
||||
"filter": "Filter",
|
||||
"general": "General",
|
||||
"github": "GitHub",
|
||||
"hidden": "Hidden",
|
||||
"horizontal": "Horizontal",
|
||||
"left": "Left",
|
||||
"links": "Links",
|
||||
"load_in_progress": "Still loading required data…",
|
||||
"loading": "Loading…",
|
||||
"logged_in": "Logged in",
|
||||
"menu": "Menu",
|
||||
"mobile": "Mobile",
|
||||
"never": "Never",
|
||||
"next": "Next",
|
||||
"none": "None",
|
||||
@@ -413,12 +426,15 @@
|
||||
"password": "Password",
|
||||
"placeholder": "Placeholder",
|
||||
"previous": "Previous",
|
||||
"right": "Right",
|
||||
"share": "Share",
|
||||
"sort": "Sort",
|
||||
"standard": "Standard",
|
||||
"started": "Started",
|
||||
"type": "Type",
|
||||
"unknown": "Unknown",
|
||||
"username": "Username"
|
||||
"username": "Username",
|
||||
"vertical": "Vertical"
|
||||
},
|
||||
"language": {
|
||||
"label": {
|
||||
@@ -453,6 +469,9 @@
|
||||
"value": "{{count}} $t(global.time.seconds.second, lowercase)"
|
||||
}
|
||||
},
|
||||
"unit": {
|
||||
"px": "px"
|
||||
},
|
||||
"update": {
|
||||
"label": {
|
||||
"available": "Update available",
|
||||
@@ -472,7 +491,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": "{{value}}{{unit}}"
|
||||
},
|
||||
"library": {
|
||||
"action": {
|
||||
@@ -779,16 +799,51 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"background_color": "Background color",
|
||||
"custom_filter": {
|
||||
"brightness": {
|
||||
"title": "Custom brightness",
|
||||
"value": "$t(reader.settings.custom_filter.brightness.title)"
|
||||
},
|
||||
"contrast": {
|
||||
"title": "Custom contrast",
|
||||
"value": "$t(reader.settings.custom_filter.contrast.title)"
|
||||
},
|
||||
"grayscale": "Grayscale",
|
||||
"hue": {
|
||||
"title": "Custom hue",
|
||||
"value": "$t(reader.settings.custom_filter.hue.title)"
|
||||
},
|
||||
"invert": "Invert",
|
||||
"rgba": {
|
||||
"alpha": "Alpha",
|
||||
"blue": "Blue",
|
||||
"green": "Green",
|
||||
"red": "Red",
|
||||
"title": "Custom color filters"
|
||||
},
|
||||
"saturate": {
|
||||
"title": "Custom saturate",
|
||||
"value": "$t(reader.settings.custom_filter.saturate.title)"
|
||||
},
|
||||
"sepia": "Sepia",
|
||||
"title": "Custom filter"
|
||||
},
|
||||
"error": {
|
||||
"label": {
|
||||
"failed_to_save_default_settings": "Could not save the default reader settings to the server",
|
||||
"failed_to_save_settings": "Could not save the reader settings to the server"
|
||||
}
|
||||
},
|
||||
"exit_mode": "Open page on exit",
|
||||
"label": {
|
||||
"behaviour": "Behaviour",
|
||||
"fit_page_to_window": "Fit page to window",
|
||||
"layout": "Layout",
|
||||
"limit_reader_width": "Limit reader width",
|
||||
"load_next_chapter": "Load next chapter at ending",
|
||||
"offset_double_spread": "Offset double spreads",
|
||||
"page_gap": "Page gap",
|
||||
"reader_type": "Reader type",
|
||||
"reader_width": "Reader width",
|
||||
"reading_direction": "Reading direction",
|
||||
@@ -798,13 +853,20 @@
|
||||
"skip_dup_chapters": "Skip duplicate chapters",
|
||||
"static_navigation": "Static navigation"
|
||||
},
|
||||
"overlay_mode": "Overlay mode",
|
||||
"page_scale": {
|
||||
"height": "Fit height",
|
||||
"original": "Original size",
|
||||
"screen": "Fit screen",
|
||||
"stretch": "Stretch small pages",
|
||||
"title": "Scale type",
|
||||
"width": "Fit width"
|
||||
},
|
||||
"progress_bar": {
|
||||
"position": "Progress bar position",
|
||||
"size": "Progress bar size",
|
||||
"style": "Progress bar style"
|
||||
},
|
||||
"reader_type": {
|
||||
"label": {
|
||||
"continuous_horizontal": "Continuous horizontal",
|
||||
@@ -818,6 +880,14 @@
|
||||
"ltr": "Left to right",
|
||||
"rtl": "Right to left"
|
||||
},
|
||||
"tap_zones": {
|
||||
"edge": "Edge",
|
||||
"invert": "Invert tap zones",
|
||||
"kindle": "Kindle",
|
||||
"l_shape": "L-Shape",
|
||||
"right_left": "Right and left",
|
||||
"title": "Tap zones"
|
||||
},
|
||||
"title": {
|
||||
"default_reader_settings": "Default Reader Settings",
|
||||
"reader_settings": "Reader Settings"
|
||||
|
||||
@@ -57,6 +57,10 @@ const { LibraryDuplicates } = loadable(
|
||||
lazyLoadFallback,
|
||||
);
|
||||
const { Appearance } = loadable(() => import('@/modules/settings/screens/Appearance.tsx'), lazyLoadFallback);
|
||||
const { DefaultReaderSettings } = loadable(
|
||||
() => import('@/modules/reader/screens/DefaultReaderSettings.tsx'),
|
||||
lazyLoadFallback,
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Adds messages only in a dev environment
|
||||
@@ -114,7 +118,7 @@ const MainApp = () => {
|
||||
<Route index element={<Settings />} />
|
||||
<Route path="about" element={<About />} />
|
||||
<Route path="categories" element={<CategorySettings />} />
|
||||
<Route path="defaultReaderSettings" element={null} />
|
||||
<Route path="defaultReaderSettings" element={<DefaultReaderSettings />} />
|
||||
<Route path="librarySettings">
|
||||
<Route index element={<LibrarySettings />} />
|
||||
<Route path="duplicates" element={<LibraryDuplicates />} />
|
||||
|
||||
6
src/UtilTypes.d.ts
vendored
6
src/UtilTypes.d.ts
vendored
@@ -26,6 +26,10 @@ type PropertiesNever<T> = { [key in keyof T]?: never };
|
||||
|
||||
type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
|
||||
|
||||
type TupleUnion<U extends string, R extends any[] = []> = {
|
||||
type TupleUnion<U extends string | number | symbol, R extends any[] = []> = {
|
||||
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
|
||||
}[U];
|
||||
|
||||
type TransformRecordToWithDefaultFlag<T extends Record<string, any>> = {
|
||||
[K in keyof T]: { value: T[K]; isDefault: boolean };
|
||||
};
|
||||
|
||||
24
src/assets/icons/svg/HiddenProgressBarIcon.tsx
Normal file
24
src/assets/icons/svg/HiddenProgressBarIcon.tsx
Normal 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>
|
||||
);
|
||||
24
src/assets/icons/svg/StandardProgressBarIcon.tsx
Normal file
24
src/assets/icons/svg/StandardProgressBarIcon.tsx
Normal 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>
|
||||
);
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
} from '@apollo/client';
|
||||
import { OperationVariables, Reference } from '@apollo/client/core';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ReadFieldFunction } from '@apollo/client/cache/core/types/common';
|
||||
import { IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
|
||||
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
|
||||
import {
|
||||
@@ -327,6 +326,7 @@ import { SOURCE_META_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts';
|
||||
import { CHAPTER_META_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts';
|
||||
import { MetadataMigrationSettings } from '@/modules/migration/Migration.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts';
|
||||
|
||||
enum GQLMethod {
|
||||
QUERY = 'QUERY',
|
||||
@@ -427,24 +427,6 @@ export const SPECIAL_ED_SOURCES = {
|
||||
],
|
||||
};
|
||||
|
||||
const updateMetadata = (
|
||||
key: string,
|
||||
existingMetas: Reference[] | undefined,
|
||||
readField: ReadFieldFunction,
|
||||
createMetaRef: () => Reference | undefined,
|
||||
): (Reference | undefined)[] | undefined => {
|
||||
if (!existingMetas) {
|
||||
return existingMetas;
|
||||
}
|
||||
|
||||
const exists = existingMetas.some((metaRef: Reference) => readField('key', metaRef) === key);
|
||||
if (exists) {
|
||||
return existingMetas;
|
||||
}
|
||||
|
||||
return [...existingMetas, createMetaRef()];
|
||||
};
|
||||
|
||||
// TODO - extract logic to reduce the size of this file... grew waaaaaaaaaaaaay too big peepoFat
|
||||
// TODO - correctly update cache after all mutations instead of refetching queries
|
||||
export class RequestManager {
|
||||
@@ -1599,7 +1581,7 @@ export class RequestManager {
|
||||
id: cache.identify({ __typename: 'SourceType', id: sourceId }),
|
||||
fields: {
|
||||
meta(existingMetas, { readField }) {
|
||||
return updateMetadata(key, existingMetas, readField, () =>
|
||||
return updateMetadataList(key, existingMetas, readField, () =>
|
||||
cache.writeFragment({
|
||||
data: data!.setSourceMeta!.meta,
|
||||
fragment: SOURCE_META_FIELDS,
|
||||
@@ -1637,7 +1619,7 @@ export class RequestManager {
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', id: sourceId, key }) });
|
||||
cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', sourceId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -2123,7 +2105,7 @@ export class RequestManager {
|
||||
id: cache.identify({ __typename: 'MangaType', id: mangaId }),
|
||||
fields: {
|
||||
meta(existingMetas, { readField }) {
|
||||
return updateMetadata(key, existingMetas, readField, () =>
|
||||
return updateMetadataList(key, existingMetas, readField, () =>
|
||||
cache.writeFragment({
|
||||
data: data!.setMangaMeta!.meta,
|
||||
fragment: MANGA_META_FIELDS,
|
||||
@@ -2161,7 +2143,7 @@ export class RequestManager {
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', id: mangaId, key }) });
|
||||
cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -2351,7 +2333,7 @@ export class RequestManager {
|
||||
id: cache.identify({ __typename: 'ChapterType', id: chapterId }),
|
||||
fields: {
|
||||
meta(existingMetas, { readField }) {
|
||||
return updateMetadata(key, existingMetas, readField, () =>
|
||||
return updateMetadataList(key, existingMetas, readField, () =>
|
||||
cache.writeFragment({
|
||||
data: data!.setChapterMeta!.meta,
|
||||
fragment: CHAPTER_META_FIELDS,
|
||||
@@ -2389,7 +2371,7 @@ export class RequestManager {
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', id: chapterId, key }) });
|
||||
cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', chapterId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -2630,7 +2612,7 @@ export class RequestManager {
|
||||
id: cache.identify({ __typename: 'CategoryType', id: categoryId }),
|
||||
fields: {
|
||||
meta(existingMetas, { readField }) {
|
||||
return updateMetadata(key, existingMetas, readField, () =>
|
||||
return updateMetadataList(key, existingMetas, readField, () =>
|
||||
cache.writeFragment({
|
||||
data: data!.setCategoryMeta!.meta,
|
||||
fragment: CATEGORY_META_FIELDS,
|
||||
@@ -2668,7 +2650,7 @@ export class RequestManager {
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', id: categoryId, key }) });
|
||||
cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', categoryId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
|
||||
@@ -22,3 +22,20 @@ export type ValueToDisplayData<Value extends string | number> = Record<
|
||||
icon: ReactNode;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface MultiValueButtonBaseProps<Value extends string | number> {
|
||||
value: Value;
|
||||
values: Value[];
|
||||
setValue: (value: Value) => void;
|
||||
valueToDisplayData: ValueToDisplayData<Value>;
|
||||
}
|
||||
|
||||
export interface MultiValueButtonDefaultableProps<Value extends string | number>
|
||||
extends OptionalProperty<MultiValueButtonBaseProps<Value>, 'value'> {
|
||||
isDefaultable?: boolean;
|
||||
onDefault?: () => void;
|
||||
}
|
||||
|
||||
export type MultiValueButtonProps<Value extends string | number> =
|
||||
| (MultiValueButtonBaseProps<Value> & PropertiesNever<MultiValueButtonDefaultableProps<Value>>)
|
||||
| MultiValueButtonDefaultableProps<Value>;
|
||||
|
||||
@@ -9,23 +9,25 @@
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { MultiValueButtonProps } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const ButtonSelect = <Value extends string | number>({
|
||||
value,
|
||||
values,
|
||||
setValue,
|
||||
valueToDisplayData,
|
||||
}: {
|
||||
value: Value;
|
||||
values: Value[];
|
||||
setValue: (value: Value) => void;
|
||||
valueToDisplayData: ValueToDisplayData<Value>;
|
||||
}) => {
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
}: MultiValueButtonProps<Value>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
|
||||
{isDefaultable && (
|
||||
<Button key="default" onClick={onDefault} variant={value === undefined ? 'contained' : 'outlined'}>
|
||||
{t('global.label.default')}
|
||||
</Button>
|
||||
)}
|
||||
{values.map((displayValue) => (
|
||||
<Button
|
||||
key={displayValue}
|
||||
|
||||
@@ -7,28 +7,58 @@
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { MultiValueButtonProps } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const ValueRotationButton = <Value extends string | number>({
|
||||
value,
|
||||
values,
|
||||
setValue,
|
||||
valueToDisplayData,
|
||||
}: {
|
||||
value: Value;
|
||||
values: Value[];
|
||||
setValue: (value: Value) => void;
|
||||
valueToDisplayData: ValueToDisplayData<Value>;
|
||||
}) => {
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
defaultIcon,
|
||||
}: MultiValueButtonProps<Value> & { defaultIcon?: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const indexOfValue = useMemo(() => values.indexOf(value), [value, values]);
|
||||
const isDefault = value === undefined;
|
||||
const indexOfValue = useMemo(() => {
|
||||
if (isDefault) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return values.indexOf(value);
|
||||
}, [value, values]);
|
||||
|
||||
if (isDefault) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => setValue(values[0])}
|
||||
sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }}
|
||||
variant="contained"
|
||||
startIcon={defaultIcon}
|
||||
size="large"
|
||||
>
|
||||
{t('global.label.default')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => setValue(values[(indexOfValue + 1) % values.length])}
|
||||
onClick={() => {
|
||||
const nextValueIndex = (indexOfValue + 1) % values.length;
|
||||
const wasLastValue = nextValueIndex === 0;
|
||||
|
||||
const isDefaultNextValue = isDefaultable && wasLastValue;
|
||||
if (isDefaultNextValue) {
|
||||
onDefault?.();
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(values[(indexOfValue + 1) % values.length]);
|
||||
}}
|
||||
sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }}
|
||||
variant="contained"
|
||||
startIcon={valueToDisplayData[value].icon}
|
||||
|
||||
37
src/modules/core/components/inputs/SliderInput.tsx
Normal file
37
src/modules/core/components/inputs/SliderInput.tsx
Normal 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>
|
||||
);
|
||||
@@ -6,20 +6,21 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Box, { BoxProps } from '@mui/material/Box';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends BoxProps {
|
||||
children: React.ReactNode;
|
||||
index: any;
|
||||
currentIndex: any;
|
||||
}
|
||||
|
||||
export function TabPanel(props: IProps) {
|
||||
const { children, index, currentIndex } = props;
|
||||
const { children, index, currentIndex, ...boxProps } = props;
|
||||
|
||||
return (
|
||||
<div role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
|
||||
<Box {...boxProps} role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
|
||||
{currentIndex === index && children}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const TabsMenu = forwardRef(
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
type TupleUnion<U extends string | number | symbol, R extends any[] = []> = {
|
||||
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
|
||||
}[U];
|
||||
|
||||
export const shouldForwardProp =
|
||||
<TCustomProps extends Record<string, unknown>>(customProps: TupleUnion<keyof TCustomProps>) =>
|
||||
(prop: string): boolean =>
|
||||
|
||||
@@ -272,7 +272,7 @@ export function Library() {
|
||||
<TabsMenu value={activeTab.order} onChange={(e, newTab) => handleTabChange(newTab)}>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
sx={{ display: 'flex' }}
|
||||
sx={{ flexGrow: 1, maxWidth: 'unset' }}
|
||||
key={tab.id}
|
||||
label={
|
||||
<TitleWithSizeTag>
|
||||
|
||||
@@ -12,10 +12,6 @@ import { ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.
|
||||
export const APP_METADATA_KEY_PREFIX = 'webUI_';
|
||||
|
||||
const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
staticNav: undefined,
|
||||
showPageNumber: undefined,
|
||||
loadNextOnEnding: undefined,
|
||||
skipDupChapters: undefined,
|
||||
migration: undefined,
|
||||
deleteChaptersManuallyMarkedRead: undefined,
|
||||
deleteChaptersWhileReading: undefined,
|
||||
@@ -60,8 +56,16 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
progressBarPosition: undefined,
|
||||
readingMode: undefined,
|
||||
pageScaleMode: undefined,
|
||||
shouldScalePage: undefined,
|
||||
shouldOffsetDoubleSpreads: undefined,
|
||||
exitMode: undefined,
|
||||
customFilter: undefined,
|
||||
shouldSkipDupChapters: undefined,
|
||||
isStaticNav: undefined,
|
||||
overlayMode: undefined,
|
||||
shouldStretchPage: undefined,
|
||||
shouldShowPageNumber: undefined,
|
||||
backgroundColor: undefined,
|
||||
pageGap: undefined,
|
||||
};
|
||||
|
||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
||||
@@ -122,6 +126,11 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
||||
// themes
|
||||
'customThemes',
|
||||
'mangaThumbnailBackdrop',
|
||||
|
||||
// reader
|
||||
'exitMode',
|
||||
'customFilter',
|
||||
'shouldSkipDupChapters',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -203,7 +212,19 @@ export const METADATA_MIGRATIONS: IMetadataMigration[] = [
|
||||
},
|
||||
{
|
||||
oldKey: 'scalePage',
|
||||
newKey: 'shouldScalePage',
|
||||
newKey: 'shouldStretchPage',
|
||||
},
|
||||
{
|
||||
oldKey: 'skipDupChapters',
|
||||
newKey: 'shouldSkipDupChapters',
|
||||
},
|
||||
{
|
||||
oldKey: 'showPageNumber',
|
||||
newKey: 'shouldShowPageNumber',
|
||||
},
|
||||
{
|
||||
oldKey: 'staticNav',
|
||||
newKey: 'isStaticNav',
|
||||
},
|
||||
],
|
||||
values: [
|
||||
|
||||
@@ -31,14 +31,19 @@ export const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = A
|
||||
return value as T;
|
||||
};
|
||||
|
||||
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
|
||||
export const convertFromGqlMeta = (
|
||||
gqlMetadata?: MetaType[],
|
||||
filter: (key: string) => boolean = () => true,
|
||||
): Metadata | undefined => {
|
||||
if (!gqlMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const metadata: Metadata = {};
|
||||
gqlMetadata.forEach(({ key, value }) => {
|
||||
if (filter(key)) {
|
||||
metadata[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return metadata;
|
||||
|
||||
@@ -42,7 +42,7 @@ import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
||||
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export const extractOriginalKey = (key: string): string => key.split('_').slice(-1)[0];
|
||||
export const extractOriginalKey = (key: string) => key.split('_').slice(-1)[0];
|
||||
|
||||
export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => {
|
||||
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys);
|
||||
|
||||
@@ -16,18 +16,19 @@ import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/Rea
|
||||
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
|
||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
||||
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
|
||||
export const ReaderPageNumber = () => {
|
||||
const isMobileWidth = MediaQuery.useIsMobileWidth();
|
||||
const { isDesktop } = ReaderService.useOverlayMode();
|
||||
const { scrollbarXSize } = useReaderScrollbarContext();
|
||||
const { readerNavBarWidth } = useNavBarContext();
|
||||
const { isMaximized } = useReaderProgressBarContext();
|
||||
const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext();
|
||||
const { progressBarType, showPageNumber } = ReaderService.useSettings();
|
||||
const { progressBarType, shouldShowPageNumber } = ReaderService.useSettings();
|
||||
|
||||
const pageName = useMemo(() => getPage(currentPageIndex, pages).name, [currentPageIndex, pages]);
|
||||
|
||||
if (!showPageNumber) {
|
||||
if (!shouldShowPageNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -35,7 +36,7 @@ export const ReaderPageNumber = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isMobileWidth && progressBarType === ProgressBarType.STANDARD) {
|
||||
if (isDesktop && progressBarType === ProgressBarType.STANDARD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ export const ReaderPageNumber = () => {
|
||||
position: 'fixed',
|
||||
left: readerNavBarWidth,
|
||||
right: 0,
|
||||
bottom: (theme) => theme.spacing(1),
|
||||
bottom: (theme) => `calc(${theme.spacing(1)} + ${scrollbarXSize}px)`,
|
||||
alignItems: 'center',
|
||||
transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`,
|
||||
}}
|
||||
|
||||
@@ -35,10 +35,10 @@ export const TapZoneLayout = () => {
|
||||
}, [tapZoneLayoutElement]),
|
||||
);
|
||||
|
||||
const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout, width, height, theme.typography.h3, {
|
||||
vertical: tapZoneInvertMode.vertical,
|
||||
horizontal: tapZoneInvertMode.horizontal,
|
||||
isRTL: readingDirection === ReadingDirection.RTL,
|
||||
const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout.value, width, height, theme.typography.h3, {
|
||||
vertical: tapZoneInvertMode.value.vertical,
|
||||
horizontal: tapZoneInvertMode.value.horizontal,
|
||||
isRTL: readingDirection.value === ReadingDirection.RTL,
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -7,36 +7,39 @@
|
||||
*/
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import { useState } from 'react';
|
||||
import { BaseReaderOverlayProps, MobileHeaderProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
import { ReaderSettings } from '@/modules/reader/components/settings/ReaderSettings.tsx';
|
||||
import { ReaderPageNumber } from '@/modules/reader/components/ReaderPageNumber.tsx';
|
||||
import { StandardReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx';
|
||||
import { ReaderNavBarDesktop } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx';
|
||||
import { ReaderOverlayHeaderMobile } from '@/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx';
|
||||
import { ReaderBottomBarMobile } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx';
|
||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
|
||||
export const ReaderOverlay = ({ isVisible }: BaseReaderOverlayProps & MobileHeaderProps) => {
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
const { isDesktop, isMobile } = ReaderService.useOverlayMode();
|
||||
|
||||
const isDesktop = !isTouchDevice;
|
||||
const isMobile = isTouchDevice;
|
||||
const [areSettingsOpen, setAreSettingsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'absolute', width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}>
|
||||
{isDesktop && (
|
||||
<>
|
||||
<StandardReaderProgressBar />
|
||||
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => {}} />
|
||||
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => setAreSettingsOpen(true)} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<>
|
||||
<ReaderOverlayHeaderMobile isVisible={isVisible} />
|
||||
<ReaderBottomBarMobile openSettings={() => {}} isVisible={isVisible} />
|
||||
<ReaderBottomBarMobile openSettings={() => setAreSettingsOpen(true)} isVisible={isVisible} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ReaderSettings isOpen={areSettingsOpen} close={() => setAreSettingsOpen(false)} />
|
||||
|
||||
<ReaderPageNumber />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,6 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import { useGetOptionForDirection } from '@/theme.tsx';
|
||||
import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
||||
import { ReaderNavContainer } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavContainer.tsx';
|
||||
import { ReaderNavBarDesktopMetadata } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopMetadata.tsx';
|
||||
import { ReaderNavBarDesktopPageNavigation } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopPageNavigation.tsx';
|
||||
@@ -28,31 +27,31 @@ import { ReaderNavBarDesktopActions } from '@/modules/reader/components/overlay/
|
||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { createUpdateReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
|
||||
const useGetPreviousNavBarStaticValue = (isVisible: boolean, staticNav: boolean) => {
|
||||
const wasNavBarStaticRef = useRef(staticNav);
|
||||
const wasNavBarStaticPreviousRef = useRef(staticNav);
|
||||
const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
|
||||
const wasNavBarStaticRef = useRef(isStaticNav);
|
||||
const wasNavBarStaticPreviousRef = useRef(isStaticNav);
|
||||
|
||||
const resetWasNavBarStaticValue = wasNavBarStaticPreviousRef.current !== wasNavBarStaticRef.current && !isVisible;
|
||||
if (resetWasNavBarStaticValue) {
|
||||
wasNavBarStaticRef.current = false;
|
||||
}
|
||||
|
||||
const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== staticNav;
|
||||
const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== isStaticNav;
|
||||
if (didNavBarStaticValueChange) {
|
||||
wasNavBarStaticRef.current = wasNavBarStaticPreviousRef.current;
|
||||
wasNavBarStaticPreviousRef.current = staticNav;
|
||||
wasNavBarStaticPreviousRef.current = isStaticNav;
|
||||
}
|
||||
|
||||
return wasNavBarStaticRef.current;
|
||||
};
|
||||
|
||||
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
|
||||
export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDesktopProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setReaderNavBarWidth } = useNavBarContext();
|
||||
@@ -60,36 +59,33 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
|
||||
const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
|
||||
const { pages, currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext();
|
||||
|
||||
const handleBack = useBackButton();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
|
||||
const updateReaderSettings = createUpdateReaderSettings(manga ?? { id: -1 }, () =>
|
||||
makeToast(t('reader.settings.error.label.failed_to_save_settings')),
|
||||
);
|
||||
|
||||
const exit = ReaderService.useExit();
|
||||
const updateReaderSettings = ReaderService.useCreateUpdateSetting(manga ?? DEFAULT_MANGA);
|
||||
const settings = ReaderService.useSettings();
|
||||
|
||||
const [navBarElement, setNavBarElement] = useState<HTMLDivElement | null>();
|
||||
useResizeObserver(
|
||||
navBarElement,
|
||||
useCallback(() => {
|
||||
if (!settings?.staticNav) {
|
||||
if (!settings?.isStaticNav) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReaderNavBarWidth(navBarElement!.offsetWidth);
|
||||
}, [navBarElement, settings?.staticNav]),
|
||||
}, [navBarElement, settings?.isStaticNav]),
|
||||
);
|
||||
useLayoutEffect(() => () => setReaderNavBarWidth(0), []);
|
||||
|
||||
const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, settings.staticNav);
|
||||
const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, settings.isStaticNav);
|
||||
const changedNavBarStaticValue = wasNavBarStatic && isVisible;
|
||||
const drawerTransitionDuration = changedNavBarStaticValue ? 0 : undefined;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant={settings.staticNav ? 'permanent' : 'persistent'}
|
||||
open={isVisible || settings.staticNav}
|
||||
variant={settings.isStaticNav ? 'permanent' : 'persistent'}
|
||||
open={isVisible || settings.isStaticNav}
|
||||
transitionDuration={drawerTransitionDuration}
|
||||
PaperProps={{
|
||||
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref),
|
||||
@@ -99,7 +95,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
|
||||
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Tooltip title={t('reader.button.exit')}>
|
||||
<IconButton onClick={handleBack} color="inherit">
|
||||
<IconButton onClick={exit} color="inherit">
|
||||
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -107,9 +103,9 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setReaderNavBarWidth(0);
|
||||
updateReaderSettings('staticNav', !settings.staticNav);
|
||||
updateReaderSettings('isStaticNav', !settings.isStaticNav);
|
||||
}}
|
||||
color={settings.staticNav ? 'primary' : 'inherit'}
|
||||
color={settings.isStaticNav ? 'primary' : 'inherit'}
|
||||
>
|
||||
<PushPinIcon />
|
||||
</IconButton>
|
||||
@@ -147,6 +143,8 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
|
||||
settings={settings}
|
||||
updateSetting={updateReaderSettings}
|
||||
openSettings={openSettings}
|
||||
isDefaultable
|
||||
onDefault={(...args) => manga && ReaderService.deleteSetting(manga, ...args)}
|
||||
/>
|
||||
</Stack>
|
||||
</ReaderNavContainer>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { OffsetDoubleSpreadIcon } from '@/assets/icons/svg/OffsetDoubleSpreadIcon.tsx';
|
||||
import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopOffsetDoubleSpread = ({
|
||||
readingMode,
|
||||
@@ -20,7 +21,7 @@ export const ReaderNavBarDesktopOffsetDoubleSpread = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (readingMode !== ReadingMode.DOUBLE_PAGE) {
|
||||
if (!isOffsetDoubleSpreadPagesEditable(readingMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,65 +9,52 @@
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
|
||||
import ExpandIcon from '@mui/icons-material/Expand';
|
||||
import CropOriginalIcon from '@mui/icons-material/CropOriginal';
|
||||
import FitScreenIcon from '@mui/icons-material/FitScreen';
|
||||
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
||||
import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { IReaderSettings, ReaderPageScaleMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderPageScaleMode> = {
|
||||
[ReaderPageScaleMode.WIDTH]: {
|
||||
title: 'reader.settings.page_scale.width',
|
||||
icon: <ExpandIcon sx={{ transform: 'rotate(90deg)' }} />,
|
||||
},
|
||||
[ReaderPageScaleMode.HEIGHT]: {
|
||||
title: 'reader.settings.page_scale.height',
|
||||
icon: <ExpandIcon />,
|
||||
},
|
||||
[ReaderPageScaleMode.SCREEN]: {
|
||||
title: 'reader.settings.page_scale.screen',
|
||||
icon: <ZoomOutMapIcon />,
|
||||
},
|
||||
[ReaderPageScaleMode.ORIGINAL]: {
|
||||
title: 'reader.settings.page_scale.original',
|
||||
icon: <CropOriginalIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_PAGE_SCALE_MODE_VALUES = Object.values(ReaderPageScaleMode).filter((value) => typeof value === 'number');
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReaderPageScaleMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
PAGE_SCALE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopPageScale = ({
|
||||
pageScaleMode,
|
||||
shouldScalePage,
|
||||
shouldStretchPage,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings, 'pageScaleMode' | 'shouldScalePage'> & {
|
||||
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldScalePage'>>(
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'pageScaleMode' | 'shouldStretchPage'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReaderPageScaleMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'>>(
|
||||
setting: Setting,
|
||||
value: IReaderSettings[Setting],
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isPageScalingPossible = pageScaleMode !== ReaderPageScaleMode.ORIGINAL;
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
|
||||
<ValueRotationButton
|
||||
value={pageScaleMode}
|
||||
{...buttonSelectInputProps}
|
||||
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
|
||||
values={READER_PAGE_SCALE_MODE_VALUES}
|
||||
setValue={(value) => updateSetting('pageScaleMode', value)}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={PAGE_SCALE_VALUE_TO_DISPLAY_DATA[pageScaleMode.value].icon}
|
||||
/>
|
||||
{isPageScalingPossible && (
|
||||
{READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode.value] && (
|
||||
<Tooltip title={t('reader.settings.label.scale_page')}>
|
||||
<CustomIconButton
|
||||
onClick={() => updateSetting('shouldScalePage', !shouldScalePage)}
|
||||
onClick={() => updateSetting('shouldStretchPage', !shouldStretchPage.value)}
|
||||
sx={{ minWidth: 0 }}
|
||||
variant="contained"
|
||||
color={shouldScalePage ? 'secondary' : 'primary'}
|
||||
color={shouldStretchPage.value ? 'secondary' : 'primary'}
|
||||
>
|
||||
<FitScreenIcon />
|
||||
</CustomIconButton>
|
||||
|
||||
@@ -14,17 +14,16 @@ import { ReaderNavBarDesktopPageScale } from '@/modules/reader/components/overla
|
||||
import { ReaderNavBarDesktopReadingMode } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx';
|
||||
import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx';
|
||||
import { ReaderNavBarDesktopReadingDirection } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx';
|
||||
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopQuickSettings = ({
|
||||
settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldScalePage, readingDirection },
|
||||
settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldStretchPage, readingDirection },
|
||||
updateSetting,
|
||||
openSettings,
|
||||
}: {
|
||||
settings: IReaderSettings;
|
||||
updateSetting: <Setting extends keyof IReaderSettings>(setting: Setting, value: IReaderSettings[Setting]) => void;
|
||||
} & Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
}: ReaderSettingsTypeProps & Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -32,19 +31,26 @@ export const ReaderNavBarDesktopQuickSettings = ({
|
||||
<ReaderNavBarDesktopReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readingMode')}
|
||||
/>
|
||||
<ReaderNavBarDesktopOffsetDoubleSpread
|
||||
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads}
|
||||
readingMode={readingMode.value}
|
||||
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads.value}
|
||||
setShouldOffsetDoubleSpreads={(value) => updateSetting('shouldOffsetDoubleSpreads', value)}
|
||||
/>
|
||||
<ReaderNavBarDesktopPageScale
|
||||
pageScaleMode={pageScaleMode}
|
||||
shouldScalePage={shouldScalePage}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
updateSetting={updateSetting}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('pageScaleMode')}
|
||||
/>
|
||||
<ReaderNavBarDesktopReadingDirection
|
||||
readingDirection={readingDirection}
|
||||
setReadingDirection={(value) => updateSetting('readingDirection', value)}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readingDirection')}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => openSettings()}
|
||||
|
||||
@@ -6,35 +6,28 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
|
||||
const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
[ReadingDirection.LTR]: {
|
||||
title: 'reader.settings.reading_direction.ltr',
|
||||
icon: <ArrowCircleRightIcon />,
|
||||
},
|
||||
[ReadingDirection.RTL]: {
|
||||
title: 'reader.settings.reading_direction.rtl',
|
||||
icon: <ArrowCircleLeftIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
READING_DIRECTION_VALUES,
|
||||
READING_DIRECTION_VALUE_TO_DISPLAY_DATA,
|
||||
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
}: Pick<IReaderSettings, 'readingDirection'> & {
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingDirection: (readingDirection: ReadingDirection) => void;
|
||||
}) => (
|
||||
<ValueRotationButton
|
||||
value={readingDirection}
|
||||
{...buttonSelectInputProps}
|
||||
value={readingDirection.isDefault ? undefined : readingDirection.value}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
|
||||
valueToDisplayData={READING_DIRECTION_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={READING_DIRECTION_VALUE_TO_DISPLAY_DATA[readingDirection.value].icon}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,45 +6,28 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx';
|
||||
import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx';
|
||||
import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx';
|
||||
import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx';
|
||||
import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.tsx';
|
||||
import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode> = {
|
||||
[ReadingMode.SINGLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.single_page',
|
||||
icon: <SinglePageIcon />,
|
||||
},
|
||||
[ReadingMode.DOUBLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.double_page',
|
||||
icon: <DoublePageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_vertical',
|
||||
icon: <ContinuousVerticalPageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_horizontal',
|
||||
icon: <ContinuousHorizontalPageIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopReadingMode = ({
|
||||
readingMode,
|
||||
setReadingMode,
|
||||
}: Pick<IReaderSettings, 'readingMode'> & {
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingMode: (mode: ReadingMode) => void;
|
||||
}) => (
|
||||
<ValueRotationButton
|
||||
value={readingMode}
|
||||
{...buttonSelectInputProps}
|
||||
value={readingMode.isDefault ? undefined : readingMode.value}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setReadingMode}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].icon}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useLayoutEffect } from 'react';
|
||||
import { ReaderBottomBarMobileProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
import { MobileReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx';
|
||||
import { ReaderChapterList } from '@/modules/reader/components/overlay/navigation/ReaderChapterList.tsx';
|
||||
import { ReaderBottomBarMobileQuickSettings } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx';
|
||||
import { ReaderBottomBarMobileQuickSettings } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export const ReaderProgressBarContainer = styled(Stack, {
|
||||
shouldForwardProp: shouldForwardProp<ReaderProgressBarContainerProps>(['progressBarPosition']),
|
||||
})<ReaderProgressBarContainerProps>(({ theme, progressBarPosition }) => ({
|
||||
position: 'fixed',
|
||||
pointerEvents: 'all',
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, {
|
||||
justifyContent: 'flex-end',
|
||||
bottom: 0,
|
||||
|
||||
@@ -11,8 +11,8 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import { ReactNode } from 'react';
|
||||
import { CurrentPageSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderProgressBar.constants.ts';
|
||||
import { getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderProgressBarCurrentPageSlot = ({
|
||||
pageName,
|
||||
|
||||
@@ -10,7 +10,8 @@ import Box from '@mui/material/Box';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReaderProgressBarSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderProgressBar.constants.ts';
|
||||
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderProgressBarSlot = ({
|
||||
pageName,
|
||||
|
||||
51
src/modules/reader/components/settings/ReaderSettings.tsx
Normal file
51
src/modules/reader/components/settings/ReaderSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
159
src/modules/reader/components/settings/ReaderSettingsTabs.tsx
Normal file
159
src/modules/reader/components/settings/ReaderSettingsTabs.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -9,8 +9,8 @@
|
||||
import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
@@ -26,18 +26,21 @@ const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
|
||||
const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderBottomBarMobileReadingDirection = ({
|
||||
export const ReaderSettingReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
}: Pick<IReaderSettings, 'readingDirection'> & {
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingDirection: (readingDirection: ReadingDirection) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.label.reading_direction')}
|
||||
value={readingDirection}
|
||||
value={readingDirection.isDefault ? undefined : readingDirection.value}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -6,10 +6,206 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
|
||||
import ExpandIcon from '@mui/icons-material/Expand';
|
||||
import CropOriginalIcon from '@mui/icons-material/CropOriginal';
|
||||
import { TooltipProps } from '@mui/material/Tooltip';
|
||||
import { Direction } from '@mui/material/styles';
|
||||
import { ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsGlobal,
|
||||
ProgressBarPosition,
|
||||
ProgressBarType,
|
||||
ReaderBackgroundColor,
|
||||
ReaderExitMode,
|
||||
ReaderOverlayMode,
|
||||
ReaderPageScaleMode,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx';
|
||||
import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx';
|
||||
import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx';
|
||||
import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
|
||||
export const READING_DIRECTION_TO_THEME_DIRECTION: Record<ReadingDirection, Direction> = {
|
||||
[ReadingDirection.LTR]: 'ltr',
|
||||
[ReadingDirection.RTL]: 'rtl',
|
||||
};
|
||||
|
||||
export const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefined> = {
|
||||
overlayMode: undefined,
|
||||
exitMode: undefined,
|
||||
customFilter: undefined,
|
||||
shouldSkipDupChapters: undefined,
|
||||
progressBarType: undefined,
|
||||
progressBarSize: undefined,
|
||||
progressBarPosition: undefined,
|
||||
shouldShowPageNumber: undefined,
|
||||
isStaticNav: undefined,
|
||||
backgroundColor: undefined,
|
||||
};
|
||||
|
||||
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
|
||||
|
||||
export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
readerWidth: { value: 50, enabled: false },
|
||||
overlayMode: ReaderOverlayMode.AUTO,
|
||||
tapZoneLayout: TapZoneLayouts.RIGHT_LEFT,
|
||||
tapZoneInvertMode: { vertical: false, horizontal: false },
|
||||
progressBarType: ProgressBarType.STANDARD,
|
||||
progressBarSize: 4,
|
||||
progressBarPosition: ProgressBarPosition.BOTTOM,
|
||||
pageScaleMode: ReaderPageScaleMode.ORIGINAL,
|
||||
shouldStretchPage: false,
|
||||
shouldOffsetDoubleSpreads: false,
|
||||
shouldSkipDupChapters: true,
|
||||
shouldShowPageNumber: true,
|
||||
isStaticNav: false,
|
||||
readingDirection: ReadingDirection.LTR,
|
||||
readingMode: ReadingMode.SINGLE_PAGE,
|
||||
exitMode: ReaderExitMode.PREVIOUS,
|
||||
backgroundColor: ReaderBackgroundColor.THEME,
|
||||
customFilter: {
|
||||
brightness: {
|
||||
value: 100,
|
||||
enabled: false,
|
||||
},
|
||||
contrast: {
|
||||
value: 100,
|
||||
enabled: false,
|
||||
},
|
||||
saturate: {
|
||||
value: 100,
|
||||
enabled: false,
|
||||
},
|
||||
hue: {
|
||||
value: 0,
|
||||
enabled: false,
|
||||
},
|
||||
rgba: {
|
||||
value: {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
alpha: 0,
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
sepia: false,
|
||||
grayscale: false,
|
||||
invert: false,
|
||||
},
|
||||
pageGap: 5,
|
||||
};
|
||||
|
||||
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
||||
[ProgressBarPosition.BOTTOM]: 'top',
|
||||
[ProgressBarPosition.LEFT]: 'right',
|
||||
[ProgressBarPosition.RIGHT]: 'left',
|
||||
};
|
||||
|
||||
export const READING_DIRECTION_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
[ReadingDirection.LTR]: {
|
||||
title: 'reader.settings.reading_direction.ltr',
|
||||
icon: <ArrowCircleRightIcon />,
|
||||
},
|
||||
[ReadingDirection.RTL]: {
|
||||
title: 'reader.settings.reading_direction.rtl',
|
||||
icon: <ArrowCircleLeftIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
|
||||
|
||||
export const PAGE_SCALE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderPageScaleMode> = {
|
||||
[ReaderPageScaleMode.WIDTH]: {
|
||||
title: 'reader.settings.page_scale.width',
|
||||
icon: <ExpandIcon sx={{ transform: 'rotate(90deg)' }} />,
|
||||
},
|
||||
[ReaderPageScaleMode.HEIGHT]: {
|
||||
title: 'reader.settings.page_scale.height',
|
||||
icon: <ExpandIcon />,
|
||||
},
|
||||
[ReaderPageScaleMode.SCREEN]: {
|
||||
title: 'reader.settings.page_scale.screen',
|
||||
icon: <ZoomOutMapIcon />,
|
||||
},
|
||||
[ReaderPageScaleMode.ORIGINAL]: {
|
||||
title: 'reader.settings.page_scale.original',
|
||||
icon: <CropOriginalIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const READER_PAGE_SCALE_MODE_VALUES = Object.values(ReaderPageScaleMode).filter(
|
||||
(value) => typeof value === 'number',
|
||||
);
|
||||
|
||||
export const READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED: Record<ReaderPageScaleMode, boolean> = {
|
||||
[ReaderPageScaleMode.WIDTH]: true,
|
||||
[ReaderPageScaleMode.HEIGHT]: true,
|
||||
[ReaderPageScaleMode.SCREEN]: true,
|
||||
[ReaderPageScaleMode.ORIGINAL]: false,
|
||||
};
|
||||
|
||||
export const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode> = {
|
||||
[ReadingMode.SINGLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.single_page',
|
||||
icon: <SinglePageIcon />,
|
||||
},
|
||||
[ReadingMode.DOUBLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.double_page',
|
||||
icon: <DoublePageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_vertical',
|
||||
icon: <ContinuousVerticalPageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_horizontal',
|
||||
icon: <ContinuousHorizontalPageIcon />,
|
||||
},
|
||||
};
|
||||
export const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export enum ReaderSettingTab {
|
||||
LAYOUT,
|
||||
GENERAL,
|
||||
FILTER,
|
||||
BEHAVIOUR,
|
||||
}
|
||||
|
||||
export const READER_SETTING_TABS: Record<
|
||||
ReaderSettingTab,
|
||||
{
|
||||
id: ReaderSettingTab;
|
||||
label: TranslationKey;
|
||||
supportsTouchDevices: boolean;
|
||||
}
|
||||
> = {
|
||||
[ReaderSettingTab.LAYOUT]: {
|
||||
id: ReaderSettingTab.LAYOUT,
|
||||
label: 'reader.settings.label.layout',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.GENERAL]: {
|
||||
id: ReaderSettingTab.GENERAL,
|
||||
label: 'global.label.general',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.FILTER]: {
|
||||
id: ReaderSettingTab.FILTER,
|
||||
label: 'reader.settings.custom_filter.title',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.BEHAVIOUR]: {
|
||||
id: ReaderSettingTab.BEHAVIOUR,
|
||||
label: 'reader.settings.label.behaviour',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
};
|
||||
|
||||
72
src/modules/reader/screens/DefaultReaderSettings.tsx
Normal file
72
src/modules/reader/screens/DefaultReaderSettings.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -6,14 +6,35 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Direction, useTheme } from '@mui/material/styles';
|
||||
import { t as translate } from 'i18next';
|
||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
|
||||
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReaderExitMode,
|
||||
ReaderOverlayMode,
|
||||
ReadingDirection,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { getReaderSettingsFor, useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import {
|
||||
updateReaderSettings,
|
||||
useDefaultReaderSettings,
|
||||
useGetReaderSettingsFor,
|
||||
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import { getMetadataKey } from '@/modules/metadata/services/MetadataReader.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { MANGA_META_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
||||
import { GLOBAL_METADATA } from '@/lib/graphql/fragments/Fragments.ts';
|
||||
import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts';
|
||||
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
||||
import { GLOBAL_READER_SETTING_KEYS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
const DIRECTION_TO_INVERTED: Record<Direction, Direction> = {
|
||||
ltr: 'rtl',
|
||||
@@ -25,6 +46,8 @@ const DIRECTION_TO_READING_DIRECTION: Record<Direction, ReadingDirection> = {
|
||||
rtl: ReadingDirection.RTL,
|
||||
};
|
||||
|
||||
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
|
||||
|
||||
export class ReaderService {
|
||||
static useNavigateToChapter(chapter?: TChapterReader): () => void {
|
||||
const navigate = useNavigate();
|
||||
@@ -32,21 +55,170 @@ export class ReaderService {
|
||||
return useCallback(() => chapter && navigate(Chapters.getReaderUrl(chapter), { replace: true }), [chapter]);
|
||||
}
|
||||
|
||||
static useSettings(): IReaderSettings {
|
||||
static useSettings(): IReaderSettingsWithDefaultFlag {
|
||||
const { manga } = useReaderStateMangaContext();
|
||||
const defaultReaderSettings = useDefaultReaderSettings();
|
||||
return useMemo(
|
||||
() => getReaderSettingsFor(manga ?? { id: -1 }, defaultReaderSettings.settings),
|
||||
[manga, defaultReaderSettings],
|
||||
);
|
||||
|
||||
return useGetReaderSettingsFor(manga ?? DEFAULT_MANGA, defaultReaderSettings.settings);
|
||||
}
|
||||
|
||||
static useGetThemeDirection(): Direction {
|
||||
const { direction } = useTheme();
|
||||
const { readingDirection } = ReaderService.useSettings();
|
||||
|
||||
return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection
|
||||
return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection.value
|
||||
? direction
|
||||
: DIRECTION_TO_INVERTED[direction];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the change immediately to the cache and sends a mutation in case "commit" is true.
|
||||
*/
|
||||
static updateSetting<Setting extends keyof IReaderSettings>(
|
||||
manga: MangaIdInfo,
|
||||
setting: Setting,
|
||||
value: IReaderSettings[Setting],
|
||||
commit: boolean = true,
|
||||
isGlobal: boolean = false,
|
||||
): void {
|
||||
if (!manga) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getMetadataKey(setting);
|
||||
|
||||
const { cache } = requestManager.graphQLClient.client;
|
||||
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
if (isGlobalSetting) {
|
||||
const reference = cache.writeFragment({
|
||||
fragment: GLOBAL_METADATA,
|
||||
data: {
|
||||
__typename: 'GlobalMetaType',
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
},
|
||||
});
|
||||
cache.modify({
|
||||
fields: {
|
||||
metas(existingMetas, { readField }) {
|
||||
return {
|
||||
...existingMetas,
|
||||
nodes: updateMetadataList(key, existingMetas?.nodes, readField, () => reference),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const reference = cache.writeFragment({
|
||||
fragment: MANGA_META_FIELDS,
|
||||
data: {
|
||||
__typename: 'MangaMetaType',
|
||||
mangaId: manga.id,
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
},
|
||||
});
|
||||
cache.modify({
|
||||
id: cache.identify({ __typename: 'MangaType', id: manga.id }),
|
||||
fields: {
|
||||
meta(existingMetas, { readField }) {
|
||||
return updateMetadataList(key, existingMetas, readField, () => reference);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (commit) {
|
||||
updateReaderSettings(manga, setting, value, isGlobal).catch(() =>
|
||||
makeToast(translate('reader.settings.error.label.failed_to_save_settings'), 'error'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static deleteSetting<Setting extends keyof IReaderSettings>(
|
||||
manga: MangaIdInfo,
|
||||
setting: Setting,
|
||||
isGlobal: boolean = false,
|
||||
): void {
|
||||
if (!manga) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getMetadataKey(setting);
|
||||
|
||||
const { cache } = requestManager.graphQLClient.client;
|
||||
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
if (isGlobalSetting) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'GlobalMetaType', key }) });
|
||||
} else {
|
||||
cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId: manga.id, key }) });
|
||||
}
|
||||
|
||||
const deleteSetting = isGlobalSetting
|
||||
? () => requestManager.deleteGlobalMeta(key).response
|
||||
: () => requestManager.deleteMangaMeta(manga.id, key).response;
|
||||
deleteSetting().catch(() =>
|
||||
makeToast(translate('reader.settings.error.label.failed_to_save_settings'), 'error'),
|
||||
);
|
||||
}
|
||||
|
||||
static useCreateUpdateSetting<Setting extends keyof IReaderSettings>(
|
||||
manga: MangaIdInfo,
|
||||
): (
|
||||
...args: OmitFirst<Parameters<typeof this.updateSetting<Setting>>>
|
||||
) => ReturnType<typeof this.updateSetting<Setting>> {
|
||||
return useCallback((...args) => this.updateSetting<Setting>(manga, ...args), [manga]);
|
||||
}
|
||||
|
||||
static useCreateDeleteSetting<Setting extends keyof IReaderSettings>(
|
||||
manga: MangaIdInfo,
|
||||
): (
|
||||
...args: OmitFirst<Parameters<typeof this.deleteSetting<Setting>>>
|
||||
) => ReturnType<typeof this.deleteSetting<Setting>> {
|
||||
return useCallback((...args) => this.deleteSetting<Setting>(manga, ...args), [manga]);
|
||||
}
|
||||
|
||||
static useOverlayMode(): { mode: ReaderOverlayMode; isDesktop: boolean; isMobile: boolean } {
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
const { overlayMode } = this.useSettings();
|
||||
|
||||
const isAutoModeSelected = overlayMode === ReaderOverlayMode.AUTO;
|
||||
const isDesktopModeSelected = overlayMode === ReaderOverlayMode.DESKTOP;
|
||||
const isMobileModeSelected = overlayMode === ReaderOverlayMode.MOBILE;
|
||||
|
||||
const isDesktop = isDesktopModeSelected || (isAutoModeSelected && !isTouchDevice);
|
||||
const isMobile = isMobileModeSelected || (isAutoModeSelected && isTouchDevice);
|
||||
|
||||
return {
|
||||
mode: isDesktop ? ReaderOverlayMode.DESKTOP : ReaderOverlayMode.MOBILE,
|
||||
isDesktop,
|
||||
isMobile,
|
||||
};
|
||||
}
|
||||
|
||||
static useExit(): () => void {
|
||||
const { manga } = useReaderStateMangaContext();
|
||||
const { exitMode } = this.useSettings();
|
||||
const handleBack = useBackButton();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const openMangaPage = useCallback(() => {
|
||||
if (!manga) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
return navigate(`/manga/${manga.id}`);
|
||||
}, [manga]);
|
||||
|
||||
switch (exitMode) {
|
||||
case ReaderExitMode.PREVIOUS:
|
||||
return handleBack;
|
||||
case ReaderExitMode.MANGA:
|
||||
return openMangaPage;
|
||||
default:
|
||||
throw new Error(`Unexpected "exitMode" (${exitMode})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import {
|
||||
requestUpdateMangaMetadata,
|
||||
requestUpdateServerMetadata,
|
||||
} from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/Reader.constants.ts';
|
||||
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
|
||||
import { extractOriginalKey, getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
|
||||
import {
|
||||
AllowedMetadataValueTypes,
|
||||
AppMetadataKeys,
|
||||
@@ -25,14 +27,69 @@ import {
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import {
|
||||
DEFAULT_READER_SETTINGS,
|
||||
GLOBAL_READER_SETTING_KEYS,
|
||||
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts';
|
||||
import { APP_METADATA_KEY_PREFIX } from '@/modules/metadata/Metadata.constants.ts';
|
||||
|
||||
const convertToSettingsWithDefaultFlag = (
|
||||
type: Extract<MetadataHolderType, 'global' | 'manga'>,
|
||||
settings: IReaderSettings,
|
||||
metadataHolder: MetadataHolder,
|
||||
): IReaderSettingsWithDefaultFlag => {
|
||||
const activeDevice = getActiveDevice();
|
||||
const istDefaultDevice = activeDevice === DEFAULT_DEVICE;
|
||||
const existingSettings = Object.keys(metadataHolder.meta ?? {})
|
||||
// settings that are not for the active device need to be filtered out, otherwise, they mess up the "isDefault" flag
|
||||
.filter((metaKey) => {
|
||||
// the default device is not added as a prefix to the key, thus, there should only be the app prefix for these reader settings
|
||||
if (istDefaultDevice) {
|
||||
return metaKey.match(/_/g)?.length === 1;
|
||||
}
|
||||
|
||||
return metaKey.startsWith(`${APP_METADATA_KEY_PREFIX}_${activeDevice}_`);
|
||||
})
|
||||
.map((metaKey) => extractOriginalKey(metaKey));
|
||||
const settingsWithDefault = Object.fromEntries(
|
||||
(Object.entries(settings) as [keyof IReaderSettings, IReaderSettings[keyof IReaderSettings]][]).map(
|
||||
([key, value]) => {
|
||||
const isGlobalSetting = GLOBAL_READER_SETTING_KEYS.includes(key);
|
||||
if (isGlobalSetting) {
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
const isDefaultSetting = type === 'manga' && !existingSettings.includes(key);
|
||||
return [
|
||||
key,
|
||||
{
|
||||
value,
|
||||
isDefault: isDefaultSetting,
|
||||
},
|
||||
];
|
||||
},
|
||||
),
|
||||
) as IReaderSettingsWithDefaultFlag;
|
||||
|
||||
return settingsWithDefault;
|
||||
};
|
||||
|
||||
const convertSettingsToMetadata = (
|
||||
settings: Partial<IReaderSettings>,
|
||||
): Metadata<string, AllowedMetadataValueTypes> => ({
|
||||
...settings,
|
||||
tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode),
|
||||
customFilter: JSON.stringify(settings.customFilter),
|
||||
readerWidth: JSON.stringify(settings.readerWidth),
|
||||
});
|
||||
|
||||
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(
|
||||
'global',
|
||||
DEFAULT_READER_SETTINGS,
|
||||
{ meta: convertSettingsToMetadata(DEFAULT_READER_SETTINGS) as Metadata },
|
||||
);
|
||||
|
||||
const convertMetadataToSettings = (
|
||||
metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
|
||||
defaultSettings: IReaderSettings,
|
||||
@@ -41,27 +98,21 @@ const convertMetadataToSettings = (
|
||||
tapZoneInvertMode:
|
||||
jsonSaveParse<IReaderSettings['tapZoneInvertMode']>((metadata.tapZoneInvertMode as string) ?? '') ??
|
||||
defaultSettings.tapZoneInvertMode,
|
||||
customFilter:
|
||||
jsonSaveParse<IReaderSettings['customFilter']>((metadata.customFilter as string) ?? '') ??
|
||||
defaultSettings.customFilter,
|
||||
readerWidth:
|
||||
jsonSaveParse<IReaderSettings['readerWidth']>((metadata.readerWidth as string) ?? '') ??
|
||||
defaultSettings.readerWidth,
|
||||
});
|
||||
|
||||
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
|
||||
type: 'global',
|
||||
metadataHolder: MetadataHolder,
|
||||
defaultSettings?: DefaultSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): DefaultSettings;
|
||||
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
|
||||
type: 'manga',
|
||||
metadataHolder: MangaIdInfo & MetadataHolder,
|
||||
defaultSettings?: DefaultSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): DefaultSettings;
|
||||
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
|
||||
const getReaderMetadata = (
|
||||
type: Extract<MetadataHolderType, 'global' | 'manga'>,
|
||||
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
|
||||
defaultSettings: DefaultSettings = DEFAULT_READER_SETTINGS as DefaultSettings,
|
||||
defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): DefaultSettings {
|
||||
return convertMetadataToSettings(
|
||||
) =>
|
||||
convertMetadataToSettings(
|
||||
getMetadataFrom(
|
||||
type as Parameters<typeof getMetadataFrom>[0],
|
||||
metadataHolder as Parameters<typeof getMetadataFrom>[1],
|
||||
@@ -70,7 +121,28 @@ function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IRead
|
||||
useEffectFn,
|
||||
),
|
||||
defaultSettings as IReaderSettings,
|
||||
) as DefaultSettings;
|
||||
);
|
||||
|
||||
function getReaderSettingsWithDefaultValueFallback(
|
||||
type: 'global',
|
||||
metadataHolder: MetadataHolder,
|
||||
defaultSettings?: IReaderSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): IReaderSettingsWithDefaultFlag;
|
||||
function getReaderSettingsWithDefaultValueFallback(
|
||||
type: 'manga',
|
||||
metadataHolder: MangaIdInfo & MetadataHolder,
|
||||
defaultSettings?: IReaderSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): IReaderSettingsWithDefaultFlag;
|
||||
function getReaderSettingsWithDefaultValueFallback(
|
||||
type: Extract<MetadataHolderType, 'global' | 'manga'>,
|
||||
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
|
||||
defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): IReaderSettingsWithDefaultFlag {
|
||||
const settings = getReaderMetadata(type, metadataHolder, defaultSettings, useEffectFn);
|
||||
return convertToSettingsWithDefaultFlag(type, settings, metadataHolder);
|
||||
}
|
||||
|
||||
const getSettings = (
|
||||
@@ -82,7 +154,10 @@ const getSettings = (
|
||||
'manga',
|
||||
{
|
||||
...metaHolder,
|
||||
meta: convertFromGqlMeta(metaHolder.meta),
|
||||
meta: convertFromGqlMeta(
|
||||
metaHolder.meta,
|
||||
(key) => !GLOBAL_READER_SETTING_KEYS.includes(extractOriginalKey(key)),
|
||||
),
|
||||
},
|
||||
defaultSettings,
|
||||
useEffectFn,
|
||||
@@ -91,12 +166,12 @@ const getSettings = (
|
||||
export const getReaderSettingsFor = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings: IReaderSettings,
|
||||
): IReaderSettings => getSettings(metaHolder, defaultSettings);
|
||||
): IReaderSettingsWithDefaultFlag => getSettings(metaHolder, defaultSettings);
|
||||
|
||||
export const useGetReaderSettingsFor = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings: IReaderSettings,
|
||||
): IReaderSettings => {
|
||||
): IReaderSettingsWithDefaultFlag => {
|
||||
const settings = getSettings(metaHolder, defaultSettings, useEffect);
|
||||
return useMemo(() => settings, [metaHolder, defaultSettings]);
|
||||
};
|
||||
@@ -106,6 +181,30 @@ export const useDefaultReaderSettings = (): {
|
||||
settings: IReaderSettings;
|
||||
loading: boolean;
|
||||
request: ReturnType<typeof requestManager.useGetGlobalMeta>;
|
||||
} => {
|
||||
const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true });
|
||||
const { data, loading } = request;
|
||||
const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]);
|
||||
const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]);
|
||||
const tmpSettings = getReaderMetadata('global', metaHolder, undefined, useEffect);
|
||||
const settings = useMemo(() => tmpSettings, [metaHolder]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
metadata,
|
||||
settings,
|
||||
loading,
|
||||
request,
|
||||
}),
|
||||
[metadata, settings, loading, request],
|
||||
);
|
||||
};
|
||||
|
||||
export const useDefaultReaderSettingsWithDefaultFlag = (): {
|
||||
metadata?: Metadata;
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
loading: boolean;
|
||||
request: ReturnType<typeof requestManager.useGetGlobalMeta>;
|
||||
} => {
|
||||
const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true });
|
||||
const { data, loading } = request;
|
||||
@@ -129,13 +228,19 @@ export const updateReaderSettings = async <Setting extends keyof IReaderSettings
|
||||
manga: Pick<MangaType, 'id'> & GqlMetaHolder,
|
||||
setting: Setting,
|
||||
value: IReaderSettings[Setting],
|
||||
): Promise<void[]> =>
|
||||
requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]);
|
||||
isGlobal: boolean = false,
|
||||
): Promise<void[]> => {
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
if (isGlobalSetting) {
|
||||
return requestUpdateServerMetadata([[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]);
|
||||
}
|
||||
|
||||
return requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]);
|
||||
};
|
||||
export const createUpdateReaderSettings =
|
||||
<Settings extends keyof IReaderSettings>(
|
||||
manga: Pick<MangaType, 'id'> & GqlMetaHolder,
|
||||
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'),
|
||||
): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) =>
|
||||
(setting, value) =>
|
||||
updateReaderSettings(manga, setting, value).catch(handleError);
|
||||
(setting, value, isGlobal) =>
|
||||
updateReaderSettings(manga, setting, value, isGlobal).catch(handleError);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
|
||||
export enum ProgressBarType {
|
||||
HIDDEN,
|
||||
@@ -39,6 +40,112 @@ export enum ReaderPageScaleMode {
|
||||
ORIGINAL,
|
||||
}
|
||||
|
||||
export enum ReaderOverlayMode {
|
||||
AUTO,
|
||||
DESKTOP,
|
||||
MOBILE,
|
||||
}
|
||||
|
||||
export enum ReaderExitMode {
|
||||
PREVIOUS,
|
||||
MANGA,
|
||||
}
|
||||
|
||||
export enum ReaderBackgroundColor {
|
||||
THEME,
|
||||
BLACK,
|
||||
GRAY,
|
||||
WHITE,
|
||||
}
|
||||
|
||||
export interface ReaderFilterRGBA {
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
export interface ReaderCustomFilter {
|
||||
brightness: {
|
||||
/**
|
||||
* percentage
|
||||
*/
|
||||
value: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
contrast: {
|
||||
/**
|
||||
* percentage
|
||||
*/
|
||||
value: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
saturate: {
|
||||
/**
|
||||
* percentage
|
||||
*/
|
||||
value: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
hue: {
|
||||
/**
|
||||
* degree
|
||||
*/
|
||||
value: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
rgba: {
|
||||
value: ReaderFilterRGBA;
|
||||
enabled: boolean;
|
||||
};
|
||||
sepia: boolean;
|
||||
grayscale: boolean;
|
||||
invert: boolean;
|
||||
}
|
||||
|
||||
export interface IReaderSettingsGlobal {
|
||||
overlayMode: ReaderOverlayMode;
|
||||
exitMode: ReaderExitMode;
|
||||
customFilter: ReaderCustomFilter;
|
||||
shouldSkipDupChapters: boolean;
|
||||
progressBarType: ProgressBarType;
|
||||
/**
|
||||
* pixel
|
||||
*/
|
||||
progressBarSize: number;
|
||||
progressBarPosition: ProgressBarPosition;
|
||||
shouldShowPageNumber: boolean;
|
||||
isStaticNav: boolean;
|
||||
backgroundColor: ReaderBackgroundColor;
|
||||
}
|
||||
|
||||
export interface IReaderSettingsManga {
|
||||
tapZoneLayout: TapZoneLayouts;
|
||||
tapZoneInvertMode: TapZoneInvertMode;
|
||||
pageScaleMode: ReaderPageScaleMode;
|
||||
shouldStretchPage: boolean;
|
||||
shouldOffsetDoubleSpreads: boolean;
|
||||
readingDirection: ReadingDirection;
|
||||
readingMode: ReadingMode;
|
||||
/**
|
||||
* pixel
|
||||
*/
|
||||
pageGap: number;
|
||||
readerWidth: {
|
||||
/**
|
||||
* percentage
|
||||
*/
|
||||
value: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReaderSettings extends IReaderSettingsGlobal, IReaderSettingsManga {}
|
||||
|
||||
export interface IReaderSettingsWithDefaultFlag
|
||||
extends IReaderSettingsGlobal,
|
||||
TransformRecordToWithDefaultFlag<IReaderSettingsManga> {}
|
||||
|
||||
export interface ReaderStateChapters {
|
||||
chapters: TChapterReader[];
|
||||
currentChapter?: TChapterReader | null;
|
||||
@@ -47,20 +154,25 @@ export interface ReaderStateChapters {
|
||||
setReaderStateChapters: React.Dispatch<React.SetStateAction<Omit<ReaderStateChapters, 'setReaderStateChapters'>>>;
|
||||
}
|
||||
|
||||
export interface IReaderSettings {
|
||||
staticNav: boolean;
|
||||
showPageNumber: boolean;
|
||||
loadNextOnEnding: boolean;
|
||||
skipDupChapters: boolean;
|
||||
readerWidth: number;
|
||||
tapZoneLayout: TapZoneLayouts;
|
||||
tapZoneInvertMode: TapZoneInvertMode;
|
||||
progressBarType: ProgressBarType;
|
||||
progressBarSize: number;
|
||||
progressBarPosition: ProgressBarPosition;
|
||||
pageScaleMode: ReaderPageScaleMode;
|
||||
shouldScalePage: boolean;
|
||||
shouldOffsetDoubleSpreads: boolean;
|
||||
readingDirection: ReadingDirection;
|
||||
readingMode: ReadingMode;
|
||||
interface ReaderSettingsTypeBaseProps {
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
updateSetting: (
|
||||
...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>
|
||||
) => ReturnType<typeof ReaderService.updateSetting>;
|
||||
}
|
||||
|
||||
export interface ReaderSettingsDefaultableProps {
|
||||
isDefaultable: boolean;
|
||||
onDefault: (setting: keyof IReaderSettings) => void;
|
||||
}
|
||||
|
||||
interface ReaderSettingsTypeDefaultableProps extends ReaderSettingsTypeBaseProps, ReaderSettingsDefaultableProps {
|
||||
isDefaultable: true;
|
||||
}
|
||||
|
||||
interface ReaderSettingsTypeNonDefaultableProps extends ReaderSettingsTypeBaseProps {}
|
||||
|
||||
export type ReaderSettingsTypeProps =
|
||||
| ReaderSettingsTypeDefaultableProps
|
||||
| (PropertiesNever<Omit<ReaderSettingsTypeDefaultableProps, keyof ReaderSettingsTypeBaseProps>> &
|
||||
ReaderSettingsTypeNonDefaultableProps);
|
||||
|
||||
12
src/modules/reader/utils/ReaderSettings.utils.tsx
Normal file
12
src/modules/reader/utils/ReaderSettings.utils.tsx
Normal 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;
|
||||
@@ -33,4 +33,4 @@ export interface ISearchSettings {
|
||||
ignoreFilters: boolean;
|
||||
}
|
||||
|
||||
export type ServerSettings = GetServerSettingsQuery['settings'];
|
||||
export type ServerSettings = Omit<GetServerSettingsQuery['settings'], '__typename'>;
|
||||
|
||||
Reference in New Issue
Block a user