Introduce "sources" util class

This commit is contained in:
schroda
2025-04-21 17:00:13 +02:00
parent 633f58b563
commit 4a9f90b746
11 changed files with 67 additions and 56 deletions

View File

@@ -31,6 +31,8 @@ import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { Sources } from '@/modules/source/services/Sources.ts';
import { SourceIdInfo } from '@/modules/source/Source.types.ts';
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
@@ -62,8 +64,8 @@ const compareSourceByName = (
};
const compareSourcesBySearchResult = (
sourceA: Pick<SourceType, 'id'>,
sourceB: Pick<SourceType, 'id'>,
sourceA: SourceIdInfo,
sourceB: SourceIdInfo,
sourceToFetchedStateMap: SourceToLoadingStateMap,
): -1 | 0 | 1 => {
const isSourceAFetched = !sourceToFetchedStateMap.get(sourceA.id)?.isLoading;
@@ -101,7 +103,7 @@ const SourceSearchPreview = React.memo(
}: {
source: Pick<SourceType, 'id' | 'displayName' | 'lang'>;
onSearchRequestFinished: (
source: Pick<SourceType, 'id'>,
source: SourceIdInfo,
isLoading: boolean,
hasResults: boolean,
emptySearch: boolean,
@@ -217,7 +219,7 @@ export const SearchAll: React.FC = () => {
const sourcesSortedByName = useMemo(() => [...sources].sort(compareSourceByName), [sources]);
const sourcesFilteredByLang = useMemo(
() => sourcesSortedByName.filter((source) => shownLangs.includes(source.lang) || Number(source.id) === 0),
() => sourcesSortedByName.filter((source) => shownLangs.includes(source.lang) || Sources.isLocalSource(source)),
[sourcesSortedByName, shownLangs],
);
const sourcesFilteredByNsfw = useMemo(
@@ -233,7 +235,7 @@ export const SearchAll: React.FC = () => {
);
const updateSourceLoadingState = useCallback(
({ id }: Pick<SourceType, 'id'>, isLoading: boolean, hasResults: boolean, emptySearch: boolean) => {
({ id }: SourceIdInfo, isLoading: boolean, hasResults: boolean, emptySearch: boolean) => {
setSourceToLoadingStateMap((currentMap) => {
const mapCopy = new Map(currentMap);
mapCopy.set(id, { isLoading, hasResults, emptySearch });

View File

@@ -153,8 +153,7 @@ function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null):
return translate('global.label.unknown');
}
const isLocalSource = Number(source.id) === 0;
if (isLocalSource) {
if (Sources.isLocalSource(source)) {
return translate('source.local_source.title');
}

View File

@@ -24,9 +24,9 @@ import { extractOriginalKey, getMetadataKey } from '@/modules/metadata/Metadata.
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { getMetadataDeleteFunction, getMetadataUpdateFunction } from '@/modules/metadata/services/MetadataUpdater.ts';
import { SourceIdInfo } from '@/modules/source/Source.types.ts';
const getAppKeyPrefixForMigration = (migrationId: number): string => {
const appKeyPrefix = METADATA_MIGRATIONS.slice(0, migrationId)
@@ -269,7 +269,7 @@ const commitMigratedMetadata = (
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder)
| (SourceIdInfo & MetadataHolder)
| undefined,
migratedMetadata: Metadata,
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
@@ -334,7 +334,7 @@ export const applyMetadataMigrations = (
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
| (SourceIdInfo & MetadataHolder),
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
): Metadata | undefined => {
const meta = { ...(metadataHolder?.meta ?? {}) };

View File

@@ -19,9 +19,9 @@ import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { doesMetadataKeyExistIn, getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts';
import { SourceIdInfo } from '@/modules/source/Source.types.ts';
const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
metadata: Metadata | undefined,
@@ -70,7 +70,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
): METADATA;
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
type: 'source',
metadataHolder: Pick<SourceType, 'id'> & MetadataHolder,
metadataHolder: SourceIdInfo & MetadataHolder,
metadataWithDefaultValues: METADATA,
prefixes?: string[],
useEffectFn?: typeof useEffect,
@@ -82,7 +82,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
| (SourceIdInfo & MetadataHolder),
metadataWithDefaultValues: METADATA,
prefixes?: string[],
useEffectFn: typeof useEffect = (fn: () => void) => fn(),

View File

@@ -7,7 +7,6 @@
*/
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
import {
@@ -21,6 +20,7 @@ import {
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { convertToGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
import { SourceIdInfo } from '@/modules/source/Source.types.ts';
const requestUpdateMetadataValue = async (
metadataHolder: GqlMetaHolder,
@@ -46,8 +46,7 @@ const requestUpdateMetadataValue = async (
await requestManager.setMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey, value).response;
break;
case 'source':
await requestManager.setSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey, value)
.response;
await requestManager.setSourceMeta((metadataHolder as SourceIdInfo).id, metadataKey, value).response;
break;
default:
throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`);
@@ -95,7 +94,7 @@ export const requestUpdateCategoryMetadata = async (
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues, keyPrefixes, isMetadataKey);
export const requestUpdateSourceMetadata = async (
source: Pick<SourceType, 'id'> & GqlMetaHolder,
source: SourceIdInfo & GqlMetaHolder,
keysToValue: MetadataKeyValuePair[],
keyPrefixes?: string[],
isMetadataKey?: boolean,
@@ -108,7 +107,7 @@ export const getMetadataUpdateFunction = (
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
| (SourceIdInfo & MetadataHolder),
): ((keyValuePair: MetadataKeyValuePair[], keyPrefixes?: string[], isMetadataKey?: boolean) => Promise<void[]>) => {
switch (type) {
case 'global':
@@ -135,7 +134,7 @@ export const getMetadataUpdateFunction = (
return (...args) =>
requestUpdateSourceMetadata(
{
id: (metadataHolder as Pick<SourceType, 'id'>).id,
id: (metadataHolder as SourceIdInfo).id,
meta: convertToGqlMeta(metadataHolder.meta),
},
...args,
@@ -168,7 +167,7 @@ export const requestDeleteMetadataValue = async (
await requestManager.deleteMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey).response;
break;
case 'source':
await requestManager.deleteSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey).response;
await requestManager.deleteSourceMeta((metadataHolder as SourceIdInfo).id, metadataKey).response;
break;
default:
throw new Error(`requestDeleteMetadataValue: unknown holderType "${holderType}"`);
@@ -215,7 +214,7 @@ export const requestDeleteCategoryMetadata = async (
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys, keyPrefixes, isMetadataKey);
export const requestDeleteSourceMetadata = async (
source: Pick<SourceType, 'id'> & GqlMetaHolder,
source: SourceIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[],
keyPrefixes?: string[],
isMetadataKey?: boolean,
@@ -228,7 +227,7 @@ export const getMetadataDeleteFunction = (
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
| (SourceIdInfo & MetadataHolder),
): ((metadataToDelete: AppMetadataKeys[], keyPrefixes?: string[], isMetadataKey?: boolean) => Promise<void[]>) => {
switch (type) {
case 'global':
@@ -255,7 +254,7 @@ export const getMetadataDeleteFunction = (
return (...args) =>
requestDeleteSourceMetadata(
{
id: (metadataHolder as Pick<SourceType, 'id'>).id,
id: (metadataHolder as SourceIdInfo).id,
meta: convertToGqlMeta(metadataHolder.meta),
},
...args,

View File

@@ -10,6 +10,7 @@ import {
GetSourceBrowseQuery,
GetSourceSettingsQuery,
SourcePreferenceChangeInput,
SourceType,
} from '@/lib/graphql/generated/graphql.ts';
export interface IPos {
@@ -57,3 +58,5 @@ export type MultiSelectListPreferenceProps = PreferenceProps &
export type EditTextPreferenceProps = PreferenceProps &
ExtractByKeyValue<SourcePreferences, '__typename', 'EditTextPreference'>;
export type SourceIdInfo = Pick<SourceType, 'id'>;

View File

@@ -36,21 +36,18 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
const isMobileWidth = MediaQuery.useIsMobileWidth();
const { source, showSourceRepo } = props;
const {
source: {
id,
name,
lang,
iconUrl,
supportsLatest,
isNsfw,
extension: { repo },
},
showSourceRepo,
} = props;
id,
name,
lang,
iconUrl,
supportsLatest,
isNsfw,
extension: { repo },
} = source;
const isLocalSource = Number(id) === 0;
const sourceName = isLocalSource ? t('source.local_source.title') : name;
const sourceName = Sources.isLocalSource(source) ? t('source.local_source.title') : name;
return (
<Card

View File

@@ -34,7 +34,6 @@ import {
GetSourceBrowseQueryVariables,
GetSourceMangasFetchMutation,
GetSourceMangasFetchMutationVariables,
SourceType,
} from '@/lib/graphql/generated/graphql.ts';
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx';
@@ -44,7 +43,7 @@ import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/modules/sour
import { makeToast } from '@/modules/core/utils/Toast.ts';
import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.ts';
import { TranslationKey } from '@/Base.types.ts';
import { IPos } from '@/modules/source/Source.types.ts';
import { IPos, SourceIdInfo } from '@/modules/source/Source.types.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { EmptyView } from '@/modules/core/components/placeholder/EmptyView.tsx';
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
@@ -53,8 +52,9 @@ import { GridLayout } from '@/modules/core/Core.types.ts';
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { Sources } from '@/modules/source/services/Sources.ts';
const DEFAULT_SOURCE: Pick<SourceType, 'id'> = { id: '-1' };
const DEFAULT_SOURCE: SourceIdInfo = { id: '-1' };
const ContentTypeMenu = styled('div')(({ theme }) => ({
display: 'flex',
@@ -332,7 +332,7 @@ export function SourceMangas() {
);
const message = !isLoading ? t(SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY[contentType]) : undefined;
const isLocalSource = sourceId === '0';
const isLocalSource = sourceId === Sources.LOCAL_SOURCE_ID;
const messageExtra = isLocalSource ? (
<>
<span>{t('source.local_source.label.checkout')} </span>

View File

@@ -26,13 +26,13 @@ import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { Sources as SourceService } from '@/modules/source/services/Sources.ts';
function sourceToLangList(sources: Pick<SourceType, 'id' | 'lang'>[]) {
const result = new Set<string>();
sources.forEach((source) => {
const isLocalSource = Number(source.id) === 0;
const lang = isLocalSource ? DefaultLanguage.OTHER : source.lang;
const lang = SourceService.isLocalSource(source) ? DefaultLanguage.OTHER : source.lang;
result.add(lang);
});
@@ -46,8 +46,7 @@ function groupByLang<Source extends Pick<SourceType, 'id' | 'name' | 'lang'>>(
const result: Record<string, Source[]> = {};
sources.forEach((source) => {
const isLocalSource = Number(source.id) === 0;
const lang = isLocalSource ? DefaultLanguage.OTHER : source.lang;
const lang = SourceService.isLocalSource(source) ? DefaultLanguage.OTHER : source.lang;
result[lang] ??= [];
result[lang].push(source);
@@ -146,7 +145,7 @@ export function Sources() {
.filter((source) => {
const isLangOther = lang === DefaultLanguage.OTHER;
if (isLangOther) {
const isLocalSource = Number(source.id) === 0;
const isLocalSource = SourceService.isLocalSource(source);
const isLangShown = shownLangs.includes(lang);
const isLangOtherSourceShown = isLangShown || isLocalSource;

View File

@@ -10,8 +10,7 @@ import { useEffect, useMemo } from 'react';
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
import { requestUpdateSourceMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { ISourceMetadata, SourceMetadataKeys } from '@/modules/source/Source.types.ts';
import { ISourceMetadata, SourceIdInfo, SourceMetadataKeys } from '@/modules/source/Source.types.ts';
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
import {
@@ -39,10 +38,7 @@ const convertGqlMetadataToAppMetadata = (
savedSearches: jsonSaveParse<ISourceMetadata['savedSearches']>(metadata.savedSearches as string) ?? undefined,
});
const getMetadata = (
metaHolder: Pick<SourceType, 'id'> & GqlMetaHolder,
useEffectFn?: typeof useEffect,
): ISourceMetadata =>
const getMetadata = (metaHolder: SourceIdInfo & GqlMetaHolder, useEffectFn?: typeof useEffect): ISourceMetadata =>
convertGqlMetadataToAppMetadata(
getMetadataFrom(
'source',
@@ -53,10 +49,9 @@ const getMetadata = (
),
);
export const getSourceMetadata = (metaHolder: Pick<SourceType, 'id'> & GqlMetaHolder): ISourceMetadata =>
getMetadata(metaHolder);
export const getSourceMetadata = (metaHolder: SourceIdInfo & GqlMetaHolder): ISourceMetadata => getMetadata(metaHolder);
export const useGetSourceMetadata = (metaHolder: Pick<SourceType, 'id'> & GqlMetaHolder): ISourceMetadata => {
export const useGetSourceMetadata = (metaHolder: SourceIdInfo & GqlMetaHolder): ISourceMetadata => {
const metadata = getMetadata(metaHolder, useEffect);
return useMemo(() => metadata, [metaHolder]);
};
@@ -65,7 +60,7 @@ export const updateSourceMetadata = async <
MetadataKeys extends SourceMetadataKeys = SourceMetadataKeys,
MetadataKey extends MetadataKeys = MetadataKeys,
>(
source: Pick<SourceType, 'id'> & GqlMetaHolder,
source: SourceIdInfo & GqlMetaHolder,
metadataKey: MetadataKey,
value: ISourceMetadata[MetadataKey],
): Promise<void[]> =>
@@ -75,7 +70,7 @@ export const updateSourceMetadata = async <
export const createUpdateSourceMetadata =
<Settings extends SourceMetadataKeys>(
source: Pick<SourceType, 'id'> & GqlMetaHolder,
source: SourceIdInfo & GqlMetaHolder,
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateSourceMetadata'),
): ((...args: OmitFirst<Parameters<typeof updateSourceMetadata<Settings>>>) => Promise<void | void[]>) =>
(metadataKey, value) =>

View File

@@ -0,0 +1,17 @@
/*
* 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 { SourceIdInfo } from '@/modules/source/Source.types.ts';
export class Sources {
static readonly LOCAL_SOURCE_ID = '0';
static isLocalSource(source: SourceIdInfo): boolean {
return source.id === Sources.LOCAL_SOURCE_ID;
}
}