Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/mark-chapters-as-read-based-on-tracker-history #708

Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ build/*
tools/scripts/github_token.json

src/lib/graphql/schema.json

# local dev
pnpm-*.yaml
compose.yml
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"ci": "yarn install --frozen-lockfile",
"dev": "vite",
"dev": "vite --host",
"preview": "vite preview",
"build": "vite build",
"test": "node -e \"console.log('imagine')\"",
Expand Down
11 changes: 2 additions & 9 deletions src/components/chapter/ChapterActionMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { ChaptersWithMeta } from '@/lib/data/ChaptersWithMeta.ts';
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { getPreviousChapters } from '@/components/chapter/util';

type BaseProps = { onClose: () => void };

Expand Down Expand Up @@ -118,15 +119,7 @@ export const ChapterActionMenuItems = ({
if (!isMarkPrevAsRead) {
return [chapter];
}

const index = allChapters.findIndex(({ id: chapterId }) => chapterId === chapter.id);

const isFirstChapter = index + 1 > allChapters.length - 1;
if (isFirstChapter) {
return [];
}

return allChapters.slice(index + 1);
return getPreviousChapters(chapter.id, allChapters);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the Chapters class - same for all the added util functions

};

const chapters = getChapters();
Expand Down
2 changes: 1 addition & 1 deletion src/components/chapter/ChapterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { ChapterCard } from '@/components/chapter/ChapterCard.tsx';
import { ResumeFab } from '@/components/manga/ResumeFAB.tsx';
import { filterAndSortChapters, useChapterOptions } from '@/components/chapter/util.tsx';
import { EmptyView } from '@/components/util/EmptyView.tsx';
import { ChaptersToolbarMenu } from '@/components/chapter/ChaptersToolbarMenu.tsx';
import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx';
import { DownloadType } from '@/lib/graphql/generated/graphql.ts';
Expand All @@ -30,6 +29,7 @@ import { Chapters } from '@/lib/data/Chapters.ts';
import { ChaptersWithMeta } from '@/lib/data/ChaptersWithMeta.ts';
import { ChapterActionMenuItems } from '@/components/chapter/ChapterActionMenuItems.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { ChaptersToolbarMenu } from '@/components/chapter/ChaptersToolbarMenu';

const ChapterListHeader = styled(Stack)(({ theme }) => ({
margin: 8,
Expand Down
53 changes: 53 additions & 0 deletions src/components/chapter/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
TranslationKey,
} from '@/typings.ts';
import { useReducerLocalStorage } from '@/util/useLocalStorage.tsx';
import { getPartialList } from '@/components/util/getPartialList';
import { Chapters } from '@/lib/data/Chapters.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler';

const defaultChapterOptions: ChapterListOptions = {
active: false,
Expand Down Expand Up @@ -114,3 +117,53 @@ export const isFilterActive = (options: ChapterListOptions) => {
const { unread, downloaded, bookmarked } = options;
return unread != null || downloaded != null || bookmarked != null;
};

/**
* @param chapterId The id of the chapter to be use as a pivot
* @param allChapters There list of chapters
* @param includePivotChapter Whether to return the chapter with the passed chapterId in the list
* @returns The second half of the list. By the default the chapters are sorted
* in descending order, so it returns the previous chapters, not including the pivot chapter.
*/
export const getPreviousChapters = (
chapterId: TChapter['id'],
allChapters: TChapter[],
includePivotChapter: boolean = false,
): TChapter[] => {
if (includePivotChapter) {
return getPartialList(chapterId, allChapters, 'second', 0);
}
return getPartialList(chapterId, allChapters, 'second');
};

/**
* @param chapterId The id of the chapter to be use as a pivot
* @param allChapters There list of chapters
* @param includePivotChapter Whether to return the chapter with the passed chapterId in the list
* @returns The second half of the list. By the default the chapters are sorted
* in descending order, so it returns the previous chapters, not including the pivot chapter.
*/
export const getNextChapters = (
chapterId: TChapter['id'],
allChapters: TChapter[],
includePivotChapter: boolean = false,
): TChapter[] => {
if (includePivotChapter) {
return getPartialList(chapterId, allChapters, 'first');
}
return getPartialList(chapterId, allChapters, 'first', 0);
};

