Optionally trigger manga details search in source

In case the manga was opened while browsing the source, the searches should be triggered for in the search instead of the library
This commit is contained in:
schroda
2025-05-30 18:46:57 +02:00
parent 70a984e2cb
commit 18da4c6b64
7 changed files with 111 additions and 34 deletions

View File

@@ -15,7 +15,13 @@ import { useGetCategoryMetadata } from '@/modules/category/services/CategoryMeta
import { NullAndUndefined } from '@/Base.types.ts'; import { NullAndUndefined } from '@/Base.types.ts';
import { LibraryOptions, LibrarySortMode } from '@/modules/library/Library.types.ts'; import { LibraryOptions, LibrarySortMode } from '@/modules/library/Library.types.ts';
import { CategoryIdInfo, CategoryMetadataInfo } from '@/modules/category/Category.types.ts'; import { CategoryIdInfo, CategoryMetadataInfo } from '@/modules/category/Category.types.ts';
import { MangaChapterCountInfo, MangaIdInfo } from '@/modules/manga/Manga.types.ts'; import {
MangaChapterCountInfo,
MangaDownloadInfo,
MangaIdInfo,
MangaStatusInfo,
MangaUnreadInfo,
} from '@/modules/manga/Manga.types.ts';
import { SourceDisplayNameInfo } from '@/modules/source/Source.types.ts'; import { SourceDisplayNameInfo } from '@/modules/source/Source.types.ts';
const triStateFilter = ( const triStateFilter = (
@@ -96,8 +102,7 @@ const trackerFilter = (trackFilters: LibraryOptions['hasTrackerBinding'], manga:
}) })
.every(Boolean); .every(Boolean);
type TMangaStatusFilter = Pick<MangaType, 'status'>; const statusFilter = (statusFilters: LibraryOptions['hasStatus'], manga: MangaStatusInfo): boolean =>
const statusFilter = (statusFilters: LibraryOptions['hasStatus'], manga: TMangaStatusFilter): boolean =>
Object.entries(statusFilters) Object.entries(statusFilters)
.map(([status, statusFilterState]) => triStateFilterBoolean(statusFilterState, status === manga.status)) .map(([status, statusFilterState]) => triStateFilterBoolean(statusFilterState, status === manga.status))
.every(Boolean); .every(Boolean);
@@ -112,10 +117,12 @@ type TMangaFilterOptions = Pick<
| 'hasTrackerBinding' | 'hasTrackerBinding'
| 'hasStatus' | 'hasStatus'
>; >;
type TMangaFilter = Pick<MangaType, 'downloadCount' | 'unreadCount' | 'bookmarkCount' | 'hasDuplicateChapters'> & type TMangaFilter = Pick<MangaType, 'bookmarkCount' | 'hasDuplicateChapters'> &
TMangaTrackerFilter & TMangaTrackerFilter &
TMangaStatusFilter & MangaStatusInfo &
MangaChapterCountInfo; MangaChapterCountInfo &
MangaDownloadInfo &
MangaUnreadInfo;
const filterManga = ( const filterManga = (
manga: TMangaFilter, manga: TMangaFilter,
{ {

View File

@@ -33,6 +33,7 @@ type MangaCardBaseProps = Pick<MangaTypeGql, 'id' | 'title' | 'sourceId'> &
export type MangaIdInfo = Pick<MangaTypeGql, 'id'>; export type MangaIdInfo = Pick<MangaTypeGql, 'id'>;
export type MangaChapterCountInfo = { chapters: Pick<MangaTypeGql['chapters'], 'totalCount'> }; export type MangaChapterCountInfo = { chapters: Pick<MangaTypeGql['chapters'], 'totalCount'> };
export type MangaInLibraryInfo = Pick<MangaTypeGql, 'inLibrary'>;
export type MangaDownloadInfo = Pick<MangaTypeGql, 'downloadCount'> & MangaChapterCountInfo; export type MangaDownloadInfo = Pick<MangaTypeGql, 'downloadCount'> & MangaChapterCountInfo;
export type MangaUnreadInfo = Pick<MangaTypeGql, 'unreadCount'> & MangaChapterCountInfo; export type MangaUnreadInfo = Pick<MangaTypeGql, 'unreadCount'> & MangaChapterCountInfo;
export type MangaThumbnailInfo = Pick<MangaTypeGql, 'thumbnailUrl' | 'thumbnailUrlLastFetched'>; export type MangaThumbnailInfo = Pick<MangaTypeGql, 'thumbnailUrl' | 'thumbnailUrlLastFetched'>;
@@ -40,10 +41,14 @@ export type MangaTrackRecordInfo = MangaIdInfo & {
trackRecords: { nodes: Pick<TrackRecordType, 'id' | 'trackerId'>[] }; trackRecords: { nodes: Pick<TrackRecordType, 'id' | 'trackerId'>[] };
}; };
export type MangaGenreInfo = Pick<MangaTypeGql, 'genre'>; export type MangaGenreInfo = Pick<MangaTypeGql, 'genre'>;
export type MangaSourceIdInfo = Pick<MangaTypeGql, 'sourceId'>;
export type MangaSourceNameInfo = { source?: Maybe<Pick<SourceType, 'name'>> }; export type MangaSourceNameInfo = { source?: Maybe<Pick<SourceType, 'name'>> };
export type MangaSourceLngInfo = { source?: Maybe<Pick<SourceType, 'lang'>> }; export type MangaSourceLngInfo = { source?: Maybe<Pick<SourceType, 'lang'>> };
export type MangaArtistInfo = Pick<MangaTypeGql, 'artist'>; export type MangaArtistInfo = Pick<MangaTypeGql, 'artist'>;
export type MangaAuthorInfo = Pick<MangaTypeGql, 'author'>; export type MangaAuthorInfo = Pick<MangaTypeGql, 'author'>;
export type MangaTitleInfo = Pick<MangaTypeGql, 'title'>;
export type MangaDescriptionInfo = Pick<MangaTypeGql, 'description'>;
export type MangaStatusInfo = Pick<MangaTypeGql, 'status'>;
export type MigrateMode = 'copy' | 'migrate'; export type MigrateMode = 'copy' | 'migrate';
@@ -92,3 +97,8 @@ export enum MangaType {
MANHWA, MANHWA,
MANHUA, MANHUA,
} }
export interface MangaLocationState {
mangaTitle: string;
mode: MangaCardMode | undefined;
}

View File

@@ -44,12 +44,26 @@ import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
import { MANGA_COVER_ASPECT_RATIO, MANGA_STATUS_TO_TRANSLATION } from '@/modules/manga/Manga.constants.ts'; import { MANGA_COVER_ASPECT_RATIO, MANGA_STATUS_TO_TRANSLATION } from '@/modules/manga/Manga.constants.ts';
import { MangaThumbnailInfo, MangaTrackRecordInfo } from '@/modules/manga/Manga.types.ts'; import {
MangaArtistInfo,
MangaAuthorInfo,
MangaDescriptionInfo,
MangaGenreInfo,
MangaIdInfo,
MangaInLibraryInfo,
MangaLocationState,
MangaSourceIdInfo,
MangaStatusInfo,
MangaThumbnailInfo,
MangaTitleInfo,
MangaTrackRecordInfo,
} from '@/modules/manga/Manga.types.ts';
import { TAppThemeContext, useAppThemeContext } from '@/modules/theme/contexts/AppThemeContext.tsx'; import { TAppThemeContext, useAppThemeContext } from '@/modules/theme/contexts/AppThemeContext.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { CustomButtonIcon } from '@/modules/core/components/buttons/CustomButtonIcon.tsx'; import { CustomButtonIcon } from '@/modules/core/components/buttons/CustomButtonIcon.tsx';
import { Sources } from '@/modules/source/services/Sources.ts'; import { Sources } from '@/modules/source/services/Sources.ts';
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { SourceIdInfo } from '@/modules/source/Source.types.ts';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@@ -163,19 +177,40 @@ function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null):
return source.displayName ?? source.id; return source.displayName ?? source.id;
} }
const LibrarySearchLink = ({ query, children }: { query: string; children?: ReactNode }) => ( const SearchLink = ({
<Link query,
component={RouterLink} sourceId,
to={AppRoutes.library.path(undefined, query)} mode,
sx={{ textDecoration: 'none', color: 'inherit' }} children,
> }: {
query: string;
sourceId: SourceIdInfo['id'] | undefined;
mode: MangaLocationState['mode'];
children?: ReactNode;
}) => {
const link = (() => {
const isSourceMode = mode === 'source' && sourceId !== undefined;
if (isSourceMode) {
return AppRoutes.sources.childRoutes.browse.path(sourceId, query);
}
return AppRoutes.library.path(undefined, query);
})();
return (
<Link component={RouterLink} to={link} sx={{ textDecoration: 'none', color: 'inherit' }}>
{children ?? query} {children ?? query}
</Link> </Link>
); );
};
const valuesToJoinedLibrarySearchLinks = (values: string[] | undefined) => const valuesToJoinedSearchLinks = (
values: string[] | undefined,
sourceId: SourceIdInfo['id'] | undefined,
mode: MangaLocationState['mode'],
) =>
values values
?.map((value) => <LibrarySearchLink query={value} />) ?.map((value) => <SearchLink key={value} query={value} sourceId={sourceId} mode={mode} />)
.reduce((acc, valueLink) => ( .reduce((acc, valueLink) => (
<> <>
{acc}, {valueLink} {acc}, {valueLink}
@@ -310,9 +345,11 @@ const Thumbnail = ({
const OPEN_CLOSE_BUTTON_HEIGHT = '35px'; const OPEN_CLOSE_BUTTON_HEIGHT = '35px';
const DESCRIPTION_COLLAPSED_SIZE = 75; const DESCRIPTION_COLLAPSED_SIZE = 75;
const DescriptionGenre = ({ const DescriptionGenre = ({
manga: { description, genre: mangaGenres }, manga: { description, genre: mangaGenres, sourceId },
mode,
}: { }: {
manga: Pick<MangaType, 'description' | 'genre'>; manga: MangaDescriptionInfo & MangaGenreInfo & MangaSourceIdInfo;
mode: MangaLocationState['mode'];
}) => { }) => {
const [descriptionElement, setDescriptionElement] = useState<HTMLSpanElement | null>(null); const [descriptionElement, setDescriptionElement] = useState<HTMLSpanElement | null>(null);
const [descriptionHeight, setDescriptionHeight] = useState<number>(); const [descriptionHeight, setDescriptionHeight] = useState<number>();
@@ -374,9 +411,9 @@ const DescriptionGenre = ({
}} }}
> >
{genres.map((genre) => ( {genres.map((genre) => (
<LibrarySearchLink key={genre} query={genre}> <SearchLink key={genre} query={genre} sourceId={sourceId} mode={mode}>
<Chip label={genre} variant="outlined" onClick={() => {}} /> <Chip label={genre} variant="outlined" onClick={() => {}} />
</LibrarySearchLink> </SearchLink>
))} ))}
</Stack> </Stack>
</> </>
@@ -385,15 +422,23 @@ const DescriptionGenre = ({
export const MangaDetails = ({ export const MangaDetails = ({
manga, manga,
mode,
}: { }: {
manga: Pick< manga: Pick<MangaType, 'realUrl'> &
MangaType, MangaIdInfo &
'id' | 'title' | 'author' | 'artist' | 'status' | 'inLibrary' | 'realUrl' | 'description' | 'genre' MangaTitleInfo &
> & MangaStatusInfo &
MangaInLibraryInfo &
MangaAuthorInfo &
MangaArtistInfo &
MangaDescriptionInfo &
MangaGenreInfo &
MangaThumbnailInfo & MangaThumbnailInfo &
MangaSourceIdInfo &
MangaTrackRecordInfo & { MangaTrackRecordInfo & {
source?: Pick<SourceType, 'id' | 'displayName'> | null; source?: Pick<SourceType, 'id' | 'displayName'> | null;
}; };
mode: MangaLocationState['mode'];
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -438,13 +483,13 @@ export const MangaDetails = ({
{manga.author && ( {manga.author && (
<Metadata <Metadata
title={t('manga.label.author')} title={t('manga.label.author')}
value={valuesToJoinedLibrarySearchLinks(Mangas.getAuthors(manga))} value={valuesToJoinedSearchLinks(Mangas.getAuthors(manga), manga.source?.id, mode)}
/> />
)} )}
{manga.artist && ( {manga.artist && (
<Metadata <Metadata
title={t('manga.label.artist')} title={t('manga.label.artist')}
value={valuesToJoinedLibrarySearchLinks(Mangas.getArtists(manga))} value={valuesToJoinedSearchLinks(Mangas.getArtists(manga), manga.source?.id, mode)}
/> />
)} )}
<Metadata <Metadata
@@ -467,7 +512,7 @@ export const MangaDetails = ({
<OpenSourceButton url={manga.realUrl} /> <OpenSourceButton url={manga.realUrl} />
</MangaButtonsContainer> </MangaButtonsContainer>
</TopContentWrapper> </TopContentWrapper>
<DescriptionGenre manga={manga} /> <DescriptionGenre manga={manga} mode={mode} />
</DetailsWrapper> </DetailsWrapper>
{CategorySelectComponent} {CategorySelectComponent}
</> </>

View File

@@ -65,7 +65,7 @@ export const MangaGridCard = memo(
{...longPressBind(() => popupState.open(optionButtonRef.current))} {...longPressBind(() => popupState.open(optionButtonRef.current))}
onClick={handleClick} onClick={handleClick}
to={mangaLinkTo} to={mangaLinkTo}
state={{ mangaTitle: title }} state={Mangas.createLocationState(manga, mode)}
sx={{ textDecoration: 'none', touchCallout: 'none' }} sx={{ textDecoration: 'none', touchCallout: 'none' }}
> >
<Box <Box

View File

@@ -44,7 +44,7 @@ export const MangaListCard = memo(
<CardActionArea <CardActionArea
component={RouterLink} component={RouterLink}
to={mangaLinkTo} to={mangaLinkTo}
state={{ mangaTitle: title }} state={Mangas.createLocationState(manga, mode)}
onClick={handleClick} onClick={handleClick}
{...longPressBind(() => popupState.open(optionButtonRef.current))} {...longPressBind(() => popupState.open(optionButtonRef.current))}
sx={{ sx={{

View File

@@ -13,7 +13,7 @@ import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus'; import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus';
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -27,11 +27,13 @@ import { GetMangaScreenQuery } from '@/lib/graphql/generated/graphql.ts';
import { GET_MANGA_SCREEN } from '@/lib/graphql/queries/MangaQuery.ts'; import { GET_MANGA_SCREEN } from '@/lib/graphql/queries/MangaQuery.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts'; import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts';
import { MangaLocationState } from '@/modules/manga/Manga.types.ts';
export const Manga: React.FC = () => { export const Manga: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { mode } = useLocation<MangaLocationState>().state ?? {};
const autofetchedRef = useRef(false); const autofetchedRef = useRef(false);
const { const {
@@ -103,7 +105,7 @@ export const Manga: React.FC = () => {
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}> <Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
{isLoading && <LoadingPlaceholder />} {isLoading && <LoadingPlaceholder />}
{manga && <MangaDetails manga={manga} />} {manga && <MangaDetails manga={manga} mode={mode} />}
{manga && <ChapterList manga={manga} isRefreshing={refreshing} />} {manga && <ChapterList manga={manga} isRefreshing={refreshing} />}
</Box> </Box>
); );

View File

@@ -29,12 +29,15 @@ import {
MangaAction, MangaAction,
MangaArtistInfo, MangaArtistInfo,
MangaAuthorInfo, MangaAuthorInfo,
MangaCardMode,
MangaDownloadInfo, MangaDownloadInfo,
MangaGenreInfo, MangaGenreInfo,
MangaIdInfo, MangaIdInfo,
MangaLocationState,
MangaSourceLngInfo, MangaSourceLngInfo,
MangaSourceNameInfo, MangaSourceNameInfo,
MangaThumbnailInfo, MangaThumbnailInfo,
MangaTitleInfo,
MangaType, MangaType,
MangaUnreadInfo, MangaUnreadInfo,
MigrateMode, MigrateMode,
@@ -604,4 +607,14 @@ export class Mangas {
static getAuthors<Manga extends MangaAuthorInfo>(manga: Manga): string[] | undefined { static getAuthors<Manga extends MangaAuthorInfo>(manga: Manga): string[] | undefined {
return manga.author?.split(ARTIST_AUTHOR_SEPARATOR_REGEX); return manga.author?.split(ARTIST_AUTHOR_SEPARATOR_REGEX);
} }
static createLocationState<Manga extends MangaTitleInfo>(
manga: Manga,
mode: MangaCardMode | undefined,
): MangaLocationState {
return {
mangaTitle: manga.title,
mode,
};
}
} }