Custom reader profiles

This commit is contained in:
schroda
2024-10-28 18:03:29 +01:00
parent 0855fbcf23
commit 0ae9d9ebb2
32 changed files with 655 additions and 106 deletions

View File

@@ -426,6 +426,8 @@
"password": "Password", "password": "Password",
"placeholder": "Placeholder", "placeholder": "Placeholder",
"previous": "Previous", "previous": "Previous",
"profile_one": "Profile",
"profile_other": "Profiles",
"right": "Right", "right": "Right",
"share": "Share", "share": "Share",
"sort": "Sort", "sort": "Sort",
@@ -862,6 +864,10 @@
"title": "Scale type", "title": "Scale type",
"width": "Fit width" "width": "Fit width"
}, },
"profiles": {
"description": "Profiles are used to define the default reader settings which then can be used for series and reading modes",
"placeholder": "Comics | length: 16, chars: letters, numbers, -, _"
},
"progress_bar": { "progress_bar": {
"position": "Progress bar position", "position": "Progress bar position",
"size": "Progress bar size", "size": "Progress bar size",

View File

@@ -67,7 +67,14 @@ const getCategoryMetadataWithDefaultValueFallback = (
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): ICategoryMetadata => ): ICategoryMetadata =>
convertGqlMetadataToAppMetadata( convertGqlMetadataToAppMetadata(
getMetadataFrom('category', meta, convertAppMetadataToGqlMetadata(defaultMetadata), true, useEffectFn), getMetadataFrom(
'category',
meta,
convertAppMetadataToGqlMetadata(defaultMetadata),
undefined,
true,
useEffectFn,
),
); );
const getMetadata = ( const getMetadata = (

View File

@@ -15,13 +15,21 @@ export enum GridLayout {
List = 2, List = 2,
} }
export type ValueToDisplayData<Value extends string | number> = Record< interface DisplayDataTranslationKey {
Value, isTitleString?: never;
{ title: TranslationKey;
title: TranslationKey; icon: ReactNode;
icon: ReactNode; }
}
>; interface DisplayDataString {
isTitleString: true;
title: string;
icon: ReactNode;
}
type DisplayData = DisplayDataTranslationKey | DisplayDataString;
export type ValueToDisplayData<Value extends string | number> = Record<Value, DisplayData>;
export interface MultiValueButtonBaseProps<Value extends string | number> { export interface MultiValueButtonBaseProps<Value extends string | number> {
value: Value; value: Value;

View File

@@ -35,7 +35,9 @@ export const ButtonSelect = <Value extends string | number>({
variant={displayValue === value ? 'contained' : 'outlined'} variant={displayValue === value ? 'contained' : 'outlined'}
startIcon={valueToDisplayData[displayValue].icon} startIcon={valueToDisplayData[displayValue].icon}
> >
{t(valueToDisplayData[displayValue].title)} {valueToDisplayData[displayValue].isTitleString
? valueToDisplayData[displayValue].title
: t(valueToDisplayData[displayValue].title)}
</Button> </Button>
))} ))}
</Stack> </Stack>

View File

@@ -64,7 +64,9 @@ export const ValueRotationButton = <Value extends string | number>({
startIcon={valueToDisplayData[value].icon} startIcon={valueToDisplayData[value].icon}
size="large" size="large"
> >
{t(valueToDisplayData[value].title)} {valueToDisplayData[value].isTitleString
? valueToDisplayData[value].title
: t(valueToDisplayData[value].title)}
</Button> </Button>
); );
}; };

View File

