Skip to content

Commit

Permalink
Feature/add manga to library category select dialog (#519)
Browse files Browse the repository at this point in the history
* Move "SearchSettings" into "LibrarySettings"

* Change property "setOpen" to "onClose" for "CategorySelect"

* Optionally show category selection when adding manga to library

* Update "ignore filters" translation

* Move "ignore_filters" setting to "metadataServerSettings"
  • Loading branch information
schroda authored Dec 27, 2023
1 parent 1345cfe commit e025efb
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 173 deletions.
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

0 comments on commit e025efb

Please sign in to comment.