/* * Copyright (C) Contributors to the Suwayomi project * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { useEffect, useMemo } from 'react'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestMangaMetadataUpdate, requestServerMetadataUpdate, } from '@/features/metadata/services/MetadataUpdater.ts'; import type { MangaType } from '@/lib/graphql/generated/graphql.ts'; import type { IReaderSettings, IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts'; import { convertFromGqlMeta } from '@/features/metadata/services/MetadataConverter.ts'; import { getMetadataFrom } from '@/features/metadata/services/MetadataReader.ts'; import type { AllowedMetadataValueTypes, GqlMetaHolder, Metadata, MetadataHolder, MetadataHolderType, } from '@/features/metadata/Metadata.types.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; import { DEFAULT_READER_SETTINGS, GLOBAL_READER_SETTING_KEYS, } from '@/features/reader/settings/ReaderSettings.constants.tsx'; import { DEFAULT_DEVICE, getActiveDevice } from '@/features/device/services/Device.ts'; import { APP_METADATA_KEY_PREFIX } from '@/features/metadata/Metadata.constants.ts'; import { extractOriginalKey } from '@/features/metadata/Metadata.utils.ts'; const convertToSettingsWithDefaultFlag = ( type: Extract, settings: IReaderSettings, metadataHolder: MetadataHolder, ): IReaderSettingsWithDefaultFlag => { const activeDevice = getActiveDevice(); const istDefaultDevice = activeDevice === DEFAULT_DEVICE; const existingSettings = Object.keys(metadataHolder.meta ?? {}) // settings that are not for the active device need to be filtered out, otherwise, they mess up the "isDefault" flag .filter((metaKey) => { // the default device is not added as a prefix to the key, thus, there should only be the app prefix for these reader settings if (istDefaultDevice) { return metaKey.match(/_/g)?.length === 1; } return metaKey.startsWith(`${APP_METADATA_KEY_PREFIX}_${activeDevice}_`); }) .map((metaKey) => extractOriginalKey(metaKey)); const settingsWithDefault = Object.fromEntries( (Object.entries(settings) as [keyof IReaderSettings, IReaderSettings[keyof IReaderSettings]][]).map( ([key, value]) => { const isGlobalSetting = GLOBAL_READER_SETTING_KEYS.includes(key); if (isGlobalSetting) { return [key, value]; } const isDefaultSetting = type === 'manga' && !existingSettings.includes(key); return [ key, { value, isDefault: isDefaultSetting, }, ]; }, ), ) as IReaderSettingsWithDefaultFlag; return settingsWithDefault; }; const convertSettingsToMetadata = ( settings: Partial, ): Metadata => ({ ...settings, tapZoneInvertMode: JSON.stringify(settings.tapZoneInvertMode), customFilter: JSON.stringify(settings.customFilter), readerWidth: JSON.stringify(settings.readerWidth), hotkeys: JSON.stringify(settings.hotkeys), autoScroll: JSON.stringify(settings.autoScroll), safeAreaInset: JSON.stringify(settings.autoScroll), }); export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag( 'global', DEFAULT_READER_SETTINGS, { meta: convertSettingsToMetadata(DEFAULT_READER_SETTINGS) as Metadata }, ); export const getReaderSettings = ( type: Extract, metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS, useEffectFn?: typeof useEffect, profile?: ReadingMode, ): IReaderSettings => getMetadataFrom( type as Parameters[0], metadataHolder as Parameters[1], defaultSettings, profile !== undefined ? [profile.toString()] : undefined, useEffectFn, ); function getReaderSettingsWithDefaultValueFallback( type: 'global', metadataHolder: MetadataHolder, defaultSettings?: IReaderSettings, useEffectFn?: typeof useEffect, profile?: ReadingMode, ): IReaderSettingsWithDefaultFlag; function getReaderSettingsWithDefaultValueFallback( type: 'manga', metadataHolder: MangaIdInfo & MetadataHolder, defaultSettings?: IReaderSettings, useEffectFn?: typeof useEffect, profile?: ReadingMode, ): IReaderSettingsWithDefaultFlag; function getReaderSettingsWithDefaultValueFallback( type: Extract, metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder, defaultSettings: IReaderSettings = DEFAULT_READER_SETTINGS, useEffectFn?: typeof useEffect, profile?: ReadingMode, ): IReaderSettingsWithDefaultFlag { const settings = getReaderSettings(type, metadataHolder, defaultSettings, useEffectFn, profile); return convertToSettingsWithDefaultFlag(type, settings, metadataHolder); } const getSettings = ( metaHolder: MangaIdInfo & GqlMetaHolder, defaultSettings?: IReaderSettings, useEffectFn?: typeof useEffect, profile?: ReadingMode, ): IReaderSettingsWithDefaultFlag => getReaderSettingsWithDefaultValueFallback( 'manga', { ...metaHolder, meta: convertFromGqlMeta( metaHolder.meta, (key) => !GLOBAL_READER_SETTING_KEYS.includes(extractOriginalKey(key)), ), }, defaultSettings, useEffectFn, profile, ); export const getReaderSettingsFor = ( metaHolder: MangaIdInfo & GqlMetaHolder, defaultSettings: IReaderSettings, ): IReaderSettingsWithDefaultFlag => getSettings(metaHolder, defaultSettings); export const useGetReaderSettingsFor = ( metaHolder: MangaIdInfo & GqlMetaHolder, defaultSettings: IReaderSettings, profile?: ReadingMode, ): IReaderSettingsWithDefaultFlag => { const settings = getSettings(metaHolder, defaultSettings, useEffect, profile); return useMemo(() => settings, [metaHolder, defaultSettings, profile]); }; export const useDefaultReaderSettings = ( profile?: ReadingMode, ): { metadata?: Metadata; settings: IReaderSettings; loading: boolean; request: ReturnType; } => { const request = requestManager.useGetGlobalMeta(); const { data, loading } = request; const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]); const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]); const tmpSettings = getReaderSettings('global', metaHolder, undefined, useEffect, profile); const settings = useMemo(() => tmpSettings, [metaHolder, profile]); return useMemo( () => ({ metadata, settings, loading, request, }), [metadata, settings, loading, request], ); }; export const useDefaultReaderSettingsWithDefaultFlag = ( profile?: ReadingMode, ): { metadata?: Metadata; settings: IReaderSettingsWithDefaultFlag; loading: boolean; request: ReturnType; } => { const request = requestManager.useGetGlobalMeta(); const { data, loading } = request; const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]); const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]); const tmpSettings = getReaderSettingsWithDefaultValueFallback('global', metaHolder, undefined, useEffect, profile); const settings = useMemo(() => tmpSettings, [metaHolder, profile]); return useMemo( () => ({ metadata, settings, loading, request, }), [metadata, settings, loading, request], ); }; export const updateReaderSettings = async ( manga: Pick & GqlMetaHolder, setting: Setting, value: IReaderSettings[Setting], isGlobal: boolean = false, profile?: ReadingMode, ): Promise => { const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting); if (isGlobalSetting) { return requestServerMetadataUpdate({ update: [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]], keyPrefixes: profile !== undefined ? [profile?.toString()] : undefined, }); } return requestMangaMetadataUpdate(manga, { update: [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]], keyPrefixes: profile !== undefined ? [profile?.toString()] : undefined, }); }; export const createUpdateReaderSettings = ( manga: Pick & GqlMetaHolder, handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'), profile?: ReadingMode, ): ((...args: OmitFirst>>) => Promise) => (setting, value, isGlobal) => updateReaderSettings(manga, setting, value, isGlobal, profile).catch(handleError);