Rename folder "modules" to "features"

This commit is contained in:
schroda
2025-08-15 22:02:58 +02:00
parent 7e6ced1d09
commit 1b4bf22542
415 changed files with 1859 additions and 1852 deletions

View File

@@ -0,0 +1,11 @@
/*
* 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/.
*/
export type MetadataHistorySettings = {
hideHistory: boolean;
};

View File

@@ -0,0 +1,58 @@
/*
* 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 Box from '@mui/material/Box';
import CardActionArea from '@mui/material/CardActionArea';
import Card from '@mui/material/Card';
import { Link } from 'react-router-dom';
import { memo } from 'react';
import { DownloadStateIndicator } from '@/features/core/components/downloads/DownloadStateIndicator.tsx';
import { ChapterHistoryListFieldsFragment } from '@/lib/graphql/generated/graphql.ts';
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { epochToDate, timeFormatter } from '@/util/DateHelper.ts';
import { ChapterCardThumbnail } from '@/features/chapter/components/cards/ChapterCardThumbnail.tsx';
import { ChapterCardMetadata } from '@/features/chapter/components/cards/ChapterCardMetadata.tsx';
import { ChapterDownloadButton } from '@/features/chapter/components/buttons/ChapterDownloadButton.tsx';
import { ChapterDownloadRetryButton } from '@/features/chapter/components/buttons/ChapterDownloadRetryButton.tsx';
import { ListCardContent } from '@/features/core/components/lists/cards/ListCardContent';
export const ChapterHistoryCard = memo(({ chapter }: { chapter: ChapterHistoryListFieldsFragment }) => {
const { manga } = chapter;
return (
<Card>
<CardActionArea
component={Link}
to={AppRoutes.reader.path(chapter.manga.id, chapter.sourceOrder)}
state={Chapters.getReaderOpenChapterLocationState(chapter)}
sx={{
color: (theme) => theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
}}
>
<ListCardContent sx={{ justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', flexGrow: 1, gap: 1 }}>
<ChapterCardThumbnail
mangaId={manga.id}
mangaTitle={manga.title}
thumbnailUrl={manga.thumbnailUrl}
thumbnailUrlLastFetched={manga.thumbnailUrlLastFetched}
/>
<ChapterCardMetadata
title={manga.title}
secondaryText={`${chapter.name}${timeFormatter.format(epochToDate(Number(chapter.lastReadAt)).valueOf())}`}
/>
</Box>
<DownloadStateIndicator chapterId={chapter.id} />
<ChapterDownloadRetryButton chapterId={chapter.id} />
<ChapterDownloadButton chapterId={chapter.id} isDownloaded={chapter.isDownloaded} />
</ListCardContent>
</CardActionArea>
</Card>
);
});

View File

@@ -0,0 +1,104 @@
/*
* 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 Typography from '@mui/material/Typography';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/features/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { StyledGroupedVirtuoso } from '@/features/core/components/virtuoso/StyledGroupedVirtuoso.tsx';
import { StyledGroupHeader } from '@/features/core/components/virtuoso/StyledGroupHeader.tsx';
import { StyledGroupItemWrapper } from '@/features/core/components/virtuoso/StyledGroupItemWrapper.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { ChapterHistoryCard } from '@/features/history/components/ChapterHistoryCard.tsx';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export const History: React.FC = () => {
const { t } = useTranslation();
useAppTitle(t('history.title'));
const {
data: chapterHistoryData,
loading: isLoading,
error,
fetchMore,
refetch,
} = requestManager.useGetRecentlyReadChapters(undefined, {
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true,
});
const hasNextPage = !!chapterHistoryData?.chapters.pageInfo.hasNextPage;
const endCursor = chapterHistoryData?.chapters.pageInfo.endCursor;
const readEntries = chapterHistoryData?.chapters.nodes ?? [];
const groupedHistory = useMemo(
() => Object.entries(Chapters.groupByDate(readEntries, 'lastReadAt')),
[readEntries],
);
const groupCounts: number[] = useMemo(
() => groupedHistory.map((group) => group[VirtuosoUtil.ITEMS].length),
[groupedHistory],
);
const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
groupCounts,
useCallback((index) => groupedHistory[index][VirtuosoUtil.GROUP], [groupedHistory]),
useCallback((index) => readEntries[index].id, [readEntries]),
);
const loadMore = useCallback(() => {
if (!hasNextPage) {
return;
}
fetchMore({ variables: { offset: readEntries.length } });
}, [hasNextPage, endCursor]);
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('History::refetch'))}
/>
);
}
if (!isLoading && readEntries.length === 0) {
return <EmptyViewAbsoluteCentered message={t('history.error.label.no_history_available')} />;
}
return (
<StyledGroupedVirtuoso
persistKey="history"
components={{
Footer: () => (isLoading ? <LoadingPlaceholder usePadding /> : null),
}}
overscan={window.innerHeight * 0.5}
endReached={loadMore}
groupCounts={groupCounts}
groupContent={(index) => (
<StyledGroupHeader isFirstItem={index === 0}>
<Typography variant="h5" component="h2">
{groupedHistory[index][VirtuosoUtil.GROUP]}
</Typography>
</StyledGroupHeader>
)}
computeItemKey={computeItemKey}
itemContent={(index) => (
<StyledGroupItemWrapper>
<ChapterHistoryCard chapter={readEntries[index]} />
</StyledGroupItemWrapper>
)}
/>
);
};

View File

@@ -0,0 +1,65 @@
/*
* 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 List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import {
createUpdateMetadataServerSettings,
useMetadataServerSettings,
} from '@/features/settings/services/ServerSettingsMetadata.ts';
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/features/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { makeToast } from '@/features/core/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { MetadataHistorySettings } from '@/features/history/History.types.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export const HistorySettings = () => {
const { t } = useTranslation();
useAppTitle(t('history.title'));
const {
settings: { hideHistory },
request: { loading, error, refetch },
} = useMetadataServerSettings();
const updateMetadataServerSettings = createUpdateMetadataServerSettings<keyof MetadataHistorySettings>((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
if (loading) {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('HistorySettings::refetch'))}
/>
);
}
return (
<List sx={{ pt: 0 }}>
<ListItem>
<ListItemText primary={t('history.settings.hide')} />
<Switch
edge="end"
checked={hideHistory}
onChange={() => updateMetadataServerSettings('hideHistory', !hideHistory)}
/>
</ListItem>
</List>
);
};