Feature/download ahead while reading (#464)
* Download next chapters when marking chapter as read while reading * Add "auto delete chapter" settings * Delete chapter after finished reading * Delete chapters after marking them as read * Improve "mark previous as read" action Send only ids of chapters that are actually unread
This commit is contained in:
@@ -32,10 +32,11 @@ import { getUploadDateString } from '@/util/date';
|
|||||||
import { DownloadStateIndicator } from '@/components/molecules/DownloadStateIndicator';
|
import { DownloadStateIndicator } from '@/components/molecules/DownloadStateIndicator';
|
||||||
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { TChapter } from '@/typings.ts';
|
import { TChapter } from '@/typings.ts';
|
||||||
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
chapter: TChapter;
|
chapter: TChapter;
|
||||||
chapterIds: number[];
|
allChapters: TChapter[];
|
||||||
downloadChapter: DownloadType | undefined;
|
downloadChapter: DownloadType | undefined;
|
||||||
showChapterNumber: boolean;
|
showChapterNumber: boolean;
|
||||||
onSelect: (selected: boolean) => void;
|
onSelect: (selected: boolean) => void;
|
||||||
@@ -46,9 +47,11 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { chapter, chapterIds, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
const { chapter, allChapters, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
||||||
const isSelecting = selected !== null;
|
const isSelecting = selected !== null;
|
||||||
|
|
||||||
|
const { settings: metadataServerSettings } = useMetadataServerSettings();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@@ -67,9 +70,29 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
const sendChange = <Key extends keyof UpdatePatchInput>(key: Key, value: UpdatePatchInput[Key]) => {
|
const sendChange = <Key extends keyof UpdatePatchInput>(key: Key, value: UpdatePatchInput[Key]) => {
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
|
const isMarkAsRead = (key === 'isRead' && value) || key === 'markPrevRead';
|
||||||
|
const shouldAutoDeleteChapters = isMarkAsRead && metadataServerSettings.deleteChaptersManuallyMarkedRead;
|
||||||
|
if (shouldAutoDeleteChapters) {
|
||||||
|
const shouldDeleteChapter = ({ isBookmarked, isDownloaded }: TChapter) =>
|
||||||
|
isDownloaded && (!isBookmarked || metadataServerSettings.deleteChaptersWithBookmark);
|
||||||
|
|
||||||
|
const chaptersToDelete = key === 'isRead' ? [chapter] : allChapters;
|
||||||
|
const chapterIdsToDelete = chaptersToDelete
|
||||||
|
.filter(shouldDeleteChapter)
|
||||||
|
.map(({ id: chapterId }) => chapterId);
|
||||||
|
|
||||||
|
requestManager.deleteDownloadedChapters(chapterIdsToDelete).response.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
if (key === 'markPrevRead') {
|
if (key === 'markPrevRead') {
|
||||||
const index = chapterIds.findIndex((chapterId) => chapterId === chapter.id);
|
const index = allChapters.findIndex(({ id: chapterId }) => chapterId === chapter.id);
|
||||||
requestManager.updateChapters(chapterIds.slice(index), { isRead: true });
|
requestManager.updateChapters(
|
||||||
|
allChapters
|
||||||
|
.slice(index)
|
||||||
|
.filter(({ isRead }) => !isRead)
|
||||||
|
.map(({ id: chapterId }) => chapterId),
|
||||||
|
{ isRead: true },
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { ChaptersToolbarMenu } from '@/components/manga/ChaptersToolbarMenu';
|
|||||||
import { SelectionFAB } from '@/components/manga/SelectionFAB';
|
import { SelectionFAB } from '@/components/manga/SelectionFAB';
|
||||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
|
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
|
||||||
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
|
||||||
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
||||||
listStyle: 'none',
|
listStyle: 'none',
|
||||||
@@ -89,7 +90,8 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
const [options, dispatch] = useChapterOptions(manga.id);
|
const [options, dispatch] = useChapterOptions(manga.id);
|
||||||
const { data: chaptersData, loading: isLoading, refetch } = requestManager.useGetMangaChapters(manga.id);
|
const { data: chaptersData, loading: isLoading, refetch } = requestManager.useGetMangaChapters(manga.id);
|
||||||
const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]);
|
const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]);
|
||||||
const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]);
|
|
||||||
|
const { settings: metadataServerSettings } = useMetadataServerSettings();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevQueueRef.current && queue) {
|
if (prevQueueRef.current && queue) {
|
||||||
@@ -164,6 +166,20 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
} else {
|
} else {
|
||||||
actionPromise = requestManager.updateChapters(chapterIds, change).response;
|
actionPromise = requestManager.updateChapters(chapterIds, change).response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldDeleteChapters =
|
||||||
|
action === 'mark_as_read' && metadataServerSettings.deleteChaptersManuallyMarkedRead;
|
||||||
|
if (shouldDeleteChapters) {
|
||||||
|
const chapterIdsToDelete = actionChapters
|
||||||
|
.filter(
|
||||||
|
({ chapter }) =>
|
||||||
|
chapter.isDownloaded &&
|
||||||
|
(!chapter.isBookmarked || metadataServerSettings.deleteChaptersWithBookmark),
|
||||||
|
)
|
||||||
|
.map(({ chapter }) => chapter.id);
|
||||||
|
|
||||||
|
requestManager.deleteDownloadedChapters(chapterIdsToDelete).response.catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actionPromise
|
actionPromise
|
||||||
@@ -285,7 +301,7 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
return (
|
return (
|
||||||
<ChapterCard
|
<ChapterCard
|
||||||
{...chaptersWithMeta[index]}
|
{...chaptersWithMeta[index]}
|
||||||
chapterIds={mangaChapterIds}
|
allChapters={chapters}
|
||||||
showChapterNumber={options.showChapterNumber}
|
showChapterNumber={options.showChapterNumber}
|
||||||
onSelect={() => handleSelection(index)}
|
onSelect={() => handleSelection(index)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2285,6 +2285,13 @@ export type StopDownloaderMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type StopDownloaderMutation = { __typename?: 'Mutation', stopDownloader: { __typename?: 'StopDownloaderPayload', clientMutationId?: string | null, downloadStatus: { __typename?: 'DownloadStatus', state: DownloaderState, queue: Array<{ __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', chapterNumber: number, fetchedAt: any, id: number, isBookmarked: boolean, isDownloaded: boolean, isRead: boolean, lastPageRead: number, lastReadAt: any, name: string, pageCount: number, realUrl?: string | null, scanlator?: string | null, sourceOrder: number, uploadDate: any, url: string, manga: { __typename?: 'MangaType', unreadCount: number, downloadCount: number, artist?: string | null, author?: string | null, chaptersLastFetchedAt?: any | null, description?: string | null, genre: Array<string>, id: number, inLibrary: boolean, inLibraryAt: any, initialized: boolean, lastFetchedAt?: any | null, realUrl?: string | null, status: MangaStatus, thumbnailUrl?: string | null, title: string, url: string, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeInUpdate, name: string, order: number, meta: Array<{ __typename?: 'CategoryMetaType', key: string, value: string }>, mangas: { __typename?: 'MangaNodeList', totalCount: number } }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, meta: Array<{ __typename?: 'MangaMetaType', key: string, value: string }>, source?: { __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean } | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } }> } } };
|
export type StopDownloaderMutation = { __typename?: 'Mutation', stopDownloader: { __typename?: 'StopDownloaderPayload', clientMutationId?: string | null, downloadStatus: { __typename?: 'DownloadStatus', state: DownloaderState, queue: Array<{ __typename?: 'DownloadType', progress: number, state: DownloadState, tries: number, chapter: { __typename?: 'ChapterType', chapterNumber: number, fetchedAt: any, id: number, isBookmarked: boolean, isDownloaded: boolean, isRead: boolean, lastPageRead: number, lastReadAt: any, name: string, pageCount: number, realUrl?: string | null, scanlator?: string | null, sourceOrder: number, uploadDate: any, url: string, manga: { __typename?: 'MangaType', unreadCount: number, downloadCount: number, artist?: string | null, author?: string | null, chaptersLastFetchedAt?: any | null, description?: string | null, genre: Array<string>, id: number, inLibrary: boolean, inLibraryAt: any, initialized: boolean, lastFetchedAt?: any | null, realUrl?: string | null, status: MangaStatus, thumbnailUrl?: string | null, title: string, url: string, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeInUpdate, name: string, order: number, meta: Array<{ __typename?: 'CategoryMetaType', key: string, value: string }>, mangas: { __typename?: 'MangaNodeList', totalCount: number } }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, meta: Array<{ __typename?: 'MangaMetaType', key: string, value: string }>, source?: { __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean } | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } }> } } };
|
||||||
|
|
||||||
|
export type DownloadAheadMutationVariables = Exact<{
|
||||||
|
input: DownloadAheadInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type DownloadAheadMutation = { __typename?: 'Mutation', downloadAhead: { __typename?: 'DownloadAheadPayload', clientMutationId?: string | null } };
|
||||||
|
|
||||||
export type GetExtensionsFetchMutationVariables = Exact<{
|
export type GetExtensionsFetchMutationVariables = Exact<{
|
||||||
input?: InputMaybe<FetchExtensionsInput>;
|
input?: InputMaybe<FetchExtensionsInput>;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -128,3 +128,11 @@ export const STOP_DOWNLOADER = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const DOWNLOAD_AHEAD = gql`
|
||||||
|
mutation DOWNLOAD_AHEAD($input: DownloadAheadInput!) {
|
||||||
|
downloadAhead(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ import {
|
|||||||
DequeueChapterDownloadMutationVariables,
|
DequeueChapterDownloadMutationVariables,
|
||||||
DequeueChapterDownloadsMutation,
|
DequeueChapterDownloadsMutation,
|
||||||
DequeueChapterDownloadsMutationVariables,
|
DequeueChapterDownloadsMutationVariables,
|
||||||
|
DownloadAheadMutation,
|
||||||
|
DownloadAheadMutationVariables,
|
||||||
DownloadStatusSubscription,
|
DownloadStatusSubscription,
|
||||||
DownloadStatusSubscriptionVariables,
|
DownloadStatusSubscriptionVariables,
|
||||||
EnqueueChapterDownloadMutation,
|
EnqueueChapterDownloadMutation,
|
||||||
@@ -171,6 +173,7 @@ import {
|
|||||||
DELETE_DOWNLOADED_CHAPTERS,
|
DELETE_DOWNLOADED_CHAPTERS,
|
||||||
DEQUEUE_CHAPTER_DOWNLOAD,
|
DEQUEUE_CHAPTER_DOWNLOAD,
|
||||||
DEQUEUE_CHAPTER_DOWNLOADS,
|
DEQUEUE_CHAPTER_DOWNLOADS,
|
||||||
|
DOWNLOAD_AHEAD,
|
||||||
ENQUEUE_CHAPTER_DOWNLOAD,
|
ENQUEUE_CHAPTER_DOWNLOAD,
|
||||||
ENQUEUE_CHAPTER_DOWNLOADS,
|
ENQUEUE_CHAPTER_DOWNLOADS,
|
||||||
REORDER_CHAPTER_DOWNLOAD,
|
REORDER_CHAPTER_DOWNLOAD,
|
||||||
@@ -1909,6 +1912,12 @@ export class RequestManager {
|
|||||||
): AbortableApolloUseMutationResponse<UpdateServerSettingsMutation, UpdateServerSettingsMutationVariables> {
|
): AbortableApolloUseMutationResponse<UpdateServerSettingsMutation, UpdateServerSettingsMutationVariables> {
|
||||||
return this.doRequest(GQLMethod.USE_MUTATION, UPDATE_SERVER_SETTINGS, undefined, options);
|
return this.doRequest(GQLMethod.USE_MUTATION, UPDATE_SERVER_SETTINGS, undefined, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public useDownloadAhead(
|
||||||
|
options?: MutationHookOptions<DownloadAheadMutation, DownloadAheadMutationVariables>,
|
||||||
|
): AbortableApolloUseMutationResponse<DownloadAheadMutation, DownloadAheadMutationVariables> {
|
||||||
|
return this.doRequest(GQLMethod.USE_MUTATION, DOWNLOAD_AHEAD, undefined, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const requestManager = new RequestManager();
|
export const requestManager = new RequestManager();
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import { ReaderNavBar } from '@/components/navbar/ReaderNavBar';
|
|||||||
import { makeToast } from '@/components/util/Toast';
|
import { makeToast } from '@/components/util/Toast';
|
||||||
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||||
import { useDebounce } from '@/components/manga/hooks.ts';
|
import { useDebounce } from '@/components/manga/hooks.ts';
|
||||||
|
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
|
||||||
const isDupChapter = async (chapterIndex: number, currentChapter: TChapter) => {
|
const isDupChapter = async (chapterIndex: number, currentChapter: TChapter) => {
|
||||||
const nextChapter = await requestManager.getChapter(currentChapter.manga.id, chapterIndex).response;
|
const nextChapter = await requestManager.getChapter(currentChapter.manga.id, chapterIndex).response;
|
||||||
@@ -121,6 +123,10 @@ export function Reader() {
|
|||||||
});
|
});
|
||||||
const [arePagesUpdated, setArePagesUpdated] = useState(false);
|
const [arePagesUpdated, setArePagesUpdated] = useState(false);
|
||||||
|
|
||||||
|
const { data: settingsData } = requestManager.useGetServerSettings();
|
||||||
|
const isDownloadAheadEnabled = !!settingsData?.settings.autoDownloadAheadLimit;
|
||||||
|
const [downloadAhead] = requestManager.useDownloadAhead();
|
||||||
|
|
||||||
const getLoadedChapter = () => {
|
const getLoadedChapter = () => {
|
||||||
const isAChapterLoaded = loadedChapter.current;
|
const isAChapterLoaded = loadedChapter.current;
|
||||||
|
|
||||||
@@ -168,6 +174,34 @@ export function Reader() {
|
|||||||
const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings();
|
const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings();
|
||||||
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
|
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
|
||||||
|
|
||||||
|
const { settings: metadataSettings } = useMetadataServerSettings();
|
||||||
|
|
||||||
|
const updateChapter = (patch: UpdateChapterPatchInput) => {
|
||||||
|
requestManager.updateChapter(chapter.id, patch).response.catch(() => {});
|
||||||
|
|
||||||
|
const shouldDeleteChapter =
|
||||||
|
patch.isRead &&
|
||||||
|
metadataSettings.deleteChaptersAutoMarkedRead &&
|
||||||
|
chapter.isDownloaded &&
|
||||||
|
(!chapter.isBookmarked || metadataSettings.deleteChaptersWithBookmark);
|
||||||
|
if (shouldDeleteChapter) {
|
||||||
|
requestManager.deleteDownloadedChapter(chapter.id).response.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDownloadAhead =
|
||||||
|
chapter.manga.inLibrary && !chapter.isRead && patch.isRead && isDownloadAheadEnabled;
|
||||||
|
if (shouldDownloadAhead) {
|
||||||
|
downloadAhead({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
mangaIds: [chapter.manga.id],
|
||||||
|
latestReadChapterIds: [chapter.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||||
setSettings({ ...settings, [key]: value });
|
setSettings({ ...settings, [key]: value });
|
||||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
||||||
@@ -255,31 +289,42 @@ export function Reader() {
|
|||||||
// do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page
|
// do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page
|
||||||
const updateLastPageRead = curPageDebounced !== -1;
|
const updateLastPageRead = curPageDebounced !== -1;
|
||||||
const updateIsRead = curPageDebounced === chapter.pageCount - 1;
|
const updateIsRead = curPageDebounced === chapter.pageCount - 1;
|
||||||
const updateChapter = updateLastPageRead || updateIsRead;
|
const shouldUpdateChapter = updateLastPageRead || updateIsRead;
|
||||||
|
if (!shouldUpdateChapter) {
|
||||||
if (updateChapter) {
|
return;
|
||||||
requestManager.updateChapter(chapter.id, {
|
|
||||||
lastPageRead: updateLastPageRead ? curPageDebounced : undefined,
|
|
||||||
isRead: updateIsRead ? true : undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [curPageDebounced]);
|
|
||||||
|
updateChapter({
|
||||||
|
lastPageRead: updateLastPageRead ? curPageDebounced : undefined,
|
||||||
|
isRead: updateIsRead ? true : undefined,
|
||||||
|
});
|
||||||
|
}, [curPageDebounced, isDownloadAheadEnabled]);
|
||||||
|
|
||||||
const nextChapter = useCallback(() => {
|
const nextChapter = useCallback(() => {
|
||||||
if (chapter.sourceOrder < manga.chapters.totalCount) {
|
const doesNextChapterExist = chapter.sourceOrder < manga.chapters.totalCount;
|
||||||
requestManager.updateChapter(chapter.id, {
|
if (!doesNextChapterExist) {
|
||||||
lastPageRead: chapter.pageCount - 1,
|
return;
|
||||||
isRead: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>
|
|
||||||
navigate(`/manga/${manga.id}/chapter/${nextChapterIndex}`, {
|
|
||||||
replace: true,
|
|
||||||
state: location.state,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [chapter.sourceOrder, manga.chapters.totalCount, chapter.pageCount, manga.id, settings.skipDupChapters]);
|
|
||||||
|
updateChapter({
|
||||||
|
lastPageRead: chapter.pageCount - 1,
|
||||||
|
isRead: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>
|
||||||
|
navigate(`/manga/${manga.id}/chapter/${nextChapterIndex}`, {
|
||||||
|
replace: true,
|
||||||
|
state: location.state,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
chapter.sourceOrder,
|
||||||
|
manga.chapters.totalCount,
|
||||||
|
chapter.pageCount,
|
||||||
|
manga.id,
|
||||||
|
settings.skipDupChapters,
|
||||||
|
isDownloadAheadEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const prevChapter = useCallback(() => {
|
const prevChapter = useCallback(() => {
|
||||||
if (chapter.sourceOrder > 1) {
|
if (chapter.sourceOrder > 1) {
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
|
|||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
||||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
||||||
import { ServerSettings } from '@/typings.ts';
|
import { MetadataServerSettingKeys, MetadataServerSettings, ServerSettings } from '@/typings.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { DownloadAheadSetting } from '@/components/settings/downloads/DownloadAheadSetting.tsx';
|
import { DownloadAheadSetting } from '@/components/settings/downloads/DownloadAheadSetting.tsx';
|
||||||
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.ts';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
|
||||||
type DownloadSettingsType = Pick<
|
type DownloadSettingsType = Pick<
|
||||||
ServerSettings,
|
ServerSettings,
|
||||||
@@ -49,12 +52,28 @@ export const DownloadSettings = () => {
|
|||||||
const { data } = requestManager.useGetServerSettings();
|
const { data } = requestManager.useGetServerSettings();
|
||||||
const downloadSettings = data ? extractDownloadSettings(data.settings) : undefined;
|
const downloadSettings = data ? extractDownloadSettings(data.settings) : undefined;
|
||||||
const [mutateSettings] = requestManager.useUpdateServerSettings();
|
const [mutateSettings] = requestManager.useUpdateServerSettings();
|
||||||
|
const { metadata, settings: metadataSettings } = useMetadataServerSettings();
|
||||||
|
|
||||||
const updateSetting = <Setting extends keyof DownloadSettingsType>(
|
const updateSetting = <Setting extends keyof DownloadSettingsType>(
|
||||||
setting: Setting,
|
setting: Setting,
|
||||||
value: DownloadSettingsType[Setting],
|
value: DownloadSettingsType[Setting],
|
||||||
) => {
|
) => {
|
||||||
mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
|
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch(() =>
|
||||||
|
makeToast(t('global.error.label.failed_to_save_changes'), 'error'),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMetadataSetting = <Setting extends MetadataServerSettingKeys>(
|
||||||
|
setting: Setting,
|
||||||
|
value: MetadataServerSettings[Setting],
|
||||||
|
) => {
|
||||||
|
if (!metadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdateServerMetadata(convertToGqlMeta(metadata) ?? [], [[setting, value]]).catch(() =>
|
||||||
|
makeToast(t('global.error.label.failed_to_save_changes'), 'error'),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -75,6 +94,46 @@ export const DownloadSettings = () => {
|
|||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<List
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component="div" id="download-settings-auto-download">
|
||||||
|
Delete chapters
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Delete chapter after manually marking it as read" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={metadataSettings.deleteChaptersManuallyMarkedRead}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMetadataSetting('deleteChaptersManuallyMarkedRead', e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Delete finished chapters while reading" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={metadataSettings.deleteChaptersAutoMarkedRead}
|
||||||
|
onChange={(e) => updateMetadataSetting('deleteChaptersAutoMarkedRead', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Allow deleting bookmarked chapters" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={metadataSettings.deleteChaptersWithBookmark}
|
||||||
|
onChange={(e) => updateMetadataSetting('deleteChaptersWithBookmark', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
<List
|
<List
|
||||||
subheader={
|
subheader={
|
||||||
<ListSubheader component="div" id="download-settings-auto-download">
|
<ListSubheader component="div" id="download-settings-auto-download">
|
||||||
|
|||||||
@@ -88,11 +88,13 @@ export type MetadataHolder<Keys extends string = string, Values = string> = {
|
|||||||
|
|
||||||
export type AllowedMetadataValueTypes = string | boolean | number | undefined;
|
export type AllowedMetadataValueTypes = string | boolean | number | undefined;
|
||||||
|
|
||||||
|
export type MetadataServerSettingKeys = keyof MetadataServerSettings;
|
||||||
|
|
||||||
export type MangaMetadataKeys = keyof IReaderSettings;
|
export type MangaMetadataKeys = keyof IReaderSettings;
|
||||||
|
|
||||||
export type SearchMetadataKeys = keyof ISearchSettings;
|
export type SearchMetadataKeys = keyof ISearchSettings;
|
||||||
|
|
||||||
export type AppMetadataKeys = MangaMetadataKeys | SearchMetadataKeys;
|
export type AppMetadataKeys = MetadataServerSettingKeys | MangaMetadataKeys | SearchMetadataKeys;
|
||||||
|
|
||||||
export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
|
export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
|
||||||
|
|
||||||
@@ -226,6 +228,12 @@ export enum ChapterOffset {
|
|||||||
NEXT = 1,
|
NEXT = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MetadataServerSettings = {
|
||||||
|
deleteChaptersManuallyMarkedRead: boolean;
|
||||||
|
deleteChaptersAutoMarkedRead: boolean;
|
||||||
|
deleteChaptersWithBookmark: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ISearchSettings {
|
export interface ISearchSettings {
|
||||||
ignoreFilters: boolean;
|
ignoreFilters: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export const requestUpdateMetadataValue = async (
|
|||||||
export const requestUpdateMetadata = async (
|
export const requestUpdateMetadata = async (
|
||||||
metadataHolder: GqlMetaHolder,
|
metadataHolder: GqlMetaHolder,
|
||||||
holderType: MetadataHolderType,
|
holderType: MetadataHolderType,
|
||||||
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
|
keysToValues: MetadataKeyValuePair[],
|
||||||
): Promise<void[]> =>
|
): Promise<void[]> =>
|
||||||
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
|
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
|
||||||
|
|
||||||
|
|||||||
34
src/util/metadataServerSettings.ts
Normal file
34
src/util/metadataServerSettings.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Metadata, MetadataServerSettings } from '@/typings';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { convertFromGqlMeta, getMetadataFrom } from '@/util/metadata';
|
||||||
|
|
||||||
|
export const getDefaultSettings = (): MetadataServerSettings => ({
|
||||||
|
deleteChaptersManuallyMarkedRead: false,
|
||||||
|
deleteChaptersAutoMarkedRead: false,
|
||||||
|
deleteChaptersWithBookmark: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMetadataServerSettingsWithDefaultFallback = (
|
||||||
|
meta?: Metadata,
|
||||||
|
defaultSettings: MetadataServerSettings = getDefaultSettings(),
|
||||||
|
applyMetadataMigration: boolean = true,
|
||||||
|
): MetadataServerSettings => getMetadataFrom({ meta }, defaultSettings, applyMetadataMigration);
|
||||||
|
export const useMetadataServerSettings = (): {
|
||||||
|
metadata?: Metadata;
|
||||||
|
settings: MetadataServerSettings;
|
||||||
|
loading: boolean;
|
||||||
|
} => {
|
||||||
|
const { data, loading } = requestManager.useGetGlobalMeta();
|
||||||
|
const metadata = convertFromGqlMeta(data?.metas.nodes);
|
||||||
|
const settings = getMetadataServerSettingsWithDefaultFallback(metadata);
|
||||||
|
|
||||||
|
return { metadata, settings, loading };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user