Feature/browse source manga long press remove add manga from library (#710)
* Extract CategorySelect usage into hook * Extract logic to change manga in library state into hook * Change manga in library state on long press when browsing source
This commit is contained in:
@@ -10,11 +10,12 @@ import Card from '@mui/material/Card';
|
|||||||
import CardActionArea from '@mui/material/CardActionArea';
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { Avatar, Box, CardContent, Link, Stack, styled, Tooltip } from '@mui/material';
|
import { Avatar, Box, Button, CardContent, Link, Stack, styled, Tooltip } from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import PopupState, { bindMenu } from 'material-ui-popup-state';
|
import PopupState, { bindMenu } from 'material-ui-popup-state';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useLongPress } from 'use-long-press';
|
import { useLongPress } from 'use-long-press';
|
||||||
|
import { isMobile } from 'react-device-detect';
|
||||||
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||||
import { SpinnerImage } from '@/components/util/SpinnerImage';
|
import { SpinnerImage } from '@/components/util/SpinnerImage';
|
||||||
import { TManga, TPartialManga } from '@/typings.ts';
|
import { TManga, TPartialManga } from '@/typings.ts';
|
||||||
@@ -26,6 +27,7 @@ import { Menu } from '@/components/menu/Menu.tsx';
|
|||||||
import { MigrateDialog } from '@/components/MigrateDialog.tsx';
|
import { MigrateDialog } from '@/components/MigrateDialog.tsx';
|
||||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||||
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx';
|
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx';
|
||||||
|
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
|
||||||
|
|
||||||
const BottomGradient = styled('div')({
|
const BottomGradient = styled('div')({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -62,7 +64,7 @@ const BadgeContainer = styled('div')({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type MangaCardMode = 'default' | 'migrate.search' | 'migrate.select';
|
type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select';
|
||||||
|
|
||||||
export interface MangaCardProps {
|
export interface MangaCardProps {
|
||||||
manga: TPartialManga;
|
manga: TPartialManga;
|
||||||
@@ -81,6 +83,7 @@ const getMangaLinkTo = (
|
|||||||
): string => {
|
): string => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'default':
|
case 'default':
|
||||||
|
case 'source':
|
||||||
return `/manga/${mangaId}/`;
|
return `/manga/${mangaId}/`;
|
||||||
case 'migrate.search':
|
case 'migrate.search':
|
||||||
return `/migrate/source/${sourceId}/manga/${mangaId}/search?query=${mangaTitle}`;
|
return `/migrate/source/${sourceId}/manga/${mangaId}/search?query=${mangaTitle}`;
|
||||||
@@ -97,21 +100,14 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
const optionButtonRef = useRef<HTMLButtonElement>(null);
|
const optionButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props;
|
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props;
|
||||||
const {
|
const { id, title, downloadCount, unreadCount: unread, latestReadChapter, firstUnreadChapter, chapters } = manga;
|
||||||
id,
|
|
||||||
title,
|
|
||||||
downloadCount,
|
|
||||||
unreadCount: unread,
|
|
||||||
inLibrary,
|
|
||||||
latestReadChapter,
|
|
||||||
firstUnreadChapter,
|
|
||||||
chapters,
|
|
||||||
} = manga;
|
|
||||||
const thumbnailUrl = Mangas.getThumbnailUrl(manga);
|
const thumbnailUrl = Mangas.getThumbnailUrl(manga);
|
||||||
const {
|
const {
|
||||||
options: { showContinueReadingButton, showUnreadBadge, showDownloadBadge },
|
options: { showContinueReadingButton, showUnreadBadge, showDownloadBadge },
|
||||||
} = useLibraryOptionsContext();
|
} = useLibraryOptionsContext();
|
||||||
|
|
||||||
|
const { CategorySelectComponent, updateLibraryState, isInLibrary } = useManageMangaLibraryState(manga);
|
||||||
|
|
||||||
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.source?.id, manga.title);
|
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.source?.id, manga.title);
|
||||||
|
|
||||||
const nextChapterIndexToRead = firstUnreadChapter?.sourceOrder ?? 1;
|
const nextChapterIndexToRead = firstUnreadChapter?.sourceOrder ?? 1;
|
||||||
@@ -121,16 +117,24 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
|
|
||||||
const handleClick = (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
|
const handleClick = (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
|
||||||
const isDefaultMode = mode === 'default';
|
const isDefaultMode = mode === 'default';
|
||||||
|
const isSourceMode = mode === 'source';
|
||||||
const isMigrateSelectMode = mode === 'migrate.select';
|
const isMigrateSelectMode = mode === 'migrate.select';
|
||||||
const isSelectionMode = selected !== null;
|
const isSelectionMode = selected !== null;
|
||||||
|
const isLongPress = !!openMenu;
|
||||||
|
|
||||||
const shouldHandleClick = isMigrateSelectMode || isSelectionMode || (isDefaultMode && !!openMenu);
|
const shouldHandleClick =
|
||||||
|
isMigrateSelectMode || isSelectionMode || ((isDefaultMode || isSourceMode) && isLongPress);
|
||||||
if (!shouldHandleClick) {
|
if (!shouldHandleClick) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (isSourceMode) {
|
||||||
|
updateLibraryState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
handleSelection?.(id, !selected, { selectRange: event.shiftKey });
|
handleSelection?.(id, !selected, { selectRange: event.shiftKey });
|
||||||
return;
|
return;
|
||||||
@@ -182,6 +186,12 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'&:hover .source-manga-library-state-button': {
|
||||||
|
display: isMobile ? 'none' : 'inline-flex',
|
||||||
|
},
|
||||||
|
'&:hover .source-manga-library-state-indicator': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
@@ -201,7 +211,7 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
alt={title}
|
alt={title}
|
||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
imgStyle={
|
imgStyle={
|
||||||
inLibraryIndicator && inLibrary
|
inLibraryIndicator && isInLibrary
|
||||||
? {
|
? {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -231,8 +241,35 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BadgeContainer>
|
<BadgeContainer>
|
||||||
{inLibraryIndicator && inLibrary && (
|
{inLibraryIndicator && (
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
<Button
|
||||||
|
className="source-manga-library-state-button"
|
||||||
|
component="div"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
updateLibraryState();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: 'none',
|
||||||
|
}}
|
||||||
|
color={isInLibrary ? 'error' : 'primary'}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
isInLibrary
|
||||||
|
? 'manga.action.library.remove.label.action'
|
||||||
|
: 'manga.button.add_to_library',
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{inLibraryIndicator && isInLibrary && (
|
||||||
|
<Typography
|
||||||
|
className="source-manga-library-state-indicator"
|
||||||
|
sx={{ backgroundColor: 'primary.dark' }}
|
||||||
|
>
|
||||||
{t('manga.button.in_library')}
|
{t('manga.button.in_library')}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -334,6 +371,7 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
|
{CategorySelectComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PopupState>
|
</PopupState>
|
||||||
@@ -382,7 +420,8 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
imageRendering: 'pixelated',
|
imageRendering: 'pixelated',
|
||||||
filter: inLibraryIndicator && inLibrary ? 'brightness(0.4)' : undefined,
|
filter:
|
||||||
|
inLibraryIndicator && isInLibrary ? 'brightness(0.4)' : undefined,
|
||||||
}}
|
}}
|
||||||
alt={manga.title}
|
alt={manga.title}
|
||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
@@ -402,7 +441,7 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Stack direction="row" alignItems="center" gap="5px">
|
<Stack direction="row" alignItems="center" gap="5px">
|
||||||
<BadgeContainer>
|
<BadgeContainer>
|
||||||
{inLibraryIndicator && inLibrary && (
|
{inLibraryIndicator && isInLibrary && (
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
{t('manga.button.in_library')}
|
{t('manga.button.in_library')}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -452,6 +491,7 @@ export const MangaCard = (props: MangaCardProps) => {
|
|||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
|
{CategorySelectComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PopupState>
|
</PopupState>
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import { Dialog } from '@mui/material';
|
|||||||
import { TManga } from '@/typings.ts';
|
import { TManga } from '@/typings.ts';
|
||||||
import { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
|
import { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
|
||||||
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
|
||||||
import { MenuItem } from '@/components/menu/MenuItem.tsx';
|
import { MenuItem } from '@/components/menu/MenuItem.tsx';
|
||||||
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
|
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
import { TrackManga } from '@/components/tracker/TrackManga.tsx';
|
import { TrackManga } from '@/components/tracker/TrackManga.tsx';
|
||||||
|
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||||
|
|
||||||
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
|
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ export const MangaActionMenuItems = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
|
||||||
const [isTrackDialogOpen, setIsTrackDialogOpen] = useState(false);
|
const [isTrackDialogOpen, setIsTrackDialogOpen] = useState(false);
|
||||||
|
|
||||||
const isSingleMode = !!manga;
|
const isSingleMode = !!manga;
|
||||||
@@ -69,6 +68,13 @@ export const MangaActionMenuItems = ({
|
|||||||
const hasUnreadChapters = !!manga?.unreadCount;
|
const hasUnreadChapters = !!manga?.unreadCount;
|
||||||
const hasReadChapters = !!manga && manga.unreadCount !== manga.chapters.totalCount;
|
const hasReadChapters = !!manga && manga.unreadCount !== manga.chapters.totalCount;
|
||||||
|
|
||||||
|
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
|
||||||
|
mangaId: manga?.id,
|
||||||
|
mangaIds: passedSelectedMangas ? Mangas.getIds(selectedMangas) : undefined,
|
||||||
|
onClose: () => onClose(true),
|
||||||
|
addToLibrary: false,
|
||||||
|
});
|
||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
handleSelection?.(manga.id, true);
|
handleSelection?.(manga.id, true);
|
||||||
onClose(true);
|
onClose(true);
|
||||||
@@ -156,7 +162,7 @@ export const MangaActionMenuItems = ({
|
|||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCategorySelectOpen(true);
|
openCategorySelect(true);
|
||||||
setHideMenu(true);
|
setHideMenu(true);
|
||||||
}}
|
}}
|
||||||
Icon={Label}
|
Icon={Label}
|
||||||
@@ -167,17 +173,7 @@ export const MangaActionMenuItems = ({
|
|||||||
Icon={FavoriteBorderIcon}
|
Icon={FavoriteBorderIcon}
|
||||||
title={getMenuItemTitle('remove_from_library', selectedMangas.length)}
|
title={getMenuItemTitle('remove_from_library', selectedMangas.length)}
|
||||||
/>
|
/>
|
||||||
{isCategorySelectOpen && (
|
{CategorySelectComponent}
|
||||||
<CategorySelect
|
|
||||||
open={isCategorySelectOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsCategorySelectOpen(false);
|
|
||||||
onClose(true);
|
|
||||||
}}
|
|
||||||
mangaId={manga?.id as undefined} // either mangaId or mangaIds is undefined, however, ts is not able to infer it correctly and raises an error
|
|
||||||
mangaIds={(passedSelectedMangas ? Mangas.getIds(selectedMangas) : undefined) as number[]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isTrackDialogOpen && (
|
{isTrackDialogOpen && (
|
||||||
<Dialog
|
<Dialog
|
||||||
open
|
open
|
||||||
|
|||||||
@@ -10,21 +10,17 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
|
|||||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||||
import PublicIcon from '@mui/icons-material/Public';
|
import PublicIcon from '@mui/icons-material/Public';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { t as translate } from 'i18next';
|
import { t as translate } from 'i18next';
|
||||||
import { Link } from '@mui/material';
|
import { Link } from '@mui/material';
|
||||||
import { ISource, TManga } from '@/typings';
|
import { ISource, TManga } from '@/typings';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|
||||||
import { makeToast } from '@/components/util/Toast';
|
import { makeToast } from '@/components/util/Toast';
|
||||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
|
||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
|
||||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
|
||||||
import { Categories } from '@/lib/data/Categories.ts';
|
|
||||||
import { CustomIconButton } from '@/components/atoms/CustomIconButton';
|
import { CustomIconButton } from '@/components/atoms/CustomIconButton';
|
||||||
import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx';
|
import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx';
|
||||||
|
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
|
||||||
|
|
||||||
const DetailsWrapper = styled('div')(({ theme }) => ({
|
const DetailsWrapper = styled('div')(({ theme }) => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -169,63 +165,13 @@ function getValueOrUnknown(val?: string | null) {
|
|||||||
export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
|
||||||
settings: { showAddToLibraryCategorySelectDialog },
|
|
||||||
loading: areSettingsLoading,
|
|
||||||
} = useMetadataServerSettings();
|
|
||||||
|
|
||||||
const categories = requestManager.useGetCategories();
|
|
||||||
const userCreatedCategories = useMemo(
|
|
||||||
() => Categories.getUserCreated(categories.data?.categories.nodes ?? []),
|
|
||||||
[categories.data?.categories.nodes],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!manga.source) {
|
if (!manga.source) {
|
||||||
makeToast(translate('source.error.label.source_not_found'), 'error');
|
makeToast(translate('source.error.label.source_not_found'), 'error');
|
||||||
}
|
}
|
||||||
}, [manga.source]);
|
}, [manga.source]);
|
||||||
|
|
||||||
const addToLibrary = (addToCategories: number[] = [], removeFromCategories: number[] = []) => {
|
const { CategorySelectComponent, updateLibraryState } = useManageMangaLibraryState(manga);
|
||||||
requestManager
|
|
||||||
.updateManga(manga.id, {
|
|
||||||
updateManga: { inLibrary: true },
|
|
||||||
updateMangaCategories: { addToCategories, removeFromCategories },
|
|
||||||
})
|
|
||||||
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
|
|
||||||
.catch(() => {
|
|
||||||
makeToast(t('library.error.label.add_to_library'), 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToLibraryClick = () => {
|
|
||||||
if (categories.loading) {
|
|
||||||
makeToast(t('global.label.load_in_progress'), 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categories.error) {
|
|
||||||
makeToast(t('category.error.label.request_failure'), 'error');
|
|
||||||
categories
|
|
||||||
.refetch()
|
|
||||||
.catch(defaultPromiseErrorHandler('MangaDetails::handleAddToLibraryClick: refetch categories'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length;
|
|
||||||
if (!showCategorySelectDialog) {
|
|
||||||
addToLibrary(Categories.getIds(Categories.getDefaults(userCreatedCategories!)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsCategorySelectOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFromLibrary = () => {
|
|
||||||
Mangas.removeFromLibrary([manga.id]).catch(defaultPromiseErrorHandler('MangaDetails::removeFromLibrary'));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -251,8 +197,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
</ThumbnailMetadataWrapper>
|
</ThumbnailMetadataWrapper>
|
||||||
<MangaButtonsContainer>
|
<MangaButtonsContainer>
|
||||||
<CustomIconButton
|
<CustomIconButton
|
||||||
disabled={areSettingsLoading || categories.loading}
|
onClick={updateLibraryState}
|
||||||
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
|
|
||||||
size="large"
|
size="large"
|
||||||
sx={{ color: manga.inLibrary ? '#2196f3' : 'inherit' }}
|
sx={{ color: manga.inLibrary ? '#2196f3' : 'inherit' }}
|
||||||
>
|
>
|
||||||
@@ -275,20 +220,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
</Genres>
|
</Genres>
|
||||||
</BottomContentWrapper>
|
</BottomContentWrapper>
|
||||||
</DetailsWrapper>
|
</DetailsWrapper>
|
||||||
{isCategorySelectOpen && (
|
{CategorySelectComponent}
|
||||||
<CategorySelect
|
|
||||||
open={isCategorySelectOpen}
|
|
||||||
onClose={(didUpdateCategories, addToCategories, removeFromCategories) => {
|
|
||||||
setIsCategorySelectOpen(false);
|
|
||||||
|
|
||||||
if (didUpdateCategories) {
|
|
||||||
addToLibrary(addToCategories, removeFromCategories);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
mangaId={manga.id}
|
|
||||||
addToLibrary
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect';
|
|
||||||
import { TManga } from '@/typings.ts';
|
import { TManga } from '@/typings.ts';
|
||||||
|
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
manga: TManga;
|
manga: TManga;
|
||||||
@@ -44,7 +44,9 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [editCategories, setEditCategories] = useState(false);
|
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
|
||||||
|
mangaId: manga.id,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -76,7 +78,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
<Tooltip title={t('manga.label.edit_categories')}>
|
<Tooltip title={t('manga.label.edit_categories')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditCategories(true);
|
openCategorySelect(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Label />
|
<Label />
|
||||||
@@ -134,7 +136,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
key="categories"
|
key="categories"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditCategories(true);
|
openCategorySelect(true);
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -148,7 +150,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CategorySelect open={editCategories} onClose={() => setEditCategories(false)} mangaId={manga.id} />
|
{CategorySelectComponent}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
117
src/components/manga/useManageMangaLibraryState.tsx
Normal file
117
src/components/manga/useManageMangaLibraryState.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||||
|
import { Categories } from '@/lib/data/Categories.ts';
|
||||||
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
|
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||||
|
import { TManga } from '@/typings.ts';
|
||||||
|
import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx';
|
||||||
|
|
||||||
|
export const useManageMangaLibraryState = (manga: Pick<TManga, 'id' | 'title' | 'inLibrary'>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [isInLibrary, setIsInLibrary] = useState(manga.inLibrary);
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings: { showAddToLibraryCategorySelectDialog },
|
||||||
|
loading: areSettingsLoading,
|
||||||
|
} = useMetadataServerSettings();
|
||||||
|
|
||||||
|
const categories = requestManager.useGetCategories();
|
||||||
|
const userCreatedCategories = useMemo(
|
||||||
|
() => Categories.getUserCreated(categories.data?.categories.nodes ?? []),
|
||||||
|
[categories.data?.categories.nodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addToLibrary = useCallback(
|
||||||
|
(didSubmit: boolean, addToCategories: number[] = [], removeFromCategories: number[] = []) => {
|
||||||
|
if (!didSubmit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestManager
|
||||||
|
.updateManga(manga.id, {
|
||||||
|
updateManga: { inLibrary: true },
|
||||||
|
updateMangaCategories: { addToCategories, removeFromCategories },
|
||||||
|
})
|
||||||
|
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
|
||||||
|
.then(() => setIsInLibrary(true))
|
||||||
|
.catch(() => {
|
||||||
|
makeToast(t('library.error.label.add_to_library'), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[manga.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeFromLibrary = useCallback(async () => {
|
||||||
|
await awaitConfirmation({
|
||||||
|
title: t('global.label.are_you_sure'),
|
||||||
|
message: t('manga.action.library.remove.dialog.label.message', { title: manga.title }),
|
||||||
|
actions: {
|
||||||
|
confirm: { title: t('global.button.remove') },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await Mangas.removeFromLibrary([manga.id]);
|
||||||
|
setIsInLibrary(false);
|
||||||
|
}, [manga.id]);
|
||||||
|
|
||||||
|
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
|
||||||
|
mangaId: manga.id,
|
||||||
|
addToLibrary: true,
|
||||||
|
onClose: addToLibrary,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateLibraryState = useCallback(() => {
|
||||||
|
if (isInLibrary) {
|
||||||
|
removeFromLibrary().catch(
|
||||||
|
defaultPromiseErrorHandler('useManageMangaLibraryState::updateLibraryState::removeFromLibrary'),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areSettingsLoading || categories.loading) {
|
||||||
|
makeToast(t('global.label.load_in_progress'), 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.error) {
|
||||||
|
makeToast(t('category.error.label.request_failure'), 'error');
|
||||||
|
categories
|
||||||
|
.refetch()
|
||||||
|
.catch(defaultPromiseErrorHandler('MangaDetails::handleAddToLibraryClick: refetch categories'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length;
|
||||||
|
if (!showCategorySelectDialog) {
|
||||||
|
addToLibrary(true, Categories.getIds(Categories.getDefaults(userCreatedCategories!)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openCategorySelect(true);
|
||||||
|
}, [isInLibrary, removeFromLibrary, addToLibrary, areSettingsLoading, categories.loading]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
CategorySelectComponent,
|
||||||
|
updateLibraryState,
|
||||||
|
/**
|
||||||
|
* In case of browsing the source, the data has to be fetched via a mutation.
|
||||||
|
* Thus, the source browse data never has the updated in library state unless it has to rerender, which does not get
|
||||||
|
* triggered by updating the manga in this hook.
|
||||||
|
*
|
||||||
|
* To work around this issue, the currently known in library state gets returned here
|
||||||
|
*/
|
||||||
|
isInLibrary: Mangas.getFromCache<TManga>(manga.id)?.inLibrary ?? isInLibrary,
|
||||||
|
};
|
||||||
|
};
|
||||||
62
src/components/molecules/ConfirmDialog.tsx
Normal file
62
src/components/molecules/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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 Dialog from '@mui/material/Dialog/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle/DialogTitle';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Button from '@mui/material/Button/Button';
|
||||||
|
|
||||||
|
type Action = {
|
||||||
|
show?: boolean;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Actions = {
|
||||||
|
cancel?: Action;
|
||||||
|
confirm?: Action;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfirmDialog = ({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
actions: passedActions,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
actions?: Actions;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
cancel: {
|
||||||
|
show: passedActions?.cancel?.show ?? true,
|
||||||
|
title: passedActions?.cancel?.title ?? t('global.button.cancel'),
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
show: passedActions?.confirm?.show ?? true,
|
||||||
|
title: passedActions?.confirm?.title ?? t('global.button.ok'),
|
||||||
|
},
|
||||||
|
} satisfies Actions;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogContent>{message}</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{actions.cancel.show && <Button onClick={onCancel}>{actions.cancel.title}</Button>}
|
||||||
|
{actions.confirm.show && <Button onClick={onConfirm}>{actions.confirm.title}</Button>}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -40,7 +40,7 @@ type MultiMangaModeProps = {
|
|||||||
mangaIds: number[];
|
mangaIds: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props =
|
export type CategorySelectProps =
|
||||||
| (BaseProps & SingleMangaModeProps & PropertiesNever<MultiMangaModeProps>)
|
| (BaseProps & SingleMangaModeProps & PropertiesNever<MultiMangaModeProps>)
|
||||||
| (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps);
|
| (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ const getCategoryCheckedState = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CategorySelect(props: Props) {
|
export function CategorySelect(props: CategorySelectProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props;
|
const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props;
|
||||||
|
|||||||
43
src/components/navbar/action/useCategorySelect.tsx
Normal file
43
src/components/navbar/action/useCategorySelect.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useMemo, useState } from 'react';
|
||||||
|
import { CategorySelect, CategorySelectProps } from '@/components/navbar/action/CategorySelect.tsx';
|
||||||
|
|
||||||
|
export const useCategorySelect = ({
|
||||||
|
mangaId,
|
||||||
|
mangaIds,
|
||||||
|
onClose,
|
||||||
|
addToLibrary,
|
||||||
|
}: Omit<CategorySelectProps, 'open' | 'onClose'> & Pick<Partial<CategorySelectProps>, 'onClose'>) => {
|
||||||
|
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
||||||
|
|
||||||
|
const CategorySelectComponent = useMemo(() => {
|
||||||
|
if (!isCategorySelectOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CategorySelect
|
||||||
|
open={isCategorySelectOpen}
|
||||||
|
onClose={(...args) => {
|
||||||
|
setIsCategorySelectOpen(false);
|
||||||
|
onClose?.(...args);
|
||||||
|
}}
|
||||||
|
mangaId={mangaId!} // either mangaId or mangaIds is undefined, however, ts is not able to infer it correctly and raises an error
|
||||||
|
mangaIds={mangaIds as undefined}
|
||||||
|
addToLibrary={addToLibrary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [mangaId, mangaIds, addToLibrary, onClose, isCategorySelectOpen]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
openCategorySelect: setIsCategorySelectOpen,
|
||||||
|
CategorySelectComponent,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -31,6 +31,7 @@ export function SourceMangaGrid(props: IMangaGridProps) {
|
|||||||
messageExtra={messageExtra}
|
messageExtra={messageExtra}
|
||||||
gridLayout={gridLayout}
|
gridLayout={gridLayout}
|
||||||
inLibraryIndicator
|
inLibraryIndicator
|
||||||
|
mode="source"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,6 +368,7 @@
|
|||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
|
"are_you_sure": "Are you sure?",
|
||||||
"browse": "Browse",
|
"browse": "Browse",
|
||||||
"client": "Client",
|
"client": "Client",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -580,6 +581,11 @@
|
|||||||
"button": {
|
"button": {
|
||||||
"selected": "Remove selected from the library"
|
"selected": "Remove selected from the library"
|
||||||
},
|
},
|
||||||
|
"dialog": {
|
||||||
|
"label": {
|
||||||
|
"message": "You are about to remove \"{{title}}\" from your library"
|
||||||
|
}
|
||||||
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"action": "Remove from the library",
|
"action": "Remove from the library",
|
||||||
"error_one": "Could not remove manga from the library",
|
"error_one": "Could not remove manga from the library",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { t as translate } from 'i18next';
|
import { t as translate } from 'i18next';
|
||||||
|
import { DocumentNode } from '@apollo/client/core';
|
||||||
import { TManga, TranslationKey } from '@/typings.ts';
|
import { TManga, TranslationKey } from '@/typings.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
import { Chapters } from '@/lib/data/Chapters.ts';
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
||||||
import { makeToast } from '@/components/util/Toast.tsx';
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||||
|
import { FULL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts';
|
||||||
|
|
||||||
export type MangaAction =
|
export type MangaAction =
|
||||||
| 'download'
|
| 'download'
|
||||||
@@ -135,6 +137,21 @@ export class Mangas {
|
|||||||
return mangas.map((manga) => manga.id);
|
return mangas.map((manga) => manga.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getFromCache<T>(
|
||||||
|
id: number,
|
||||||
|
fragment: DocumentNode = FULL_MANGA_FIELDS,
|
||||||
|
fragmentName: string = 'FULL_MANGA_FIELDS',
|
||||||
|
): T | null {
|
||||||
|
return requestManager.graphQLClient.client.cache.readFragment<T>({
|
||||||
|
id: requestManager.graphQLClient.client.cache.identify({
|
||||||
|
__typename: 'MangaType',
|
||||||
|
id,
|
||||||
|
}),
|
||||||
|
fragment,
|
||||||
|
fragmentName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static isNotDownloaded({ downloadCount }: MangaDownloadInfo): boolean {
|
static isNotDownloaded({ downloadCount }: MangaDownloadInfo): boolean {
|
||||||
return downloadCount === 0;
|
return downloadCount === 0;
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/lib/ui/AwaitableDialog.tsx
Normal file
47
src/lib/ui/AwaitableDialog.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 { createRoot } from 'react-dom/client';
|
||||||
|
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
||||||
|
import { ConfirmDialog } from '@/components/molecules/ConfirmDialog.tsx';
|
||||||
|
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
|
||||||
|
import { getCurrentTheme } from '@/theme.ts';
|
||||||
|
|
||||||
|
export const awaitConfirmation = async (
|
||||||
|
dialogProps: Omit<React.ComponentProps<typeof ConfirmDialog>, 'onCancel' | 'onConfirm'>,
|
||||||
|
) => {
|
||||||
|
const dialogContainer = document.createElement('div');
|
||||||
|
document.body.appendChild(dialogContainer);
|
||||||
|
|
||||||
|
const root = createRoot(dialogContainer);
|
||||||
|
|
||||||
|
const confirmationPromise = new ControlledPromise();
|
||||||
|
const handleConfirmation = (accepted: boolean) => {
|
||||||
|
if (accepted) {
|
||||||
|
confirmationPromise.resolve();
|
||||||
|
} else {
|
||||||
|
confirmationPromise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
root.unmount();
|
||||||
|
document.body.removeChild(dialogContainer);
|
||||||
|
};
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<ThemeProvider theme={getCurrentTheme()}>
|
||||||
|
<ConfirmDialog
|
||||||
|
{...dialogProps}
|
||||||
|
onCancel={() => handleConfirmation(false)}
|
||||||
|
onConfirm={() => handleConfirmation(true)}
|
||||||
|
/>
|
||||||
|
,
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return confirmationPromise.promise;
|
||||||
|
};
|
||||||
@@ -21,6 +21,8 @@ declare module '@mui/material/styles/createPalette' {
|
|||||||
type DefaultMuiPalette = Omit<MuiPalette, 'custom'>;
|
type DefaultMuiPalette = Omit<MuiPalette, 'custom'>;
|
||||||
type DefaultMuiTheme = Omit<Theme, 'palette'> & { palette: DefaultMuiPalette };
|
type DefaultMuiTheme = Omit<Theme, 'palette'> & { palette: DefaultMuiPalette };
|
||||||
|
|
||||||
|
let theme: Theme;
|
||||||
|
export const getCurrentTheme = () => theme;
|
||||||
export const createTheme = (dark?: boolean) => {
|
export const createTheme = (dark?: boolean) => {
|
||||||
const baseTheme: DefaultMuiTheme = createMuiTheme({
|
const baseTheme: DefaultMuiTheme = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -60,5 +62,7 @@ export const createTheme = (dark?: boolean) => {
|
|||||||
baseTheme,
|
baseTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
theme = suwayomiTheme;
|
||||||
|
|
||||||
return suwayomiTheme;
|
return suwayomiTheme;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user