@@ -66,9 +66,9 @@ type MutableListSettingProps = Pick<TextSettingProps, 'settingName' | 'placehold
description?: string; description?: string;
dialogDisclaimer?: JSX.Element | string; dialogDisclaimer?: JSX.Element | string;
addItemButtonTitle?: string; addItemButtonTitle?: string;
handleChange: (values: string[]) => void; handleChange: (values: string[], removedValues: string[]) => void;
allowDuplicates?: boolean; allowDuplicates?: boolean;
validateItem?: (value: string) => boolean; validateItem?: (value: string, tmpValues?: string[]) => boolean;
invalidItemError?: string; invalidItemError?: string;
}; };
@@ -128,7 +128,7 @@ export const MutableListSetting = ({
return; return;
} }
if (!validateItem(newValue)) { if (!validateItem?.(newValue, dialogValues)) {
makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error'); makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error');
return; return;
} }
@@ -138,7 +138,11 @@ export const MutableListSetting = ({
const saveChanges = () => { const saveChanges = () => {
closeDialog(true); closeDialog(true);
handleChange(dialogValues.filter((dialogValue) => dialogValue !== ''));
const updatedValues = dialogValues.filter((dialogValue) => dialogValue !== '');
const removedValues = values.filter((value) => !updatedValues.includes(value));
handleChange(updatedValues, removedValues);
}; };
return ( return (
@@ -193,6 +197,7 @@ export const MutableListSetting = ({
<List> <List>
{dialogValues.map((dialogValue, index) => ( {dialogValues.map((dialogValue, index) => (
<MutableListItem <MutableListItem
key={dialogValue}
settingName="" settingName=""
placeholder={placeholder} placeholder={placeholder}
handleChange={(newValue: string) => updateSetting(index, newValue)} handleChange={(newValue: string) => updateSetting(index, newValue)}
@@ -230,6 +235,7 @@ export const MutableListSetting = ({
handleChange={(newValue: string) => updateSetting(dialogValues.length, newValue)} handleChange={(newValue: string) => updateSetting(dialogValues.length, newValue)}
isDialogOpen={isAddItemDialogOpen} isDialogOpen={isAddItemDialogOpen}
setIsDialogOpen={setIsAddItemDialogOpen} setIsDialogOpen={setIsAddItemDialogOpen}
validate={(value) => validateItem?.(value, dialogValues) ?? true}
/> />
)} )}
</> </>

View File

@@ -47,6 +47,8 @@ export const TextSettingDialog = ({
const [dialogValue, setDialogValue] = useState(value ?? ''); const [dialogValue, setDialogValue] = useState(value ?? '');
const [isValidValue, setIsValidValue] = useState(true); const [isValidValue, setIsValidValue] = useState(true);
const error = !isValidValue && !!dialogValue.length;
const TextFieldComponent = useMemo(() => (isPassword ? PasswordTextField : TextField), [isPassword]); const TextFieldComponent = useMemo(() => (isPassword ? PasswordTextField : TextField), [isPassword]);
useEffect(() => { useEffect(() => {
@@ -85,8 +87,8 @@ export const TextSettingDialog = ({
autoFocus autoFocus
placeholder={placeholder} placeholder={placeholder}
value={dialogValue} value={dialogValue}
error={!isValidValue} error={error}
helperText={!isValidValue ? t('global.error.label.invalid_input') : ''} helperText={error ? t('global.error.label.invalid_input') : ''}
onChange={(e) => { onChange={(e) => {
const newValue = e.target.value; const newValue = e.target.value;

View File

@@ -9,7 +9,7 @@
import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata.types.ts'; import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata.types.ts';
import { ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; import { ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
export const APP_METADATA_KEY_PREFIX = 'webUI_'; export const APP_METADATA_KEY_PREFIX = 'webUI';
const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = { const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
migration: undefined, migration: undefined,
@@ -66,6 +66,9 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
shouldShowPageNumber: undefined, shouldShowPageNumber: undefined,
backgroundColor: undefined, backgroundColor: undefined,
pageGap: undefined, pageGap: undefined,
profiles: undefined,
readingModesDefaultProfile: undefined,
defaultProfile: undefined,
}; };
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT); export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
@@ -131,6 +134,7 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
'exitMode', 'exitMode',
'customFilter', 'customFilter',
'shouldSkipDupChapters', 'shouldSkipDupChapters',
'profiles',
]; ];
/** /**

View File

@@ -31,8 +31,8 @@ export const applyAppKeyPrefixMigration = (meta: Metadata, migration: IMetadataM
const { oldPrefix, newPrefix } = migration.appKeyPrefix; const { oldPrefix, newPrefix } = migration.appKeyPrefix;
const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix); const oldAppMetadata = getAppMetadataFrom(meta, undefined, oldPrefix);
const newAppMetadata = getAppMetadataFrom(meta, newPrefix); const newAppMetadata = getAppMetadataFrom(meta, undefined, newPrefix);
const missingMetadataKeys = Object.keys(oldAppMetadata).filter((key) => !Object.keys(newAppMetadata).includes(key)); const missingMetadataKeys = Object.keys(oldAppMetadata).filter((key) => !Object.keys(newAppMetadata).includes(key));
@@ -54,7 +54,7 @@ const applyMetadataValueMigration = (meta: Metadata, migration: IMetadataMigrati
return migratedMetadata; return migratedMetadata;
} }
const appMetadata = getAppMetadataFrom(meta, appKeyPrefix); const appMetadata = getAppMetadataFrom(meta, undefined, appKeyPrefix);
const metadataValueChanges = migration.values; const metadataValueChanges = migration.values;
metadataValueChanges.forEach(({ key, oldValue, newValue }) => { metadataValueChanges.forEach(({ key, oldValue, newValue }) => {
@@ -72,11 +72,11 @@ const applyMetadataValueMigration = (meta: Metadata, migration: IMetadataMigrati
return; return;
} }
if (!doesMetadataKeyExistIn(meta, key, appKeyPrefix)) { if (!doesMetadataKeyExistIn(meta, key, undefined, appKeyPrefix)) {
return; return;
} }
migrateValue(getMetadataKey(key, appKeyPrefix)); migrateValue(getMetadataKey(key, undefined, appKeyPrefix));
}); });
return migratedMetadata; return migratedMetadata;

View File

@@ -13,7 +13,7 @@ import {
METADATA_MIGRATIONS, METADATA_MIGRATIONS,
VALID_APP_METADATA_KEYS, VALID_APP_METADATA_KEYS,
} from '@/modules/metadata/Metadata.constants.ts'; } from '@/modules/metadata/Metadata.constants.ts';
import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts'; import { getActiveDevice } from '@/modules/device/services/Device.ts';
import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts'; import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts';
import { convertToGqlMeta, convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts'; import { convertToGqlMeta, convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
import { import {
@@ -44,21 +44,44 @@ import { SourceType } from '@/lib/graphql/generated/graphql.ts';
export const extractOriginalKey = (key: string) => key.split('_').slice(-1)[0]; export const extractOriginalKey = (key: string) => key.split('_').slice(-1)[0];
export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => { /**
* Returns the key with the provided prefixes.
*
* In case the key is not a global key ({@link GLOBAL_METADATA_KEYS}), the active device name ({@link getActiveDevice})
* will be added as the second prefix.
*
* **Important**
*
* "default" (case-insensitive) is a reserved value and will be handled as if there is no prefix to add
*
* The format is <prefixes>\_<key>
*
* e.g.: <base_prefix>\_[device_name]\_[optional_prefix1]\_[optional_prefix2]\_<key>
*/
export const getMetadataKey = (key: string, prefixes: string[] = [], appPrefix: string = APP_METADATA_KEY_PREFIX) => {
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys); const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys);
const addActiveDevicePrefix = !isGlobalMetadataKey && getActiveDevice() !== DEFAULT_DEVICE; const addActiveDevicePrefix = !isGlobalMetadataKey;
return `${appPrefix}${addActiveDevicePrefix ? `${getActiveDevice()}_` : ''}${key}`; const finalPrefix = [appPrefix, ...(addActiveDevicePrefix ? [getActiveDevice()] : []), ...prefixes].filter(
(prefix) => prefix.toLowerCase() !== 'default',
);
return `${finalPrefix.join('_')}_${key}`;
}; };
export const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string, appPrefix?: string): boolean => export const doesMetadataKeyExistIn = (
Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix)); meta: Metadata | undefined,
key: string,
prefixes?: string[],
appPrefix?: string,
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, prefixes, appPrefix));
export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>( export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
{ meta }: MetadataHolder, { meta }: MetadataHolder,
key: Key, key: Key,
defaultValue?: Value, defaultValue?: Value,
applyMigrations: boolean = true, prefixes?: string[],
applyMigrations?: boolean,
): Value | undefined => { ): Value | undefined => {
const requiresMigration = Number(meta?.migration) !== METADATA_MIGRATIONS.length; const requiresMigration = Number(meta?.migration) !== METADATA_MIGRATIONS.length;
const doMigration = requiresMigration && applyMigrations; const doMigration = requiresMigration && applyMigrations;
@@ -66,13 +89,13 @@ export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends
if ( if (
metadata === undefined || metadata === undefined ||
!doesMetadataKeyExistIn(metadata, key) || !doesMetadataKeyExistIn(metadata, key, prefixes) ||
metadata[getMetadataKey(key)] === undefined metadata[getMetadataKey(key, prefixes)] === undefined
) { ) {
return defaultValue; return defaultValue;
} }
return convertValueFromMetadata(metadata[getMetadataKey(key)]); return convertValueFromMetadata(metadata[getMetadataKey(key, prefixes)]);
}; };
const getMetadataUpdateFunction = ( const getMetadataUpdateFunction = (
@@ -172,6 +195,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
type: 'global', type: 'global',
metadataHolder: MetadataHolder, metadataHolder: MetadataHolder,
metadataWithDefaultValues: METADATA, metadataWithDefaultValues: METADATA,
prefixes?: string[],
applyMigrations?: boolean, applyMigrations?: boolean,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): METADATA; ): METADATA;
@@ -179,6 +203,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
type: 'manga', type: 'manga',
metadataHolder: MangaIdInfo & MetadataHolder, metadataHolder: MangaIdInfo & MetadataHolder,
metadataWithDefaultValues: METADATA, metadataWithDefaultValues: METADATA,
prefixes?: string[],
applyMigrations?: boolean, applyMigrations?: boolean,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): METADATA; ): METADATA;
@@ -186,6 +211,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
type: 'chapter', type: 'chapter',
metadataHolder: ChapterIdInfo & MetadataHolder, metadataHolder: ChapterIdInfo & MetadataHolder,
metadataWithDefaultValues: METADATA, metadataWithDefaultValues: METADATA,
prefixes?: string[],
applyMigrations?: boolean, applyMigrations?: boolean,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): METADATA; ): METADATA;
@@ -193,6 +219,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
type: 'category', type: 'category',
metadataHolder: CategoryIdInfo & MetadataHolder, metadataHolder: CategoryIdInfo & MetadataHolder,
metadataWithDefaultValues: METADATA, metadataWithDefaultValues: METADATA,
prefixes?: string[],
applyMigrations?: boolean, applyMigrations?: boolean,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): METADATA; ): METADATA;
@@ -200,6 +227,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
type: 'source', type: 'source',
metadataHolder: Pick<SourceType, 'id'> & MetadataHolder, metadataHolder: Pick<SourceType, 'id'> & MetadataHolder,
metadataWithDefaultValues: METADATA, metadataWithDefaultValues: METADATA,
prefixes?: string[],
applyMigrations?: boolean, applyMigrations?: boolean,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): METADATA; ): METADATA;
@@ -212,13 +240,14 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
| (CategoryIdInfo & MetadataHolder) | (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder), | (Pick<SourceType, 'id'> & MetadataHolder),
metadataWithDefaultValues: METADATA, metadataWithDefaultValues: METADATA,
applyMigrations?: boolean, prefixes?: string[],
applyMigrations: boolean = true,
useEffectFn: typeof useEffect = (fn: () => void) => fn(), useEffectFn: typeof useEffect = (fn: () => void) => fn(),
): METADATA { ): METADATA {
const wasMigrated = const wasMigrated =
!!metadataHolder?.meta && !!metadataHolder?.meta &&
!!applyMigrations && applyMigrations &&
Number(getMetadataValueFrom(metadataHolder, 'migration')) !== METADATA_MIGRATIONS.length; Number(getMetadataValueFrom(metadataHolder, 'migration', 0, undefined, true)) !== METADATA_MIGRATIONS.length;
const appMetadata = {} as METADATA; const appMetadata = {} as METADATA;
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => { Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
@@ -226,6 +255,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
metadataHolder, metadataHolder,
key as AppMetadataKeys, key as AppMetadataKeys,
defaultValue, defaultValue,
prefixes,
applyMigrations, applyMigrations,
); );
}); });
@@ -257,11 +287,15 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
return appMetadata; return appMetadata;
} }
export const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => { export const getAppMetadataFrom = (
meta: Metadata,
prefixes: string[] = [],
appPrefix: string = APP_METADATA_KEY_PREFIX,
): Metadata => {
const appMetadata: Metadata = {}; const appMetadata: Metadata = {};
Object.entries(meta).forEach(([key, value]) => { Object.entries(meta).forEach(([key, value]) => {
if (key.startsWith(appPrefix)) { if (key.startsWith([appPrefix, ...prefixes].join('_'))) {
appMetadata[key] = value; appMetadata[key] = value;
} }
}); });

View File

@@ -25,8 +25,9 @@ export const requestUpdateMetadataValue = async (
holderType: MetadataHolderType, holderType: MetadataHolderType,
key: AppMetadataKeys, key: AppMetadataKeys,
value: AllowedMetadataValueTypes, value: AllowedMetadataValueTypes,
keyPrefixes?: string[],
): Promise<void> => { ): Promise<void> => {
const metadataKey = getMetadataKey(key); const metadataKey = getMetadataKey(key, keyPrefixes);
switch (holderType) { switch (holderType) {
case 'category': case 'category':
@@ -54,38 +55,50 @@ export const requestUpdateMetadata = async (
metadataHolder: GqlMetaHolder, metadataHolder: GqlMetaHolder,
holderType: MetadataHolderType, holderType: MetadataHolderType,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => ): Promise<void[]> =>
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value))); Promise.all(
keysToValues.map(([key, value]) =>
requestUpdateMetadataValue(metadataHolder, holderType, key, value, keyPrefixes),
),
);
export const requestUpdateServerMetadata = async (keysToValues: MetadataKeyValuePair[]): Promise<void[]> => export const requestUpdateServerMetadata = async (
requestUpdateMetadata({}, 'global', keysToValues); keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata({}, 'global', keysToValues, keyPrefixes);
export const requestUpdateMangaMetadata = async ( export const requestUpdateMangaMetadata = async (
manga: MangaIdInfo & GqlMetaHolder, manga: MangaIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues); keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues, keyPrefixes);
export const requestUpdateChapterMetadata = async ( export const requestUpdateChapterMetadata = async (
chapter: ChapterIdInfo & GqlMetaHolder, chapter: ChapterIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues); keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues, keyPrefixes);
export const requestUpdateCategoryMetadata = async ( export const requestUpdateCategoryMetadata = async (
category: CategoryIdInfo & GqlMetaHolder, category: CategoryIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues); keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues, keyPrefixes);
export const requestUpdateSourceMetadata = async ( export const requestUpdateSourceMetadata = async (
source: Pick<SourceType, 'id'> & GqlMetaHolder, source: Pick<SourceType, 'id'> & GqlMetaHolder,
keysToValue: MetadataKeyValuePair[], keysToValue: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue); keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue, keyPrefixes);
export const requestDeleteMetadataValue = async ( export const requestDeleteMetadataValue = async (
metadataHolder: GqlMetaHolder, metadataHolder: GqlMetaHolder,
holderType: MetadataHolderType, holderType: MetadataHolderType,
key: AppMetadataKeys, key: AppMetadataKeys,
keyPrefixes?: string[],
): Promise<void> => { ): Promise<void> => {
const metadataKey = getMetadataKey(key); const metadataKey = getMetadataKey(key, keyPrefixes);
switch (holderType) { switch (holderType) {
case 'category': case 'category':
@@ -112,29 +125,34 @@ export async function requestDeleteMetadata(
metadataHolder: GqlMetaHolder, metadataHolder: GqlMetaHolder,
holderType: MetadataHolderType, holderType: MetadataHolderType,
keys: AppMetadataKeys[], keys: AppMetadataKeys[],
keyPrefixes?: string[],
): Promise<void[]> { ): Promise<void[]> {
return Promise.all(keys.map((key) => requestDeleteMetadataValue(metadataHolder, holderType, key))); return Promise.all(keys.map((key) => requestDeleteMetadataValue(metadataHolder, holderType, key, keyPrefixes)));
} }
export const requestDeleteServerMetadata = async (keys: AppMetadataKeys[]): Promise<void[]> => export const requestDeleteServerMetadata = async (keys: AppMetadataKeys[], keyPrefixes?: string[]): Promise<void[]> =>
requestDeleteMetadata({}, 'global', keys); requestDeleteMetadata({}, 'global', keys, keyPrefixes);
export const requestDeleteMangaMetadata = async ( export const requestDeleteMangaMetadata = async (
manga: MangaIdInfo & GqlMetaHolder, manga: MangaIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[], keys: AppMetadataKeys[],
): Promise<void[]> => requestDeleteMetadata(manga, 'manga', keys); keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(manga, 'manga', keys, keyPrefixes);
export const requestDeleteChapterMetadata = async ( export const requestDeleteChapterMetadata = async (
chapter: ChapterIdInfo & GqlMetaHolder, chapter: ChapterIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[], keys: AppMetadataKeys[],
): Promise<void[]> => requestDeleteMetadata(chapter, 'chapter', keys); keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(chapter, 'chapter', keys, keyPrefixes);
export const requestDeleteCategoryMetadata = async ( export const requestDeleteCategoryMetadata = async (
category: CategoryIdInfo & GqlMetaHolder, category: CategoryIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[], keys: AppMetadataKeys[],
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys); keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys, keyPrefixes);
export const requestDeleteSourceMetadata = async ( export const requestDeleteSourceMetadata = async (
source: Pick<SourceType, 'id'> & GqlMetaHolder, source: Pick<SourceType, 'id'> & GqlMetaHolder,
keys: AppMetadataKeys[], keys: AppMetadataKeys[],
): Promise<void[]> => requestDeleteMetadata(source, 'source', keys); keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(source, 'source', keys, keyPrefixes);

View File

@@ -0,0 +1,35 @@
/*
* 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 ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts';
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
import { ValueRotationButton } from '@/modules/core/components/buttons/ValueRotationButton.tsx';
import {
createProfileValueToDisplayData,
getValidReaderProfile,
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
export const ReaderNavBarDesktopProfile = ({
defaultProfile,
profiles,
updateSetting,
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'defaultProfile' | 'profiles'> &
Pick<MultiValueButtonDefaultableProps<string>, 'isDefaultable' | 'onDefault'> & {
updateSetting: (profile: string) => void;
}) => (
<ValueRotationButton
{...buttonSelectInputProps}
value={defaultProfile.isDefault ? undefined : getValidReaderProfile(defaultProfile.value, profiles)}
values={profiles}
setValue={updateSetting}
valueToDisplayData={createProfileValueToDisplayData(profiles, true)}
defaultIcon={<ManageAccountsIcon />}
/>
);

View File

@@ -16,9 +16,18 @@ import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/modules/reader/componen
import { ReaderNavBarDesktopReadingDirection } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx'; import { ReaderNavBarDesktopReadingDirection } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx';
import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts'; import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts';
import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; import { ReaderNavBarDesktopProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { ReaderNavBarDesktopProfile } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopProfile.tsx';
export const ReaderNavBarDesktopQuickSettings = ({ export const ReaderNavBarDesktopQuickSettings = ({
settings: { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldStretchPage, readingDirection }, settings: {
defaultProfile,
profiles,
readingMode,
shouldOffsetDoubleSpreads,
pageScaleMode,
shouldStretchPage,
readingDirection,
},
updateSetting, updateSetting,
openSettings, openSettings,
isDefaultable, isDefaultable,
@@ -28,6 +37,13 @@ export const ReaderNavBarDesktopQuickSettings = ({
return ( return (
<Stack sx={{ gap: 1 }}> <Stack sx={{ gap: 1 }}>
<ReaderNavBarDesktopProfile
defaultProfile={defaultProfile}
profiles={profiles}
updateSetting={(value) => updateSetting('defaultProfile', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('defaultProfile')}
/>
<ReaderNavBarDesktopReadingMode <ReaderNavBarDesktopReadingMode
readingMode={readingMode} readingMode={readingMode}
setReadingMode={(value) => updateSetting('readingMode', value)} setReadingMode={(value) => updateSetting('readingMode', value)}

View File

@@ -12,11 +12,12 @@ import { ReaderSettingReadingDirection } from '@/modules/reader/components/setti
import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { ReaderSettingProfile } from '@/modules/reader/components/settings/layout/profiles/ReaderSettingProfile.tsx';
const DEFAULT_MANGA: MangaIdInfo = { id: -1 }; const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
export const ReaderBottomBarMobileQuickSettings = () => { export const ReaderBottomBarMobileQuickSettings = () => {
const { manga } = useReaderStateMangaContext(); const { manga } = useReaderStateMangaContext();
const { readingMode, readingDirection } = ReaderService.useSettings(); const { defaultProfile, profiles, readingMode, readingDirection } = ReaderService.useSettings();
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? DEFAULT_MANGA); const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? DEFAULT_MANGA);
if (!manga) { if (!manga) {
@@ -25,6 +26,13 @@ export const ReaderBottomBarMobileQuickSettings = () => {
return ( return (
<Stack sx={{ gap: 2 }}> <Stack sx={{ gap: 2 }}>
<ReaderSettingProfile
defaultProfile={defaultProfile}
profiles={profiles}
updateSetting={(value) => ReaderService.updateSetting(manga, 'defaultProfile', value)}
isDefaultable
onDefault={() => deleteSetting('defaultProfile')}
/>
<ReaderSettingReadingMode <ReaderSettingReadingMode
readingMode={readingMode} readingMode={readingMode}
setReadingMode={(value) => ReaderService.updateSetting(manga, 'readingMode', value)} setReadingMode={(value) => ReaderService.updateSetting(manga, 'readingMode', value)}

View File

@@ -21,6 +21,7 @@ import { ReaderLayoutSettings } from '@/modules/reader/components/settings/layou
import { ReaderGeneralSettings } from '@/modules/reader/components/settings/general/ReaderGeneralSettings.tsx'; import { ReaderGeneralSettings } from '@/modules/reader/components/settings/general/ReaderGeneralSettings.tsx';
import { ReaderFilterSettings } from '@/modules/reader/components/settings/filters/ReaderFilterSettings.tsx'; import { ReaderFilterSettings } from '@/modules/reader/components/settings/filters/ReaderFilterSettings.tsx';
import { ReaderBehaviourSettings } from '@/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx'; import { ReaderBehaviourSettings } from '@/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx';
import { ReaderDefaultLayoutSettings } from '@/modules/reader/components/settings/layout/ReaderDefaultLayoutSettings.tsx';
interface BaseProps { interface BaseProps {
activeTab: number; activeTab: number;
@@ -91,13 +92,23 @@ export const ReaderSettingsTabs = ({
switch (id as ReaderSettingTab) { switch (id as ReaderSettingTab) {
case ReaderSettingTab.LAYOUT: case ReaderSettingTab.LAYOUT:
if (areDefaultSettings) {
return (
<TabPanel key={id} index={id} currentIndex={activeTab}>
<ReaderDefaultLayoutSettings
settings={settings}
updateSetting={(...args) => updateSetting(...args)}
/>
</TabPanel>
);
}
return ( return (
<TabPanel key={id} index={id} currentIndex={activeTab}> <TabPanel key={id} index={id} currentIndex={activeTab}>
<ReaderLayoutSettings <ReaderLayoutSettings
settings={settings} settings={settings}
updateSetting={(...args) => updateSetting(...args)} updateSetting={(...args) => updateSetting(...args)}
setShowPreview={setShowPreview!} setShowPreview={setShowPreview!}
// @ts-expect-error - TS2322: Type boolean is not assignable to type true
isDefaultable={!areDefaultSettings} isDefaultable={!areDefaultSettings}
onDefault={(...args) => deleteSetting?.(...args)} onDefault={(...args) => deleteSetting?.(...args)}
/> />

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import Stack from '@mui/material/Stack';
import { ComponentProps } from 'react';
import { ReaderLayoutSettings } from '@/modules/reader/components/settings/layout/ReaderLayoutSettings.tsx';
import { ReaderSettingProfiles } from '@/modules/reader/components/settings/layout/profiles/ReaderSettingProfiles.tsx';
import { ReaderSettingReadingModesDefaultProfile } from '@/modules/reader/components/settings/layout/profiles/ReaderSettingReadingModesDefaultProfile.tsx';
import { ReaderSettingProfileSettings } from '@/modules/reader/components/settings/layout/profiles/ReaderSettingProfileSettings.tsx';
import { DEFAULT_READER_PROFILE } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
export const ReaderDefaultLayoutSettings = (
props: Omit<ComponentProps<typeof ReaderLayoutSettings>, 'setShowPreview'>,
) => {
const { settings, updateSetting } = props;
return (
<Stack sx={{ gap: 2, pb: 2 }}>
<Stack sx={{ gap: 2 }}>
<ReaderSettingProfiles
profiles={settings.profiles}
updateSetting={(profiles, removedProfiles) => {
if (removedProfiles.length) {
updateSetting(
'readingModesDefaultProfile',
Object.fromEntries(
Object.entries(settings.readingModesDefaultProfile).map(
([readingMode, profile]) => [
readingMode,
removedProfiles.includes(profile) ? DEFAULT_READER_PROFILE : profile,
],
),
) as IReaderSettings['readingModesDefaultProfile'],
);
}
updateSetting('profiles', profiles);
}}
/>
<Stack sx={{ px: 2 }}>
<ReaderSettingReadingModesDefaultProfile
profiles={settings.profiles}
readingModesDefaultProfile={settings.readingModesDefaultProfile}
updateSetting={(readingModesDefaultProfile) =>
updateSetting('readingModesDefaultProfile', readingModesDefaultProfile)
}
/>
</Stack>
</Stack>
{settings.profiles.map((profile) => (
<ReaderSettingProfileSettings key={profile} profile={profile} {...props} />
))}
</Stack>
);
};

View File

@@ -18,6 +18,7 @@ import { ReaderSettingStretchPage } from '@/modules/reader/components/settings/l
import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts'; import { ReaderSettingsTypeProps } from '@/modules/reader/types/Reader.types.ts';
import { ReaderSettingPageGap } from '@/modules/reader/components/settings/layout/ReaderSettingPageGap.tsx'; import { ReaderSettingPageGap } from '@/modules/reader/components/settings/layout/ReaderSettingPageGap.tsx';
import { ReaderSettingWidth } from '@/modules/reader/components/settings/layout/ReaderSettingWidth.tsx'; import { ReaderSettingWidth } from '@/modules/reader/components/settings/layout/ReaderSettingWidth.tsx';
import { ReaderSettingProfile } from '@/modules/reader/components/settings/layout/profiles/ReaderSettingProfile.tsx';
export const ReaderLayoutSettings = ({ export const ReaderLayoutSettings = ({
setShowPreview, setShowPreview,
@@ -29,6 +30,15 @@ export const ReaderLayoutSettings = ({
setShowPreview: ContextType<typeof ReaderTapZoneContext>['setShowPreview']; setShowPreview: ContextType<typeof ReaderTapZoneContext>['setShowPreview'];
}) => ( }) => (
<Stack sx={{ gap: 2 }}> <Stack sx={{ gap: 2 }}>
{isDefaultable && (
<ReaderSettingProfile
defaultProfile={settings.defaultProfile}
profiles={settings.profiles}
updateSetting={(value) => updateSetting('defaultProfile', value)}
isDefaultable={isDefaultable}
onDefault={() => onDefault?.('defaultProfile')}
/>
)}
<ReaderSettingReadingMode <ReaderSettingReadingMode
readingMode={settings.readingMode} readingMode={settings.readingMode}
setReadingMode={(value) => updateSetting('readingMode', value)} setReadingMode={(value) => updateSetting('readingMode', value)}

View File

@@ -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 { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts';
import { MultiValueButtonDefaultableProps } from '@/modules/core/Core.types.ts';
import {
createProfileValueToDisplayData,
getValidReaderProfile,
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
export const ReaderSettingProfile = ({
defaultProfile,
profiles,
updateSetting,
...buttonSelectInputProps
}: Pick<IReaderSettingsWithDefaultFlag, 'defaultProfile' | 'profiles'> &
Pick<MultiValueButtonDefaultableProps<string>, 'isDefaultable' | 'onDefault'> & {
updateSetting: (profile: string) => void;
}) => {
const { t } = useTranslation();
return (
<ButtonSelectInput
{...buttonSelectInputProps}
label={t('global.label.profile_one')}
value={defaultProfile.isDefault ? undefined : getValidReaderProfile(defaultProfile.value, profiles)}
values={profiles}
setValue={updateSetting}
valueToDisplayData={createProfileValueToDisplayData(profiles)}
/>
);
};

View File

@@ -0,0 +1,40 @@
/*
* 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 } from 'react';
import { useTranslation } from 'react-i18next';
import { ReaderLayoutSettings } from '@/modules/reader/components/settings/layout/ReaderLayoutSettings.tsx';
import { useDefaultReaderSettingsWithDefaultFlag } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
import { DEFAULT_READER_PROFILE } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingProfileSettings = ({
profile,
updateSetting,
...props
}: Pick<ComponentProps<typeof ReaderLayoutSettings>, 'updateSetting'> & { profile: string }) => {
const { t } = useTranslation();
const { settings } = useDefaultReaderSettingsWithDefaultFlag(profile);
return (
<Stack sx={{ gap: 2 }}>
<ListSubheader component="div" id={`${profile}-settings`}>
{profile === DEFAULT_READER_PROFILE ? t('global.label.standard') : profile}
</ListSubheader>
<Stack sx={{ px: 2 }}>
<ReaderLayoutSettings
{...props}
setShowPreview={() => {}}
settings={settings}
updateSetting={(setting, value, commit) => updateSetting(setting, value, commit, true, profile)}
/>
</Stack>
</Stack>
);
};

View File

@@ -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 { useTranslation } from 'react-i18next';
import { MutableListSetting } from '@/modules/core/components/settings/MutableListSetting.tsx';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
import { DEFAULT_READER_PROFILE } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderSettingProfiles = ({
profiles,
updateSetting,
}: Pick<IReaderSettings, 'profiles'> & {
updateSetting: (profiles: IReaderSettings['profiles'], removedProfiles: string[]) => void;
}) => {
const { t } = useTranslation();
return (
<MutableListSetting
settingName={t('global.label.profile_other')}
description={t('reader.settings.profiles.description')}
handleChange={(profileList, removedProfiles) =>
updateSetting(
[
...new Set(
[DEFAULT_READER_PROFILE, ...profileList].filter(
(device) => device !== t('global.label.standard'),
),
),
],
removedProfiles,
)
}
valueInfos={profiles.map((profile) => [
profile === DEFAULT_READER_PROFILE ? t('global.label.standard') : profile,
{ mutable: false, deletable: profile !== DEFAULT_READER_PROFILE },
])}
addItemButtonTitle={t('global.button.create')}
validateItem={(profileToValidate, tmpProfiles) =>
profileToValidate.length <= 16 &&
!!profileToValidate.match(/^[a-zA-Z0-9\-_]+$/g) &&
![...profiles, ...(tmpProfiles ?? [])]
.map((profile) => profile.toLowerCase())
.includes(profileToValidate.toLowerCase())
}
placeholder={t('reader.settings.profiles.placeholder')}
/>
);
};

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next';
import Typography from '@mui/material/Typography';
import {
READING_MODE_VALUE_TO_DISPLAY_DATA,
READING_MODE_VALUES,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
import { createProfileValueToDisplayData } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
export const ReaderSettingReadingModesDefaultProfile = ({
profiles,
readingModesDefaultProfile,
updateSetting,
}: Pick<IReaderSettings, 'profiles' | 'readingModesDefaultProfile'> & {
updateSetting: (readingModesDefaultProfile: IReaderSettings['readingModesDefaultProfile']) => void;
}) => {
const { t } = useTranslation();
return (
<Stack sx={{ gap: 2 }}>
<Typography>Reading mode default profile</Typography>
{READING_MODE_VALUES.map((readingMode) => (
<Stack key={readingMode} sx={{ gap: 1 }}>
<ButtonSelectInput
label={t(READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode].title)}
value={readingModesDefaultProfile[readingMode]}
values={profiles}
setValue={(profile) =>
updateSetting({
...readingModesDefaultProfile,
[readingMode]: profile,
})
}
valueToDisplayData={createProfileValueToDisplayData(profiles)}
/>
</Stack>
))}
</Stack>
);
};

View File

@@ -33,12 +33,14 @@ import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHoriz
import { TranslationKey } from '@/Base.types.ts'; import { TranslationKey } from '@/Base.types.ts';
import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
export const DEFAULT_READER_PROFILE = 'default';
export const READING_DIRECTION_TO_THEME_DIRECTION: Record<ReadingDirection, Direction> = { export const READING_DIRECTION_TO_THEME_DIRECTION: Record<ReadingDirection, Direction> = {
[ReadingDirection.LTR]: 'ltr', [ReadingDirection.LTR]: 'ltr',
[ReadingDirection.RTL]: 'rtl', [ReadingDirection.RTL]: 'rtl',
}; };
export const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefined> = { const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefined> = {
overlayMode: undefined, overlayMode: undefined,
exitMode: undefined, exitMode: undefined,
customFilter: undefined, customFilter: undefined,
@@ -49,6 +51,8 @@ export const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, u
shouldShowPageNumber: undefined, shouldShowPageNumber: undefined,
isStaticNav: undefined, isStaticNav: undefined,
backgroundColor: undefined, backgroundColor: undefined,
profiles: undefined,
readingModesDefaultProfile: undefined,
}; };
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT); export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
@@ -102,6 +106,14 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
invert: false, invert: false,
}, },
pageGap: 5, pageGap: 5,
profiles: [DEFAULT_READER_PROFILE],
readingModesDefaultProfile: {
[ReadingMode.SINGLE_PAGE]: DEFAULT_READER_PROFILE,
[ReadingMode.DOUBLE_PAGE]: DEFAULT_READER_PROFILE,
[ReadingMode.CONTINUOUS_VERTICAL]: DEFAULT_READER_PROFILE,
[ReadingMode.CONTINUOUS_HORIZONTAL]: DEFAULT_READER_PROFILE,
},
defaultProfile: DEFAULT_READER_PROFILE,
}; };
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = { export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
@@ -153,7 +165,7 @@ export const READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED: Record<ReaderPageScaleMo
[ReaderPageScaleMode.ORIGINAL]: false, [ReaderPageScaleMode.ORIGINAL]: false,
}; };
export const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode> = { export const READING_MODE_VALUE_TO_DISPLAY_DATA = {
[ReadingMode.SINGLE_PAGE]: { [ReadingMode.SINGLE_PAGE]: {
title: 'reader.settings.reader_type.label.single_page', title: 'reader.settings.reader_type.label.single_page',
icon: <SinglePageIcon />, icon: <SinglePageIcon />,
@@ -170,7 +182,8 @@ export const READING_MODE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode>
title: 'reader.settings.reader_type.label.continuous_horizontal', title: 'reader.settings.reader_type.label.continuous_horizontal',
icon: <ContinuousHorizontalPageIcon />, icon: <ContinuousHorizontalPageIcon />,
}, },
}; } satisfies ValueToDisplayData<ReadingMode>;
export const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number'); export const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
export enum ReaderSettingTab { export enum ReaderSettingTab {

View File

@@ -10,11 +10,14 @@ import { ReactNode } from 'react';
import { ReaderStatePagesContextProvider } from '@/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx'; import { ReaderStatePagesContextProvider } from '@/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx';
import { ReaderStateChaptersContextProvider } from '@/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx'; import { ReaderStateChaptersContextProvider } from '@/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx';
import { ReaderStateMangaContextProvider } from '@/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx'; import { ReaderStateMangaContextProvider } from '@/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx';
import { ReaderStateSettingsContextProvider } from '@/modules/reader/contexts/state/ReaderStateSettingsContextProvider.tsx';
export const ReaderStateContextProvider = ({ children }: { children: ReactNode }) => ( export const ReaderStateContextProvider = ({ children }: { children: ReactNode }) => (
<ReaderStateMangaContextProvider> <ReaderStateMangaContextProvider>
<ReaderStateChaptersContextProvider> <ReaderStateChaptersContextProvider>
<ReaderStatePagesContextProvider>{children}</ReaderStatePagesContextProvider> <ReaderStateSettingsContextProvider>
<ReaderStatePagesContextProvider>{children}</ReaderStatePagesContextProvider>
</ReaderStateSettingsContextProvider>
</ReaderStateChaptersContextProvider> </ReaderStateChaptersContextProvider>
</ReaderStateMangaContextProvider> </ReaderStateMangaContextProvider>
); );

View File

@@ -0,0 +1,23 @@
/*
* 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 { createContext, useContext } from 'react';
import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts';
import { DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
type TReaderStateSettingsContext = {
settings: IReaderSettingsWithDefaultFlag;
setSettings: (settings: IReaderSettingsWithDefaultFlag) => void;
};
export const ReaderStateSettingsContext = createContext<TReaderStateSettingsContext>({
settings: DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG,
setSettings: () => undefined,
});
export const useReaderStateSettingsContext = () => useContext(ReaderStateSettingsContext);

View File

@@ -0,0 +1,21 @@
/*
* 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 { ContextType, ReactNode, useMemo, useState } from 'react';
import { ReaderStateSettingsContext } from '@/modules/reader/contexts/state/ReaderStateSettingsContext.tsx';
import { DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
export const ReaderStateSettingsContextProvider = ({ children }: { children: ReactNode }) => {
const [settings, setSettings] = useState<ContextType<typeof ReaderStateSettingsContext>['settings']>(
DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG,
);
const value = useMemo(() => ({ settings, setSettings }), [settings]);
return <ReaderStateSettingsContext.Provider value={value}>{children}</ReaderStateSettingsContext.Provider>;
};

View File

@@ -42,8 +42,9 @@ export const DefaultReaderSettings = () => {
key: Setting, key: Setting,
value: IReaderSettings[Setting], value: IReaderSettings[Setting],
commit?: boolean, commit?: boolean,
profile?: string,
) => { ) => {
ReaderService.updateSetting({ id: -1 }, key, value, commit, true); ReaderService.updateSetting({ id: -1 }, key, value, commit, true, profile);
}; };
if (loading) { if (loading) {
@@ -66,7 +67,7 @@ export const DefaultReaderSettings = () => {
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
areDefaultSettings areDefaultSettings
settings={settings} settings={settings}
updateSetting={(setting, value, commit) => updateSetting(setting, value, commit)} updateSetting={(setting, value, commit, _, profile) => updateSetting(setting, value, commit, profile)}
/> />
); );
}; };

View File

@@ -20,11 +20,7 @@ import {
ReadingDirection, ReadingDirection,
} from '@/modules/reader/types/Reader.types.ts'; } from '@/modules/reader/types/Reader.types.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { import { updateReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
updateReaderSettings,
useDefaultReaderSettings,
useGetReaderSettingsFor,
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { getMetadataKey } from '@/modules/metadata/services/MetadataReader.ts'; import { getMetadataKey } from '@/modules/metadata/services/MetadataReader.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -35,6 +31,7 @@ import { GLOBAL_METADATA } from '@/lib/graphql/fragments/Fragments.ts';
import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts'; import { updateMetadataList } from '@/modules/metadata/services/MetadataApolloCacheHandler.ts';
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
import { GLOBAL_READER_SETTING_KEYS } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; import { GLOBAL_READER_SETTING_KEYS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { useReaderStateSettingsContext } from '@/modules/reader/contexts/state/ReaderStateSettingsContext.tsx';
const DIRECTION_TO_INVERTED: Record<Direction, Direction> = { const DIRECTION_TO_INVERTED: Record<Direction, Direction> = {
ltr: 'rtl', ltr: 'rtl',
@@ -46,8 +43,6 @@ const DIRECTION_TO_READING_DIRECTION: Record<Direction, ReadingDirection> = {
rtl: ReadingDirection.RTL, rtl: ReadingDirection.RTL,
}; };
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
export class ReaderService { export class ReaderService {
static useNavigateToChapter(chapter?: TChapterReader): () => void { static useNavigateToChapter(chapter?: TChapterReader): () => void {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -56,10 +51,7 @@ export class ReaderService {
} }
static useSettings(): IReaderSettingsWithDefaultFlag { static useSettings(): IReaderSettingsWithDefaultFlag {
const { manga } = useReaderStateMangaContext(); return useReaderStateSettingsContext().settings;
const defaultReaderSettings = useDefaultReaderSettings();
return useGetReaderSettingsFor(manga ?? DEFAULT_MANGA, defaultReaderSettings.settings);
} }
static useGetThemeDirection(): Direction { static useGetThemeDirection(): Direction {
@@ -80,12 +72,12 @@ export class ReaderService {
value: IReaderSettings[Setting], value: IReaderSettings[Setting],
commit: boolean = true, commit: boolean = true,
isGlobal: boolean = false, isGlobal: boolean = false,
profile?: string,
): void { ): void {
if (!manga) { if (!manga) {
return; return;
} }
const key = getMetadataKey(setting, profile ? [profile] : undefined);
const key = getMetadataKey(setting);
const { cache } = requestManager.graphQLClient.client; const { cache } = requestManager.graphQLClient.client;
@@ -130,7 +122,7 @@ export class ReaderService {
} }
if (commit) { if (commit) {
updateReaderSettings(manga, setting, value, isGlobal).catch(() => updateReaderSettings(manga, setting, value, isGlobal, profile).catch(() =>
makeToast(translate('reader.settings.error.label.failed_to_save_settings'), 'error'), makeToast(translate('reader.settings.error.label.failed_to_save_settings'), 'error'),
); );
} }
@@ -140,12 +132,13 @@ export class ReaderService {
manga: MangaIdInfo, manga: MangaIdInfo,
setting: Setting, setting: Setting,
isGlobal: boolean = false, isGlobal: boolean = false,
profile?: string,
): void { ): void {
if (!manga) { if (!manga) {
return; return;
} }
const key = getMetadataKey(setting); const key = getMetadataKey(setting, profile ? [profile] : undefined);
const { cache } = requestManager.graphQLClient.client; const { cache } = requestManager.graphQLClient.client;
@@ -166,18 +159,27 @@ export class ReaderService {
static useCreateUpdateSetting<Setting extends keyof IReaderSettings>( static useCreateUpdateSetting<Setting extends keyof IReaderSettings>(
manga: MangaIdInfo, manga: MangaIdInfo,
profile?: string,
): ( ): (
...args: OmitFirst<Parameters<typeof this.updateSetting<Setting>>> ...args: OmitFirst<Parameters<typeof this.updateSetting<Setting>>>
) => ReturnType<typeof this.updateSetting<Setting>> { ) => ReturnType<typeof this.updateSetting<Setting>> {
return useCallback((...args) => this.updateSetting<Setting>(manga, ...args), [manga]); return useCallback(
(setting, value, commit, isGlobal) =>
this.updateSetting<Setting>(manga, setting, value, commit, isGlobal, profile),
[manga, profile],
);
} }
static useCreateDeleteSetting<Setting extends keyof IReaderSettings>( static useCreateDeleteSetting<Setting extends keyof IReaderSettings>(
manga: MangaIdInfo, manga: MangaIdInfo,
profile?: string,
): ( ): (
...args: OmitFirst<Parameters<typeof this.deleteSetting<Setting>>> ...args: OmitFirst<Parameters<typeof this.deleteSetting<Setting>>>
) => ReturnType<typeof this.deleteSetting<Setting>> { ) => ReturnType<typeof this.deleteSetting<Setting>> {
return useCallback((...args) => this.deleteSetting<Setting>(manga, ...args), [manga]); return useCallback(
(setting, isGlobal) => this.deleteSetting<Setting>(manga, setting, isGlobal, profile),
[manga, profile],
);
} }
static useOverlayMode(): { mode: ReaderOverlayMode; isDesktop: boolean; isMobile: boolean } { static useOverlayMode(): { mode: ReaderOverlayMode; isDesktop: boolean; isMobile: boolean } {

View File

@@ -28,12 +28,21 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'
import { jsonSaveParse } from '@/lib/HelperFunctions.ts'; import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { import {
DEFAULT_READER_PROFILE,
DEFAULT_READER_SETTINGS, DEFAULT_READER_SETTINGS,
GLOBAL_READER_SETTING_KEYS, GLOBAL_READER_SETTING_KEYS,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx'; } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts'; import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts';
import { APP_METADATA_KEY_PREFIX } from '@/modules/metadata/Metadata.constants.ts'; import { APP_METADATA_KEY_PREFIX } from '@/modules/metadata/Metadata.constants.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 = ( const convertToSettingsWithDefaultFlag = (
type: Extract<MetadataHolderType, 'global' | 'manga'>, type: Extract<MetadataHolderType, 'global' | 'manga'>,
settings: IReaderSettings, settings: IReaderSettings,
@@ -82,6 +91,8 @@ const convertSettingsToMetadata = (
tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode), tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode),
customFilter: JSON.stringify(settings.customFilter), customFilter: JSON.stringify(settings.customFilter),
readerWidth: JSON.stringify(settings.readerWidth), readerWidth: JSON.stringify(settings.readerWidth),
profiles: JSON.stringify(settings.profiles),
readingModesDefaultProfile: JSON.stringify(settings.readingModesDefaultProfile),
}); });
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag( export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(
@@ -93,30 +104,48 @@ export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDe
const convertMetadataToSettings = ( const convertMetadataToSettings = (
metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>, metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
): IReaderSettings => ({ ): IReaderSettings => {
...(metadata as unknown as IReaderSettings), const profiles =
tapZoneInvertMode: jsonSaveParse<IReaderSettings['profiles']>((metadata.profiles as string) ?? '') ?? defaultSettings.profiles;
jsonSaveParse<IReaderSettings['tapZoneInvertMode']>((metadata.tapZoneInvertMode as string) ?? '') ??
defaultSettings.tapZoneInvertMode,
customFilter:
jsonSaveParse<IReaderSettings['customFilter']>((metadata.customFilter as string) ?? '') ??
defaultSettings.customFilter,
readerWidth:
jsonSaveParse<IReaderSettings['readerWidth']>((metadata.readerWidth as string) ?? '') ??
defaultSettings.readerWidth,
});
const getReaderMetadata = ( return {
...(metadata as unknown as IReaderSettings),
tapZoneInvertMode:
jsonSaveParse<IReaderSettings['tapZoneInvertMode']>((metadata.tapZoneInvertMode as string) ?? '') ??
defaultSettings.tapZoneInvertMode,
customFilter:
jsonSaveParse<IReaderSettings['customFilter']>((metadata.customFilter as string) ?? '') ??
defaultSettings.customFilter,
readerWidth:
jsonSaveParse<IReaderSettings['readerWidth']>((metadata.readerWidth as string) ?? '') ??
defaultSettings.readerWidth,
profiles,
readingModesDefaultProfile: Object.fromEntries(
Object.entries(
jsonSaveParse<IReaderSettings['readingModesDefaultProfile']>(
(metadata.readingModesDefaultProfile as string) ?? '',
) ?? defaultSettings.readingModesDefaultProfile,
).map(([readingMode, profile]) => [
readingMode,
profiles.includes(profile) ? profile : DEFAULT_READER_PROFILE,
]),
) as IReaderSettings['readingModesDefaultProfile'],
};
};
export const getReaderSettings = (
type: Extract<MetadataHolderType, 'global' | 'manga'>, type: Extract<MetadataHolderType, 'global' | 'manga'>,
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS, defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
profile: string = DEFAULT_READER_PROFILE,
) => ) =>
convertMetadataToSettings( convertMetadataToSettings(
getMetadataFrom( getMetadataFrom(
type as Parameters<typeof getMetadataFrom>[0], type as Parameters<typeof getMetadataFrom>[0],
metadataHolder as Parameters<typeof getMetadataFrom>[1], metadataHolder as Parameters<typeof getMetadataFrom>[1],
convertSettingsToMetadata(defaultSettings), convertSettingsToMetadata(defaultSettings),
[profile],
true, true,
useEffectFn, useEffectFn,
), ),
@@ -128,20 +157,23 @@ function getReaderSettingsWithDefaultValueFallback(
metadataHolder: MetadataHolder, metadataHolder: MetadataHolder,
defaultSettings?: IReaderSettings, defaultSettings?: IReaderSettings,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
profile?: string,
): IReaderSettingsWithDefaultFlag; ): IReaderSettingsWithDefaultFlag;
function getReaderSettingsWithDefaultValueFallback( function getReaderSettingsWithDefaultValueFallback(
type: 'manga', type: 'manga',
metadataHolder: MangaIdInfo & MetadataHolder, metadataHolder: MangaIdInfo & MetadataHolder,
defaultSettings?: IReaderSettings, defaultSettings?: IReaderSettings,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
profile?: string,
): IReaderSettingsWithDefaultFlag; ): IReaderSettingsWithDefaultFlag;
function getReaderSettingsWithDefaultValueFallback( function getReaderSettingsWithDefaultValueFallback(
type: Extract<MetadataHolderType, 'global' | 'manga'>, type: Extract<MetadataHolderType, 'global' | 'manga'>,
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS, defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
profile?: string,
): IReaderSettingsWithDefaultFlag { ): IReaderSettingsWithDefaultFlag {
const settings = getReaderMetadata(type, metadataHolder, defaultSettings, useEffectFn); const settings = getReaderSettings(type, metadataHolder, defaultSettings, useEffectFn, profile);
return convertToSettingsWithDefaultFlag(type, settings, metadataHolder); return convertToSettingsWithDefaultFlag(type, settings, metadataHolder);
} }
@@ -149,7 +181,8 @@ const getSettings = (
metaHolder: MangaIdInfo & GqlMetaHolder, metaHolder: MangaIdInfo & GqlMetaHolder,
defaultSettings?: IReaderSettings, defaultSettings?: IReaderSettings,
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
) => profile?: string,
): IReaderSettingsWithDefaultFlag =>
getReaderSettingsWithDefaultValueFallback( getReaderSettingsWithDefaultValueFallback(
'manga', 'manga',
{ {
@@ -161,6 +194,7 @@ const getSettings = (
}, },
defaultSettings, defaultSettings,
useEffectFn, useEffectFn,
profile,
); );
export const getReaderSettingsFor = ( export const getReaderSettingsFor = (
@@ -171,12 +205,15 @@ export const getReaderSettingsFor = (
export const useGetReaderSettingsFor = ( export const useGetReaderSettingsFor = (
metaHolder: MangaIdInfo & GqlMetaHolder, metaHolder: MangaIdInfo & GqlMetaHolder,
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
profile?: string,
): IReaderSettingsWithDefaultFlag => { ): IReaderSettingsWithDefaultFlag => {
const settings = getSettings(metaHolder, defaultSettings, useEffect); const settings = getSettings(metaHolder, defaultSettings, useEffect, profile);
return useMemo(() => settings, [metaHolder, defaultSettings]); return useMemo(() => settings, [metaHolder, defaultSettings, profile]);
}; };
export const useDefaultReaderSettings = (): { export const useDefaultReaderSettings = (
profile?: string,
): {
metadata?: Metadata; metadata?: Metadata;
settings: IReaderSettings; settings: IReaderSettings;
loading: boolean; loading: boolean;
@@ -186,8 +223,8 @@ export const useDefaultReaderSettings = (): {
const { data, loading } = request; const { data, loading } = request;
const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]); const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]);
const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]); const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]);
const tmpSettings = getReaderMetadata('global', metaHolder, undefined, useEffect); const tmpSettings = getReaderSettings('global', metaHolder, undefined, useEffect, profile);
const settings = useMemo(() => tmpSettings, [metaHolder]); const settings = useMemo(() => tmpSettings, [metaHolder, profile]);
return useMemo( return useMemo(
() => ({ () => ({
@@ -200,7 +237,9 @@ export const useDefaultReaderSettings = (): {
); );
}; };
export const useDefaultReaderSettingsWithDefaultFlag = (): { export const useDefaultReaderSettingsWithDefaultFlag = (
profile?: string,
): {
metadata?: Metadata; metadata?: Metadata;
settings: IReaderSettingsWithDefaultFlag; settings: IReaderSettingsWithDefaultFlag;
loading: boolean; loading: boolean;
@@ -210,8 +249,8 @@ export const useDefaultReaderSettingsWithDefaultFlag = (): {
const { data, loading } = request; const { data, loading } = request;
const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]); const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]);
const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]); const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]);
const tmpSettings = getReaderSettingsWithDefaultValueFallback('global', metaHolder, undefined, useEffect); const tmpSettings = getReaderSettingsWithDefaultValueFallback('global', metaHolder, undefined, useEffect, profile);
const settings = useMemo(() => tmpSettings, [metaHolder]); const settings = useMemo(() => tmpSettings, [metaHolder, profile]);
return useMemo( return useMemo(
() => ({ () => ({
@@ -229,18 +268,27 @@ export const updateReaderSettings = async <Setting extends keyof IReaderSettings
setting: Setting, setting: Setting,
value: IReaderSettings[Setting], value: IReaderSettings[Setting],
isGlobal: boolean = false, isGlobal: boolean = false,
profile?: string,
): Promise<void[]> => { ): Promise<void[]> => {
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting); const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
if (isGlobalSetting) { if (isGlobalSetting) {
return requestUpdateServerMetadata([[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); return requestUpdateServerMetadata(
[[setting, convertSettingsToMetadata({ [setting]: value })[setting]]],
profile ? [profile] : undefined,
);
} }
return requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); return requestUpdateMangaMetadata(
manga,
[[setting, convertSettingsToMetadata({ [setting]: value })[setting]]],
profile ? [profile] : undefined,
);
}; };
export const createUpdateReaderSettings = export const createUpdateReaderSettings =
<Settings extends keyof IReaderSettings>( <Settings extends keyof IReaderSettings>(
manga: Pick<MangaType, 'id'> & GqlMetaHolder, manga: Pick<MangaType, 'id'> & GqlMetaHolder,
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'), handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'),
profile?: string,
): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) => ): ((...args: OmitFirst<Parameters<typeof updateReaderSettings<Settings>>>) => Promise<void | void[]>) =>
(setting, value, isGlobal) => (setting, value, isGlobal) =>
updateReaderSettings(manga, setting, value, isGlobal).catch(handleError); updateReaderSettings(manga, setting, value, isGlobal, profile).catch(handleError);

View File

@@ -117,6 +117,8 @@ export interface IReaderSettingsGlobal {
shouldShowPageNumber: boolean; shouldShowPageNumber: boolean;
isStaticNav: boolean; isStaticNav: boolean;
backgroundColor: ReaderBackgroundColor; backgroundColor: ReaderBackgroundColor;
profiles: string[];
readingModesDefaultProfile: Record<ReadingMode, string>;
} }
export interface IReaderSettingsManga { export interface IReaderSettingsManga {
@@ -138,6 +140,7 @@ export interface IReaderSettingsManga {
value: number; value: number;
enabled: boolean; enabled: boolean;
}; };
defaultProfile: string;
} }
export interface IReaderSettings extends IReaderSettingsGlobal, IReaderSettingsManga {} export interface IReaderSettings extends IReaderSettingsGlobal, IReaderSettingsManga {}

View File

@@ -6,7 +6,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { DEFAULT_READER_PROFILE } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
export const isOffsetDoubleSpreadPagesEditable = (readingMode: IReaderSettings['readingMode']): boolean => export const isOffsetDoubleSpreadPagesEditable = (readingMode: IReaderSettings['readingMode']): boolean =>
readingMode === ReadingMode.DOUBLE_PAGE; readingMode === ReadingMode.DOUBLE_PAGE;
export const createProfileValueToDisplayData = (
profiles: string[],
setIcon: boolean = false,
): ValueToDisplayData<string> =>
Object.fromEntries(
profiles.map((profile) => [
profile,
{
isTitleString: profile !== DEFAULT_READER_PROFILE,
title: profile === DEFAULT_READER_PROFILE ? 'global.label.standard' : profile,
icon: setIcon ? <ManageAccountsIcon /> : null,
},
]),
) as ValueToDisplayData<string>;
export const getValidReaderProfile = (profile: string, profiles: string[]): string =>
profiles.includes(profile) ? profile : DEFAULT_READER_PROFILE;

View File

@@ -51,8 +51,9 @@ const getMetadataServerSettingsWithDefaultFallback = (
useEffectFn?: typeof useEffect, useEffectFn?: typeof useEffect,
): MetadataServerSettings => ): MetadataServerSettings =>
convertMetadataToSettings( convertMetadataToSettings(
getMetadataFrom('global', { meta }, convertSettingsToMetadata(defaultSettings), true, useEffectFn), getMetadataFrom('global', { meta }, convertSettingsToMetadata(defaultSettings), undefined, true, useEffectFn),
); );
export const useMetadataServerSettings = (): { export const useMetadataServerSettings = (): {
metadata?: Metadata; metadata?: Metadata;
settings: MetadataServerSettings; settings: MetadataServerSettings;

View File

@@ -48,6 +48,7 @@ const getMetadata = (
'source', 'source',
{ ...metaHolder, meta: convertFromGqlMeta(metaHolder.meta) }, { ...metaHolder, meta: convertFromGqlMeta(metaHolder.meta) },
convertAppMetadataToGqlMetadata(DEFAULT_SOURCE_METADATA), convertAppMetadataToGqlMetadata(DEFAULT_SOURCE_METADATA),
undefined,
true, true,
useEffectFn, useEffectFn,
), ),