Structure "reader" in sub-features
This commit is contained in:
414
src/features/reader/settings/ReaderSettings.constants.tsx
Normal file
414
src/features/reader/settings/ReaderSettings.constants.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* 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 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 { ScrollDirection, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsGlobal,
|
||||
ProgressBarPosition,
|
||||
ProgressBarType,
|
||||
ReaderBackgroundColor,
|
||||
ReaderBlendMode,
|
||||
ReaderExitMode,
|
||||
ReaderHotkey,
|
||||
ReaderOverlayMode,
|
||||
ReaderPageScaleMode,
|
||||
ReaderScrollAmount,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/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 '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { WebtoonPageIcon } from '@/assets/icons/svg/WebtoonPageIcon.tsx';
|
||||
|
||||
export const AUTO_SCROLL_SPEED = {
|
||||
min: 0.5,
|
||||
max: 60,
|
||||
step: 0.5,
|
||||
default: 5,
|
||||
};
|
||||
|
||||
export const SCROLL_AMOUNT = {
|
||||
min: 5,
|
||||
max: 100,
|
||||
default: ReaderScrollAmount.LARGE,
|
||||
step: 5,
|
||||
};
|
||||
|
||||
export const PROGRESS_BAR_SIZE = {
|
||||
min: 2,
|
||||
max: 20,
|
||||
step: 1,
|
||||
default: 4,
|
||||
};
|
||||
|
||||
export const IMAGE_PRE_LOAD_AMOUNT = {
|
||||
min: 1,
|
||||
max: 20,
|
||||
default: 5,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
export const PAGE_GAP = {
|
||||
min: 0,
|
||||
max: 20,
|
||||
default: 5,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
export const CUSTOM_FILTER = {
|
||||
brightness: {
|
||||
min: 5,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
contrast: {
|
||||
min: 5,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
saturate: {
|
||||
min: 0,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
hue: {
|
||||
min: 0,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
rgba: {
|
||||
red: {
|
||||
min: 0,
|
||||
max: 255,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
green: {
|
||||
min: 0,
|
||||
max: 255,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
blue: {
|
||||
min: 0,
|
||||
max: 255,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
alpha: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const READING_DIRECTION_TO_THEME_DIRECTION: Record<ReadingDirection, Direction> = {
|
||||
[ReadingDirection.LTR]: 'ltr',
|
||||
[ReadingDirection.RTL]: 'rtl',
|
||||
};
|
||||
|
||||
const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefined> = {
|
||||
overlayMode: undefined,
|
||||
exitMode: undefined,
|
||||
customFilter: undefined,
|
||||
shouldSkipDupChapters: undefined,
|
||||
shouldSkipFilteredChapters: undefined,
|
||||
progressBarType: undefined,
|
||||
progressBarSize: undefined,
|
||||
progressBarPosition: undefined,
|
||||
progressBarPositionAutoVertical: undefined,
|
||||
shouldShowPageNumber: undefined,
|
||||
isStaticNav: undefined,
|
||||
backgroundColor: undefined,
|
||||
hotkeys: undefined,
|
||||
imagePreLoadAmount: undefined,
|
||||
shouldUseAutoWebtoonMode: undefined,
|
||||
autoScroll: undefined,
|
||||
shouldShowReadingModePreview: undefined,
|
||||
shouldShowTapZoneLayoutPreview: undefined,
|
||||
shouldInformAboutMissingChapter: undefined,
|
||||
shouldInformAboutScanlatorChange: undefined,
|
||||
scrollAmount: undefined,
|
||||
shouldUseInfiniteScroll: undefined,
|
||||
shouldShowTransitionPage: 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: PROGRESS_BAR_SIZE.default,
|
||||
progressBarPosition: ProgressBarPosition.AUTO,
|
||||
progressBarPositionAutoVertical: ProgressBarPosition.RIGHT,
|
||||
pageScaleMode: ReaderPageScaleMode.ORIGINAL,
|
||||
shouldStretchPage: false,
|
||||
shouldOffsetDoubleSpreads: false,
|
||||
shouldSkipDupChapters: true,
|
||||
shouldSkipFilteredChapters: false,
|
||||
shouldShowPageNumber: true,
|
||||
isStaticNav: false,
|
||||
readingDirection: ReadingDirection.LTR,
|
||||
readingMode: ReadingMode.SINGLE_PAGE,
|
||||
exitMode: ReaderExitMode.PREVIOUS,
|
||||
backgroundColor: ReaderBackgroundColor.THEME,
|
||||
customFilter: {
|
||||
brightness: {
|
||||
value: CUSTOM_FILTER.brightness.default,
|
||||
enabled: false,
|
||||
},
|
||||
contrast: {
|
||||
value: CUSTOM_FILTER.contrast.default,
|
||||
enabled: false,
|
||||
},
|
||||
saturate: {
|
||||
value: CUSTOM_FILTER.saturate.default,
|
||||
enabled: false,
|
||||
},
|
||||
hue: {
|
||||
value: CUSTOM_FILTER.hue.default,
|
||||
enabled: false,
|
||||
},
|
||||
rgba: {
|
||||
value: {
|
||||
red: CUSTOM_FILTER.rgba.red.default,
|
||||
green: CUSTOM_FILTER.rgba.green.default,
|
||||
blue: CUSTOM_FILTER.rgba.blue.default,
|
||||
alpha: CUSTOM_FILTER.rgba.alpha.default,
|
||||
blendMode: ReaderBlendMode.DEFAULT,
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
sepia: false,
|
||||
grayscale: false,
|
||||
invert: false,
|
||||
},
|
||||
pageGap: PAGE_GAP.default,
|
||||
hotkeys: {
|
||||
[ReaderHotkey.PREVIOUS_PAGE]: ['arrowleft', 'a'],
|
||||
[ReaderHotkey.NEXT_PAGE]: ['arrowright', 'd'],
|
||||
[ReaderHotkey.SCROLL_BACKWARD]: ['arrowup', 'w'],
|
||||
[ReaderHotkey.SCROLL_FORWARD]: ['arrowdown', 's'],
|
||||
[ReaderHotkey.PREVIOUS_CHAPTER]: ['comma'],
|
||||
[ReaderHotkey.NEXT_CHAPTER]: ['period'],
|
||||
[ReaderHotkey.TOGGLE_MENU]: ['m'],
|
||||
[ReaderHotkey.CYCLE_SCALE_TYPE]: ['i'],
|
||||
[ReaderHotkey.STRETCH_IMAGE]: ['f'],
|
||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
||||
[ReaderHotkey.CYCLE_READING_MODE]: ['r'],
|
||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: ['t'],
|
||||
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: ['space'],
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: ['b'],
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: ['v'],
|
||||
[ReaderHotkey.EXIT_READER]: ['c'],
|
||||
},
|
||||
imagePreLoadAmount: IMAGE_PRE_LOAD_AMOUNT.default,
|
||||
shouldUseAutoWebtoonMode: true,
|
||||
autoScroll: {
|
||||
value: AUTO_SCROLL_SPEED.default,
|
||||
smooth: true,
|
||||
},
|
||||
shouldShowReadingModePreview: true,
|
||||
shouldShowTapZoneLayoutPreview: true,
|
||||
shouldInformAboutMissingChapter: true,
|
||||
shouldInformAboutScanlatorChange: true,
|
||||
scrollAmount: ReaderScrollAmount.LARGE,
|
||||
shouldUseInfiniteScroll: true,
|
||||
shouldShowTransitionPage: true,
|
||||
};
|
||||
|
||||
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
||||
[ProgressBarPosition.BOTTOM]: 'top',
|
||||
[ProgressBarPosition.LEFT]: 'right',
|
||||
[ProgressBarPosition.RIGHT]: 'left',
|
||||
// should never get accessed
|
||||
[ProgressBarPosition.AUTO]: '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 = {
|
||||
[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 />,
|
||||
},
|
||||
[ReadingMode.WEBTOON]: {
|
||||
title: 'reader.settings.reader_type.label.webtoon',
|
||||
icon: <WebtoonPageIcon />,
|
||||
},
|
||||
} satisfies ValueToDisplayData<ReadingMode>;
|
||||
|
||||
export const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export enum ReaderSettingTab {
|
||||
LAYOUT,
|
||||
GENERAL,
|
||||
FILTER,
|
||||
BEHAVIOUR,
|
||||
HOTKEYS,
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
[ReaderSettingTab.HOTKEYS]: {
|
||||
id: ReaderSettingTab.HOTKEYS,
|
||||
label: 'hotkeys.title_other',
|
||||
supportsTouchDevices: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const READER_HOTKEYS = Object.values(ReaderHotkey).filter(
|
||||
(hotkey) => typeof hotkey === 'number',
|
||||
) as ReaderHotkey[];
|
||||
|
||||
export const READER_BACKGROUND_TO_COLOR = {
|
||||
[ReaderBackgroundColor.THEME]: 'background.default',
|
||||
[ReaderBackgroundColor.BLACK]: 'common.black',
|
||||
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
||||
[ReaderBackgroundColor.WHITE]: 'common.white',
|
||||
} as const satisfies Record<ReaderBackgroundColor, string>;
|
||||
|
||||
export const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<
|
||||
ReadingMode,
|
||||
Exclude<ScrollDirection, ScrollDirection.XY>
|
||||
> = {
|
||||
[ReadingMode.SINGLE_PAGE]: ScrollDirection.Y,
|
||||
[ReadingMode.DOUBLE_PAGE]: ScrollDirection.Y,
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: ScrollDirection.Y,
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: ScrollDirection.X,
|
||||
[ReadingMode.WEBTOON]: ScrollDirection.Y,
|
||||
};
|
||||
|
||||
export const READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA = {
|
||||
[ReaderBlendMode.DEFAULT]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.default',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.MULTIPLY]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.multiply',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.SCREEN]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.screen',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.OVERLAY]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.overlay',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.DARKEN]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.darken',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.LIGHTEN]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.lighten',
|
||||
icon: null,
|
||||
},
|
||||
} satisfies ValueToDisplayData<ReaderBlendMode>;
|
||||
|
||||
export const READER_BLEND_MODE_VALUES = Object.values(ReaderBlendMode);
|
||||
98
src/features/reader/settings/ReaderSettings.utils.tsx
Normal file
98
src/features/reader/settings/ReaderSettings.utils.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { NamedExoticComponent, RefAttributes } from 'react';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ProgressBarPosition,
|
||||
ProgressBarPositionAutoVertical,
|
||||
ReaderPagerProps,
|
||||
ReaderPageScaleMode,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { MangaGenreInfo, MangaSourceLngInfo, MangaSourceNameInfo } from '@/features/manga/Manga.types.ts';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { ReaderPagedPager } from '@/features/reader/viewer/pager/components/ReaderPagedPager.tsx';
|
||||
import { ReaderDoublePagedPager } from '@/features/reader/viewer/pager/components/ReaderDoublePagedPager.tsx';
|
||||
import { ReaderVerticalPager } from '@/features/reader/viewer/pager/components/ReaderVerticalPager.tsx';
|
||||
import { ReaderHorizontalPager } from '@/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx';
|
||||
|
||||
export const isOffsetDoubleSpreadPagesEditable = (readingMode: IReaderSettings['readingMode']): boolean =>
|
||||
readingMode === ReadingMode.DOUBLE_PAGE;
|
||||
|
||||
export const isReaderWidthEditable = (pageScaleMode: IReaderSettings['pageScaleMode']): boolean =>
|
||||
[ReaderPageScaleMode.WIDTH, ReaderPageScaleMode.SCREEN].includes(pageScaleMode);
|
||||
|
||||
export const isHeightPageScaleMode = (pageScaleMode: ReaderPageScaleMode): boolean =>
|
||||
[ReaderPageScaleMode.HEIGHT, ReaderPageScaleMode.SCREEN].includes(pageScaleMode);
|
||||
|
||||
export const shouldApplyReaderWidth = (
|
||||
readerWidth: IReaderSettings['readerWidth'] | undefined,
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
): boolean => !!readerWidth?.enabled && isReaderWidthEditable(pageScaleMode);
|
||||
|
||||
export const getSetReaderWidth = (
|
||||
readerWidth: IReaderSettings['readerWidth'] | undefined,
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
): number | undefined => {
|
||||
if (!shouldApplyReaderWidth(readerWidth, pageScaleMode)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return readerWidth?.value;
|
||||
};
|
||||
|
||||
export const isContinuousReadingMode = (readingMode: IReaderSettings['readingMode']): boolean =>
|
||||
[ReadingMode.CONTINUOUS_VERTICAL, ReadingMode.CONTINUOUS_HORIZONTAL, ReadingMode.WEBTOON].includes(readingMode);
|
||||
|
||||
export const isContinuousVerticalReadingMode = (readingMode: IReaderSettings['readingMode']): boolean =>
|
||||
[ReadingMode.CONTINUOUS_VERTICAL, ReadingMode.WEBTOON].includes(readingMode);
|
||||
|
||||
export const isAutoWebtoonMode = (
|
||||
manga: MangaGenreInfo & MangaSourceNameInfo & MangaSourceLngInfo,
|
||||
shouldUseAutoWebtoonMode: IReaderSettings['shouldUseAutoWebtoonMode'],
|
||||
readingMode: IReaderSettingsWithDefaultFlag['readingMode'],
|
||||
): boolean => shouldUseAutoWebtoonMode && readingMode.isDefault && Mangas.isLongStripType(manga);
|
||||
|
||||
export const getPagerForReadingMode = (
|
||||
readingMode: ReadingMode,
|
||||
): NamedExoticComponent<ReaderPagerProps & RefAttributes<HTMLDivElement>> => {
|
||||
switch (readingMode) {
|
||||
case ReadingMode.SINGLE_PAGE:
|
||||
return ReaderPagedPager;
|
||||
case ReadingMode.DOUBLE_PAGE:
|
||||
return ReaderDoublePagedPager;
|
||||
case ReadingMode.CONTINUOUS_VERTICAL:
|
||||
case ReadingMode.WEBTOON:
|
||||
return ReaderVerticalPager;
|
||||
case ReadingMode.CONTINUOUS_HORIZONTAL:
|
||||
return ReaderHorizontalPager;
|
||||
default:
|
||||
throw new Error(`Unexpected "ReadingMode" (${readingMode})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getProgressBarPosition = (
|
||||
progressBarPosition: ProgressBarPosition,
|
||||
progressBarPositionAutoVertical: keyof typeof ProgressBarPositionAutoVertical,
|
||||
offsetY: number = 0,
|
||||
offsetX: number = 0,
|
||||
): Exclude<ProgressBarPosition, ProgressBarPosition.AUTO> => {
|
||||
if (progressBarPosition !== ProgressBarPosition.AUTO) {
|
||||
return progressBarPosition;
|
||||
}
|
||||
|
||||
const isVerticalSpaceLarger = window.innerHeight - offsetY > window.innerWidth - offsetX;
|
||||
|
||||
if (isVerticalSpaceLarger) {
|
||||
return progressBarPositionAutoVertical;
|
||||
}
|
||||
|
||||
return ProgressBarPosition.BOTTOM;
|
||||
};
|
||||
257
src/features/reader/settings/ReaderSettingsMetadata.ts
Normal file
257
src/features/reader/settings/ReaderSettingsMetadata.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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 { useEffect, useMemo } from 'react';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies,no-restricted-imports
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import {
|
||||
requestUpdateMangaMetadata,
|
||||
requestUpdateServerMetadata,
|
||||
} from '@/features/metadata/services/MetadataUpdater.ts';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { IReaderSettings, IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import { convertFromGqlMeta } from '@/features/metadata/services/MetadataConverter.ts';
|
||||
import { getMetadataFrom } from '@/features/metadata/services/MetadataReader.ts';
|
||||
import {
|
||||
AllowedMetadataValueTypes,
|
||||
GqlMetaHolder,
|
||||
Metadata,
|
||||
MetadataHolder,
|
||||
MetadataHolderType,
|
||||
} from '@/features/metadata/Metadata.types.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||
import {
|
||||
DEFAULT_READER_SETTINGS,
|
||||
GLOBAL_READER_SETTING_KEYS,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { DEFAULT_DEVICE, getActiveDevice } from '@/features/device/services/Device.ts';
|
||||
import { APP_METADATA_KEY_PREFIX } from '@/features/metadata/Metadata.constants.ts';
|
||||
import { extractOriginalKey } from '@/features/metadata/Metadata.utils.ts';
|
||||
|
||||
export const convertFromReaderSettingsWithDefaultFlag = (settings: IReaderSettingsWithDefaultFlag): IReaderSettings =>
|
||||
Object.fromEntries(
|
||||
Object.entries(settings).map(([key, value]) => [
|
||||
key,
|
||||
Object.hasOwn(value, 'value') && Object.hasOwn(value, 'isDefault') ? value.value : value,
|
||||
]),
|
||||
) as IReaderSettings;
|
||||
|
||||
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),
|
||||
hotkeys: JSON.stringify(settings.hotkeys),
|
||||
autoScroll: JSON.stringify(settings.autoScroll),
|
||||
});
|
||||
|
||||
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(
|
||||
'global',
|
||||
DEFAULT_READER_SETTINGS,
|
||||
{ meta: convertSettingsToMetadata(DEFAULT_READER_SETTINGS) as Metadata },
|
||||
);
|
||||
|
||||
export const getReaderSettings = (
|
||||
type: Extract<MetadataHolderType, 'global' | 'manga'>,
|
||||
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
|
||||
defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
|
||||
useEffectFn?: typeof useEffect,
|
||||
profile?: ReadingMode,
|
||||
): IReaderSettings =>
|
||||
getMetadataFrom(
|
||||
type as Parameters<typeof getMetadataFrom>[0],
|
||||
metadataHolder as Parameters<typeof getMetadataFrom>[1],
|
||||
defaultSettings,
|
||||
profile !== undefined ? [profile.toString()] : undefined,
|
||||
useEffectFn,
|
||||
);
|
||||
|
||||
function getReaderSettingsWithDefaultValueFallback(
|
||||
type: 'global',
|
||||
metadataHolder: MetadataHolder,
|
||||
defaultSettings?: IReaderSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
profile?: ReadingMode,
|
||||
): IReaderSettingsWithDefaultFlag;
|
||||
function getReaderSettingsWithDefaultValueFallback(
|
||||
type: 'manga',
|
||||
metadataHolder: MangaIdInfo & MetadataHolder,
|
||||
defaultSettings?: IReaderSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
profile?: ReadingMode,
|
||||
): IReaderSettingsWithDefaultFlag;
|
||||
function getReaderSettingsWithDefaultValueFallback(
|
||||
type: Extract<MetadataHolderType, 'global' | 'manga'>,
|
||||
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
|
||||
defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
|
||||
useEffectFn?: typeof useEffect,
|
||||
profile?: ReadingMode,
|
||||
): IReaderSettingsWithDefaultFlag {
|
||||
const settings = getReaderSettings(type, metadataHolder, defaultSettings, useEffectFn, profile);
|
||||
return convertToSettingsWithDefaultFlag(type, settings, metadataHolder);
|
||||
}
|
||||
|
||||
const getSettings = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings?: IReaderSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
profile?: ReadingMode,
|
||||
): IReaderSettingsWithDefaultFlag =>
|
||||
getReaderSettingsWithDefaultValueFallback(
|
||||
'manga',
|
||||
{
|
||||
...metaHolder,
|
||||
meta: convertFromGqlMeta(
|
||||
metaHolder.meta,
|
||||
(key) => !GLOBAL_READER_SETTING_KEYS.includes(extractOriginalKey(key)),
|
||||
),
|
||||
},
|
||||
defaultSettings,
|
||||
useEffectFn,
|
||||
profile,
|
||||
);
|
||||
|
||||
export const getReaderSettingsFor = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings: IReaderSettings,
|
||||
): IReaderSettingsWithDefaultFlag => getSettings(metaHolder, defaultSettings);
|
||||
|
||||
export const useGetReaderSettingsFor = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings: IReaderSettings,
|
||||
profile?: ReadingMode,
|
||||
): IReaderSettingsWithDefaultFlag => {
|
||||
const settings = getSettings(metaHolder, defaultSettings, useEffect, profile);
|
||||
return useMemo(() => settings, [metaHolder, defaultSettings, profile]);
|
||||
};
|
||||
|
||||
export const useDefaultReaderSettings = (
|
||||
profile?: ReadingMode,
|
||||
): {
|
||||
metadata?: Metadata;
|
||||
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 = getReaderSettings('global', metaHolder, undefined, useEffect, profile);
|
||||
const settings = useMemo(() => tmpSettings, [metaHolder, profile]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
metadata,
|
||||
settings,
|
||||
loading,
|
||||
request,
|
||||
}),
|
||||
[metadata, settings, loading, request],
|
||||
);
|
||||
};
|
||||
|
||||
export const useDefaultReaderSettingsWithDefaultFlag = (
|
||||
profile?: ReadingMode,
|
||||
): {
|
||||
metadata?: Metadata;
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
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 = getReaderSettingsWithDefaultValueFallback('global', metaHolder, undefined, useEffect, profile);
|
||||
const settings = useMemo(() => tmpSettings, [metaHolder, profile]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
metadata,
|
||||
settings,
|
||||
loading,
|
||||
request,
|
||||
}),
|
||||
[metadata, settings, loading, request],
|
||||
);
|
||||
};
|
||||
|
||||
export const updateReaderSettings = async <Setting extends keyof IReaderSettings = keyof IReaderSettings>(
|
||||
manga: Pick<MangaType, 'id'> & GqlMetaHolder,
|
||||
setting: Setting,
|
||||
value: IReaderSettings[Setting],
|
||||
isGlobal: boolean = false,
|
||||
profile?: ReadingMode,
|
||||
): Promise<void[]> => {
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
if (isGlobalSetting) {
|
||||
return requestUpdateServerMetadata(
|
||||
[[setting, convertSettingsToMetadata({ [setting]: value })[setting]]],
|
||||
profile !== undefined ? [profile?.toString()] : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
return requestUpdateMangaMetadata(
|
||||
manga,
|
||||
[[setting, convertSettingsToMetadata({ [setting]: value })[setting]]],
|
||||
profile !== undefined ? [profile?.toString()] : undefined,
|
||||
);
|
||||
};
|
||||
export const createUpdateReaderSettings =
|
||||
<Settings extends keyof IReaderSettings>(
|
||||
manga: Pick<MangaType, 'id'> & GqlMetaHolder,
|
||||
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'),
|
||||
profile?: ReadingMode,
|
||||
): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) =>
|
||||
(setting, value, isGlobal) =>
|
||||
updateReaderSettings(manga, setting, value, isGlobal, profile).catch(handleError);
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { IReaderSettingsWithDefaultFlag, ReaderSettingsTypeProps } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { ReaderSettingExitMode } from '@/features/reader/settings/behaviour/components/ReaderSettingExitMode.tsx';
|
||||
import { isOffsetDoubleSpreadPagesEditable } from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import {
|
||||
DEFAULT_READER_SETTINGS,
|
||||
IMAGE_PRE_LOAD_AMOUNT,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ReaderSettingAutoScroll } from '@/features/reader/auto-scroll/settings/ReaderSettingAutoScroll.tsx';
|
||||
import { ReaderSettingScrollAmount } from '@/features/reader/settings/behaviour/components/ReaderSettingScrollAmount.tsx';
|
||||
|
||||
export const ReaderBehaviourSettings = ({
|
||||
settings,
|
||||
updateSetting,
|
||||
onDefault,
|
||||
isDefaultable,
|
||||
}: {
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
updateSetting: (
|
||||
...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>
|
||||
) => ReturnType<typeof ReaderService.updateSetting>;
|
||||
} & ReaderSettingsTypeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<ReaderSettingExitMode
|
||||
exitMode={settings.exitMode}
|
||||
setExitMode={(value) => updateSetting('exitMode', value)}
|
||||
/>
|
||||
<ReaderSettingScrollAmount
|
||||
scrollAmount={settings.scrollAmount}
|
||||
setScrollAmount={(value, commit) => updateSetting('scrollAmount', value, commit)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.skip_dup_chapters')}
|
||||
checked={settings.shouldSkipDupChapters}
|
||||
onChange={(_, checked) => updateSetting('shouldSkipDupChapters', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.label.skip_filtered_chapters')}</Typography>
|
||||
{isDefaultable && (
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.label.unchangeable_in_reader')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldSkipFilteredChapters}
|
||||
onChange={(_, checked) => updateSetting('shouldSkipFilteredChapters', checked)}
|
||||
disabled={isDefaultable}
|
||||
/>
|
||||
{isOffsetDoubleSpreadPagesEditable(settings.readingMode.value) && (
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.offset_double_spread')}
|
||||
checked={settings.shouldOffsetDoubleSpreads.value}
|
||||
onChange={(_, checked) => updateSetting('shouldOffsetDoubleSpreads', checked)}
|
||||
/>
|
||||
)}
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.infinite_scroll.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.infinite_scroll.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldUseInfiniteScroll}
|
||||
onChange={(_, checked) => updateSetting('shouldUseInfiniteScroll', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.preview.reading_mode.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.preview.reading_mode.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldShowReadingModePreview}
|
||||
onChange={(_, checked) => updateSetting('shouldShowReadingModePreview', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.preview.tap_zones.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.preview.tap_zones.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldShowTapZoneLayoutPreview}
|
||||
onChange={(_, checked) => updateSetting('shouldShowTapZoneLayoutPreview', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.auto_webtoon_mode.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.auto_webtoon_mode.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldUseAutoWebtoonMode}
|
||||
onChange={(_, checked) => updateSetting('shouldUseAutoWebtoonMode', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.show_transition_page')}
|
||||
checked={settings.shouldShowTransitionPage}
|
||||
onChange={(_, checked) => updateSetting('shouldShowTransitionPage', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.chapter_transition.warning.missing_chapter')}
|
||||
checked={settings.shouldInformAboutMissingChapter}
|
||||
onChange={(_, checked) => updateSetting('shouldInformAboutMissingChapter', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.chapter_transition.warning.scanlator_change')}
|
||||
checked={settings.shouldInformAboutScanlatorChange}
|
||||
onChange={(_, checked) => updateSetting('shouldInformAboutScanlatorChange', checked)}
|
||||
/>
|
||||
<ReaderSettingAutoScroll
|
||||
autoScroll={settings.autoScroll}
|
||||
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||
/>
|
||||
<SliderInput
|
||||
label={t('reader.settings.image_preload_amount')}
|
||||
value={settings.imagePreLoadAmount}
|
||||
onDefault={() => onDefault?.('imagePreLoadAmount')}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.imagePreLoadAmount,
|
||||
value: settings.imagePreLoadAmount,
|
||||
step: IMAGE_PRE_LOAD_AMOUNT.step,
|
||||
min: IMAGE_PRE_LOAD_AMOUNT.min,
|
||||
max: IMAGE_PRE_LOAD_AMOUNT.max,
|
||||
onChange: (_, value) => {
|
||||
updateSetting('imagePreLoadAmount', value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
updateSetting('imagePreLoadAmount', value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</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 '@/features/reader/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/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,74 @@
|
||||
/*
|
||||
* 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, ReaderScrollAmount } from '@/features/reader/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { DEFAULT_READER_SETTINGS, SCROLL_AMOUNT } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderScrollAmount> = {
|
||||
[ReaderScrollAmount.TINY]: {
|
||||
title: 'global.label.tiny',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderScrollAmount.SMALL]: {
|
||||
title: 'global.label.small',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderScrollAmount.MEDIUM]: {
|
||||
title: 'global.label.medium',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderScrollAmount.LARGE]: {
|
||||
title: 'global.label.large',
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_SCROLL_AMOUNT_VALUES = Object.values(ReaderScrollAmount).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingScrollAmount = ({
|
||||
scrollAmount,
|
||||
setScrollAmount,
|
||||
}: Pick<IReaderSettings, 'scrollAmount'> & {
|
||||
setScrollAmount: (amount: ReaderScrollAmount, commit: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.scroll_amount')}
|
||||
value={scrollAmount}
|
||||
values={READER_SCROLL_AMOUNT_VALUES}
|
||||
setValue={(value) => setScrollAmount(value as number, true)}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
<SliderInput
|
||||
label={t('reader.settings.label.custom_scroll_amount')}
|
||||
value={t('global.value', { value: scrollAmount, unit: '%' })}
|
||||
onDefault={() => setScrollAmount(DEFAULT_READER_SETTINGS.scrollAmount, true)}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.scrollAmount,
|
||||
value: scrollAmount,
|
||||
...SCROLL_AMOUNT,
|
||||
onChange: (_, value) => {
|
||||
setScrollAmount(value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
setScrollAmount(value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
|
||||
export const DefaultSettingFootnote = ({ areDefaultSettings }: { areDefaultSettings?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
if (!isTouchDevice || areDefaultSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'end' }}>
|
||||
<Typography variant="caption">{t('reader.settings.default_setting_footnote')}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
193
src/features/reader/settings/components/ReaderSettingsTabs.tsx
Normal file
193
src/features/reader/settings/components/ReaderSettingsTabs.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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 '@/features/core/components/tabs/TabsMenu.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/features/reader/Reader.types.ts';
|
||||
import { useReaderTapZoneContext } from '@/features/reader/tap-zones/contexts/ReaderTapZoneContext.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { READER_SETTING_TABS, ReaderSettingTab } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { TabPanel } from '@/features/core/components/tabs/TabPanel.tsx';
|
||||
import { ReaderLayoutSettings } from '@/features/reader/settings/layout/ReaderLayoutSettings.tsx';
|
||||
import { ReaderGeneralSettings } from '@/features/reader/settings/general/ReaderGeneralSettings.tsx';
|
||||
import { ReaderFilterSettings } from '@/features/reader/filters/settings/ReaderFilterSettings.tsx';
|
||||
import { ReaderBehaviourSettings } from '@/features/reader/settings/behaviour/ReaderBehaviourSettings.tsx';
|
||||
import { ReaderDefaultLayoutSettings } from '@/features/reader/settings/layout/ReaderDefaultLayoutSettings.tsx';
|
||||
import { ReaderHotkeysSettings } from '@/features/reader/hotkeys/settings/ReaderHotkeysSettings.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderSettingsTabs = ({
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
areDefaultSettings,
|
||||
settings,
|
||||
updateSetting,
|
||||
deleteSetting,
|
||||
setShowPreview,
|
||||
mode: overlayMode,
|
||||
setTransparent,
|
||||
}: Pick<TReaderTapZoneContext, 'setShowPreview'> &
|
||||
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'mode'> & {
|
||||
activeTab: number;
|
||||
setActiveTab: (tab: number) => void;
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
updateSetting: (...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>) => void;
|
||||
areDefaultSettings?: boolean;
|
||||
deleteSetting: (setting: keyof IReaderSettings) => void;
|
||||
setTransparent?: (transparent: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
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:
|
||||
if (areDefaultSettings) {
|
||||
return (
|
||||
<TabPanel key={id} index={id} currentIndex={activeTab}>
|
||||
<ReaderDefaultLayoutSettings
|
||||
readingMode={settings.readingMode}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TabPanel key={id} index={id} currentIndex={activeTab}>
|
||||
<ReaderLayoutSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
setShowPreview={setShowPreview!}
|
||||
isDefaultable={!areDefaultSettings}
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
isSeriesMode
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
</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)}
|
||||
isDefaultable
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
case ReaderSettingTab.BEHAVIOUR:
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
index={id}
|
||||
currentIndex={activeTab}
|
||||
sx={{ p: areDefaultSettings ? 2 : undefined }}
|
||||
>
|
||||
<ReaderBehaviourSettings
|
||||
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.HOTKEYS:
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
index={id}
|
||||
currentIndex={activeTab}
|
||||
sx={{ p: areDefaultSettings ? 2 : undefined }}
|
||||
>
|
||||
<ReaderHotkeysSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
isDefaultable
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected "ReaderSettingTab" (${id})`);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderSettingsTabs = withPropsFrom(
|
||||
BaseReaderSettingsTabs,
|
||||
[useReaderTapZoneContext, ReaderService.useOverlayMode],
|
||||
['setShowPreview', 'mode'],
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 '@/features/reader/overlay/progress-bar/settings/components/ReaderSettingProgressBarType.tsx';
|
||||
import { ReaderSettingProgressBarSize } from '@/features/reader/overlay/progress-bar/settings/components/ReaderSettingProgressBarSize.tsx';
|
||||
import { ReaderSettingProgressBarPosition } from '@/features/reader/overlay/progress-bar/settings/components/ReaderSettingProgressBarPosition.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarType,
|
||||
ReaderOverlayMode,
|
||||
ReaderSettingsTypeProps,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderSettingOverlayMode } from '@/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { ReaderSettingBackgroundColor } from '@/features/reader/settings/general/components/ReaderSettingBackgroundColor.tsx';
|
||||
|
||||
export const ReaderGeneralSettings = ({
|
||||
overlayMode,
|
||||
settings,
|
||||
updateSetting,
|
||||
onDefault,
|
||||
}: 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)}
|
||||
onDefault={() => onDefault?.('progressBarSize')}
|
||||
/>
|
||||
<ReaderSettingProgressBarPosition
|
||||
progressBarPosition={settings.progressBarPosition}
|
||||
progressBarPositionAutoVertical={settings.progressBarPositionAutoVertical}
|
||||
updateSetting={updateSetting}
|
||||
/>
|
||||
{(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 '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReaderBackgroundColor } from '@/features/reader/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,53 @@
|
||||
/*
|
||||
* 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 { ComponentProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderLayoutSettings } from '@/features/reader/settings/layout/ReaderLayoutSettings.tsx';
|
||||
import { ReaderSettingProfileSettings } from '@/features/reader/settings/layout/components/ReaderSettingProfileSettings.tsx';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ReaderSettingReadingMode } from '@/features/reader/settings/layout/components/ReaderSettingReadingMode.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
|
||||
export const ReaderDefaultLayoutSettings = ({
|
||||
profiles = READING_MODE_VALUES,
|
||||
readingMode,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof ReaderLayoutSettings>, 'setShowPreview' | 'settings'> & {
|
||||
profiles?: ReadingMode[];
|
||||
readingMode?: IReaderSettingsWithDefaultFlag['readingMode'];
|
||||
}) => {
|
||||
const { updateSetting, isSeriesMode } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2, pb: Number(!isSeriesMode) * 2 }}>
|
||||
{readingMode !== undefined && (
|
||||
<Stack sx={{ pt: 2, px: 2 }}>
|
||||
<ReaderSettingReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{profiles.map((profile) => (
|
||||
<ReaderSettingProfileSettings
|
||||
key={profile}
|
||||
profile={profile}
|
||||
title={t(READING_MODE_VALUE_TO_DISPLAY_DATA[profile].title)}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
123
src/features/reader/settings/layout/ReaderLayoutSettings.tsx
Normal file
123
src/features/reader/settings/layout/ReaderLayoutSettings.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 Typography from '@mui/material/Typography';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderSettingReadingMode } from '@/features/reader/settings/layout/components/ReaderSettingReadingMode.tsx';
|
||||
import { ReaderSettingReadingDirection } from '@/features/reader/settings/layout/components/ReaderSettingReadingDirection.tsx';
|
||||
import { ReaderSettingTapZoneLayout } from '@/features/reader/settings/layout/components/ReaderSettingTapZoneLayout.tsx';
|
||||
import { ReaderSettingTapZoneInvertMode } from '@/features/reader/settings/layout/components/ReaderSettingTapZoneInvertMode.tsx';
|
||||
import { ReaderSettingPageScaleMode } from '@/features/reader/settings/layout/components/ReaderSettingPageScaleMode.tsx';
|
||||
import { ReaderSettingStretchPage } from '@/features/reader/settings/layout/components/ReaderSettingStretchPage.tsx';
|
||||
import { ReaderSettingsTypeProps } from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderSettingPageGap } from '@/features/reader/settings/layout/components/ReaderSettingPageGap.tsx';
|
||||
import { ReaderSettingWidth } from '@/features/reader/settings/layout/components/ReaderSettingWidth.tsx';
|
||||
import { DefaultSettingFootnote } from '@/features/reader/settings/components/DefaultSettingFootnote.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { ReaderDefaultLayoutSettings } from '@/features/reader/settings/layout/ReaderDefaultLayoutSettings.tsx';
|
||||
|
||||
export const ReaderLayoutSettings = ({
|
||||
setShowPreview,
|
||||
settings,
|
||||
updateSetting,
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
isSeriesMode,
|
||||
setTransparent,
|
||||
}: ReaderSettingsTypeProps & {
|
||||
setShowPreview: TReaderTapZoneContext['setShowPreview'];
|
||||
isSeriesMode?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<DefaultSettingFootnote areDefaultSettings={!isDefaultable} />
|
||||
{isSeriesMode && (
|
||||
<>
|
||||
<Typography>{t('reader.settings.source_series')}</Typography>
|
||||
<ReaderSettingReadingMode
|
||||
readingMode={settings.readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readingMode')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ReaderSettingPageGap
|
||||
pageGap={settings.pageGap}
|
||||
readingMode={settings.readingMode}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('pageGap')}
|
||||
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}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readerWidth')}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
{isSeriesMode && (
|
||||
<>
|
||||
<Divider />
|
||||
<ReaderDefaultLayoutSettings
|
||||
profiles={[settings.readingMode.value]}
|
||||
updateSetting={updateSetting}
|
||||
isSeriesMode={isSeriesMode}
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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 { IReaderSettings, IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { DEFAULT_READER_SETTINGS, PAGE_GAP } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { isContinuousReadingMode } from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderSettingPageGap = ({
|
||||
pageGap,
|
||||
readingMode,
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'pageGap' | 'readingMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<IReaderSettings['pageGap']>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: (gap: number, commit: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isChangeable = readingMode.value !== ReadingMode.WEBTOON && isContinuousReadingMode(readingMode.value);
|
||||
if (!isChangeable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SliderInput
|
||||
label={t('reader.settings.label.page_gap')}
|
||||
value={t('global.value', { value: pageGap.value, unit: t('global.unit.px') })}
|
||||
onDefault={isDefaultable ? onDefault : undefined}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.pageGap,
|
||||
value: pageGap.value,
|
||||
step: PAGE_GAP.step,
|
||||
min: PAGE_GAP.min,
|
||||
max: PAGE_GAP.max,
|
||||
onChange: (_, value) => {
|
||||
updateSetting(value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
updateSetting(value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
PAGE_SCALE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/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}
|
||||
defaultValue={pageScaleMode.isDefault ? pageScaleMode.value : undefined}
|
||||
values={READER_PAGE_SCALE_MODE_VALUES}
|
||||
setValue={setPageScaleMode}
|
||||
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 ListSubheader from '@mui/material/ListSubheader';
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ReaderLayoutSettings } from '@/features/reader/settings/layout/ReaderLayoutSettings.tsx';
|
||||
import { useDefaultReaderSettingsWithDefaultFlag } from '@/features/reader/settings/ReaderSettingsMetadata.ts';
|
||||
import { ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
|
||||
export const ReaderSettingProfileSettings = ({
|
||||
profile,
|
||||
title,
|
||||
updateSetting,
|
||||
isSeriesMode,
|
||||
...props
|
||||
}: Pick<ComponentProps<typeof ReaderLayoutSettings>, 'updateSetting' | 'isSeriesMode'> & {
|
||||
profile: ReadingMode;
|
||||
title: string;
|
||||
}) => {
|
||||
const { settings } = useDefaultReaderSettingsWithDefaultFlag(profile);
|
||||
|
||||
const adjustedSettings = useMemo(
|
||||
() => ({
|
||||
...settings,
|
||||
readingMode: {
|
||||
value: profile,
|
||||
isDefault: true,
|
||||
},
|
||||
}),
|
||||
[settings, profile],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
{!isSeriesMode ? (
|
||||
<ListSubheader component="div" id={`${profile}-settings`}>
|
||||
{title}
|
||||
</ListSubheader>
|
||||
) : (
|
||||
<Typography>{title}</Typography>
|
||||
)}
|
||||
<Stack sx={{ px: Number(!isSeriesMode) * 2 }}>
|
||||
<ReaderLayoutSettings
|
||||
{...props}
|
||||
setShowPreview={() => {}}
|
||||
settings={adjustedSettings}
|
||||
updateSetting={(setting, value, commit) => updateSetting(setting, value, commit, true, profile)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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 ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const 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');
|
||||
|
||||
export const ReaderSettingReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
...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.isDefault ? undefined : readingDirection.value}
|
||||
defaultValue={readingDirection.isDefault ? readingDirection.value : undefined}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/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}
|
||||
defaultValue={readingMode.isDefault ? readingMode.value : undefined}
|
||||
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 '@/features/reader/Reader.types.ts';
|
||||
import { READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED } from '@/features/reader/settings/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,101 @@
|
||||
/*
|
||||
* 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 '@/features/core/Core.types.ts';
|
||||
import { IReaderSettings, IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import { TapZoneInvertMode } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/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();
|
||||
|
||||
const tapZonesInvertOption = convertTapZoneInvertModeToOption(tapZoneInvertMode.value);
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.tap_zones.invert')}
|
||||
value={tapZoneInvertMode.isDefault ? undefined : tapZonesInvertOption}
|
||||
defaultValue={tapZoneInvertMode.isDefault ? tapZonesInvertOption : undefined}
|
||||
values={TAP_ZONES_INVERT_OPTION_VALUES}
|
||||
setValue={(value) => setTapZoneInvertMode(TAP_ZONES_INVERT_OPTION_TO_SETTING[value])}
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { TapZoneLayouts } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/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,
|
||||
},
|
||||
[TapZoneLayouts.DISABLED]: {
|
||||
title: 'global.label.disabled',
|
||||
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}
|
||||
defaultValue={tapZoneLayout.isDefault ? tapZoneLayout.value : undefined}
|
||||
values={READER_TAP_ZONE_LAYOUT_VALUES}
|
||||
setValue={setTapZoneLayout}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { isReaderWidthEditable } from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
import { ResetButton } from '@/features/core/components/buttons/ResetButton.tsx';
|
||||
|
||||
export const ReaderSettingWidth = ({
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
updateSetting,
|
||||
setTransparent,
|
||||
}: Pick<IReaderSettings, 'readerWidth' | 'pageScaleMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<IReaderSettings['readerWidth']['value']>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: (...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>) => void;
|
||||
setTransparent?: (transparent: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isReaderWidthEditable(pageScaleMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.limit_reader_width')}
|
||||
checked={readerWidth.enabled}
|
||||
onChange={(_, checked) => updateSetting('readerWidth', { ...readerWidth, enabled: checked }, true)}
|
||||
/>
|
||||
{isDefaultable && <ResetButton onClick={onDefault} variant="outlined" />}
|
||||
</Stack>
|
||||
{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) => {
|
||||
setTransparent?.(true);
|
||||
updateSetting('readerWidth', { ...readerWidth, value: value as number }, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
setTransparent?.(false);
|
||||
updateSetting('readerWidth', { ...readerWidth, value: value as number }, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
import { useDefaultReaderSettingsWithDefaultFlag } from '@/features/reader/settings/ReaderSettingsMetadata.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/features/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { IReaderSettings, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderSettingsTabs } from '@/features/reader/settings/components/ReaderSettingsTabs.tsx';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { GLOBAL_READER_SETTINGS_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
|
||||
|
||||
export const GlobalReaderSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useAppTitle(t('reader.settings.title.reader'));
|
||||
|
||||
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,
|
||||
profile?: ReadingMode,
|
||||
) => {
|
||||
ReaderService.updateSetting(GLOBAL_READER_SETTINGS_MANGA, key, value, commit, true, profile);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('DefaultReaderSettings::refetch'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReaderSettingsTabs
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
areDefaultSettings
|
||||
settings={settings}
|
||||
updateSetting={(setting, value, commit, _, profile) => updateSetting(setting, value, commit, profile)}
|
||||
deleteSetting={(setting) => ReaderService.deleteSetting(GLOBAL_READER_SETTINGS_MANGA, setting, true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
62
src/features/reader/settings/screens/ReaderSettings.tsx
Normal file
62
src/features/reader/settings/screens/ReaderSettings.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderSettingsTabs } from '@/features/reader/settings/components/ReaderSettingsTabs.tsx';
|
||||
import { ReaderSettingTab } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { useDisableAllHotkeysWhileMounted } from '@/features/hotkeys/Hotkeys.utils.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
|
||||
export const ReaderSettings = ({ isOpen, close }: { isOpen: boolean; close: () => void }) => {
|
||||
const { manga } = useReaderStateMangaContext();
|
||||
const settings = ReaderService.useSettings();
|
||||
|
||||
useDisableAllHotkeysWhileMounted(isOpen);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [isTransparent, setIsTransparent] = useState(false);
|
||||
|
||||
if (!manga) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
onClose={close}
|
||||
hideBackdrop={activeTab === ReaderSettingTab.FILTER}
|
||||
sx={{
|
||||
...applyStyles(isTransparent, {
|
||||
opacity: 0.75,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<ReaderSettingsTabs
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
settings={settings}
|
||||
updateSetting={(...args) => ReaderService.updateSetting(manga, ...args)}
|
||||
deleteSetting={(...args) => ReaderService.deleteSetting(manga, ...args)}
|
||||
setTransparent={setIsTransparent}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user