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/add manga to library category select dialog #519

Merged
merged 5 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 2 deletions src/components/library/useGetVisibleLibraryMangas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { StringParam, useQueryParam } from 'use-query-params';
import { useMemo } from 'react';
import { LibrarySortMode, NullAndUndefined, TManga } from '@/typings.ts';
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
import { useSearchSettings } from '@/util/searchSettings.ts';
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';

const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: TManga): boolean => {
switch (unread) {
Expand Down Expand Up @@ -105,7 +105,7 @@ export const useGetVisibleLibraryMangas = (mangas: TManga[]) => {
const [query] = useQueryParam('query', StringParam);
const { options } = useLibraryOptionsContext();
const { unread, downloaded } = options;
const { settings } = useSearchSettings();
const { settings } = useMetadataServerSettings();

const filteredMangas = useMemo(
() => filterManga(mangas, query, unread, downloaded, settings.ignoreFilters),
Expand Down
4 changes: 2 additions & 2 deletions src/components/manga/MangaActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ export const MangaActionMenu = ({
{isCategorySelectOpen && (
<CategorySelect
open={isCategorySelectOpen}
setOpen={(open) => {
setIsCategorySelectOpen(open);
onClose={() => {
setIsCategorySelectOpen(false);
bindMenuProps.onClose();
}}
mangaId={manga.id}
Expand Down
148 changes: 86 additions & 62 deletions src/components/manga/MangaDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import PublicIcon from '@mui/icons-material/Public';
import { styled } from '@mui/material/styles';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import Button from '@mui/material/Button';
import { ISource, TManga } from '@/typings';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/components/util/Toast';
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';

const DetailsWrapper = styled('div')(({ theme }) => ({
width: '100%',
Expand Down Expand Up @@ -167,12 +169,13 @@ function getValueOrUnknown(val?: string | null) {

export const MangaDetails: React.FC<IProps> = ({ manga }) => {
const { t } = useTranslation();
const { data: categoriesData, loading: areCategoriesLoading } = requestManager.useGetCategories();
const categories = categoriesData?.categories.nodes ?? [];
const defaultCategoryIds = categories
.filter((category) => category.default && category.id !== 0)
.map((category) => category.id);
const [updateMangaCategories] = requestManager.useUpdateMangaCategories();

const {
settings: { showAddToLibraryCategorySelectDialog },
loading: areSettingsLoading,
} = useMetadataServerSettings();

const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);

useEffect(() => {
if (!manga.source) {
Expand All @@ -181,18 +184,23 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
}, [manga.source]);

const addToLibrary = () => {
Promise.all([
requestManager.updateManga(manga.id, { inLibrary: true }).response,
updateMangaCategories({
variables: { input: { id: manga.id, patch: { addToCategories: defaultCategoryIds } } },
}),
])
.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
requestManager
.updateManga(manga.id, { inLibrary: true })
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
.catch(() => {
makeToast(t('library.error.label.add_to_library'), 'error');
});
};

const handleAddToLibraryClick = () => {
if (!showAddToLibraryCategorySelectDialog) {
addToLibrary();
return;
}

setIsCategorySelectOpen(true);
};

const removeFromLibrary = () => {
Promise.all([requestManager.updateManga(manga.id, { inLibrary: false }).response])
.then(() => makeToast(t('library.info.label.removed_from_library'), 'success'))
Expand All @@ -202,53 +210,69 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
};

return (
<DetailsWrapper>
<TopContentWrapper>
<ThumbnailMetadataWrapper>
<Thumbnail>
{manga.thumbnailUrl && (
<img src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} alt="Manga Thumbnail" />
)}
</Thumbnail>
<Metadata>
<h1>{manga.title}</h1>
<h3>
{`${t('manga.label.author')}: `}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
{`${t('manga.label.artist')}: `}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>{`${t('manga.label.status')}: ${manga.status}`}</h3>
<h3>{`${t('source.title')}: ${getSourceName(manga.source)}`}</h3>
</Metadata>
</ThumbnailMetadataWrapper>
<MangaButtonsContainer inLibrary={manga.inLibrary}>
<div>
<Button
disabled={areCategoriesLoading}
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
onClick={manga.inLibrary ? removeFromLibrary : addToLibrary}
size="large"
>
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
</Button>
</div>
<OpenSourceButton url={manga.realUrl} />
</MangaButtonsContainer>
</TopContentWrapper>
<BottomContentWrapper>
<Description>
<h4>{t('settings.about.title')}</h4>
<p style={{ whiteSpace: 'pre-line' }}>{manga.description}</p>
</Description>
<Genres>
{manga.genre.map((g) => (
<h5 key={g}>{g}</h5>
))}
</Genres>
</BottomContentWrapper>
</DetailsWrapper>
<>
<DetailsWrapper>
<TopContentWrapper>
<ThumbnailMetadataWrapper>
<Thumbnail>
{manga.thumbnailUrl && (
<img src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} alt="Manga Thumbnail" />
)}
</Thumbnail>
<Metadata>
<h1>{manga.title}</h1>
<h3>
{`${t('manga.label.author')}: `}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
{`${t('manga.label.artist')}: `}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>{`${t('manga.label.status')}: ${manga.status}`}</h3>
<h3>{`${t('source.title')}: ${getSourceName(manga.source)}`}</h3>
</Metadata>
</ThumbnailMetadataWrapper>
<MangaButtonsContainer inLibrary={manga.inLibrary}>
<div>
<Button
disabled={areSettingsLoading}
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
size="large"
>
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
</Button>
</div>
<OpenSourceButton url={manga.realUrl} />
</MangaButtonsContainer>
</TopContentWrapper>
<BottomContentWrapper>
<Description>
<h4>{t('settings.about.title')}</h4>
<p style={{ whiteSpace: 'pre-line' }}>{manga.description}</p>
</Description>
<Genres>
{manga.genre.map((g) => (
<h5 key={g}>{g}</h5>
))}
</Genres>
</BottomContentWrapper>
</DetailsWrapper>
{isCategorySelectOpen && (
<CategorySelect
open={isCategorySelectOpen}
onClose={(didUpdateCategories) => {
setIsCategorySelectOpen(false);

if (didUpdateCategories) {
addToLibrary();
}
}}
mangaId={manga.id}
addToLibrary
/>
)}
</>
);
};
2 changes: 1 addition & 1 deletion src/components/manga/MangaToolbarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
</>
)}

<CategorySelect open={editCategories} setOpen={setEditCategories} mangaId={manga.id} />
<CategorySelect open={editCategories} onClose={() => setEditCategories(false)} mangaId={manga.id} />
</>
);
};
4 changes: 2 additions & 2 deletions src/components/manga/MangasSelectionFABActionItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ export const MangasSelectionFABActionItems = ({
{isCategorySelectOpen && (
<CategorySelect
open={isCategorySelectOpen}
setOpen={(open) => {
setIsCategorySelectOpen(open);
onClose={() => {
setIsCategorySelectOpen(false);
handleClose(true);
}}
mangaIds={Mangas.getIds(selectedMangas)}
Expand Down
22 changes: 16 additions & 6 deletions src/components/navbar/action/CategorySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Mangas } from '@/lib/data/Mangas.ts';
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
import { TCategory } from '@/typings.ts';

type BaseProps = {
open: boolean;
setOpen: (value: boolean) => void;
onClose: (didUpdateCategories: boolean) => void;
};

type SingleMangaModeProps = {
mangaId: number;
addToLibrary?: boolean;
};

type MultiMangaModeProps = {
Expand Down Expand Up @@ -69,10 +71,13 @@ const getCategoryCheckedState = (
return undefined;
};

const getDefaultCategoryIds = (categories: TCategory[]) =>
categories.filter(({ default: isDefault }) => isDefault).map(({ id }) => id);

export function CategorySelect(props: Props) {
const { t } = useTranslation();

const { open, setOpen, mangaId, mangaIds: passedMangaIds } = props;
const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props;

const isSingleSelectionMode = mangaId !== undefined;
const mangaIds = passedMangaIds ?? [mangaId];
Expand All @@ -89,19 +94,24 @@ export function CategorySelect(props: Props) {
return cats;
}, [categoriesData]);

const defaultCategoryIds = useMemo(
() => (addToLibrary ? getDefaultCategoryIds(allCategories) : []),
[allCategories],
);

const { handleSelection, setSelectionForKey, getSelectionForKey } = useSelectableCollection<
number,
'categoriesToAdd' | 'categoriesToRemove'
>(allCategories.length, {
currentKey: 'categoriesToAdd',
initialState: {
categoriesToAdd: mangaCategoryIds,
categoriesToAdd: [...mangaCategoryIds, ...defaultCategoryIds],
categoriesToRemove: [],
},
});

useEffect(() => {
setSelectionForKey('categoriesToAdd', mangaCategoryIds);
setSelectionForKey('categoriesToAdd', [...mangaCategoryIds, ...defaultCategoryIds]);
setSelectionForKey('categoriesToRemove', []);
}, [mangaCategoryIds]);

Expand All @@ -111,11 +121,11 @@ export function CategorySelect(props: Props) {
const handleCancel = () => {
setSelectionForKey('categoriesToAdd', mangaCategoryIds);
setSelectionForKey('categoriesToRemove', []);
setOpen(false);
onClose(false);
};

const handleOk = () => {
setOpen(false);
onClose(true);

const addToCategories = isSingleSelectionMode
? categoriesToAdd.filter((categoryId) => !mangaCategoryIds.includes(categoryId))
Expand Down
46 changes: 31 additions & 15 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@
"discord": "Discord",
"display": "Display",
"filter": "Filter",
"general": "General",
"github": "GitHub",
"links": "Links",
"loading": "Loading…",
Expand Down Expand Up @@ -402,6 +403,24 @@
}
},
"settings": {
"general": {
"add_to_library": {
"category_selection": {
"label": {
"description": "Show the category selection dialog when adding a manga to the library",
"title": "Category selection dialog"
}
}
},
"search": {
"ignore_filters": {
"label": {
"description": "Search results will include manga that do not match the current filters",
"title": "Ignore filters when searching"
}
}
}
},
"global_update": {
"auto_update": {
"interval": {
Expand Down Expand Up @@ -447,6 +466,18 @@
},
"manga": {
"action": {
"category": {
"button": {
"selected": "Change categories of selected"
},
"label": {
"action": "Change categories",
"error_one": "Could not change the categories of the manga",
"error_other": "Could not change the categories of the manga",
"success_one": "Changed categories of manga",
"success_other": "Changed categories of {{count}} manga"
}
},
"library": {
"remove": {
"button": {
Expand All @@ -460,18 +491,6 @@
"success_other": "Removed {{count}} manga from the library"
}
}
},
"category": {
"button": {
"selected": "Change categories of selected"
},
"label": {
"action": "Change categories",
"error_one": "Could not change the categories of the manga",
"error_other": "Could not change the categories of the manga",
"success_one": "Changed categories of manga",
"success_other": "Changed categories of {{count}} manga"
}
}
},
"button": {
Expand Down Expand Up @@ -558,9 +577,6 @@
"source_search_failed": "Could not search source"
}
},
"label": {
"ignore_filters": "Ignore filters when searching"
},
"title": {
"global_search": "Global Search",
"search": "Search"
Expand Down
Loading