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:
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
>
|
}: {
|
||||||
{children ?? query}
|
query: string;
|
||||||
</Link>
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
const valuesToJoinedLibrarySearchLinks = (values: string[] | undefined) =>
|
return AppRoutes.library.path(undefined, query);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link component={RouterLink} to={link} sx={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
{children ?? query}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user