Skip to content

Commit 9e5fa7d

Browse files
jicedteaNamNH
and
NamNH
authored
Add history page (#897)
* Add History module and integrate it into the app Introduce the History module with components for displaying chapter updates, including UI for grouped updates by date. Update routing and constants to incorporate the new /history path. * Replace "Updates" and "History" with unified "Recent" module Merged the "Updates" and "History" screens into a single unified "Recent" module with corresponding navigation updates. Adjusted GraphQL types and localization files to reflect the changes. This simplifies navigation and improves user experience. * Refactor History screen to remove unused components. Removed unused states, layout effects, and props related to last update timestamp. Renamed variables for better clarity and updated labels and error messages to align with the history context. Simplified component structure for improved maintainability. * ``` Refactor tab handling and integrate dynamic height adjustments. Renamed enums for better clarity and consistency. Implemented a `tabsMenuHeight` prop to dynamically adjust component layouts based on observed height changes using `useResizeObserver`. Updated the `Updates` component to consider the calculated heights for improved rendering. ``` * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * Refactor `groupByDate` parameter and add `lastReadAt` field Renamed `updates` to `histories` in `groupByDate` for clarity and added a debugging log for input data. Updated GraphQL fragment to include the `lastReadAt` field to support additional functionality. * "Optimize GraphQL type definition formatting in generated file Condensed the formatting of the GetChaptersHistoryQuery type to a single line for improved readability and consistency. This change does not affect functionality but enhances maintainability of the generated code." * Remove UpdateChecker component from History screen The UpdateChecker component and its related navbar action have been removed from the History screen. This simplifies the navbar setup and eliminates unused functionality in this context. * Refactor chapter fragment and type references Updated `ChapterRecentListFieldsFragment` to `ChapterUpdateListFieldsFragment` for better alignment with naming conventions. Adjusted type imports and query structures to maintain consistency across files. No functional changes introduced. * Refactor padding calculation logic in Updates screen. Renamed state variable and refactored its logic for clarity and consistency. The updated logic now uses a more descriptive name, `listPadding`, to enhance code readability and maintainability. * Pass `tabsMenuHeight` prop to History component Added `tabsMenuHeight` prop to the `History` component to properly calculate the height for the `StyledGroupedVirtuoso` element. This ensures consistency with other components and improves layout rendering. * Revert package.json * Add last page read tracking to chapter history Introduced "lastPageRead" and "pageCount" fields to track progress in chapter history. Updated UI to display the last page read along with total pages for chapters. Adjusted localization files to support the new "page" label. * Remove last page read display from ChapterHistoryCard The "last page read" information was removed from the ChapterHistoryCard component for a cleaner UI. This change simplifies the card's design by showing only the chapter name and relevant download state. * Forcing an empty commit. * Replace "Recent" screen with separate "Updates" and "History". The "Recent" feature has been split into two distinct screens, "Updates" and "History", for improved clarity and functionality. Routes, icons, and components have been updated accordingly, and redundant code related to "Recent" has been removed. * Clean * Revert vi.json * Remove unnecessary `useLocation` hook and update state logic. The `useLocation` hook was removed as it was not used elsewhere in the component. The state property for the routing logic was updated to use `Chapters.getReaderOpenChapterLocationState` for better encapsulation and clarity. * Update history localization by removing unused "page" key Removed the "page" key from the "history" section in the localization file as it is no longer in use. This cleanup helps keep the translation file consistent and easier to maintain. --------- Co-authored-by: NamNH <namitonguyen@gmail.com>
1 parent c4ba483 commit 9e5fa7d

File tree

10 files changed

+404
-2
lines changed

10 files changed

+404
-2
lines changed

public/locales/en.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -1522,5 +1522,13 @@
15221522
}
15231523
},
15241524
"title": "Updates"
1525+
},
1526+
"history": {
1527+
"error": {
1528+
"label": {
1529+
"no_history_available": "You have not read any series yet."
1530+
}
1531+
},
1532+
"title": "History"
15251533
}
1526-
}
1534+
}

