Ensure stored client metadata values respect constraints

This commit is contained in:
schroda
2025-06-24 22:22:53 +02:00
parent 09b5f7e558
commit 56f01efc1e
20 changed files with 520 additions and 195 deletions

View File

@@ -0,0 +1,14 @@
/*
* 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/.
*/
export const DOWNLOAD_AHEAD = {
min: 2,
max: 10,
default: 2,
step: 1,
};

View File

@@ -18,10 +18,7 @@ import { makeToast } from '@/modules/core/utils/Toast.ts';
import { MetadataDownloadSettings } from '@/modules/downloads/Downloads.types.ts';
import { MetadataServerSettings } from '@/modules/settings/Settings.types.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
const MIN_LIMIT = 2;
const MAX_LIMIT = 10;
const DEFAULT_LIMIT = MIN_LIMIT;
import { DOWNLOAD_AHEAD } from '@/modules/downloads/Downloads.constants.ts';
export const DownloadAheadSetting = ({
downloadAheadLimit,
@@ -33,7 +30,7 @@ export const DownloadAheadSetting = ({
const shouldDownloadAhead = !!downloadAheadLimit;
const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue(
'lastDownloadAheadLimit',
DEFAULT_LIMIT,
DOWNLOAD_AHEAD.default,
downloadAheadLimit,
getPersistedServerSetting,
);
@@ -63,9 +60,10 @@ export const DownloadAheadSetting = ({
count: currentDownloadAheadLimit,
})}
value={currentDownloadAheadLimit}
minValue={MIN_LIMIT}
maxValue={MAX_LIMIT}
defaultValue={DEFAULT_LIMIT}
minValue={DOWNLOAD_AHEAD.min}
maxValue={DOWNLOAD_AHEAD.max}
defaultValue={DOWNLOAD_AHEAD.default}
stepSize={DOWNLOAD_AHEAD.step}
showSlider
dialogDescription={t('download.settings.download_ahead.label.description')}
dialogDisclaimer={t('download.settings.download_ahead.label.disclaimer')}

View File

@@ -7,103 +7,337 @@
*/
import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata.types.ts';
import { ProgressBarPosition, ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import {
IReaderSettings,
ProgressBarPosition,
ReaderCustomFilter,
ReaderPageScaleMode,
ReadingMode,
} from '@/modules/reader/types/Reader.types.ts';
import {
AUTO_SCROLL_SPEED,
CUSTOM_FILTER,
DEFAULT_READER_SETTINGS,
IMAGE_PRE_LOAD_AMOUNT,
PAGE_GAP,
PROGRESS_BAR_SIZE,
SCROLL_AMOUNT,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { coerceIn } from '@/lib/HelperFunctions.ts';
import { DOWNLOAD_AHEAD } from '@/modules/downloads/Downloads.constants.ts';
import { MANGA_GRID_WIDTH } from '@/modules/settings/Settings.constants.ts';
export const APP_METADATA_KEY_PREFIX = 'webUI';
// At the moment any non-primitive types need to be specified as "string" and handled in the according "MetadataService".
// "auto" can be used to try to automatically convert the value to a specific type (string, number, boolean, undefined, null)
export const APP_METADATA_KEY_TO_TYPE = {
migration: 'number',
deleteChaptersManuallyMarkedRead: 'boolean',
deleteChaptersWhileReading: 'number',
deleteChaptersWithBookmark: 'boolean',
downloadAheadLimit: 'number',
showAddToLibraryCategorySelectDialog: 'boolean',
ignoreFilters: 'boolean',
removeMangaFromCategories: 'boolean',
showTabSize: 'boolean',
devices: 'string', // string[]
migrateChapters: 'boolean',
migrateCategories: 'boolean',
migrateTracking: 'boolean',
deleteChapters: 'boolean',
migrateSortSettings: 'string', // SortSettings (object)
hideLibraryEntries: 'boolean',
updateProgressAfterReading: 'boolean',
updateProgressManualMarkRead: 'boolean',
webUIInformAvailableUpdate: 'boolean',
serverInformAvailableUpdate: 'boolean',
readerWidth: 'string', // object
savedSearches: 'string', // object
showContinueReadingButton: 'boolean',
showDownloadBadge: 'boolean',
showUnreadBadge: 'boolean',
gridLayout: 'number', // GridLayout (enum)
sortBy: 'auto', // LibrarySortMode undefined null
sortDesc: 'auto', // boolean undefined null
hasDownloadedChapters: 'auto', // boolean undefined null
hasBookmarkedChapters: 'auto', // boolean undefined null
hasUnreadChapters: 'auto', // boolean undefined null
hasReadChapters: 'auto', // boolean undefined null
hasDuplicateChapters: 'auto', // boolean undefined null
hasTrackerBinding: 'string', // object
hasStatus: 'string', // object
customThemes: 'string', // object
mangaThumbnailBackdrop: 'boolean',
mangaDynamicColorSchemes: 'boolean',
tapZoneLayout: 'number', // TapZoneLayouts (enum)
tapZoneInvertMode: 'string', // TapZoneInvertMode (object)
readingDirection: 'number', // ReadingDirection (enum)
progressBarType: 'number', // ProgressBarType (enum)
progressBarSize: 'number',
progressBarPosition: 'number', // ProgressBarPosition (enum)
progressBarPositionAutoVertical: 'number', // TProgressBarPositionAutoVertical (enum)
readingMode: 'number', // ReadingMode (enum)
pageScaleMode: 'number', // ReaderPageScaleMode (enum)
shouldOffsetDoubleSpreads: 'boolean',
exitMode: 'number', // ReaderExitMode (enum)
customFilter: 'string', // ReaderCustomFilter (object)
shouldSkipDupChapters: 'boolean',
isStaticNav: 'boolean',
overlayMode: 'number', // ReaderOverlayMode (enum)
shouldStretchPage: 'boolean',
shouldShowPageNumber: 'boolean',
backgroundColor: 'number', // ReaderBackgroundColor (enum)
pageGap: 'number',
hotkeys: 'string', // object
imagePreLoadAmount: 'number',
shouldUseAutoWebtoonMode: 'boolean',
autoScroll: 'string', // object
shouldShowReadingModePreview: 'boolean',
shouldShowTapZoneLayoutPreview: 'boolean',
shouldInformAboutMissingChapter: 'boolean',
shouldInformAboutScanlatorChange: 'boolean',
hideHistory: 'boolean',
scrollAmount: 'number', // ReaderScrollAmount (enum)
reverse: 'boolean',
bookmarked: 'auto', // boolean undefined null
downloaded: 'auto', // boolean undefined null
unread: 'auto', // boolean undefined null
showChapterNumber: 'boolean',
extensionLanguages: 'string', // string[]
sourceLanguages: 'string', // string[]
showNsfw: 'boolean',
shouldUseInfiniteScroll: 'boolean',
shouldShowTransitionPage: 'boolean',
appTheme: 'string',
themeMode: 'string', // ThemeMode (enum)
shouldUsePureBlackMode: 'boolean',
mangaGridItemWidth: 'number',
isPinned: 'boolean',
isEnabled: 'boolean',
lastUsedSourceId: 'string',
shouldShowOnlySourcesWithResults: 'boolean',
excludedScanlators: 'string', // string[]
} as const satisfies Record<AppMetadataKeys, 'auto' | 'string' | 'number' | 'boolean'>;
export const APP_METADATA: Record<
AppMetadataKeys,
{
type: 'auto' | 'string' | 'number' | 'boolean';
toValidValue?: (value: any) => any;
}
> = {
migration: {
type: 'number',
},
deleteChaptersManuallyMarkedRead: {
type: 'boolean',
},
deleteChaptersWhileReading: {
type: 'number',
},
deleteChaptersWithBookmark: {
type: 'boolean',
},
downloadAheadLimit: {
type: 'number',
toValidValue: (value: number) => coerceIn(value, DOWNLOAD_AHEAD.min, DOWNLOAD_AHEAD.max),
},
showAddToLibraryCategorySelectDialog: {
type: 'boolean',
},
ignoreFilters: {
type: 'boolean',
},
removeMangaFromCategories: {
type: 'boolean',
},
showTabSize: {
type: 'boolean',
},
devices: {
type: 'string', // string[]
},
migrateChapters: {
type: 'boolean',
},
migrateCategories: {
type: 'boolean',
},
migrateTracking: {
type: 'boolean',
},
deleteChapters: {
type: 'boolean',
},
migrateSortSettings: {
type: 'string', // SortSettings (object)
},
hideLibraryEntries: {
type: 'boolean',
},
updateProgressAfterReading: {
type: 'boolean',
},
updateProgressManualMarkRead: {
type: 'boolean',
},
webUIInformAvailableUpdate: {
type: 'boolean',
},
serverInformAvailableUpdate: {
type: 'boolean',
},
readerWidth: {
type: 'string', // object
},
savedSearches: {
type: 'string', // object
},
showContinueReadingButton: {
type: 'boolean',
},
showDownloadBadge: {
type: 'boolean',
},
showUnreadBadge: {
type: 'boolean',
},
gridLayout: {
type: 'number', // GridLayout (enum)
},
sortBy: {
type: 'auto', // LibrarySortMode undefined null
},
sortDesc: {
type: 'auto', // boolean undefined null
},
hasDownloadedChapters: {
type: 'auto', // boolean undefined null
},
hasBookmarkedChapters: {
type: 'auto', // boolean undefined null
},
hasUnreadChapters: {
type: 'auto', // boolean undefined null
},
hasReadChapters: {
type: 'auto', // boolean undefined null
},
hasDuplicateChapters: {
type: 'auto', // boolean undefined null
},
hasTrackerBinding: {
type: 'string', // object
},
hasStatus: {
type: 'string', // object
},
customThemes: {
type: 'string', // object
},
mangaThumbnailBackdrop: {
type: 'boolean',
},
mangaDynamicColorSchemes: {
type: 'boolean',
},
tapZoneLayout: {
type: 'number', // TapZoneLayouts (enum)
},
tapZoneInvertMode: {
type: 'string', // TapZoneInvertMode (object)
},
readingDirection: {
type: 'number', // ReadingDirection (enum)
},
progressBarType: {
type: 'number', // ProgressBarType (enum)
},
progressBarSize: {
type: 'number',
toValidValue: (value: number) => coerceIn(value, PROGRESS_BAR_SIZE.min, PROGRESS_BAR_SIZE.max),
},
progressBarPosition: {
type: 'number', // ProgressBarPosition (enum)
},
progressBarPositionAutoVertical: {
type: 'number', // TProgressBarPositionAutoVertical (enum)
},
readingMode: {
type: 'number', // ReadingMode (enum)
},
pageScaleMode: {
type: 'number', // ReaderPageScaleMode (enum)
},
shouldOffsetDoubleSpreads: {
type: 'boolean',
},
exitMode: {
type: 'number', // ReaderExitMode (enum)
},
customFilter: {
type: 'string', // ReaderCustomFilter (object)
toValidValue: (value: ReaderCustomFilter): ReaderCustomFilter => ({
...value,
brightness: {
...value.brightness,
value: coerceIn(value.brightness.value, CUSTOM_FILTER.brightness.min, CUSTOM_FILTER.brightness.max),
},
contrast: {
...value.contrast,
value: coerceIn(value.contrast.value, CUSTOM_FILTER.contrast.min, CUSTOM_FILTER.contrast.max),
},
saturate: {
...value.saturate,
value: coerceIn(value.saturate.value, CUSTOM_FILTER.saturate.min, CUSTOM_FILTER.saturate.max),
},
hue: {
...value.hue,
value: coerceIn(value.hue.value, CUSTOM_FILTER.hue.min, CUSTOM_FILTER.hue.max),
},
rgba: {
...value.rgba,
value: {
...value.rgba.value,
red: coerceIn(value.rgba.value.red, CUSTOM_FILTER.rgba.red.min, CUSTOM_FILTER.rgba.red.max),
green: coerceIn(value.rgba.value.green, CUSTOM_FILTER.rgba.green.min, CUSTOM_FILTER.rgba.green.max),
blue: coerceIn(value.rgba.value.blue, CUSTOM_FILTER.rgba.blue.min, CUSTOM_FILTER.rgba.blue.max),
alpha: coerceIn(value.rgba.value.alpha, CUSTOM_FILTER.rgba.alpha.min, CUSTOM_FILTER.rgba.alpha.max),
},
},
}),
},
shouldSkipDupChapters: {
type: 'boolean',
},
isStaticNav: {
type: 'boolean',
},
overlayMode: {
type: 'number', // ReaderOverlayMode (enum)
},
shouldStretchPage: {
type: 'boolean',
},
shouldShowPageNumber: {
type: 'boolean',
},
backgroundColor: {
type: 'number', // ReaderBackgroundColor (enum)
},
pageGap: {
type: 'number',
toValidValue: (value: number) => coerceIn(value, PAGE_GAP.min, PAGE_GAP.max),
},
hotkeys: {
type: 'string', // object
},
imagePreLoadAmount: {
type: 'number',
toValidValue: (value: number) => coerceIn(value, IMAGE_PRE_LOAD_AMOUNT.min, IMAGE_PRE_LOAD_AMOUNT.max),
},
shouldUseAutoWebtoonMode: {
type: 'boolean',
},
autoScroll: {
type: 'string', // object
toValidValue: (value: IReaderSettings['autoScroll']): IReaderSettings['autoScroll'] => ({
...value,
value: coerceIn(value.value, AUTO_SCROLL_SPEED.min, AUTO_SCROLL_SPEED.max),
}),
},
shouldShowReadingModePreview: {
type: 'boolean',
},
shouldShowTapZoneLayoutPreview: {
type: 'boolean',
},
shouldInformAboutMissingChapter: {
type: 'boolean',
},
shouldInformAboutScanlatorChange: {
type: 'boolean',
},
hideHistory: {
type: 'boolean',
},
scrollAmount: {
type: 'number', // ReaderScrollAmount (enum)
toValidValue: (value: number) => coerceIn(value, SCROLL_AMOUNT.min, SCROLL_AMOUNT.max),
},
reverse: {
type: 'boolean',
},
bookmarked: {
type: 'auto', // boolean undefined null
},
downloaded: {
type: 'auto', // boolean undefined null
},
unread: {
type: 'auto', // boolean undefined null
},
showChapterNumber: {
type: 'boolean',
},
extensionLanguages: {
type: 'string', // string[]
},
sourceLanguages: {
type: 'string', // string[]
},
showNsfw: {
type: 'boolean',
},
shouldUseInfiniteScroll: {
type: 'boolean',
},
shouldShowTransitionPage: {
type: 'boolean',
},
appTheme: {
type: 'string',
},
themeMode: {
type: 'string', // ThemeMode (enum)
},
shouldUsePureBlackMode: {
type: 'boolean',
},
mangaGridItemWidth: {
type: 'number',
toValidValue: (value: number) => coerceIn(value, MANGA_GRID_WIDTH.min, MANGA_GRID_WIDTH.max),
},
isPinned: {
type: 'boolean',
},
isEnabled: {
type: 'boolean',
},
lastUsedSourceId: {
type: 'string',
},
shouldShowOnlySourcesWithResults: {
type: 'boolean',
},
excludedScanlators: {
type: 'string', // string[]
},
} as const;
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_KEY_TO_TYPE);
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA);
export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
// metadata applied migration id

View File

@@ -8,13 +8,13 @@
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
import { AllowedMetadataValueTypes, AppMetadataKeys, Metadata } from '@/modules/metadata/Metadata.types.ts';
import { APP_METADATA_KEY_TO_TYPE } from '@/modules/metadata/Metadata.constants.ts';
import { APP_METADATA } from '@/modules/metadata/Metadata.constants.ts';
export const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
key: string,
value: string,
): T => {
const typeOfKey = APP_METADATA_KEY_TO_TYPE[key as AppMetadataKeys];
const typeOfKey = APP_METADATA[key as AppMetadataKeys].type;
const isAutoType = typeOfKey === 'auto';
if ((isAutoType && !Number.isNaN(+value)) || typeOfKey === 'number') {

View File

@@ -22,6 +22,7 @@ import { doesMetadataKeyExistIn, getMetadataKey } from '@/modules/metadata/Metad
import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts';
import { SourceIdInfo } from '@/modules/source/Source.types.ts';
import { ChapterIdInfo } from '@/modules/chapter/Chapter.types.ts';
import { APP_METADATA } from '@/modules/metadata/Metadata.constants.ts';
const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
metadata: Metadata | undefined,
@@ -37,7 +38,9 @@ const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends Allowed
return defaultValue;
}
return convertValueFromMetadata(key, metadata[getMetadataKey(key, prefixes)]);
const convertedValue = convertValueFromMetadata(key, metadata[getMetadataKey(key, prefixes)]);
return APP_METADATA[key].toValidValue?.(convertedValue) ?? convertedValue;
};
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(

View File

@@ -16,7 +16,10 @@ import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.ts
import { ReaderSettingExitMode } from '@/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx';
import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import {
DEFAULT_READER_SETTINGS,
IMAGE_PRE_LOAD_AMOUNT,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { ReaderSettingAutoScroll } from '@/modules/reader/components/settings/behaviour/ReaderSettingAutoScroll.tsx';
import { ReaderSettingScrollAmount } from '@/modules/reader/components/settings/behaviour/ReaderSettingScrollAmount.tsx';
@@ -129,9 +132,9 @@ export const ReaderBehaviourSettings = ({
slider: {
defaultValue: DEFAULT_READER_SETTINGS.imagePreLoadAmount,
value: settings.imagePreLoadAmount,
step: 1,
min: 1,
max: 20,
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);
},

View File

@@ -7,15 +7,11 @@
*/
import { useTranslation } from 'react-i18next';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderScrollAmount } from '@/modules/reader/types/Reader.types.ts';
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
import {
ReaderScrollAmount,
DEFAULT_READER_SETTINGS,
SCROLL_AMOUNT,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { DEFAULT_READER_SETTINGS, SCROLL_AMOUNT } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderScrollAmount> = {
[ReaderScrollAmount.TINY]: {

View File

@@ -12,7 +12,7 @@ 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';
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingBrightness = ({
brightness,
@@ -48,9 +48,9 @@ export const ReaderSettingBrightness = ({
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.brightness.value,
value: brightness.value,
step: 1,
min: 5,
max: 200,
step: CUSTOM_FILTER.brightness.step,
min: CUSTOM_FILTER.brightness.min,
max: CUSTOM_FILTER.brightness.max,
onChange: (_, value) => {
updateSetting('brightness', { ...brightness, value: value as number }, false);
},

View File

@@ -11,7 +11,7 @@ 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';
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingContrast = ({
contrast,
@@ -47,9 +47,9 @@ export const ReaderSettingContrast = ({
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.contrast.value,
value: contrast.value,
step: 1,
min: 5,
max: 200,
step: CUSTOM_FILTER.contrast.step,
min: CUSTOM_FILTER.contrast.min,
max: CUSTOM_FILTER.contrast.max,
onChange: (_, newValue) => {
updateSetting('contrast', { ...contrast, value: newValue as number }, false);
},

View File

@@ -11,7 +11,7 @@ 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';
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingHue = ({
hue,
@@ -43,9 +43,9 @@ export const ReaderSettingHue = ({
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.hue.value,
value: hue.value,
step: 1,
min: 0,
max: 200,
step: CUSTOM_FILTER.hue.step,
min: CUSTOM_FILTER.hue.min,
max: CUSTOM_FILTER.hue.max,
onChange: (_, newValue) => {
updateSetting('hue', { ...hue, value: newValue as number }, false);
},

View File

@@ -14,6 +14,7 @@ import { TranslationKey } from '@/Base.types.ts';
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
import {
CUSTOM_FILTER,
DEFAULT_READER_SETTINGS,
READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA,
READER_BLEND_MODE_VALUES,
@@ -29,13 +30,6 @@ const RGBA_TYPE_TO_TRANSLATION_KEY: Record<RGBAType, TranslationKey> = {
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,
@@ -84,9 +78,9 @@ export const ReaderSettingRGBA = ({
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],
step: CUSTOM_FILTER.rgba[key as RGBAType].step,
min: CUSTOM_FILTER.rgba[key as RGBAType].min,
max: CUSTOM_FILTER.rgba[key as RGBAType].max,
onChange: (_, newValue) => {
updateSetting(
'rgba',

View File

@@ -11,7 +11,7 @@ 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';
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingSaturate = ({
saturate,
@@ -47,9 +47,9 @@ export const ReaderSettingSaturate = ({
slider: {
defaultValue: DEFAULT_READER_SETTINGS.customFilter.saturate.value,
value: saturate.value,
step: 1,
min: 0,
max: 200,
step: CUSTOM_FILTER.saturate.step,
min: CUSTOM_FILTER.saturate.min,
max: CUSTOM_FILTER.saturate.max,
onChange: (_, value) => {
updateSetting('saturate', { ...saturate, value: value as number }, false);
},

View File

@@ -9,7 +9,7 @@
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';
import { AUTO_SCROLL_SPEED, DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingProgressBarSize = ({
overlayMode,
@@ -37,9 +37,9 @@ export const ReaderSettingProgressBarSize = ({
slider: {
defaultValue: DEFAULT_READER_SETTINGS.progressBarSize,
value: progressBarSize,
step: 1,
min: 2,
max: 20,
step: AUTO_SCROLL_SPEED.step,
min: AUTO_SCROLL_SPEED.min,
max: AUTO_SCROLL_SPEED.max,
onChange: (_, value) => {
setProgressBarSize(value as number, false);
},

View File

@@ -9,7 +9,7 @@
import { useTranslation } from 'react-i18next';
import { IReaderSettings, 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';
import { DEFAULT_READER_SETTINGS, PAGE_GAP } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
@@ -37,11 +37,11 @@ export const ReaderSettingPageGap = ({
onDefault={isDefaultable ? onDefault : undefined}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.readerWidth.value,
defaultValue: DEFAULT_READER_SETTINGS.pageGap,
value: pageGap.value,
step: 1,
min: 0,
max: 20,
step: PAGE_GAP.step,
min: PAGE_GAP.min,
max: PAGE_GAP.max,
onChange: (_, value) => {
updateSetting(value as number, false);
},

View File

@@ -25,6 +25,7 @@ import {
ReaderHotkey,
ReaderOverlayMode,
ReaderPageScaleMode,
ReaderScrollAmount,
ReadingDirection,
ReadingMode,
} from '@/modules/reader/types/Reader.types.ts';
@@ -36,15 +37,93 @@ import { TranslationKey } from '@/Base.types.ts';
import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
import { WebtoonPageIcon } from '@/assets/icons/svg/WebtoonPageIcon.tsx';
/**
* percentage values
*/
export enum ReaderScrollAmount {
TINY = 10,
SMALL = 25,
MEDIUM = 75,
LARGE = 95,
}
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',
@@ -84,7 +163,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
tapZoneLayout: TapZoneLayouts.RIGHT_LEFT,
tapZoneInvertMode: { vertical: false, horizontal: false },
progressBarType: ProgressBarType.STANDARD,
progressBarSize: 4,
progressBarSize: PROGRESS_BAR_SIZE.default,
progressBarPosition: ProgressBarPosition.AUTO,
progressBarPositionAutoVertical: ProgressBarPosition.RIGHT,
pageScaleMode: ReaderPageScaleMode.ORIGINAL,
@@ -99,27 +178,27 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
backgroundColor: ReaderBackgroundColor.THEME,
customFilter: {
brightness: {
value: 100,
value: CUSTOM_FILTER.brightness.default,
enabled: false,
},
contrast: {
value: 100,
value: CUSTOM_FILTER.contrast.default,
enabled: false,
},
saturate: {
value: 100,
value: CUSTOM_FILTER.saturate.default,
enabled: false,
},
hue: {
value: 0,
value: CUSTOM_FILTER.hue.default,
enabled: false,
},
rgba: {
value: {
red: 0,
green: 0,
blue: 0,
alpha: 0,
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,
@@ -128,7 +207,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
grayscale: false,
invert: false,
},
pageGap: 5,
pageGap: PAGE_GAP.default,
hotkeys: {
[ReaderHotkey.PREVIOUS_PAGE]: ['arrowleft', 'a'],
[ReaderHotkey.NEXT_PAGE]: ['arrowright', 'd'],
@@ -147,10 +226,10 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: ['v'],
[ReaderHotkey.EXIT_READER]: ['c'],
},
imagePreLoadAmount: 5,
imagePreLoadAmount: IMAGE_PRE_LOAD_AMOUNT.default,
shouldUseAutoWebtoonMode: true,
autoScroll: {
value: 5,
value: AUTO_SCROLL_SPEED.default,
smooth: true,
},
shouldShowReadingModePreview: true,
@@ -303,18 +382,6 @@ export const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<
[ReadingMode.WEBTOON]: ScrollDirection.Y,
};
export const AUTO_SCROLL_SPEED = {
min: 0.5,
max: 60,
step: 0.5,
};
export const SCROLL_AMOUNT = {
min: 5,
max: 100,
step: 5,
};
export const READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA = {
[ReaderBlendMode.DEFAULT]: {
title: 'reader.settings.custom_filter.rgba.blend_mode.default',

View File

@@ -9,16 +9,18 @@
import { memo, ReactNode, useCallback, useMemo, useState } from 'react';
import { Direction, useTheme } from '@mui/material/styles';
import { useAutomaticScrolling } from '@/modules/core/hooks/useAutomaticScrolling.ts';
import { IReaderSettings, ReadingMode, TReaderAutoScrollContext } from '@/modules/reader/types/Reader.types.ts';
import {
IReaderSettings,
ReaderScrollAmount,
ReadingMode,
TReaderAutoScrollContext,
} from '@/modules/reader/types/Reader.types.ts';
import { ReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
import { ReaderControls } from '@/modules/reader/services/ReaderControls';
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import {
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION,
ReaderScrollAmount,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
const BaseReaderAutoScrollContextProvider = ({
children,

View File

@@ -24,15 +24,13 @@ import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/R
import {
PageInViewportType,
ProgressBarPosition,
ReaderScrollAmount,
ReaderTransitionPageMode,
ReadingDirection,
ReadingMode,
} from '@/modules/reader/types/Reader.types.ts';
import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts';
import {
ReaderScrollAmount,
READING_DIRECTION_TO_THEME_DIRECTION,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import {
isATransitionPageVisible,
isEndOfPageInViewport,

View File

@@ -14,7 +14,6 @@ import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types
import { TMangaReader } from '@/modules/manga/Manga.types.ts';
import { useAutomaticScrolling } from '@/modules/core/hooks/useAutomaticScrolling.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { ReaderScrollAmount } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export enum ProgressBarType {
HIDDEN,
@@ -130,6 +129,16 @@ export interface ReaderCustomFilter {
invert: boolean;
}
/**
* percentage values
*/
export enum ReaderScrollAmount {
TINY = 10,
SMALL = 25,
MEDIUM = 75,
LARGE = 95,
}
export interface IReaderSettingsGlobal {
overlayMode: ReaderOverlayMode;
exitMode: ReaderExitMode;

View File

@@ -13,6 +13,13 @@ import { GridLayout } from '@/modules/core/Core.types.ts';
import { getDefaultLanguages } from '@/modules/core/utils/Languages.ts';
import { ThemeMode } from '@/modules/theme/contexts/AppThemeContext.tsx';
export const MANGA_GRID_WIDTH = {
min: 100,
max: 1000,
step: 10,
default: 300,
};
export const SERVER_SETTINGS_METADATA_DEFAULT: MetadataServerSettings = {
// downloads
deleteChaptersManuallyMarkedRead: false,
@@ -66,5 +73,5 @@ export const SERVER_SETTINGS_METADATA_DEFAULT: MetadataServerSettings = {
customThemes: {},
mangaThumbnailBackdrop: true,
mangaDynamicColorSchemes: true,
mangaGridItemWidth: 300,
mangaGridItemWidth: MANGA_GRID_WIDTH.default,
};

View File

@@ -34,7 +34,7 @@ import { MetadataThemeSettings } from '@/modules/theme/AppTheme.types.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { AppStorage } from '@/lib/storage/AppStorage.ts';
import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts';
import { SERVER_SETTINGS_METADATA_DEFAULT } from '@/modules/settings/Settings.constants.ts';
import { MANGA_GRID_WIDTH, SERVER_SETTINGS_METADATA_DEFAULT } from '@/modules/settings/Settings.constants.ts';
export const Appearance = () => {
const { t, i18n } = useTranslation();
@@ -155,9 +155,9 @@ export const Appearance = () => {
settingValue={`px: ${mangaGridItemWidth}`}
value={mangaGridItemWidth}
defaultValue={SERVER_SETTINGS_METADATA_DEFAULT.mangaGridItemWidth}
minValue={100}
maxValue={1000}
stepSize={10}
minValue={MANGA_GRID_WIDTH.min}
maxValue={MANGA_GRID_WIDTH.max}
stepSize={MANGA_GRID_WIDTH.step}
valueUnit="px"
showSlider
handleUpdate={(width) => updateMetadataSetting('mangaGridItemWidth', width)}