/**
* @description This fucntion takes a chapter Id and set all chapters with index bellow the index of the chapter to that id
* to read, and the rest of the chapters as unread. Technically setting the chapter with the passed id as the current unread chapter.
* @param chapterId Chapter Id
* @param allChapters List of chapters
*/
export const setChapterAsLastRead = (chapterId: TChapter['id'], allChapters: TChapter[]) => {
const readChapters = getPreviousChapters(chapterId, allChapters, true);
const unreadChapterId = getNextChapters(chapterId, allChapters).map((chapter) => chapter.id);

Chapters.markAsRead(readChapters, true).catch(defaultPromiseErrorHandler('ChapterActionMenuItems::performAction'));
Chapters.markAsUnread(unreadChapterId).catch(defaultPromiseErrorHandler('ChapterActionMenuItems::performAction'));
};
7 changes: 0 additions & 7 deletions src/components/manga/MangaDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ const DetailsWrapper = styled('div')(({ theme }) => ({

const TopContentWrapper = styled('div')(() => ({
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
}));

const ThumbnailMetadataWrapper = styled('div')(() => ({
Expand All @@ -58,9 +55,6 @@ const Thumbnail = styled('div')(() => ({
height: 'auto',
},
maxWidth: '50%',
// [theme.breakpoints.up('md')]: {
// minWidth: '100px',
// },
}));

const Metadata = styled('div')(({ theme }) => ({
Expand Down Expand Up @@ -92,7 +86,6 @@ const BottomContentWrapper = styled('div')(({ theme }) => ({
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
Expand Down
31 changes: 30 additions & 1 deletion src/components/manga/TrackMangaButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/components/util/Toast.tsx';
import { TrackManga } from '@/components/tracker/TrackManga.tsx';
import { Trackers } from '@/lib/data/Trackers.ts';
import { TManga } from '@/typings.ts';
import { TChapter, TManga } from '@/typings.ts';
import { CustomIconButton } from '@/components/atoms/CustomIconButton.tsx';
import { setChapterAsLastRead } from '@/components/chapter/util.tsx';

export const TrackMangaButton = ({ manga }: { manga: TManga }) => {
const { t } = useTranslation();
Expand All @@ -28,8 +29,36 @@ export const TrackMangaButton = ({ manga }: { manga: TManga }) => {

const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers));
const mangaChaptersQuery = requestManager.useGetMangaChapters(manga.id);
const mangaChapters = mangaChaptersQuery.data?.chapters.nodes;

const refreshTracker = () => {
mangaTrackers.map((trackRecord) =>
requestManager
.fetchTrackBind(trackRecord.id)
.response.catch(() => makeToast(t('tracking.error.label.could_not_fetch_track_info'), 'error')),
);
};

const updateChapterFromTracker = () => {
const lastestTrackersRead = Math.max(...mangaTrackers.map((trackRecord) => trackRecord.lastChapterRead));
const latestLocalRead = manga.latestReadChapter?.chapterNumber ?? 0;
const localBehindTracker = !mangaChapters?.some((chapter) => chapter.chapterNumber === lastestTrackersRead);
if (localBehindTracker) {
requestManager.getMangaChaptersFetch(manga.id);
}
if (!localBehindTracker) {
const chapterToBeUpdated = mangaChapters?.find((chapter) => chapter.chapterNumber === lastestTrackersRead);
if (chapterToBeUpdated && latestLocalRead < lastestTrackersRead) {
setChapterAsLastRead(chapterToBeUpdated?.id, mangaChapters as TChapter[]);
}
}
};

const handleClick = (openPopup: () => void) => {
refreshTracker();
updateChapterFromTracker();

if (trackerList.error) {
makeToast(t('tracking.error.label.could_not_load_track_info'), 'error');
return;
Expand Down
11 changes: 1 addition & 10 deletions src/components/tracker/TrackManga.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@

import { useNavigate } from 'react-router-dom';
import { Box } from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import DialogContent from '@mui/material/DialogContent';
import { useTranslation } from 'react-i18next';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { EmptyView } from '@/components/util/EmptyView.tsx';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
import { Trackers } from '@/lib/data/Trackers.ts';
import { TrackerCard, TrackerMode } from '@/components/tracker/TrackerCard.tsx';
import { TManga } from '@/typings.ts';
import { makeToast } from '@/components/util/Toast.tsx';

const getTrackerMode = (id: number, trackersInUse: number[], searchModeForTracker?: number): TrackerMode => {
if (id === searchModeForTracker) {
Expand All @@ -32,7 +30,6 @@ const getTrackerMode = (id: number, trackersInUse: number[], searchModeForTracke
};

export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords'> }) => {
const { t } = useTranslation();
const navigate = useNavigate();

const [searchModeForTracker, setSearchModeForTracker] = useState<number>();
Expand All @@ -47,12 +44,6 @@ export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords
const isSearchActive = searchModeForTracker !== undefined;
const OptionalDialogContent = useMemo(() => (isSearchActive ? Box : DialogContent), [isSearchActive]);

useEffect(() => {
Promise.all(manga.trackRecords.nodes.map((trackRecord) => requestManager.fetchTrackBind(trackRecord.id))).catch(
() => makeToast(t('tracking.error.label.could_not_fetch_track_info'), 'error'),
);
}, [manga.id]);

const trackerComponents = useMemo(
() =>
loggedInTrackers.map((tracker) => {
Expand Down
24 changes: 24 additions & 0 deletions src/components/util/findElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { findIndexOfElement } from '@/components/util/findIndexOfElement.ts';

// fieldToSearch string | any[]
export const findElement = <T>(
elements: T[],
fieldToSearch: string,
fieldToMatch: unknown,
isFieldToSearchArray?: boolean,
): T | undefined => {
const index = findIndexOfElement(elements, fieldToSearch, fieldToMatch, isFieldToSearchArray);

if (!index) {
return undefined;
}
return elements[index];
};
35 changes: 35 additions & 0 deletions src/components/util/findIndexOfElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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/.
*/

/**
*
* @param elements The array that will be search
* @param fieldToSearch The key of the elements that will be search
* @param fieldToMatch The value the fieldToSearch key needs to match
* @param isFieldToSearchArray Whether the key of the fieldToSearch is an array or not. Default to false
* @example findIndexOfElement(mangas, "id", passedManga.id)
* @returns The index of the element if found, or undefine if not found.
*/
export const findIndexOfElement = <T>(
elements: T[],
fieldToSearch: string,
fieldToMatch: unknown,
isFieldToSearchArray: boolean = false,
): number | undefined => {
let elementFoundIndex: number;

if (isFieldToSearchArray) {
elementFoundIndex = elements.findIndex((element: T | any) =>
element[fieldToSearch].some((field: any) => field === fieldToMatch),
);
} else {
// do a some() logic checking for boolean, so fieldToMatch fieldToMatch
elementFoundIndex = elements.findIndex((element: T | any) => element[fieldToSearch] === fieldToMatch);
}
return elementFoundIndex;
};
42 changes: 42 additions & 0 deletions src/components/util/getPartialList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { findIndexOfElement } from '@/components/util/findIndexOfElement.ts';

/**
*@description This function takes a element's id and a list of the same element, and it return either the first
of second half of the list using the element id as the pivot point.
* @param elementId The Id of the emeent to be use as pivot.
* @param allElements The list of elements to be firtered.
* @param halfOfList There part of the list to be return. Either the first half or the second.
* @param indexOffset The offsett to set for the index. By default set to 1, so the first have will not include the pivot element
* and the second half will include the first element.
* @returns The first of the second half of a list using the elementId passed as the pivots.
*/
export const getPartialList = <T>(
elementId: number,
allElements: T[],
halfOfList: 'first' | 'second' = 'first',
indexOffset: number = 1,
): T[] => {
const index = findIndexOfElement(allElements, 'id', elementId);
if (index === undefined) {
return [] as T[];
}
if (halfOfList === 'second') {
if (index + indexOffset > allElements.length - 1) {
return [] as T[];
}
if (index === 0) {
return allElements;
}
return allElements.slice(index + indexOffset);
}

return allElements.slice(0, index + indexOffset);
};