src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const { CategorySettings } = loadable(
3939
const { SourceConfigure } = loadable(() => import('@/modules/source/screens/SourceConfigure.tsx'), lazyLoadFallback);
4040
const { SourceMangas } = loadable(() => import('@/modules/source/screens/SourceMangas.tsx'), lazyLoadFallback);
4141
const { Updates } = loadable(() => import('@/modules/updates/screens/Updates.tsx'), lazyLoadFallback);
42+
const { History } = loadable(() => import('@/modules/history/screens/History.tsx'), lazyLoadFallback);
4243
const { LibrarySettings } = loadable(() => import('@/modules/library/screens/LibrarySettings.tsx'), lazyLoadFallback);
4344
const { DownloadSettings } = loadable(
4445
() => import('@/modules/downloads/screens/DownloadSettings.tsx'),
@@ -158,6 +159,7 @@ const MainApp = () => {
158159
</Route>
159160
<Route path={AppRoutes.library.match} element={<Library />} />
160161
<Route path={AppRoutes.updates.match} element={<Updates />} />
162+
<Route path={AppRoutes.history.match} element={<History />} />
161163
<Route path={AppRoutes.browse.match} element={<Browse />} />
162164
<Route path={AppRoutes.migrate.match}>
163165
<Route index element={<Migrate />} />

src/lib/graphql/fragments/ChapterFragments.ts

+14
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const CHAPTER_LIST_FIELDS = gql`
6464
6565
fetchedAt
6666
uploadDate
67+
lastReadAt
6768
}
6869
`;
6970

@@ -79,3 +80,16 @@ export const CHAPTER_UPDATE_LIST_FIELDS = gql`
7980
}
8081
}
8182
`;
83+
84+
export const CHAPTER_HISTORY_LIST_FIELDS = gql`
85+
${CHAPTER_LIST_FIELDS}
86+
${MANGA_BASE_FIELDS}
87+
88+
fragment CHAPTER_HISTORY_LIST_FIELDS on ChapterType {
89+
...CHAPTER_LIST_FIELDS
90+
91+
manga {
92+
...MANGA_BASE_FIELDS
93+
}
94+
}
95+
`;

src/lib/graphql/generated/graphql.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -2773,6 +2773,8 @@ export type ChapterListFieldsFragment = { __typename?: 'ChapterType', fetchedAt:
27732773

27742774
export type ChapterUpdateListFieldsFragment = { __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } };
27752775

2776+
export type ChapterHistoryListFieldsFragment = { __typename?: 'ChapterType', lastReadAt: string, lastPageRead: number, pageCount: number, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } };
2777+
27762778
export type DownloadTypeFieldsFragment = { __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', id: number, name: string, sourceOrder: number, isDownloaded: boolean }, manga: { __typename?: 'MangaType', id: number, title: string, downloadCount: number } };
27772779

27782780
export type DownloadStatusFieldsFragment = { __typename?: 'DownloadStatus', state: DownloaderState, queue: Array<{ __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', id: number, name: string, sourceOrder: number, isDownloaded: boolean }, manga: { __typename?: 'MangaType', id: number, title: string, downloadCount: number } }> };
@@ -3376,9 +3378,21 @@ export type GetChaptersUpdatesQueryVariables = Exact<{
33763378
order?: InputMaybe<Array<ChapterOrderInput> | ChapterOrderInput>;
33773379
}>;
33783380

3379-
33803381
export type GetChaptersUpdatesQuery = { __typename?: 'Query', chapters: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } };
33813382

3383+
export type GetChaptersHistoryQueryVariables = Exact<{
3384+
after?: InputMaybe<Scalars['Cursor']['input']>;
3385+
before?: InputMaybe<Scalars['Cursor']['input']>;
3386+
condition?: InputMaybe<ChapterConditionInput>;
3387+
filter?: InputMaybe<ChapterFilterInput>;
3388+
first?: InputMaybe<Scalars['Int']['input']>;
3389+
last?: InputMaybe<Scalars['Int']['input']>;
3390+
offset?: InputMaybe<Scalars['Int']['input']>;
3391+
order?: InputMaybe<Array<ChapterOrderInput> | ChapterOrderInput>;
3392+
}>;
3393+
3394+
export type GetChaptersHistoryQuery = { __typename?: 'Query', chapters: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', lastReadAt: string, lastPageRead: number, pageCount: number, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } };
3395+
33823396
export type GetMangasChapterIdsWithStateQueryVariables = Exact<{
33833397
mangaIds: Array<Scalars['Int']['input']> | Scalars['Int']['input'];
33843398
isDownloaded?: InputMaybe<Scalars['Boolean']['input']>;

src/lib/graphql/queries/ChapterQuery.ts

+37
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
CHAPTER_READER_FIELDS,
1414
CHAPTER_STATE_FIELDS,
1515
CHAPTER_UPDATE_LIST_FIELDS,
16+
CHAPTER_HISTORY_LIST_FIELDS,
1617
} from '@/lib/graphql/fragments/ChapterFragments.ts';
1718

1819
// returns the current chapters from the database
@@ -123,6 +124,42 @@ export const GET_CHAPTERS_UPDATES = gql`
123124
}
124125
`;
125126

127+
// returns the current chapters from the database
128+
export const GET_CHAPTERS_HISTORY = gql`
129+
${CHAPTER_HISTORY_LIST_FIELDS}
130+
${PAGE_INFO}
131+
132+
query GET_CHAPTERS_HISTORY(
133+
$after: Cursor
134+
$before: Cursor
135+
$condition: ChapterConditionInput
136+
$filter: ChapterFilterInput
137+
$first: Int
138+
$last: Int
139+
$offset: Int
140+
$order: [ChapterOrderInput!]
141+
) {
142+
chapters(
143+
after: $after
144+
before: $before
145+
condition: $condition
146+
filter: $filter
147+
first: $first
148+
last: $last
149+
offset: $offset
150+
order: $order
151+
) {
152+
nodes {
153+
...CHAPTER_HISTORY_LIST_FIELDS
154+
}
155+
pageInfo {
156+
...PAGE_INFO
157+
}
158+
totalCount
159+
}
160+
}
161+
`;
162+
126163
export const GET_MANGAS_CHAPTER_IDS_WITH_STATE = gql`
127164
${CHAPTER_STATE_FIELDS}
128165

src/lib/requests/RequestManager.ts

+39
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ import {
8787
GetChaptersMangaQueryVariables,
8888
GetChaptersUpdatesQuery,
8989
GetChaptersUpdatesQueryVariables,
90+
GetChaptersHistoryQuery,
91+
GetChaptersHistoryQueryVariables,
9092
GetDownloadStatusQuery,
9193
GetDownloadStatusQueryVariables,
9294
GetExtensionsFetchMutation,
@@ -267,6 +269,7 @@ import {
267269
STOP_DOWNLOADER,
268270
} from '@/lib/graphql/mutations/DownloaderMutation.ts';
269271
import {
272+
GET_CHAPTERS_HISTORY,
270273
GET_CHAPTERS_MANGA,
271274
GET_CHAPTERS_UPDATES,
272275
GET_MANGAS_CHAPTER_IDS_WITH_STATE,
@@ -2886,6 +2889,42 @@ export class RequestManager {
28862889
} as typeof result;
28872890
}
28882891

2892+
public useGetRecentlyReadChapters(
2893+
initialPages: number = 1,
2894+
options?: QueryHookOptions<GetChaptersHistoryQuery, GetChaptersHistoryQueryVariables>,
2895+
): AbortableApolloUseQueryResponse<GetChaptersHistoryQuery, GetChaptersHistoryQueryVariables> {
2896+
const PAGE_SIZE = 50;
2897+
const CACHE_KEY = 'useGetRecentlyReadChapters';
2898+
2899+
const offset = this.cache.getResponseFor<number>(CACHE_KEY, undefined) ?? 0;
2900+
const [lastOffset] = useState(offset);
2901+
2902+
const result = this.useGetChapters<GetChaptersHistoryQuery, GetChaptersHistoryQueryVariables>(
2903+
GET_CHAPTERS_HISTORY,
2904+
{
2905+
filter: { lastReadAt: { isNull: false, notEqualToAll: ['0'] } },
2906+
order: [
2907+
{ by: ChapterOrderBy.LastReadAt, byType: SortOrder.Desc },
2908+
{ by: ChapterOrderBy.SourceOrder, byType: SortOrder.Desc },
2909+
],
2910+
first: initialPages * PAGE_SIZE + lastOffset,
2911+
},
2912+
options,
2913+
);
2914+
2915+
return {
2916+
...result,
2917+
fetchMore: (...args: Parameters<(typeof result)['fetchMore']>) => {
2918+
const fetchMoreOptions = args[0] ?? {};
2919+
this.cache.cacheResponse(CACHE_KEY, undefined, fetchMoreOptions.variables?.offset);
2920+
return result.fetchMore({
2921+
...fetchMoreOptions,
2922+
variables: { first: PAGE_SIZE, ...fetchMoreOptions.variables },
2923+
});
2924+
},
2925+
} as typeof result;
2926+
}
2927+
28892928
public startGlobalUpdate(
28902929
categories?: undefined,
28912930
options?: MutationOptions<UpdateLibraryMangasMutation, UpdateLibraryMangasMutationVariables>,

src/modules/core/AppRoute.constants.ts

+8
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ export const AppRoutes = {
129129
match: 'updates',
130130
path: '/updates',
131131
},
132+
history: {
133+
match: 'history',
134+
path: '/history',
135+
},
136+
recent: {
137+
match: 'recent',
138+
path: '/recent',
139+
},
132140
browse: {
133141
match: 'browse',
134142
path: '/browse',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright (C) Contributors to the Suwayomi project
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import DownloadIcon from '@mui/icons-material/Download';
10+
import Box from '@mui/material/Box';
11+
import CardActionArea from '@mui/material/CardActionArea';
12+
import Avatar from '@mui/material/Avatar';
13+
import Card from '@mui/material/Card';
14+
import CardContent from '@mui/material/CardContent';
15+
import IconButton from '@mui/material/IconButton';
16+
import { Link } from 'react-router-dom';
17+
import Refresh from '@mui/icons-material/Refresh';
18+
import { useTranslation } from 'react-i18next';
19+
import { memo } from 'react';
20+
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
21+
import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx';
22+
import { ChapterHistoryListFieldsFragment, DownloadState } from '@/lib/graphql/generated/graphql.ts';
23+
import { Mangas } from '@/modules/manga/services/Mangas.ts';
24+
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
25+
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
26+
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
27+
import { requestManager } from '@/lib/requests/RequestManager.ts';
28+
import { makeToast } from '@/modules/core/utils/Toast.ts';
29+
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
30+
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
31+
32+
export const ChapterHistoryCard = memo(({ chapter }: { chapter: ChapterHistoryListFieldsFragment }) => {
33+
const { manga } = chapter;
34+
const download = Chapters.useDownloadStatusFromCache(chapter.id);
35+
36+
const { t } = useTranslation();
37+
38+
const handleRetry = async () => {
39+
try {
40+
await requestManager.addChapterToDownloadQueue(chapter.id).response;
41+
} catch (e) {
42+
makeToast(t('download.queue.error.label.failed_to_remove'), 'error', getErrorMessage(e));
43+
}
44+
};
45+
46+
const downloadChapter = () => {
47+
requestManager
48+
.addChapterToDownloadQueue(chapter.id)
49+
.response.catch((e) =>
50+
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
51+
);
52+
};
53+
54+
return (
55+
<Card>
56+
<CardActionArea
57+
component={Link}
58+
to={AppRoutes.reader.path(chapter.manga.id, chapter.sourceOrder)}
59+
state={Chapters.getReaderOpenChapterLocationState(chapter)}
60+
sx={{
61+
color: (theme) => theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
62+
}}
63+
>
64+
<CardContent
65+
sx={{
66+
display: 'flex',
67+
justifyContent: 'space-between',
68+
alignItems: 'center',
69+
padding: 1.5,
70+
}}
71+
>
72+
<Box sx={{ display: 'flex', flexGrow: 1 }}>
73+
<Link to={AppRoutes.manga.path(chapter.manga.id)} style={{ textDecoration: 'none' }}>
74+
<Avatar
75+
variant="rounded"
76+
sx={{
77+
width: 56,
78+
height: 56,
79+
flex: '0 0 auto',
80+
marginRight: 1,
81+
background: 'transparent',
82+
}}
83+
>
84+
<SpinnerImage
85+
imgStyle={{
86+
objectFit: 'cover',
87+
width: '100%',
88+
height: '100%',
89+
imageRendering: 'pixelated',
90+
}}
91+
spinnerStyle={{ small: true }}
92+
alt={manga.title}
93+
src={Mangas.getThumbnailUrl(manga)}
94+
/>
95+
</Avatar>
96+
</Link>
97+
<Box
98+
sx={{
99+
display: 'flex',
100+
flexDirection: 'column',
101+
justifyContent: 'center',
102+
flexGrow: 1,
103+
flexShrink: 1,
104+
wordBreak: 'break-word',
105+
}}
106+
>
107+
<TypographyMaxLines variant="h6" component="h3">
108+
{manga.title}
109+
</TypographyMaxLines>
110+
<TypographyMaxLines variant="caption" display="block" lines={1}>
111+
{chapter.name}
112+
</TypographyMaxLines>
113+
</Box>
114+
</Box>
115+
<DownloadStateIndicator chapterId={chapter.id} />
116+
{download?.state === DownloadState.Error && (
117+
<CustomTooltip title={t('global.button.retry')}>
118+
<IconButton
119+
onClick={(e) => {
120+
e.preventDefault();
121+
e.stopPropagation();
122+
handleRetry();
123+
}}
124+
size="large"
125+
>
126+
<Refresh />
127+
</IconButton>
128+
</CustomTooltip>
129+
)}
130+
{download == null && !chapter.isDownloaded && (
131+
<CustomTooltip title={t('chapter.action.download.add.label.action')}>
132+
<IconButton
133+
onClick={(e) => {
134+
e.stopPropagation();
135+
e.preventDefault();
136+
downloadChapter();
137+
}}
138+
size="large"
139+
>
140+
<DownloadIcon />
141+
</IconButton>
142+
</CustomTooltip>
143+
)}
144+
</CardContent>
145+
</CardActionArea>
146+
</Card>
147+
);
148+
});

0 commit comments

Comments
 (0)