Custom reader profiles
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 } {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user