Feature/manga migration (#536)
* Add "migration" tab to "Browse" screen * Add missing useEffect cleanup for navbar title and actions * Make "SourceGridLayout" reusable * Add migration tab to "Browse" * Add migration logic
This commit is contained in:
@@ -37,6 +37,7 @@ import { ServerUpdateChecker } from '@/components/settings/ServerUpdateChecker.t
|
|||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { ExtensionSettings } from '@/screens/settings/ExtensionSettings.tsx';
|
import { ExtensionSettings } from '@/screens/settings/ExtensionSettings.tsx';
|
||||||
import { WebUISettings } from '@/screens/settings/WebUISettings.tsx';
|
import { WebUISettings } from '@/screens/settings/WebUISettings.tsx';
|
||||||
|
import { Migrate } from '@/screens/Migrate.tsx';
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// Adds messages only in a dev environment
|
// Adds messages only in a dev environment
|
||||||
@@ -120,6 +121,10 @@ export const App: React.FC = () => (
|
|||||||
<Route path="updates" element={<Updates />} />
|
<Route path="updates" element={<Updates />} />
|
||||||
<Route path="extensions" element={<Extensions />} />
|
<Route path="extensions" element={<Extensions />} />
|
||||||
<Route path="browse" element={<Browse />} />
|
<Route path="browse" element={<Browse />} />
|
||||||
|
<Route path="migrate/source/:sourceId">
|
||||||
|
<Route index element={<Migrate />} />
|
||||||
|
<Route path="manga/:mangaId/search" element={<SearchAll />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Container>
|
</Container>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { Avatar, Box, CardContent, Stack, styled, Tooltip } from '@mui/material';
|
import { Avatar, Box, CardContent, 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 { useState } from 'react';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
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';
|
||||||
@@ -22,6 +23,7 @@ import { SelectableCollectionReturnType } from '@/components/collection/useSelec
|
|||||||
import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx';
|
import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx';
|
||||||
import { MangaActionMenuItems, SingleModeProps } from '@/components/manga/MangaActionMenuItems.tsx';
|
import { MangaActionMenuItems, SingleModeProps } from '@/components/manga/MangaActionMenuItems.tsx';
|
||||||
import { Menu } from '@/components/menu/Menu.tsx';
|
import { Menu } from '@/components/menu/Menu.tsx';
|
||||||
|
import { MigrateDialog } from '@/components/MigrateDialog.tsx';
|
||||||
|
|
||||||
const BottomGradient = styled('div')({
|
const BottomGradient = styled('div')({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -66,18 +68,34 @@ const BadgeContainer = styled('div')({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IProps {
|
type MangaCardMode = 'default' | 'migrate.search' | 'migrate.select';
|
||||||
|
|
||||||
|
export interface MangaCardProps {
|
||||||
manga: TPartialManga;
|
manga: TPartialManga;
|
||||||
gridLayout?: GridLayout;
|
gridLayout?: GridLayout;
|
||||||
inLibraryIndicator?: boolean;
|
inLibraryIndicator?: boolean;
|
||||||
selected?: boolean | null;
|
selected?: boolean | null;
|
||||||
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
|
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
|
||||||
|
mode?: MangaCardMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MangaCard = (props: IProps) => {
|
const getMangaLinkTo = (mode: MangaCardMode, mangaId: number, sourceId: string, mangaTitle: string): string => {
|
||||||
|
switch (mode) {
|
||||||
|
case 'default':
|
||||||
|
return `/manga/${mangaId}/`;
|
||||||
|
case 'migrate.search':
|
||||||
|
return `/migrate/source/${sourceId}/manga/${mangaId}/search?query=${mangaTitle}`;
|
||||||
|
case 'migrate.select':
|
||||||
|
return '';
|
||||||
|
default:
|
||||||
|
throw new Error(`getMangaLinkTo: unexpected MangaCardMode "${mode}"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MangaCard = (props: MangaCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection } = props;
|
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props;
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@@ -93,182 +111,322 @@ export const MangaCard = (props: IProps) => {
|
|||||||
options: { showContinueReadingButton, showUnreadBadge, showDownloadBadge },
|
options: { showContinueReadingButton, showUnreadBadge, showDownloadBadge },
|
||||||
} = useLibraryOptionsContext();
|
} = useLibraryOptionsContext();
|
||||||
|
|
||||||
const mangaLinkTo = `/manga/${id}/`;
|
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.source?.id, manga.title);
|
||||||
|
|
||||||
const nextChapterIndexToRead = (latestReadChapter?.sourceOrder ?? 0) + 1;
|
const nextChapterIndexToRead = (latestReadChapter?.sourceOrder ?? 0) + 1;
|
||||||
const isLatestChapterRead = chapters?.totalCount === latestReadChapter?.sourceOrder;
|
const isLatestChapterRead = chapters?.totalCount === latestReadChapter?.sourceOrder;
|
||||||
|
|
||||||
|
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
|
||||||
|
|
||||||
if (gridLayout !== GridLayout.List) {
|
if (gridLayout !== GridLayout.List) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isMigrateDialogOpen && (
|
||||||
|
<MigrateDialog mangaIdToMigrateTo={manga.id} onClose={() => setIsMigrateDialogOpen(false)} />
|
||||||
|
)}
|
||||||
|
<PopupState variant="popover" popupId="manga-card-action-menu">
|
||||||
|
{(popupState) => (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
onClick={(e) => {
|
||||||
|
const isMigrateSelectMode = mode === 'migrate.select';
|
||||||
|
const isSelectionMode = selected !== null;
|
||||||
|
|
||||||
|
const handleClick = isMigrateSelectMode || isSelectionMode;
|
||||||
|
if (!handleClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isMigrateSelectMode) {
|
||||||
|
setIsMigrateDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelection?.(id, !selected);
|
||||||
|
}}
|
||||||
|
to={mangaLinkTo}
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
margin: '2px',
|
||||||
|
outline: selected ? '4px solid' : undefined,
|
||||||
|
borderRadius: selected ? '1px' : undefined,
|
||||||
|
outlineColor: (theme) => theme.palette.primary.main,
|
||||||
|
backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined),
|
||||||
|
'@media (hover: hover) and (pointer: fine)': {
|
||||||
|
'&:hover .manga-option-button': {
|
||||||
|
visibility: 'visible',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
// force standard aspect ratio of manga covers
|
||||||
|
aspectRatio: '225/350',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardActionArea
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
alignItems="start"
|
||||||
|
justifyContent="space-between"
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 5,
|
||||||
|
left: 5,
|
||||||
|
right: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BadgeContainer>
|
||||||
|
{inLibraryIndicator && inLibrary && (
|
||||||
|
<Typography
|
||||||
|
sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
|
||||||
|
>
|
||||||
|
{t('manga.button.in_library')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{showUnreadBadge && (unread ?? 0) > 0 && (
|
||||||
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
|
{unread}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{showDownloadBadge && (downloadCount ?? 0) > 0 && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'success.dark',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{downloadCount}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</BadgeContainer>
|
||||||
|
<MangaOptionButton
|
||||||
|
popupState={popupState}
|
||||||
|
id={id}
|
||||||
|
selected={selected}
|
||||||
|
handleSelection={handleSelection}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<SpinnerImage
|
||||||
|
alt={title}
|
||||||
|
src={requestManager.getValidImgUrlFor(thumbnailUrl)}
|
||||||
|
imgStyle={
|
||||||
|
inLibraryIndicator && inLibrary
|
||||||
|
? {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
filter: 'brightness(0.4)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spinnerStyle={{
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<>
|
||||||
|
{gridLayout !== GridLayout.Comfortable && (
|
||||||
|
<>
|
||||||
|
<BottomGradient />
|
||||||
|
<BottomGradientDoubledDown />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent={
|
||||||
|
gridLayout !== GridLayout.Comfortable ? 'space-between' : 'end'
|
||||||
|
}
|
||||||
|
alignItems="end"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
margin: '0.5em 0',
|
||||||
|
padding: '0 0.5em',
|
||||||
|
gap: '0.5em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{gridLayout !== GridLayout.Comfortable && (
|
||||||
|
<Tooltip title={title} placement="top">
|
||||||
|
<GridMangaTitle
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
textShadow: '0px 0px 3px #000000',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</GridMangaTitle>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<ContinueReadingButton
|
||||||
|
showContinueReadingButton={showContinueReadingButton}
|
||||||
|
isLatestChapterRead={isLatestChapterRead}
|
||||||
|
nextChapterIndexToRead={nextChapterIndexToRead}
|
||||||
|
mangaLinkTo={mangaLinkTo}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
{gridLayout === GridLayout.Comfortable && (
|
||||||
|
<Tooltip title={title} placement="top">
|
||||||
|
<GridMangaTitle
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
bottom: 0,
|
||||||
|
margin: '0.5em 0',
|
||||||
|
padding: '0 0.5em',
|
||||||
|
color: 'text.primary',
|
||||||
|
height: '3rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</GridMangaTitle>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
|
{!!handleSelection && popupState.isOpen && (
|
||||||
|
<Menu {...bindMenu(popupState)}>
|
||||||
|
{(onClose, setHideMenu) => (
|
||||||
|
<MangaActionMenuItems
|
||||||
|
manga={manga as SingleModeProps['manga']}
|
||||||
|
handleSelection={handleSelection}
|
||||||
|
onClose={onClose}
|
||||||
|
setHideMenu={setHideMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PopupState>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isMigrateDialogOpen && (
|
||||||
|
<MigrateDialog mangaIdToMigrateTo={manga.id} onClose={() => setIsMigrateDialogOpen(false)} />
|
||||||
|
)}
|
||||||
<PopupState variant="popover" popupId="manga-card-action-menu">
|
<PopupState variant="popover" popupId="manga-card-action-menu">
|
||||||
{(popupState) => (
|
{(popupState) => (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Card>
|
||||||
onClick={(e) => {
|
<CardActionArea
|
||||||
if (selected === null) {
|
component={Link}
|
||||||
return;
|
to={mangaLinkTo}
|
||||||
}
|
onClick={(e) => {
|
||||||
|
if (selected === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSelection?.(id, !selected);
|
handleSelection?.(id, !selected);
|
||||||
}}
|
|
||||||
to={mangaLinkTo}
|
|
||||||
style={{ textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
margin: '2px',
|
|
||||||
outline: selected ? '4px solid' : undefined,
|
|
||||||
borderRadius: selected ? '1px' : undefined,
|
|
||||||
outlineColor: (theme) => theme.palette.primary.main,
|
|
||||||
backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined),
|
|
||||||
'@media (hover: hover) and (pointer: fine)': {
|
|
||||||
'&:hover .manga-option-button': {
|
|
||||||
visibility: 'visible',
|
|
||||||
pointerEvents: 'all',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<CardContent
|
||||||
sx={{
|
sx={{
|
||||||
// force standard aspect ratio of manga covers
|
|
||||||
aspectRatio: '225/350',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 2,
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardActionArea
|
<Avatar
|
||||||
|
variant="rounded"
|
||||||
|
sx={
|
||||||
|
inLibraryIndicator && inLibrary
|
||||||
|
? {
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
marginRight: 2,
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
filter: 'brightness(0.4)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
marginRight: 2,
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
src={requestManager.getValidImgUrlFor(thumbnailUrl)}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
display: 'flex',
|
||||||
height: '100%',
|
flexDirection: 'row',
|
||||||
|
flexGrow: 1,
|
||||||
|
width: 'min-content',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Tooltip title={title} placement="top">
|
||||||
alignItems="start"
|
<MangaTitle variant="h5">{title}</MangaTitle>
|
||||||
justifyContent="space-between"
|
</Tooltip>
|
||||||
direction="row"
|
</Box>
|
||||||
sx={{
|
<Stack direction="row" alignItems="center" gap="5px">
|
||||||
position: 'absolute',
|
<BadgeContainer>
|
||||||
top: 5,
|
{inLibraryIndicator && inLibrary && (
|
||||||
left: 5,
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
right: 5,
|
{t('manga.button.in_library')}
|
||||||
}}
|
</Typography>
|
||||||
>
|
|
||||||
<BadgeContainer>
|
|
||||||
{inLibraryIndicator && inLibrary && (
|
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}>
|
|
||||||
{t('manga.button.in_library')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{showUnreadBadge && (unread ?? 0) > 0 && (
|
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
|
||||||
{unread}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{showDownloadBadge && (downloadCount ?? 0) > 0 && (
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
backgroundColor: 'success.dark',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{downloadCount}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</BadgeContainer>
|
|
||||||
<MangaOptionButton
|
|
||||||
popupState={popupState}
|
|
||||||
id={id}
|
|
||||||
selected={selected}
|
|
||||||
handleSelection={handleSelection}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<SpinnerImage
|
|
||||||
alt={title}
|
|
||||||
src={requestManager.getValidImgUrlFor(thumbnailUrl)}
|
|
||||||
imgStyle={
|
|
||||||
inLibraryIndicator && inLibrary
|
|
||||||
? {
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
objectFit: 'cover',
|
|
||||||
filter: 'brightness(0.4)',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
objectFit: 'cover',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spinnerStyle={{
|
|
||||||
display: 'grid',
|
|
||||||
placeItems: 'center',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<>
|
|
||||||
{gridLayout !== GridLayout.Comfortable && (
|
|
||||||
<>
|
|
||||||
<BottomGradient />
|
|
||||||
<BottomGradientDoubledDown />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<Stack
|
{showUnreadBadge && unread! > 0 && (
|
||||||
direction="row"
|
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
||||||
justifyContent={
|
{unread}
|
||||||
gridLayout !== GridLayout.Comfortable ? 'space-between' : 'end'
|
</Typography>
|
||||||
}
|
)}
|
||||||
alignItems="end"
|
{showDownloadBadge && downloadCount! > 0 && (
|
||||||
sx={{
|
<Typography
|
||||||
position: 'absolute',
|
sx={{
|
||||||
bottom: 0,
|
backgroundColor: 'success.dark',
|
||||||
width: '100%',
|
}}
|
||||||
margin: '0.5em 0',
|
>
|
||||||
padding: '0 0.5em',
|
{downloadCount}
|
||||||
gap: '0.5em',
|
</Typography>
|
||||||
}}
|
)}
|
||||||
>
|
</BadgeContainer>
|
||||||
{gridLayout !== GridLayout.Comfortable && (
|
<ContinueReadingButton
|
||||||
<Tooltip title={title} placement="top">
|
showContinueReadingButton={showContinueReadingButton}
|
||||||
<GridMangaTitle
|
isLatestChapterRead={isLatestChapterRead}
|
||||||
sx={{
|
nextChapterIndexToRead={nextChapterIndexToRead}
|
||||||
color: 'white',
|
mangaLinkTo={mangaLinkTo}
|
||||||
textShadow: '0px 0px 3px #000000',
|
/>
|
||||||
}}
|
<MangaOptionButton
|
||||||
>
|
popupState={popupState}
|
||||||
{title}
|
id={id}
|
||||||
</GridMangaTitle>
|
selected={selected}
|
||||||
</Tooltip>
|
handleSelection={handleSelection}
|
||||||
)}
|
asCheckbox
|
||||||
<ContinueReadingButton
|
/>
|
||||||
showContinueReadingButton={showContinueReadingButton}
|
</Stack>
|
||||||
isLatestChapterRead={isLatestChapterRead}
|
</CardContent>
|
||||||
nextChapterIndexToRead={nextChapterIndexToRead}
|
</CardActionArea>
|
||||||
mangaLinkTo={mangaLinkTo}
|
</Card>
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
</CardActionArea>
|
|
||||||
</Card>
|
|
||||||
{gridLayout === GridLayout.Comfortable && (
|
|
||||||
<Tooltip title={title} placement="top">
|
|
||||||
<GridMangaTitle
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
bottom: 0,
|
|
||||||
margin: '0.5em 0',
|
|
||||||
padding: '0 0.5em',
|
|
||||||
color: 'text.primary',
|
|
||||||
height: '3rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</GridMangaTitle>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Link>
|
|
||||||
{!!handleSelection && popupState.isOpen && (
|
{!!handleSelection && popupState.isOpen && (
|
||||||
<Menu {...bindMenu(popupState)}>
|
<Menu {...bindMenu(popupState)}>
|
||||||
{(onClose, setHideMenu) => (
|
{(onClose, setHideMenu) => (
|
||||||
@@ -284,120 +442,6 @@ export const MangaCard = (props: IProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PopupState>
|
</PopupState>
|
||||||
);
|
</>
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopupState variant="popover" popupId="manga-card-action-menu">
|
|
||||||
{(popupState) => (
|
|
||||||
<>
|
|
||||||
<Card>
|
|
||||||
<CardActionArea
|
|
||||||
component={Link}
|
|
||||||
to={mangaLinkTo}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (selected === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
handleSelection?.(id, !selected);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardContent
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 2,
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
variant="rounded"
|
|
||||||
sx={
|
|
||||||
inLibraryIndicator && inLibrary
|
|
||||||
? {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
flex: '0 0 auto',
|
|
||||||
marginRight: 2,
|
|
||||||
imageRendering: 'pixelated',
|
|
||||||
filter: 'brightness(0.4)',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
width: 56,
|
|
||||||
height: 56,
|
|
||||||
flex: '0 0 auto',
|
|
||||||
marginRight: 2,
|
|
||||||
imageRendering: 'pixelated',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
src={requestManager.getValidImgUrlFor(thumbnailUrl)}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexGrow: 1,
|
|
||||||
width: 'min-content',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip title={title} placement="top">
|
|
||||||
<MangaTitle variant="h5">{title}</MangaTitle>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Stack direction="row" alignItems="center" gap="5px">
|
|
||||||
<BadgeContainer>
|
|
||||||
{inLibraryIndicator && inLibrary && (
|
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>
|
|
||||||
{t('manga.button.in_library')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{showUnreadBadge && unread! > 0 && (
|
|
||||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>{unread}</Typography>
|
|
||||||
)}
|
|
||||||
{showDownloadBadge && downloadCount! > 0 && (
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
backgroundColor: 'success.dark',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{downloadCount}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</BadgeContainer>
|
|
||||||
<ContinueReadingButton
|
|
||||||
showContinueReadingButton={showContinueReadingButton}
|
|
||||||
isLatestChapterRead={isLatestChapterRead}
|
|
||||||
nextChapterIndexToRead={nextChapterIndexToRead}
|
|
||||||
mangaLinkTo={mangaLinkTo}
|
|
||||||
/>
|
|
||||||
<MangaOptionButton
|
|
||||||
popupState={popupState}
|
|
||||||
id={id}
|
|
||||||
selected={selected}
|
|
||||||
handleSelection={handleSelection}
|
|
||||||
asCheckbox
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</CardContent>
|
|
||||||
</CardActionArea>
|
|
||||||
</Card>
|
|
||||||
{!!handleSelection && popupState.isOpen && (
|
|
||||||
<Menu {...bindMenu(popupState)}>
|
|
||||||
{(onClose, setHideMenu) => (
|
|
||||||
<MangaActionMenuItems
|
|
||||||
manga={manga as SingleModeProps['manga']}
|
|
||||||
handleSelection={handleSelection}
|
|
||||||
onClose={onClose}
|
|
||||||
setHideMenu={setHideMenu}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</PopupState>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { EmptyView } from '@/components/util/EmptyView';
|
import { EmptyView } from '@/components/util/EmptyView';
|
||||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
|
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
|
||||||
import { MangaCard } from '@/components/MangaCard';
|
import { MangaCard, MangaCardProps } from '@/components/MangaCard';
|
||||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||||
import { useLocalStorage } from '@/util/useLocalStorage';
|
import { useLocalStorage } from '@/util/useLocalStorage';
|
||||||
import { TManga, TPartialManga } from '@/typings.ts';
|
import { TManga, TPartialManga } from '@/typings.ts';
|
||||||
@@ -49,6 +49,7 @@ const createMangaCard = (
|
|||||||
isSelectModeActive: boolean = false,
|
isSelectModeActive: boolean = false,
|
||||||
selectedMangaIds?: TManga['id'][],
|
selectedMangaIds?: TManga['id'][],
|
||||||
handleSelection?: DefaultGridProps['handleSelection'],
|
handleSelection?: DefaultGridProps['handleSelection'],
|
||||||
|
mode?: MangaCardProps['mode'],
|
||||||
) => (
|
) => (
|
||||||
<MangaCard
|
<MangaCard
|
||||||
key={manga.id}
|
key={manga.id}
|
||||||
@@ -57,10 +58,11 @@ const createMangaCard = (
|
|||||||
inLibraryIndicator={inLibraryIndicator}
|
inLibraryIndicator={inLibraryIndicator}
|
||||||
selected={isSelectModeActive ? selectedMangaIds?.includes(manga.id) : null}
|
selected={isSelectModeActive ? selectedMangaIds?.includes(manga.id) : null}
|
||||||
handleSelection={handleSelection}
|
handleSelection={handleSelection}
|
||||||
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
type DefaultGridProps = {
|
type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
mangas: TPartialManga[];
|
mangas: TPartialManga[];
|
||||||
inLibraryIndicator?: boolean;
|
inLibraryIndicator?: boolean;
|
||||||
@@ -80,6 +82,7 @@ const HorizontalGrid = ({
|
|||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
selectedMangaIds,
|
selectedMangaIds,
|
||||||
handleSelection,
|
handleSelection,
|
||||||
|
mode,
|
||||||
}: DefaultGridProps) => (
|
}: DefaultGridProps) => (
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
@@ -105,6 +108,7 @@ const HorizontalGrid = ({
|
|||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
selectedMangaIds,
|
selectedMangaIds,
|
||||||
handleSelection,
|
handleSelection,
|
||||||
|
mode,
|
||||||
)}
|
)}
|
||||||
</GridItemContainer>
|
</GridItemContainer>
|
||||||
))
|
))
|
||||||
@@ -123,6 +127,7 @@ const VerticalGrid = ({
|
|||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
selectedMangaIds,
|
selectedMangaIds,
|
||||||
handleSelection,
|
handleSelection,
|
||||||
|
mode,
|
||||||
}: DefaultGridProps & {
|
}: DefaultGridProps & {
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
@@ -171,6 +176,7 @@ const VerticalGrid = ({
|
|||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
selectedMangaIds,
|
selectedMangaIds,
|
||||||
handleSelection,
|
handleSelection,
|
||||||
|
mode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -212,6 +218,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
selectedMangaIds,
|
selectedMangaIds,
|
||||||
handleSelection,
|
handleSelection,
|
||||||
|
mode,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth);
|
const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth);
|
||||||
@@ -287,6 +294,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
isSelectModeActive={isSelectModeActive}
|
isSelectModeActive={isSelectModeActive}
|
||||||
selectedMangaIds={selectedMangaIds}
|
selectedMangaIds={selectedMangaIds}
|
||||||
handleSelection={handleSelection}
|
handleSelection={handleSelection}
|
||||||
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<VerticalGrid
|
<VerticalGrid
|
||||||
@@ -300,6 +308,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
|||||||
isSelectModeActive={isSelectModeActive}
|
isSelectModeActive={isSelectModeActive}
|
||||||
selectedMangaIds={selectedMangaIds}
|
selectedMangaIds={selectedMangaIds}
|
||||||
handleSelection={handleSelection}
|
handleSelection={handleSelection}
|
||||||
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
97
src/components/MigrateDialog.tsx
Normal file
97
src/components/MigrateDialog.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
|
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
|
||||||
|
import { Mangas, MigrateMode } from '@/lib/data/Mangas.ts';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
|
||||||
|
export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrateTo: number; onClose: () => void }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { mangaId: mangaIdAsString } = useParams<{ mangaId: string }>();
|
||||||
|
const mangaId = Number(mangaIdAsString);
|
||||||
|
|
||||||
|
const [includeChapters, setIncludeChapters] = useState(true);
|
||||||
|
const [includeCategories, setIncludeCategories] = useState(true);
|
||||||
|
|
||||||
|
const [isMigrationInProcess, setIsMigrationInProcess] = useState(false);
|
||||||
|
|
||||||
|
const migrate = async (mode: MigrateMode) => {
|
||||||
|
if (mangaId == null) {
|
||||||
|
throw new Error(`MigrateDialog::migrate: unexpected mangaId "${mangaId}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeToast(t('migrate.label.info'), 'info');
|
||||||
|
|
||||||
|
setIsMigrationInProcess(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Mangas.migrate(mangaId, mangaIdToMigrateTo, {
|
||||||
|
mode,
|
||||||
|
migrateChapters: includeChapters,
|
||||||
|
migrateCategories: includeCategories,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(`/manga/${mangaIdToMigrateTo}`);
|
||||||
|
} catch (e) {
|
||||||
|
setIsMigrationInProcess(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open fullWidth onClose={onClose}>
|
||||||
|
<DialogTitle>{t('migrate.dialog.title')}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<FormGroup>
|
||||||
|
<CheckboxInput
|
||||||
|
disabled={isMigrationInProcess}
|
||||||
|
label={t('chapter.title')}
|
||||||
|
checked={includeChapters}
|
||||||
|
onChange={(_, checked) => setIncludeChapters(checked)}
|
||||||
|
/>
|
||||||
|
<CheckboxInput
|
||||||
|
disabled={isMigrationInProcess}
|
||||||
|
label={t('category.title.category_one')}
|
||||||
|
checked={includeCategories}
|
||||||
|
onChange={(_, checked) => setIncludeCategories(checked)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between">
|
||||||
|
<Button disabled={isMigrationInProcess} component={Link} to={`/manga/${mangaIdToMigrateTo}`}>
|
||||||
|
{t('migrate.dialog.action.button.show_entry')}
|
||||||
|
</Button>
|
||||||
|
<Stack direction="row">
|
||||||
|
<Button disabled={isMigrationInProcess} onClick={onClose}>
|
||||||
|
{t('global.button.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button disabled={isMigrationInProcess} onClick={() => migrate('copy')}>
|
||||||
|
{t('global.button.copy')}
|
||||||
|
</Button>
|
||||||
|
<Button disabled={isMigrationInProcess} onClick={() => migrate('migrate')}>
|
||||||
|
{t('global.button.migrate')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
60
src/components/MigrationCard.tsx
Normal file
60
src/components/MigrationCard.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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 Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import { Box, CardActionArea, Chip } from '@mui/material';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { translateExtensionLanguage } from '@/screens/util/Extensions.ts';
|
||||||
|
|
||||||
|
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
|
||||||
|
mangaCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO - cleanup source/extension components
|
||||||
|
export const MigrationCard = ({ id, name, lang, iconUrl, mangaCount }: TMigratableSource) => (
|
||||||
|
<Card>
|
||||||
|
<CardActionArea component={Link} to={`/migrate/source/${id}/`}>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Avatar
|
||||||
|
variant="rounded"
|
||||||
|
sx={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
mr: 2,
|
||||||
|
}}
|
||||||
|
alt={name}
|
||||||
|
src={requestManager.getValidImgUrlFor(iconUrl)}
|
||||||
|
/>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="h5" component="h2">
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" display="block">
|
||||||
|
{translateExtensionLanguage(lang)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Chip sx={{ borderRadius: '5px' }} size="small" label={mangaCount} />
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
@@ -15,6 +15,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||||
import Label from '@mui/icons-material/Label';
|
import Label from '@mui/icons-material/Label';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
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';
|
||||||
@@ -28,7 +30,7 @@ const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as
|
|||||||
type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void };
|
type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void };
|
||||||
|
|
||||||
export type SingleModeProps = {
|
export type SingleModeProps = {
|
||||||
manga: Pick<TManga, 'id'> & MangaDownloadInfo & MangaUnreadInfo;
|
manga: Pick<TManga, 'id' | 'title' | 'source'> & MangaDownloadInfo & MangaUnreadInfo;
|
||||||
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
|
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ export const MangaActionMenuItems = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
||||||
|
|
||||||
const isSingleMode = !!manga;
|
const isSingleMode = !!manga;
|
||||||
@@ -129,6 +133,23 @@ export const MangaActionMenuItems = ({
|
|||||||
title={getMenuItemTitle('mark_as_unread', readMangas.length)}
|
title={getMenuItemTitle('mark_as_unread', readMangas.length)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isSingleMode && (
|
||||||
|
<Link
|
||||||
|
to={`/migrate/source/${manga?.source?.id}/manga/${manga?.id}/search?query=${manga?.title}`}
|
||||||
|
state={{ mangaTitle: manga?.title }}
|
||||||
|
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/migrate/source/${manga?.source?.id}/manga/${manga?.id}/search?query=${manga?.title}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon={SyncAltIcon}
|
||||||
|
title={getMenuItemTitle('migrate', selectedMangas.length)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCategorySelectOpen(true);
|
setIsCategorySelectOpen(true);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect';
|
import { CategorySelect } from '@/components/navbar/action/CategorySelect';
|
||||||
import { TManga } from '@/typings.ts';
|
import { TManga } from '@/typings.ts';
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ interface IProps {
|
|||||||
|
|
||||||
export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
|
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
|
||||||
|
|
||||||
@@ -57,6 +60,17 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
|||||||
<Refresh />
|
<Refresh />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title={t('global.button.migrate')}>
|
||||||
|
<Link
|
||||||
|
to={`/migrate/source/${manga.source?.id}/manga/${manga.id}/search?query=${manga.title}`}
|
||||||
|
state={{ mangaTitle: manga.title }}
|
||||||
|
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||||
|
>
|
||||||
|
<IconButton disabled={refreshing}>
|
||||||
|
<SyncAltIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
{manga.inLibrary && (
|
{manga.inLibrary && (
|
||||||
<Tooltip title={t('manga.label.edit_categories')}>
|
<Tooltip title={t('manga.label.edit_categories')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp
|
|||||||
<>
|
<>
|
||||||
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('category.title.categories')}
|
primary={t('category.title.category_other')}
|
||||||
secondary={
|
secondary={
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
@@ -179,7 +179,7 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp
|
|||||||
|
|
||||||
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle sx={{ paddingLeft: 0 }}>{t('category.title.categories')}</DialogTitle>
|
<DialogTitle sx={{ paddingLeft: 0 }}>{t('category.title.category_other')}</DialogTitle>
|
||||||
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
|
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
|
||||||
<CheckboxContainer>
|
<CheckboxContainer>
|
||||||
{dialogCategories.map((category) => (
|
{dialogCategories.map((category) => (
|
||||||
|
|||||||
@@ -10,17 +10,18 @@ import { IconButton, Menu, MenuItem, FormControlLabel, Radio, Tooltip } from '@m
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||||
|
|
||||||
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
||||||
export function SourceGridLayout() {
|
export function GridLayouts({
|
||||||
|
gridLayout,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
gridLayout: GridLayout;
|
||||||
|
onChange: (gridLayout: GridLayout) => void;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
|
||||||
options: { SourcegridLayout },
|
|
||||||
setOptions,
|
|
||||||
} = useLibraryOptionsContext();
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const handleClick = (event: any) => {
|
const handleClick = (event: any) => {
|
||||||
@@ -30,10 +31,8 @@ export function SourceGridLayout() {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
function setGridContextOptions(e: React.ChangeEvent<HTMLInputElement>, checked: boolean) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
if (checked) {
|
onChange(parseInt(e.target.name, 10));
|
||||||
setOptions((prev: any) => ({ ...prev, SourcegridLayout: parseInt(e.target.name, 10) }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,8 +63,8 @@ export function SourceGridLayout() {
|
|||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.Compact.toString()}
|
name={GridLayout.Compact.toString()}
|
||||||
checked={SourcegridLayout === GridLayout.Compact || SourcegridLayout === undefined}
|
checked={gridLayout === GridLayout.Compact}
|
||||||
onChange={setGridContextOptions}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -76,8 +75,8 @@ export function SourceGridLayout() {
|
|||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.Comfortable.toString()}
|
name={GridLayout.Comfortable.toString()}
|
||||||
checked={SourcegridLayout === GridLayout.Comfortable}
|
checked={gridLayout === GridLayout.Comfortable}
|
||||||
onChange={setGridContextOptions}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -88,8 +87,8 @@ export function SourceGridLayout() {
|
|||||||
control={
|
control={
|
||||||
<Radio
|
<Radio
|
||||||
name={GridLayout.List.toString()}
|
name={GridLayout.List.toString()}
|
||||||
checked={SourcegridLayout === GridLayout.List}
|
checked={gridLayout === GridLayout.List}
|
||||||
onChange={setGridContextOptions}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
23
src/components/source/SourceGridLayout.tsx
Normal file
23
src/components/source/SourceGridLayout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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 { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||||
|
import { GridLayouts } from '@/components/source/GridLayouts.tsx';
|
||||||
|
|
||||||
|
export function SourceGridLayout() {
|
||||||
|
const {
|
||||||
|
options: { SourcegridLayout },
|
||||||
|
setOptions,
|
||||||
|
} = useLibraryOptionsContext();
|
||||||
|
|
||||||
|
function setGridContextOptions(gridLayout: GridLayout) {
|
||||||
|
setOptions((prev: any) => ({ ...prev, SourcegridLayout: gridLayout }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return <GridLayouts gridLayout={SourcegridLayout} onChange={setGridContextOptions} />;
|
||||||
|
}
|
||||||
@@ -22,10 +22,6 @@
|
|||||||
"category_name": "Category Name",
|
"category_name": "Category Name",
|
||||||
"use_as_default_category": "Default category when adding new manga to the library"
|
"use_as_default_category": "Default category when adding new manga to the library"
|
||||||
},
|
},
|
||||||
"title": {
|
|
||||||
"categories": "Categories",
|
|
||||||
"set_categories": "Set categories"
|
|
||||||
},
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"inclusion": {
|
"inclusion": {
|
||||||
"label": {
|
"label": {
|
||||||
@@ -33,6 +29,11 @@
|
|||||||
"include": "Include: {{includedCategoriesText}}"
|
"include": "Include: {{includedCategoriesText}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"category_one": "Category",
|
||||||
|
"category_other": "Categories",
|
||||||
|
"set_categories": "Set categories"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chapter": {
|
"chapter": {
|
||||||
@@ -161,15 +162,15 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"auto_download": {
|
"auto_download": {
|
||||||
"label": {
|
|
||||||
"ignore_with_unread_chapters": "Ignore automatic chapter downloads for entries with unread chapters",
|
|
||||||
"new_chapters": "Download new chapters"
|
|
||||||
},
|
|
||||||
"categories": {
|
"categories": {
|
||||||
"label": {
|
"label": {
|
||||||
"include_in_download": "Entries in excluded categories will not be downloaded even if they are also in included categories"
|
"include_in_download": "Entries in excluded categories will not be downloaded even if they are also in included categories"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"label": {
|
||||||
|
"ignore_with_unread_chapters": "Ignore automatic chapter downloads for entries with unread chapters",
|
||||||
|
"new_chapters": "Download new chapters"
|
||||||
|
},
|
||||||
"title": "Auto-download"
|
"title": "Auto-download"
|
||||||
},
|
},
|
||||||
"delete_chapters": {
|
"delete_chapters": {
|
||||||
@@ -289,10 +290,12 @@
|
|||||||
"browse": "Browse",
|
"browse": "Browse",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
"copy": "Copy",
|
||||||
"deselect": "Deselect",
|
"deselect": "Deselect",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"latest": "Latest",
|
"latest": "Latest",
|
||||||
|
"migrate": "Migrate",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"open_site": "Open Site",
|
"open_site": "Open Site",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
@@ -549,6 +552,12 @@
|
|||||||
"success_other": "Removed {{count}} manga from the library"
|
"success_other": "Removed {{count}} manga from the library"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"migrate": {
|
||||||
|
"label": {
|
||||||
|
"error": "Could not migrate manga",
|
||||||
|
"success": "Successfully migrated manga"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
@@ -573,6 +582,23 @@
|
|||||||
"title_one": "Manga",
|
"title_one": "Manga",
|
||||||
"title_other": "Manga"
|
"title_other": "Manga"
|
||||||
},
|
},
|
||||||
|
"migrate": {
|
||||||
|
"dialog": {
|
||||||
|
"action": {
|
||||||
|
"button": {
|
||||||
|
"show_entry": "Show entry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Select data to include"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"info": "Migrating manga…"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"title": "$t(migrate.title) \"{{title}}\""
|
||||||
|
},
|
||||||
|
"title": "Migrate"
|
||||||
|
},
|
||||||
"reader": {
|
"reader": {
|
||||||
"button": {
|
"button": {
|
||||||
"close_menu": "Close menu",
|
"close_menu": "Close menu",
|
||||||
@@ -605,10 +631,10 @@
|
|||||||
"load_next_chapter": "Load next chapter at ending",
|
"load_next_chapter": "Load next chapter at ending",
|
||||||
"offset_first_page": "Offset first page",
|
"offset_first_page": "Offset first page",
|
||||||
"reader_type": "Reader type",
|
"reader_type": "Reader type",
|
||||||
|
"reader_width": "Reader width",
|
||||||
"show_page_number": "Show page number",
|
"show_page_number": "Show page number",
|
||||||
"skip_dup_chapters": "Skip duplicate chapters",
|
"skip_dup_chapters": "Skip duplicate chapters",
|
||||||
"static_navigation": "Static navigation",
|
"static_navigation": "Static navigation"
|
||||||
"reader_width": "Reader width"
|
|
||||||
},
|
},
|
||||||
"reader_type": {
|
"reader_type": {
|
||||||
"label": {
|
"label": {
|
||||||
@@ -926,4 +952,4 @@
|
|||||||
},
|
},
|
||||||
"title": "Updates"
|
"title": "Updates"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,7 @@ export type ChapterIdInfo = Pick<TChapter, 'id'>;
|
|||||||
export type ChapterDownloadInfo = ChapterIdInfo & Pick<TChapter, 'isDownloaded'>;
|
export type ChapterDownloadInfo = ChapterIdInfo & Pick<TChapter, 'isDownloaded'>;
|
||||||
export type ChapterBookmarkInfo = ChapterIdInfo & Pick<TChapter, 'isBookmarked'>;
|
export type ChapterBookmarkInfo = ChapterIdInfo & Pick<TChapter, 'isBookmarked'>;
|
||||||
export type ChapterReadInfo = ChapterIdInfo & Pick<TChapter, 'isRead'>;
|
export type ChapterReadInfo = ChapterIdInfo & Pick<TChapter, 'isRead'>;
|
||||||
|
export type ChapterNumberInfo = ChapterIdInfo & Pick<TChapter, 'chapterNumber'>;
|
||||||
|
|
||||||
export class Chapters {
|
export class Chapters {
|
||||||
static getIds(chapters: { id: number }[]): number[] {
|
static getIds(chapters: { id: number }[]): number[] {
|
||||||
@@ -130,6 +131,23 @@ export class Chapters {
|
|||||||
return chapters.filter((chapter) => !Chapters.isRead(chapter));
|
return chapters.filter((chapter) => !Chapters.isRead(chapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getMatchingChapterNumberChapters<Chapter extends ChapterNumberInfo>(
|
||||||
|
chaptersA: Chapter[],
|
||||||
|
chaptersB: Chapter[],
|
||||||
|
): [ChapterA: Chapter, ChapterB: Chapter][] {
|
||||||
|
return chaptersA
|
||||||
|
.map((chapterA) => {
|
||||||
|
const matchingChapter = chaptersB.find((chapterB) => chapterA.chapterNumber === chapterB.chapterNumber);
|
||||||
|
|
||||||
|
if (!matchingChapter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [chapterA, matchingChapter];
|
||||||
|
})
|
||||||
|
.filter((matchingChapters) => matchingChapters !== null) as [Chapter, Chapter][];
|
||||||
|
}
|
||||||
|
|
||||||
static async download(chapterIds: number[]): Promise<void> {
|
static async download(chapterIds: number[]): Promise<void> {
|
||||||
return Chapters.executeAction(
|
return Chapters.executeAction(
|
||||||
'download',
|
'download',
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|||||||
import {
|
import {
|
||||||
ChapterConditionInput,
|
ChapterConditionInput,
|
||||||
GetMangasChapterIdsWithStateQuery,
|
GetMangasChapterIdsWithStateQuery,
|
||||||
|
GetMangaToMigrateQuery,
|
||||||
|
GetMangaToMigrateToFetchMutation,
|
||||||
UpdateMangaCategoriesPatchInput,
|
UpdateMangaCategoriesPatchInput,
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
} from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { Chapters } from '@/lib/data/Chapters.ts';
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
||||||
@@ -23,7 +25,8 @@ export type MangaAction =
|
|||||||
| 'mark_as_read'
|
| 'mark_as_read'
|
||||||
| 'mark_as_unread'
|
| 'mark_as_unread'
|
||||||
| 'remove_from_library'
|
| 'remove_from_library'
|
||||||
| 'change_categories';
|
| 'change_categories'
|
||||||
|
| 'migrate';
|
||||||
|
|
||||||
export const actionToTranslationKey: {
|
export const actionToTranslationKey: {
|
||||||
[key in MangaAction]: {
|
[key in MangaAction]: {
|
||||||
@@ -83,11 +86,38 @@ export const actionToTranslationKey: {
|
|||||||
success: 'manga.action.category.label.success',
|
success: 'manga.action.category.label.success',
|
||||||
error: 'manga.action.category.label.error',
|
error: 'manga.action.category.label.error',
|
||||||
},
|
},
|
||||||
|
migrate: {
|
||||||
|
action: {
|
||||||
|
single: 'global.button.migrate',
|
||||||
|
selected: 'global.button.migrate', // not supported
|
||||||
|
},
|
||||||
|
success: 'manga.action.migrate.label.success',
|
||||||
|
error: 'manga.action.migrate.label.error',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MangaChapterCountInfo = { chapters: Pick<TManga['chapters'], 'totalCount'> };
|
export type MangaChapterCountInfo = { chapters: Pick<TManga['chapters'], 'totalCount'> };
|
||||||
export type MangaDownloadInfo = Pick<TManga, 'downloadCount'> & MangaChapterCountInfo;
|
export type MangaDownloadInfo = Pick<TManga, 'downloadCount'> & MangaChapterCountInfo;
|
||||||
export type MangaUnreadInfo = Pick<TManga, 'unreadCount'> & MangaChapterCountInfo;
|
export type MangaUnreadInfo = Pick<TManga, 'unreadCount'> & MangaChapterCountInfo;
|
||||||
|
|
||||||
|
export type MigrateMode = 'copy' | 'migrate';
|
||||||
|
|
||||||
|
type MarkAsReadOptions = { wasManuallyMarkedAsRead: boolean };
|
||||||
|
type ChangeCategoriesOptions = { changeCategoriesPatch: UpdateMangaCategoriesPatchInput };
|
||||||
|
type MigrateOptions = {
|
||||||
|
mangaIdToMigrateTo: number;
|
||||||
|
mode: MigrateMode;
|
||||||
|
migrateChapters?: boolean;
|
||||||
|
migrateCategories?: boolean;
|
||||||
|
};
|
||||||
|
type PerformActionOptions<Action extends MangaAction> = Action extends 'mark_as_read'
|
||||||
|
? MarkAsReadOptions & PropertiesNever<ChangeCategoriesOptions> & PropertiesNever<MigrateOptions>
|
||||||
|
: Action extends 'change_categories'
|
||||||
|
? PropertiesNever<MarkAsReadOptions> & ChangeCategoriesOptions & PropertiesNever<MigrateOptions>
|
||||||
|
: Action extends 'migrate'
|
||||||
|
? PropertiesNever<MarkAsReadOptions> & PropertiesNever<ChangeCategoriesOptions> & MigrateOptions
|
||||||
|
: Partial<MarkAsReadOptions> & Partial<ChangeCategoriesOptions> & Partial<MigrateOptions>;
|
||||||
|
|
||||||
export class Mangas {
|
export class Mangas {
|
||||||
static getIds(mangas: { id: number }[]): number[] {
|
static getIds(mangas: { id: number }[]): number[] {
|
||||||
return mangas.map((manga) => manga.id);
|
return mangas.map((manga) => manga.id);
|
||||||
@@ -185,6 +215,89 @@ export class Mangas {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async migrateChapters(
|
||||||
|
mangaToMigrate: GetMangaToMigrateQuery['manga'],
|
||||||
|
mangaToMigrateToInfo: GetMangaToMigrateToFetchMutation,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!mangaToMigrate.chapters || !mangaToMigrateToInfo.fetchChapters?.chapters) {
|
||||||
|
throw new Error('Chapters are missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const chaptersToMigrate = mangaToMigrate.chapters.nodes;
|
||||||
|
|
||||||
|
const chaptersToMigrateTo = mangaToMigrateToInfo.fetchChapters?.chapters;
|
||||||
|
const migratableChapters = Chapters.getMatchingChapterNumberChapters(chaptersToMigrate, chaptersToMigrateTo);
|
||||||
|
|
||||||
|
const readChapters: number[] = [];
|
||||||
|
const bookmarkedChapters: number[] = [];
|
||||||
|
|
||||||
|
migratableChapters.forEach(([chapterToMigrate, chapterToMigrateTo]) => {
|
||||||
|
const { isRead, isBookmarked } = chapterToMigrate;
|
||||||
|
|
||||||
|
if (isRead) {
|
||||||
|
readChapters.push(chapterToMigrateTo.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBookmarked) {
|
||||||
|
bookmarkedChapters.push(chapterToMigrateTo.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
requestManager.updateChapters(readChapters, { isRead: true }).response,
|
||||||
|
requestManager.updateChapters(bookmarkedChapters, { isBookmarked: true }).response,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async migrateCategories(
|
||||||
|
mangaToMigrate: GetMangaToMigrateQuery['manga'],
|
||||||
|
mangaToMigrateTo: GetMangaToMigrateToFetchMutation['fetchManga']['manga'],
|
||||||
|
): Promise<void> {
|
||||||
|
if (!mangaToMigrate?.categories) {
|
||||||
|
throw new Error('Categories are missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
requestManager.updateMangasCategories([mangaToMigrateTo.id], {
|
||||||
|
addToCategories: mangaToMigrate.categories.nodes.map((category) => category.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async migrate(
|
||||||
|
mangaId: number,
|
||||||
|
mangaIdToMigrateTo: number,
|
||||||
|
{ mode, migrateChapters, migrateCategories }: Omit<MigrateOptions, 'mangaIdToMigrateTo'>,
|
||||||
|
): Promise<void> {
|
||||||
|
return Mangas.executeAction('migrate', 1, async () => {
|
||||||
|
const [{ data: mangaToMigrateData }, { data: mangaToMigrateToData }] = await Promise.all([
|
||||||
|
requestManager.getMangaToMigrate(mangaId, { migrateChapters, migrateCategories }).response,
|
||||||
|
requestManager.getMangaToMigrateToFetch(mangaIdToMigrateTo, { migrateChapters, migrateCategories })
|
||||||
|
.response,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!mangaToMigrateData.manga || !mangaToMigrateToData?.fetchManga.manga) {
|
||||||
|
throw new Error('Mangas::migrate: missing manga data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
migrateChapters &&
|
||||||
|
(!mangaToMigrateData.manga.chapters || !mangaToMigrateToData.fetchChapters?.chapters)
|
||||||
|
) {
|
||||||
|
throw new Error('Mangas::migrate: missing chapters data');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
migrateChapters ? Mangas.migrateChapters(mangaToMigrateData.manga, mangaToMigrateToData) : undefined,
|
||||||
|
migrateCategories
|
||||||
|
? Mangas.migrateCategories(mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga)
|
||||||
|
: undefined,
|
||||||
|
!mangaToMigrateToData.fetchManga.manga.inLibrary
|
||||||
|
? requestManager.updateManga(mangaIdToMigrateTo, { inLibrary: true }).response
|
||||||
|
: undefined,
|
||||||
|
mode === 'migrate' ? requestManager.updateManga(mangaId, { inLibrary: false }).response : undefined,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static async executeAction(
|
private static async executeAction(
|
||||||
action: MangaAction,
|
action: MangaAction,
|
||||||
itemCount: number,
|
itemCount: number,
|
||||||
@@ -205,11 +318,9 @@ export class Mangas {
|
|||||||
{
|
{
|
||||||
wasManuallyMarkedAsRead,
|
wasManuallyMarkedAsRead,
|
||||||
changeCategoriesPatch,
|
changeCategoriesPatch,
|
||||||
}: Action extends 'mark_as_read'
|
mangaIdToMigrateTo,
|
||||||
? { wasManuallyMarkedAsRead: boolean; changeCategoriesPatch?: never }
|
...migrateOptions
|
||||||
: Action extends 'change_categories'
|
}: PerformActionOptions<Action>,
|
||||||
? { wasManuallyMarkedAsRead?: never; changeCategoriesPatch: UpdateMangaCategoriesPatchInput }
|
|
||||||
: { wasManuallyMarkedAsRead?: boolean; changeCategoriesPatch?: UpdateMangaCategoriesPatchInput },
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'download':
|
case 'download':
|
||||||
@@ -224,6 +335,9 @@ export class Mangas {
|
|||||||
return Mangas.removeFromLibrary(mangaIds);
|
return Mangas.removeFromLibrary(mangaIds);
|
||||||
case 'change_categories':
|
case 'change_categories':
|
||||||
return Mangas.changeCategories(mangaIds, changeCategoriesPatch!);
|
return Mangas.changeCategories(mangaIds, changeCategoriesPatch!);
|
||||||
|
case 'migrate': {
|
||||||
|
return Mangas.migrate(mangaIds[0], mangaIdToMigrateTo!, migrateOptions as unknown as MigrateOptions);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Mangas::performAction: unknown action "${action}"`);
|
throw new Error(`Mangas::performAction: unknown action "${action}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ export type PageInfoFieldPolicy = {
|
|||||||
hasPreviousPage?: FieldPolicy<any> | FieldReadFunction<any>,
|
hasPreviousPage?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
startCursor?: FieldPolicy<any> | FieldReadFunction<any>
|
startCursor?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
export type PartialSettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | PartialSettingsTypeKeySpecifier)[];
|
export type PartialSettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | PartialSettingsTypeKeySpecifier)[];
|
||||||
export type PartialSettingsTypeFieldPolicy = {
|
export type PartialSettingsTypeFieldPolicy = {
|
||||||
autoDownloadAheadLimit?: FieldPolicy<any> | FieldReadFunction<any>,
|
autoDownloadAheadLimit?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -528,6 +528,11 @@ export type PartialSettingsTypeFieldPolicy = {
|
|||||||
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
|
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
|
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrSessionName?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrSessionTtl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrTimeout?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
|
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -631,7 +636,7 @@ export type SetSettingsPayloadFieldPolicy = {
|
|||||||
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
|
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
settings?: FieldPolicy<any> | FieldReadFunction<any>
|
settings?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
export type SettingsKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsKeySpecifier)[];
|
export type SettingsKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsKeySpecifier)[];
|
||||||
export type SettingsFieldPolicy = {
|
export type SettingsFieldPolicy = {
|
||||||
autoDownloadAheadLimit?: FieldPolicy<any> | FieldReadFunction<any>,
|
autoDownloadAheadLimit?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -651,6 +656,11 @@ export type SettingsFieldPolicy = {
|
|||||||
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
|
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
|
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrSessionName?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrSessionTtl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrTimeout?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
|
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -668,7 +678,7 @@ export type SettingsFieldPolicy = {
|
|||||||
webUIInterface?: FieldPolicy<any> | FieldReadFunction<any>,
|
webUIInterface?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
webUIUpdateCheckInterval?: FieldPolicy<any> | FieldReadFunction<any>
|
webUIUpdateCheckInterval?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
export type SettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsTypeKeySpecifier)[];
|
export type SettingsTypeKeySpecifier = ('autoDownloadAheadLimit' | 'autoDownloadNewChapters' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxSourcesInParallel' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPort' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsTypeKeySpecifier)[];
|
||||||
export type SettingsTypeFieldPolicy = {
|
export type SettingsTypeFieldPolicy = {
|
||||||
autoDownloadAheadLimit?: FieldPolicy<any> | FieldReadFunction<any>,
|
autoDownloadAheadLimit?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
autoDownloadNewChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -688,6 +698,11 @@ export type SettingsTypeFieldPolicy = {
|
|||||||
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
|
excludeNotStarted?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
excludeUnreadChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
|
extensionRepos?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrSessionName?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrSessionTtl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrTimeout?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
flareSolverrUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
|
globalUpdateInterval?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -785,7 +800,7 @@ export type TrackRecordNodeListFieldPolicy = {
|
|||||||
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
|
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
|
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
export type TrackRecordTypeKeySpecifier = ('displayScore' | 'finishDate' | 'id' | 'lastChapterRead' | 'libraryId' | 'manga' | 'mangaId' | 'remoteId' | 'remoteUrl' | 'score' | 'startDate' | 'status' | 'syncId' | 'title' | 'totalChapters' | 'tracker' | TrackRecordTypeKeySpecifier)[];
|
export type TrackRecordTypeKeySpecifier = ('displayScore' | 'finishDate' | 'id' | 'lastChapterRead' | 'libraryId' | 'manga' | 'mangaId' | 'remoteId' | 'remoteUrl' | 'score' | 'startDate' | 'status' | 'title' | 'totalChapters' | 'tracker' | 'trackerId' | TrackRecordTypeKeySpecifier)[];
|
||||||
export type TrackRecordTypeFieldPolicy = {
|
export type TrackRecordTypeFieldPolicy = {
|
||||||
displayScore?: FieldPolicy<any> | FieldReadFunction<any>,
|
displayScore?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
finishDate?: FieldPolicy<any> | FieldReadFunction<any>,
|
finishDate?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -799,25 +814,31 @@ export type TrackRecordTypeFieldPolicy = {
|
|||||||
score?: FieldPolicy<any> | FieldReadFunction<any>,
|
score?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
startDate?: FieldPolicy<any> | FieldReadFunction<any>,
|
startDate?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
status?: FieldPolicy<any> | FieldReadFunction<any>,
|
status?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
syncId?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
title?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
totalChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
tracker?: FieldPolicy<any> | FieldReadFunction<any>
|
|
||||||
};
|
|
||||||
export type TrackSearchTypeKeySpecifier = ('coverUrl' | 'mediaId' | 'publishingStatus' | 'publishingType' | 'startDate' | 'summary' | 'syncId' | 'title' | 'totalChapters' | 'tracker' | 'trackingUrl' | TrackSearchTypeKeySpecifier)[];
|
|
||||||
export type TrackSearchTypeFieldPolicy = {
|
|
||||||
coverUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
mediaId?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
publishingStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
publishingType?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
startDate?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
summary?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
syncId?: FieldPolicy<any> | FieldReadFunction<any>,
|
|
||||||
title?: FieldPolicy<any> | FieldReadFunction<any>,
|
title?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
totalChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
totalChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
tracker?: FieldPolicy<any> | FieldReadFunction<any>,
|
tracker?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
trackerId?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
|
};
|
||||||
|
export type TrackSearchTypeKeySpecifier = ('coverUrl' | 'id' | 'publishingStatus' | 'publishingType' | 'remoteId' | 'startDate' | 'summary' | 'title' | 'totalChapters' | 'tracker' | 'trackerId' | 'trackingUrl' | TrackSearchTypeKeySpecifier)[];
|
||||||
|
export type TrackSearchTypeFieldPolicy = {
|
||||||
|
coverUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
id?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
publishingStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
publishingType?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
remoteId?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
startDate?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
summary?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
title?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
totalChapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
tracker?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
trackerId?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
trackingUrl?: FieldPolicy<any> | FieldReadFunction<any>
|
trackingUrl?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
|
export type TrackStatusTypeKeySpecifier = ('name' | 'value' | TrackStatusTypeKeySpecifier)[];
|
||||||
|
export type TrackStatusTypeFieldPolicy = {
|
||||||
|
name?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
value?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
|
};
|
||||||
export type TrackerEdgeKeySpecifier = ('cursor' | 'node' | TrackerEdgeKeySpecifier)[];
|
export type TrackerEdgeKeySpecifier = ('cursor' | 'node' | TrackerEdgeKeySpecifier)[];
|
||||||
export type TrackerEdgeFieldPolicy = {
|
export type TrackerEdgeFieldPolicy = {
|
||||||
cursor?: FieldPolicy<any> | FieldReadFunction<any>,
|
cursor?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
@@ -830,13 +851,15 @@ export type TrackerNodeListFieldPolicy = {
|
|||||||
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
|
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
|
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
export type TrackerTypeKeySpecifier = ('authUrl' | 'icon' | 'id' | 'isLoggedIn' | 'name' | 'trackRecords' | TrackerTypeKeySpecifier)[];
|
export type TrackerTypeKeySpecifier = ('authUrl' | 'icon' | 'id' | 'isLoggedIn' | 'name' | 'scores' | 'statuses' | 'trackRecords' | TrackerTypeKeySpecifier)[];
|
||||||
export type TrackerTypeFieldPolicy = {
|
export type TrackerTypeFieldPolicy = {
|
||||||
authUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
authUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
icon?: FieldPolicy<any> | FieldReadFunction<any>,
|
icon?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
id?: FieldPolicy<any> | FieldReadFunction<any>,
|
id?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
isLoggedIn?: FieldPolicy<any> | FieldReadFunction<any>,
|
isLoggedIn?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
name?: FieldPolicy<any> | FieldReadFunction<any>,
|
name?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
scores?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
|
statuses?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||||
trackRecords?: FieldPolicy<any> | FieldReadFunction<any>
|
trackRecords?: FieldPolicy<any> | FieldReadFunction<any>
|
||||||
};
|
};
|
||||||
export type TriStateFilterKeySpecifier = ('default' | 'name' | TriStateFilterKeySpecifier)[];
|
export type TriStateFilterKeySpecifier = ('default' | 'name' | TriStateFilterKeySpecifier)[];
|
||||||
@@ -1351,6 +1374,10 @@ export type StrictTypedTypePolicies = {
|
|||||||
keyFields?: false | TrackSearchTypeKeySpecifier | (() => undefined | TrackSearchTypeKeySpecifier),
|
keyFields?: false | TrackSearchTypeKeySpecifier | (() => undefined | TrackSearchTypeKeySpecifier),
|
||||||
fields?: TrackSearchTypeFieldPolicy,
|
fields?: TrackSearchTypeFieldPolicy,
|
||||||
},
|
},
|
||||||
|
TrackStatusType?: Omit<TypePolicy, "fields" | "keyFields"> & {
|
||||||
|
keyFields?: false | TrackStatusTypeKeySpecifier | (() => undefined | TrackStatusTypeKeySpecifier),
|
||||||
|
fields?: TrackStatusTypeFieldPolicy,
|
||||||
|
},
|
||||||
TrackerEdge?: Omit<TypePolicy, "fields" | "keyFields"> & {
|
TrackerEdge?: Omit<TypePolicy, "fields" | "keyFields"> & {
|
||||||
keyFields?: false | TrackerEdgeKeySpecifier | (() => undefined | TrackerEdgeKeySpecifier),
|
keyFields?: false | TrackerEdgeKeySpecifier | (() => undefined | TrackerEdgeKeySpecifier),
|
||||||
fields?: TrackerEdgeFieldPolicy,
|
fields?: TrackerEdgeFieldPolicy,
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export type BackupRestoreStatus = {
|
|||||||
export type BindTrackInput = {
|
export type BindTrackInput = {
|
||||||
clientMutationId?: InputMaybe<Scalars['String']['input']>;
|
clientMutationId?: InputMaybe<Scalars['String']['input']>;
|
||||||
mangaId: Scalars['Int']['input'];
|
mangaId: Scalars['Int']['input'];
|
||||||
track: TrackSearchTypeInput;
|
remoteId: Scalars['LongString']['input'];
|
||||||
|
trackerId: Scalars['Int']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BindTrackPayload = {
|
export type BindTrackPayload = {
|
||||||
@@ -1369,6 +1370,11 @@ export type PartialSettingsType = Settings & {
|
|||||||
excludeNotStarted?: Maybe<Scalars['Boolean']['output']>;
|
excludeNotStarted?: Maybe<Scalars['Boolean']['output']>;
|
||||||
excludeUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
|
excludeUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
|
||||||
extensionRepos?: Maybe<Array<Scalars['String']['output']>>;
|
extensionRepos?: Maybe<Array<Scalars['String']['output']>>;
|
||||||
|
flareSolverrEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
|
flareSolverrSessionName?: Maybe<Scalars['String']['output']>;
|
||||||
|
flareSolverrSessionTtl?: Maybe<Scalars['Int']['output']>;
|
||||||
|
flareSolverrTimeout?: Maybe<Scalars['Int']['output']>;
|
||||||
|
flareSolverrUrl?: Maybe<Scalars['String']['output']>;
|
||||||
globalUpdateInterval?: Maybe<Scalars['Float']['output']>;
|
globalUpdateInterval?: Maybe<Scalars['Float']['output']>;
|
||||||
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
|
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
|
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
@@ -1406,6 +1412,11 @@ export type PartialSettingsTypeInput = {
|
|||||||
excludeNotStarted?: InputMaybe<Scalars['Boolean']['input']>;
|
excludeNotStarted?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
excludeUnreadChapters?: InputMaybe<Scalars['Boolean']['input']>;
|
excludeUnreadChapters?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
extensionRepos?: InputMaybe<Array<Scalars['String']['input']>>;
|
extensionRepos?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||||
|
flareSolverrEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
flareSolverrSessionName?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
flareSolverrSessionTtl?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
flareSolverrTimeout?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
flareSolverrUrl?: InputMaybe<Scalars['String']['input']>;
|
||||||
globalUpdateInterval?: InputMaybe<Scalars['Float']['input']>;
|
globalUpdateInterval?: InputMaybe<Scalars['Float']['input']>;
|
||||||
gqlDebugLogsEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
gqlDebugLogsEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
initialOpenInBrowserEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
initialOpenInBrowserEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
@@ -1746,6 +1757,11 @@ export type Settings = {
|
|||||||
excludeNotStarted?: Maybe<Scalars['Boolean']['output']>;
|
excludeNotStarted?: Maybe<Scalars['Boolean']['output']>;
|
||||||
excludeUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
|
excludeUnreadChapters?: Maybe<Scalars['Boolean']['output']>;
|
||||||
extensionRepos?: Maybe<Array<Scalars['String']['output']>>;
|
extensionRepos?: Maybe<Array<Scalars['String']['output']>>;
|
||||||
|
flareSolverrEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
|
flareSolverrSessionName?: Maybe<Scalars['String']['output']>;
|
||||||
|
flareSolverrSessionTtl?: Maybe<Scalars['Int']['output']>;
|
||||||
|
flareSolverrTimeout?: Maybe<Scalars['Int']['output']>;
|
||||||
|
flareSolverrUrl?: Maybe<Scalars['String']['output']>;
|
||||||
globalUpdateInterval?: Maybe<Scalars['Float']['output']>;
|
globalUpdateInterval?: Maybe<Scalars['Float']['output']>;
|
||||||
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
|
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
|
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
@@ -1784,6 +1800,11 @@ export type SettingsType = Settings & {
|
|||||||
excludeNotStarted: Scalars['Boolean']['output'];
|
excludeNotStarted: Scalars['Boolean']['output'];
|
||||||
excludeUnreadChapters: Scalars['Boolean']['output'];
|
excludeUnreadChapters: Scalars['Boolean']['output'];
|
||||||
extensionRepos: Array<Scalars['String']['output']>;
|
extensionRepos: Array<Scalars['String']['output']>;
|
||||||
|
flareSolverrEnabled: Scalars['Boolean']['output'];
|
||||||
|
flareSolverrSessionName: Scalars['String']['output'];
|
||||||
|
flareSolverrSessionTtl: Scalars['Int']['output'];
|
||||||
|
flareSolverrTimeout: Scalars['Int']['output'];
|
||||||
|
flareSolverrUrl: Scalars['String']['output'];
|
||||||
globalUpdateInterval: Scalars['Float']['output'];
|
globalUpdateInterval: Scalars['Float']['output'];
|
||||||
gqlDebugLogsEnabled: Scalars['Boolean']['output'];
|
gqlDebugLogsEnabled: Scalars['Boolean']['output'];
|
||||||
initialOpenInBrowserEnabled: Scalars['Boolean']['output'];
|
initialOpenInBrowserEnabled: Scalars['Boolean']['output'];
|
||||||
@@ -1983,9 +2004,9 @@ export type TrackRecordConditionInput = {
|
|||||||
score?: InputMaybe<Scalars['Float']['input']>;
|
score?: InputMaybe<Scalars['Float']['input']>;
|
||||||
startDate?: InputMaybe<Scalars['LongString']['input']>;
|
startDate?: InputMaybe<Scalars['LongString']['input']>;
|
||||||
status?: InputMaybe<Scalars['Int']['input']>;
|
status?: InputMaybe<Scalars['Int']['input']>;
|
||||||
syncId?: InputMaybe<Scalars['Int']['input']>;
|
|
||||||
title?: InputMaybe<Scalars['String']['input']>;
|
title?: InputMaybe<Scalars['String']['input']>;
|
||||||
totalChapters?: InputMaybe<Scalars['Int']['input']>;
|
totalChapters?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
trackerId?: InputMaybe<Scalars['Int']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackRecordEdge = Edge & {
|
export type TrackRecordEdge = Edge & {
|
||||||
@@ -2008,9 +2029,9 @@ export type TrackRecordFilterInput = {
|
|||||||
score?: InputMaybe<DoubleFilterInput>;
|
score?: InputMaybe<DoubleFilterInput>;
|
||||||
startDate?: InputMaybe<LongFilterInput>;
|
startDate?: InputMaybe<LongFilterInput>;
|
||||||
status?: InputMaybe<IntFilterInput>;
|
status?: InputMaybe<IntFilterInput>;
|
||||||
syncId?: InputMaybe<IntFilterInput>;
|
|
||||||
title?: InputMaybe<StringFilterInput>;
|
title?: InputMaybe<StringFilterInput>;
|
||||||
totalChapters?: InputMaybe<IntFilterInput>;
|
totalChapters?: InputMaybe<IntFilterInput>;
|
||||||
|
trackerId?: InputMaybe<IntFilterInput>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackRecordNodeList = NodeList & {
|
export type TrackRecordNodeList = NodeList & {
|
||||||
@@ -2029,9 +2050,9 @@ export enum TrackRecordOrderBy {
|
|||||||
RemoteId = 'REMOTE_ID',
|
RemoteId = 'REMOTE_ID',
|
||||||
Score = 'SCORE',
|
Score = 'SCORE',
|
||||||
StartDate = 'START_DATE',
|
StartDate = 'START_DATE',
|
||||||
SyncId = 'SYNC_ID',
|
|
||||||
Title = 'TITLE',
|
Title = 'TITLE',
|
||||||
TotalChapters = 'TOTAL_CHAPTERS'
|
TotalChapters = 'TOTAL_CHAPTERS',
|
||||||
|
TrackerId = 'TRACKER_ID'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrackRecordType = {
|
export type TrackRecordType = {
|
||||||
@@ -2048,38 +2069,32 @@ export type TrackRecordType = {
|
|||||||
score: Scalars['Float']['output'];
|
score: Scalars['Float']['output'];
|
||||||
startDate: Scalars['LongString']['output'];
|
startDate: Scalars['LongString']['output'];
|
||||||
status: Scalars['Int']['output'];
|
status: Scalars['Int']['output'];
|
||||||
syncId: Scalars['Int']['output'];
|
|
||||||
title: Scalars['String']['output'];
|
title: Scalars['String']['output'];
|
||||||
totalChapters: Scalars['Int']['output'];
|
totalChapters: Scalars['Int']['output'];
|
||||||
tracker: TrackerType;
|
tracker: TrackerType;
|
||||||
|
trackerId: Scalars['Int']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackSearchType = {
|
export type TrackSearchType = {
|
||||||
__typename?: 'TrackSearchType';
|
__typename?: 'TrackSearchType';
|
||||||
coverUrl: Scalars['String']['output'];
|
coverUrl: Scalars['String']['output'];
|
||||||
mediaId: Scalars['LongString']['output'];
|
id: Scalars['Int']['output'];
|
||||||
publishingStatus: Scalars['String']['output'];
|
publishingStatus: Scalars['String']['output'];
|
||||||
publishingType: Scalars['String']['output'];
|
publishingType: Scalars['String']['output'];
|
||||||
|
remoteId: Scalars['LongString']['output'];
|
||||||
startDate: Scalars['String']['output'];
|
startDate: Scalars['String']['output'];
|
||||||
summary: Scalars['String']['output'];
|
summary: Scalars['String']['output'];
|
||||||
syncId: Scalars['Int']['output'];
|
|
||||||
title: Scalars['String']['output'];
|
title: Scalars['String']['output'];
|
||||||
totalChapters: Scalars['Int']['output'];
|
totalChapters: Scalars['Int']['output'];
|
||||||
tracker: TrackerType;
|
tracker: TrackerType;
|
||||||
|
trackerId: Scalars['Int']['output'];
|
||||||
trackingUrl: Scalars['String']['output'];
|
trackingUrl: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackSearchTypeInput = {
|
export type TrackStatusType = {
|
||||||
coverUrl: Scalars['String']['input'];
|
__typename?: 'TrackStatusType';
|
||||||
mediaId: Scalars['LongString']['input'];
|
name: Scalars['String']['output'];
|
||||||
publishingStatus: Scalars['String']['input'];
|
value: Scalars['Int']['output'];
|
||||||
publishingType: Scalars['String']['input'];
|
|
||||||
startDate: Scalars['String']['input'];
|
|
||||||
summary: Scalars['String']['input'];
|
|
||||||
syncId: Scalars['Int']['input'];
|
|
||||||
title: Scalars['String']['input'];
|
|
||||||
totalChapters: Scalars['Int']['input'];
|
|
||||||
trackingUrl: Scalars['String']['input'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackerConditionInput = {
|
export type TrackerConditionInput = {
|
||||||
@@ -2116,6 +2131,8 @@ export type TrackerType = {
|
|||||||
id: Scalars['Int']['output'];
|
id: Scalars['Int']['output'];
|
||||||
isLoggedIn: Scalars['Boolean']['output'];
|
isLoggedIn: Scalars['Boolean']['output'];
|
||||||
name: Scalars['String']['output'];
|
name: Scalars['String']['output'];
|
||||||
|
scores: Array<Scalars['String']['output']>;
|
||||||
|
statuses: Array<TrackStatusType>;
|
||||||
trackRecords: TrackRecordNodeList;
|
trackRecords: TrackRecordNodeList;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2756,6 +2773,15 @@ export type GetMangaFetchMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type GetMangaFetchMutation = { __typename?: 'Mutation', fetchManga: { __typename?: 'FetchMangaPayload', clientMutationId?: string | null, 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, lastReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestFetchedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestUploadedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, 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, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } } | null } } };
|
export type GetMangaFetchMutation = { __typename?: 'Mutation', fetchManga: { __typename?: 'FetchMangaPayload', clientMutationId?: string | null, 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, lastReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestFetchedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestUploadedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, 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, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } } | null } } };
|
||||||
|
|
||||||
|
export type GetMangaToMigrateToFetchMutationVariables = Exact<{
|
||||||
|
id: Scalars['Int']['input'];
|
||||||
|
migrateChapters: Scalars['Boolean']['input'];
|
||||||
|
migrateCategories: Scalars['Boolean']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetMangaToMigrateToFetchMutation = { __typename?: 'Mutation', fetchManga: { __typename?: 'FetchMangaPayload', clientMutationId?: string | null, manga: { __typename?: 'MangaType', id: number, title: string, inLibrary: boolean, categories?: { __typename?: 'CategoryNodeList', nodes: Array<{ __typename?: 'CategoryType', id: number }> } } }, fetchChapters?: { __typename?: 'FetchChaptersPayload', chapters: Array<{ __typename?: 'ChapterType', id: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number } }> } };
|
||||||
|
|
||||||
export type SetMangaMetadataMutationVariables = Exact<{
|
export type SetMangaMetadataMutationVariables = Exact<{
|
||||||
input: SetMangaMetaInput;
|
input: SetMangaMetaInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -2983,6 +3009,15 @@ export type GetMangaQueryVariables = Exact<{
|
|||||||
|
|
||||||
export type GetMangaQuery = { __typename?: 'Query', 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, lastReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestFetchedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestUploadedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, 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, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } } | null } };
|
export type GetMangaQuery = { __typename?: 'Query', 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, lastReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestFetchedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestUploadedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, 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, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } } | null } };
|
||||||
|
|
||||||
|
export type GetMangaToMigrateQueryVariables = Exact<{
|
||||||
|
id: Scalars['Int']['input'];
|
||||||
|
migrateChapters: Scalars['Boolean']['input'];
|
||||||
|
migrateCategories: Scalars['Boolean']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetMangaToMigrateQuery = { __typename?: 'Query', manga: { __typename?: 'MangaType', id: number, inLibrary: boolean, title: string, chapters?: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', id: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number } }> }, categories?: { __typename?: 'CategoryNodeList', nodes: Array<{ __typename?: 'CategoryType', id: number }> } } };
|
||||||
|
|
||||||
export type GetMangasQueryVariables = Exact<{
|
export type GetMangasQueryVariables = Exact<{
|
||||||
after?: InputMaybe<Scalars['Cursor']['input']>;
|
after?: InputMaybe<Scalars['Cursor']['input']>;
|
||||||
before?: InputMaybe<Scalars['Cursor']['input']>;
|
before?: InputMaybe<Scalars['Cursor']['input']>;
|
||||||
@@ -2998,6 +3033,13 @@ export type GetMangasQueryVariables = Exact<{
|
|||||||
|
|
||||||
export type GetMangasQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __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, lastReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestFetchedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestUploadedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, 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, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } } | null }>, pageInfo: { __typename?: 'PageInfo', endCursor?: any | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: any | null } } };
|
export type GetMangasQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __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, lastReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestReadChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestFetchedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, latestUploadedChapter?: { __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', id: number, title: string, inLibrary: boolean, thumbnailUrl?: string | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, 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, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } } | null }>, pageInfo: { __typename?: 'PageInfo', endCursor?: any | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: any | null } } };
|
||||||
|
|
||||||
|
export type GetMigratableSourceMangasQueryVariables = Exact<{
|
||||||
|
sourceId: Scalars['LongString']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetMigratableSourceMangasQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', nodes: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, source?: { __typename?: 'SourceType', id: any } | null, categories: { __typename?: 'CategoryNodeList', nodes: Array<{ __typename?: 'CategoryType', id: number }> } }> } };
|
||||||
|
|
||||||
export type GetAboutQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetAboutQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
@@ -3035,6 +3077,11 @@ export type GetSourcesQueryVariables = Exact<{ [key: string]: never; }>;
|
|||||||
|
|
||||||
export type GetSourcesQuery = { __typename?: 'Query', sources: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } }> } };
|
export type GetSourcesQuery = { __typename?: 'Query', sources: { __typename?: 'SourceNodeList', nodes: Array<{ __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean, extension: { __typename?: 'ExtensionType', pkgName: string, repo?: string | null } }> } };
|
||||||
|
|
||||||
|
export type GetMigratableSourcesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetMigratableSourcesQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', nodes: Array<{ __typename?: 'MangaType', sourceId: any, source?: { __typename?: 'SourceType', id: any, name: string, lang: string, iconUrl: string } | null }> } };
|
||||||
|
|
||||||
export type GetUpdateStatusQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetUpdateStatusQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,37 @@ export const GET_MANGA_FETCH = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// makes the server fetch and return the manga
|
||||||
|
export const GET_MANGA_TO_MIGRATE_TO_FETCH = gql`
|
||||||
|
mutation GET_MANGA_TO_MIGRATE_TO_FETCH($id: Int!, $migrateChapters: Boolean!, $migrateCategories: Boolean!) {
|
||||||
|
fetchManga(input: { id: $id }) {
|
||||||
|
clientMutationId
|
||||||
|
manga {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
inLibrary
|
||||||
|
categories @include(if: $migrateCategories) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchChapters(input: { mangaId: $id }) @include(if: $migrateChapters) {
|
||||||
|
chapters {
|
||||||
|
id
|
||||||
|
manga {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
chapterNumber
|
||||||
|
isRead
|
||||||
|
isDownloaded
|
||||||
|
isBookmarked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const SET_MANGA_METADATA = gql`
|
export const SET_MANGA_METADATA = gql`
|
||||||
mutation SET_MANGA_METADATA($input: SetMangaMetaInput!) {
|
mutation SET_MANGA_METADATA($input: SetMangaMetaInput!) {
|
||||||
setMangaMeta(input: $input) {
|
setMangaMeta(input: $input) {
|
||||||
|
|||||||
@@ -20,6 +20,35 @@ export const GET_MANGA = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// returns the current manga from the database
|
||||||
|
export const GET_MANGA_TO_MIGRATE = gql`
|
||||||
|
query GET_MANGA_TO_MIGRATE($id: Int!, $migrateChapters: Boolean!, $migrateCategories: Boolean!) {
|
||||||
|
manga(id: $id) {
|
||||||
|
id
|
||||||
|
inLibrary
|
||||||
|
title
|
||||||
|
chapters @include(if: $migrateChapters) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
manga {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
chapterNumber
|
||||||
|
isRead
|
||||||
|
isDownloaded
|
||||||
|
isBookmarked
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
categories @include(if: $migrateCategories) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// returns the current manga from the database
|
// returns the current manga from the database
|
||||||
export const GET_MANGAS = gql`
|
export const GET_MANGAS = gql`
|
||||||
${FULL_MANGA_FIELDS}
|
${FULL_MANGA_FIELDS}
|
||||||
@@ -57,3 +86,23 @@ export const GET_MANGAS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_MIGRATABLE_SOURCE_MANGAS = gql`
|
||||||
|
query GET_MIGRATABLE_SOURCE_MANGAS($sourceId: LongString!) {
|
||||||
|
mangas(condition: { sourceId: $sourceId, inLibrary: true }) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
thumbnailUrl
|
||||||
|
source {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -28,3 +28,19 @@ export const GET_SOURCES = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_MIGRATABLE_SOURCES = gql`
|
||||||
|
query GET_MIGRATABLE_SOURCES {
|
||||||
|
mangas(condition: { inLibrary: true }) {
|
||||||
|
nodes {
|
||||||
|
sourceId
|
||||||
|
source {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
lang
|
||||||
|
iconUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -172,6 +172,14 @@ import {
|
|||||||
UpdateMangaCategoriesPatchInput,
|
UpdateMangaCategoriesPatchInput,
|
||||||
GetWebuiUpdateStatusQuery,
|
GetWebuiUpdateStatusQuery,
|
||||||
GetWebuiUpdateStatusQueryVariables,
|
GetWebuiUpdateStatusQueryVariables,
|
||||||
|
GetMigratableSourcesQuery,
|
||||||
|
GetMigratableSourcesQueryVariables,
|
||||||
|
GetMigratableSourceMangasQuery,
|
||||||
|
GetMigratableSourceMangasQueryVariables,
|
||||||
|
GetMangaToMigrateQuery,
|
||||||
|
GetMangaToMigrateQueryVariables,
|
||||||
|
GetMangaToMigrateToFetchMutation,
|
||||||
|
GetMangaToMigrateToFetchMutationVariables,
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
} from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts';
|
import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts';
|
||||||
import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
|
import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
|
||||||
@@ -187,16 +195,22 @@ import {
|
|||||||
INSTALL_EXTERNAL_EXTENSION,
|
INSTALL_EXTERNAL_EXTENSION,
|
||||||
UPDATE_EXTENSION,
|
UPDATE_EXTENSION,
|
||||||
} from '@/lib/graphql/mutations/ExtensionMutation.ts';
|
} from '@/lib/graphql/mutations/ExtensionMutation.ts';
|
||||||
import { GET_SOURCE, GET_SOURCES } from '@/lib/graphql/queries/SourceQuery.ts';
|
import { GET_MIGRATABLE_SOURCES, GET_SOURCE, GET_SOURCES } from '@/lib/graphql/queries/SourceQuery.ts';
|
||||||
import {
|
import {
|
||||||
GET_MANGA_FETCH,
|
GET_MANGA_FETCH,
|
||||||
|
GET_MANGA_TO_MIGRATE_TO_FETCH,
|
||||||
SET_MANGA_METADATA,
|
SET_MANGA_METADATA,
|
||||||
UPDATE_MANGA,
|
UPDATE_MANGA,
|
||||||
UPDATE_MANGA_CATEGORIES,
|
UPDATE_MANGA_CATEGORIES,
|
||||||
UPDATE_MANGAS,
|
UPDATE_MANGAS,
|
||||||
UPDATE_MANGAS_CATEGORIES,
|
UPDATE_MANGAS_CATEGORIES,
|
||||||
} from '@/lib/graphql/mutations/MangaMutation.ts';
|
} from '@/lib/graphql/mutations/MangaMutation.ts';
|
||||||
import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts';
|
import {
|
||||||
|
GET_MANGA,
|
||||||
|
GET_MANGA_TO_MIGRATE,
|
||||||
|
GET_MANGAS,
|
||||||
|
GET_MIGRATABLE_SOURCE_MANGAS,
|
||||||
|
} from '@/lib/graphql/queries/MangaQuery.ts';
|
||||||
import { GET_CATEGORIES, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts';
|
import { GET_CATEGORIES, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts';
|
||||||
import { GET_SOURCE_MANGAS_FETCH, UPDATE_SOURCE_PREFERENCES } from '@/lib/graphql/mutations/SourceMutation.ts';
|
import { GET_SOURCE_MANGAS_FETCH, UPDATE_SOURCE_PREFERENCES } from '@/lib/graphql/mutations/SourceMutation.ts';
|
||||||
import {
|
import {
|
||||||
@@ -1330,6 +1344,33 @@ export class RequestManager {
|
|||||||
return this.doRequest(GQLMethod.USE_QUERY, GET_MANGA, { id: Number(mangaId) }, options);
|
return this.doRequest(GQLMethod.USE_QUERY, GET_MANGA, { id: Number(mangaId) }, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getManga(
|
||||||
|
mangaId: number | string,
|
||||||
|
options?: QueryOptions<GetMangaQueryVariables, GetMangaQuery>,
|
||||||
|
): AbortabaleApolloQueryResponse<GetMangaQuery> {
|
||||||
|
return this.doRequest(GQLMethod.QUERY, GET_MANGA, { id: Number(mangaId) }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMangaToMigrate(
|
||||||
|
mangaId: number | string,
|
||||||
|
{
|
||||||
|
migrateChapters = false,
|
||||||
|
migrateCategories = false,
|
||||||
|
apolloOptions: options,
|
||||||
|
}: {
|
||||||
|
migrateChapters?: boolean;
|
||||||
|
migrateCategories?: boolean;
|
||||||
|
apolloOptions?: QueryOptions<GetMangaToMigrateQueryVariables, GetMangaToMigrateQuery>;
|
||||||
|
} = {},
|
||||||
|
): AbortabaleApolloQueryResponse<GetMangaToMigrateQuery> {
|
||||||
|
return this.doRequest(
|
||||||
|
GQLMethod.QUERY,
|
||||||
|
GET_MANGA_TO_MIGRATE,
|
||||||
|
{ id: Number(mangaId), migrateChapters, migrateCategories },
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public getMangaFetch(
|
public getMangaFetch(
|
||||||
mangaId: number | string,
|
mangaId: number | string,
|
||||||
options?: MutationOptions<GetMangaFetchMutation, GetMangaFetchMutationVariables>,
|
options?: MutationOptions<GetMangaFetchMutation, GetMangaFetchMutationVariables>,
|
||||||
@@ -1346,6 +1387,33 @@ export class RequestManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMangaToMigrateToFetch(
|
||||||
|
mangaId: number | string,
|
||||||
|
{
|
||||||
|
migrateChapters = false,
|
||||||
|
migrateCategories = false,
|
||||||
|
apolloOptions: options,
|
||||||
|
}: {
|
||||||
|
migrateChapters?: boolean;
|
||||||
|
migrateCategories?: boolean;
|
||||||
|
apolloOptions?: MutationOptions<
|
||||||
|
GetMangaToMigrateToFetchMutation,
|
||||||
|
GetMangaToMigrateToFetchMutationVariables
|
||||||
|
>;
|
||||||
|
} = {},
|
||||||
|
): AbortableApolloMutationResponse<GetMangaToMigrateToFetchMutation> {
|
||||||
|
return this.doRequest<GetMangaToMigrateToFetchMutation, GetMangaToMigrateToFetchMutationVariables>(
|
||||||
|
GQLMethod.MUTATION,
|
||||||
|
GET_MANGA_TO_MIGRATE_TO_FETCH,
|
||||||
|
{
|
||||||
|
id: Number(mangaId),
|
||||||
|
migrateChapters,
|
||||||
|
migrateCategories,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public useGetMangas(
|
public useGetMangas(
|
||||||
variables: GetMangasQueryVariables,
|
variables: GetMangasQueryVariables,
|
||||||
options?: QueryHookOptions<GetMangasQuery, GetMangasQueryVariables>,
|
options?: QueryHookOptions<GetMangasQuery, GetMangasQueryVariables>,
|
||||||
@@ -1360,6 +1428,13 @@ export class RequestManager {
|
|||||||
return this.doRequest(GQLMethod.QUERY, GET_MANGAS, variables, options);
|
return this.doRequest(GQLMethod.QUERY, GET_MANGAS, variables, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public useGetMigratableSourceMangas(
|
||||||
|
sourceId: string,
|
||||||
|
options?: QueryHookOptions<GetMigratableSourceMangasQuery, GetMigratableSourceMangasQueryVariables>,
|
||||||
|
): AbortableApolloUseQueryResponse<GetMigratableSourceMangasQuery, GetMigratableSourceMangasQueryVariables> {
|
||||||
|
return this.doRequest(GQLMethod.USE_QUERY, GET_MIGRATABLE_SOURCE_MANGAS, { sourceId }, options);
|
||||||
|
}
|
||||||
|
|
||||||
public getMangaThumbnailUrl(mangaId: number): string {
|
public getMangaThumbnailUrl(mangaId: number): string {
|
||||||
return this.getValidImgUrlFor(`manga/${mangaId}/thumbnail`);
|
return this.getValidImgUrlFor(`manga/${mangaId}/thumbnail`);
|
||||||
}
|
}
|
||||||
@@ -2182,6 +2257,12 @@ export class RequestManager {
|
|||||||
): AbortableApolloUseQueryResponse<GetWebuiUpdateStatusQuery, GetWebuiUpdateStatusQueryVariables> {
|
): AbortableApolloUseQueryResponse<GetWebuiUpdateStatusQuery, GetWebuiUpdateStatusQueryVariables> {
|
||||||
return this.doRequest(GQLMethod.USE_QUERY, GET_WEBUI_UPDATE_STATUS, undefined, options);
|
return this.doRequest(GQLMethod.USE_QUERY, GET_WEBUI_UPDATE_STATUS, undefined, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public useGetMigratableSources(
|
||||||
|
options?: QueryHookOptions<GetMigratableSourcesQuery, GetMigratableSourcesQueryVariables>,
|
||||||
|
): AbortableApolloUseQueryResponse<GetMigratableSourcesQuery, GetMigratableSourcesQueryVariables> {
|
||||||
|
return this.doRequest(GQLMethod.USE_QUERY, GET_MIGRATABLE_SOURCES, undefined, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const requestManager = new RequestManager();
|
export const requestManager = new RequestManager();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Sources } from '@/screens/Sources';
|
import { Sources } from '@/screens/Sources';
|
||||||
@@ -14,17 +14,25 @@ import { Extensions } from '@/screens/Extensions';
|
|||||||
import { TabPanel } from '@/components/tabs/TabPanel.tsx';
|
import { TabPanel } from '@/components/tabs/TabPanel.tsx';
|
||||||
import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx';
|
import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx';
|
||||||
import { TabsMenu } from '@/components/tabs/TabsMenu.tsx';
|
import { TabsMenu } from '@/components/tabs/TabsMenu.tsx';
|
||||||
|
import { Migration } from '@/screens/Migration.tsx';
|
||||||
|
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||||
|
|
||||||
export function Browse() {
|
export function Browse() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { setTitle } = useContext(NavBarContext);
|
||||||
|
|
||||||
const [tabNum, setTabNum] = useState<number>(0);
|
const [tabNum, setTabNum] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(t('global.label.browse'));
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsWrapper>
|
<TabsWrapper>
|
||||||
<TabsMenu value={tabNum} tabsCount={2} onChange={(e, newTab) => setTabNum(newTab)}>
|
<TabsMenu value={tabNum} tabsCount={2} onChange={(e, newTab) => setTabNum(newTab)}>
|
||||||
<Tab sx={{ textTransform: 'none' }} label={t('source.title')} />
|
<Tab sx={{ textTransform: 'none' }} label={t('source.title')} />
|
||||||
<Tab sx={{ textTransform: 'none' }} label={t('extension.title')} />
|
<Tab sx={{ textTransform: 'none' }} label={t('extension.title')} />
|
||||||
|
<Tab sx={{ textTransform: 'none' }} label={t('migrate.title')} />
|
||||||
</TabsMenu>
|
</TabsMenu>
|
||||||
<TabPanel index={0} currentIndex={tabNum}>
|
<TabPanel index={0} currentIndex={tabNum}>
|
||||||
<Sources />
|
<Sources />
|
||||||
@@ -32,6 +40,9 @@ export function Browse() {
|
|||||||
<TabPanel index={1} currentIndex={tabNum}>
|
<TabPanel index={1} currentIndex={tabNum}>
|
||||||
<Extensions />
|
<Extensions />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel index={2} currentIndex={tabNum}>
|
||||||
|
<Migration />
|
||||||
|
</TabPanel>
|
||||||
</TabsWrapper>
|
</TabsWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ export const DownloadQueue: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, status, isQueueEmpty]);
|
}, [t, status, isQueueEmpty]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ function getExtensionsInfo(extensions: PartialExtension[]): {
|
|||||||
|
|
||||||
export function Extensions() {
|
export function Extensions() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { setAction } = useContext(NavBarContext);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
@@ -104,7 +105,6 @@ export function Extensions() {
|
|||||||
const areMultipleReposInUse = (serverSettingsData?.settings.extensionRepos.length ?? 0) > 1;
|
const areMultipleReposInUse = (serverSettingsData?.settings.extensionRepos.length ?? 0) > 1;
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const { setTitle, setAction } = useContext(NavBarContext);
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
@@ -169,7 +169,6 @@ export function Extensions() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('extension.title'));
|
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch />
|
<AppbarSearch />
|
||||||
@@ -182,6 +181,10 @@ export function Extensions() {
|
|||||||
<LangSelect shownLangs={shownLangs} setShownLangs={setShownLangs} allLangs={allLangs} />
|
<LangSelect shownLangs={shownLangs} setShownLangs={setShownLangs} allLangs={allLangs} />
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, shownLangs, allLangs]);
|
}, [t, shownLangs, allLangs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ export const Manga: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(manga?.title ?? t('manga.title'));
|
setTitle(manga?.title ?? t('manga.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, manga?.title]);
|
}, [t, manga?.title]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,6 +91,10 @@ export const Manga: React.FC = () => {
|
|||||||
{manga && <MangaToolbarMenu manga={manga} onRefresh={refresh} refreshing={refreshing} />}
|
{manga && <MangaToolbarMenu manga={manga} onRefresh={refresh} refreshing={refreshing} />}
|
||||||
</Stack>,
|
</Stack>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, error, isValidating, refreshing, manga, refresh]);
|
}, [t, error, isValidating, refreshing, manga, refresh]);
|
||||||
|
|
||||||
if (error && !manga) {
|
if (error && !manga) {
|
||||||
|
|||||||
110
src/screens/Migrate.tsx
Normal file
110
src/screens/Migrate.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useContext, useEffect, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { TMigratableSource } from '@/components/MigrationCard.tsx';
|
||||||
|
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||||
|
import { EmptyView } from '@/components/util/EmptyView.tsx';
|
||||||
|
import { MangaGrid } from '@/components/MangaGrid.tsx';
|
||||||
|
import { TPartialManga } from '@/typings.ts';
|
||||||
|
import { GridLayouts } from '@/components/source/GridLayouts.tsx';
|
||||||
|
import { useLocalStorage } from '@/util/useLocalStorage.tsx';
|
||||||
|
import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx';
|
||||||
|
|
||||||
|
export const Migrate = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setTitle, setAction } = useContext(NavBarContext);
|
||||||
|
|
||||||
|
const { sourceId: paramSourceId } = useParams<{ sourceId: string }>();
|
||||||
|
|
||||||
|
const [gridLayout, setGridLayout] = useLocalStorage('migrateGridLayout', GridLayout.List);
|
||||||
|
|
||||||
|
const fragmentSource = requestManager.graphQLClient.client.cache.readFragment<
|
||||||
|
Pick<TMigratableSource, 'id' | 'name'>
|
||||||
|
>({
|
||||||
|
id: requestManager.graphQLClient.client.cache.identify({ __typename: 'SourceType', id: paramSourceId }),
|
||||||
|
fragment: gql`
|
||||||
|
fragment MigratableSource on SourceType {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isKnownSource, setIsKnownSource] = useState(fragmentSource !== null ? true : undefined);
|
||||||
|
const {
|
||||||
|
data: migratableSourceData,
|
||||||
|
loading: isSourceLoading,
|
||||||
|
error: sourceError,
|
||||||
|
} = requestManager.useGetSource(paramSourceId, { skip: !!isKnownSource });
|
||||||
|
|
||||||
|
const { sourceId, name } = {
|
||||||
|
sourceId: paramSourceId,
|
||||||
|
name: paramSourceId,
|
||||||
|
...fragmentSource,
|
||||||
|
...migratableSourceData?.source,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: migratableSourceMangasData,
|
||||||
|
loading: areMangasLoading,
|
||||||
|
error: mangasError,
|
||||||
|
} = requestManager.useGetMigratableSourceMangas(sourceId, {
|
||||||
|
skip: !isKnownSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(name ?? sourceId ?? t('migrate.title'));
|
||||||
|
setAction(<GridLayouts gridLayout={gridLayout} onChange={setGridLayout} />);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
|
}, [t, name, sourceId, gridLayout]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSourceLoading || isKnownSource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsKnownSource(
|
||||||
|
!!migratableSourceData ||
|
||||||
|
!!sourceError?.message.includes("The field at path '/source' was declared as a non null type"),
|
||||||
|
);
|
||||||
|
}, [isSourceLoading, sourceError]);
|
||||||
|
|
||||||
|
const isLoadingSource = isSourceLoading || (!sourceError && !isKnownSource);
|
||||||
|
const isLoading = isLoadingSource || areMangasLoading;
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingPlaceholder />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasErrorSource = sourceError && isKnownSource === false;
|
||||||
|
const hasError = hasErrorSource || mangasError;
|
||||||
|
if (hasError) {
|
||||||
|
const error = (hasErrorSource ? sourceError : mangasError)!;
|
||||||
|
return <EmptyView message={t('global.error.label.failed_to_load_data')} messageExtra={error.message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MangaGrid
|
||||||
|
hasNextPage={false}
|
||||||
|
loadMore={() => {}}
|
||||||
|
isLoading={areMangasLoading}
|
||||||
|
mangas={(migratableSourceMangasData?.mangas.nodes ?? []) as TPartialManga[]}
|
||||||
|
gridLayout={gridLayout}
|
||||||
|
mode="migrate.search"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
src/screens/Migration.tsx
Normal file
69
src/screens/Migration.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useTranslation } from 'react-i18next';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||||
|
import { EmptyView } from '@/components/util/EmptyView.tsx';
|
||||||
|
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { MigrationCard, TMigratableSource } from '@/components/MigrationCard.tsx';
|
||||||
|
import { StyledGroupItemWrapper } from '@/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||||
|
|
||||||
|
type TMigratableSourcesResult = GetMigratableSourcesQuery['mangas']['nodes'];
|
||||||
|
type TMigratableSources = Record<string, TMigratableSource>;
|
||||||
|
|
||||||
|
const getMigratableSources = (mangas?: TMigratableSourcesResult): TMigratableSources => {
|
||||||
|
if (!mangas) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueSources: TMigratableSources = {};
|
||||||
|
|
||||||
|
mangas.forEach(({ sourceId, source }) => {
|
||||||
|
const uniqueSource = uniqueSources[sourceId] ?? {
|
||||||
|
...{ id: sourceId, name: sourceId, lang: 'unknown', iconUrl: null, mangaCount: 0, ...source },
|
||||||
|
};
|
||||||
|
|
||||||
|
uniqueSources[sourceId] = {
|
||||||
|
...uniqueSource,
|
||||||
|
mangaCount: uniqueSource.mangaCount + 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueSources;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Migration = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data, loading, error } = requestManager.useGetMigratableSources();
|
||||||
|
const migratableSources = useMemo(() => getMigratableSources(data?.mangas.nodes), [data?.mangas.nodes]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingPlaceholder />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <EmptyView message={t('global.error.label.failed_to_load_data')} messageExtra={error.message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{Object.values(migratableSources).map((migratableSource, index) => (
|
||||||
|
<StyledGroupItemWrapper
|
||||||
|
key={migratableSource.id}
|
||||||
|
isLastItem={index === Object.values(migratableSources).length - 1}
|
||||||
|
>
|
||||||
|
<MigrationCard {...migratableSource} />
|
||||||
|
</StyledGroupItemWrapper>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { Card, CardActionArea, Typography } from '@mui/material';
|
import { Card, CardActionArea, Typography } from '@mui/material';
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { StringParam, useQueryParam } from 'use-query-params';
|
import { StringParam, useQueryParam } from 'use-query-params';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ISource } from '@/typings';
|
import { ISource } from '@/typings';
|
||||||
@@ -21,6 +21,7 @@ import { LangSelect } from '@/components/navbar/action/LangSelect';
|
|||||||
import { MangaGrid } from '@/components/MangaGrid';
|
import { MangaGrid } from '@/components/MangaGrid';
|
||||||
import { useDebounce } from '@/util/useDebounce.ts';
|
import { useDebounce } from '@/util/useDebounce.ts';
|
||||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
|
||||||
|
import { MangaCardProps } from '@/components/MangaCard.tsx';
|
||||||
|
|
||||||
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
|
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
|
||||||
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
|
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
|
||||||
@@ -84,6 +85,7 @@ const SourceSearchPreview = React.memo(
|
|||||||
onSearchRequestFinished,
|
onSearchRequestFinished,
|
||||||
searchString,
|
searchString,
|
||||||
emptyQuery,
|
emptyQuery,
|
||||||
|
mode,
|
||||||
}: {
|
}: {
|
||||||
source: ISource;
|
source: ISource;
|
||||||
onSearchRequestFinished: (
|
onSearchRequestFinished: (
|
||||||
@@ -94,7 +96,7 @@ const SourceSearchPreview = React.memo(
|
|||||||
) => void;
|
) => void;
|
||||||
searchString: string | null | undefined;
|
searchString: string | null | undefined;
|
||||||
emptyQuery: boolean;
|
emptyQuery: boolean;
|
||||||
}) => {
|
} & Pick<MangaCardProps, 'mode'>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { id, displayName, lang } = source;
|
const { id, displayName, lang } = source;
|
||||||
@@ -150,6 +152,7 @@ const SourceSearchPreview = React.memo(
|
|||||||
noFaces
|
noFaces
|
||||||
message={errorMessage}
|
message={errorMessage}
|
||||||
inLibraryIndicator
|
inLibraryIndicator
|
||||||
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -163,6 +166,11 @@ export const SearchAll: React.FC = () => {
|
|||||||
|
|
||||||
useSetDefaultBackTo('sources');
|
useSetDefaultBackTo('sources');
|
||||||
|
|
||||||
|
const { pathname, state } = useLocation<{ mangaTitle?: string }>();
|
||||||
|
const isMigrateMode = pathname.startsWith('/migrate/source');
|
||||||
|
|
||||||
|
const mangaTitle = state?.mangaTitle;
|
||||||
|
|
||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
|
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
|
||||||
|
|
||||||
@@ -203,7 +211,7 @@ export const SearchAll: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('search.title.global_search'));
|
setTitle(t(isMigrateMode ? 'migrate.search.title' : 'search.title.global_search', { title: mangaTitle }));
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<AppbarSearch autoOpen />
|
<AppbarSearch autoOpen />
|
||||||
@@ -215,6 +223,11 @@ export const SearchAll: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, shownLangs, setShownLangs, sources]);
|
}, [t, shownLangs, setShownLangs, sources]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -234,6 +247,7 @@ export const SearchAll: React.FC = () => {
|
|||||||
onSearchRequestFinished={updateSourceLoadingState}
|
onSearchRequestFinished={updateSourceLoadingState}
|
||||||
searchString={searchString}
|
searchString={searchString}
|
||||||
emptyQuery={!query}
|
emptyQuery={!query}
|
||||||
|
mode={isMigrateMode ? 'migrate.select' : undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export function Settings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('settings.title'));
|
setTitle(t('settings.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||||
@@ -68,7 +73,7 @@ export function Settings() {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ListAltIcon />
|
<ListAltIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={t('category.title.categories')} />
|
<ListItemText primary={t('category.title.category_other')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to="/settings/defaultReaderSettings">
|
<ListItemLink to="/settings/defaultReaderSettings">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export function SourceConfigure() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('source.configuration.title'));
|
setTitle(t('source.configuration.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { sourceId } = useParams<{ sourceId: string }>();
|
const { sourceId } = useParams<{ sourceId: string }>();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '@/lib/requests/RequestManager.ts';
|
} from '@/lib/requests/RequestManager.ts';
|
||||||
import { useDebounce } from '@/util/useDebounce.ts';
|
import { useDebounce } from '@/util/useDebounce.ts';
|
||||||
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||||
import { SourceGridLayout } from '@/components/source/GridLayouts';
|
import { SourceGridLayout } from '@/components/source/SourceGridLayout';
|
||||||
import { AppbarSearch } from '@/components/util/AppbarSearch';
|
import { AppbarSearch } from '@/components/util/AppbarSearch';
|
||||||
import { SourceOptions } from '@/components/source/SourceOptions';
|
import { SourceOptions } from '@/components/source/SourceOptions';
|
||||||
import { SourceMangaGrid } from '@/components/source/SourceMangaGrid';
|
import { SourceMangaGrid } from '@/components/source/SourceMangaGrid';
|
||||||
@@ -366,6 +366,11 @@ export function SourceMangas() {
|
|||||||
)}
|
)}
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, source]);
|
}, [t, source]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function groupByLang(sources: ISource[]) {
|
|||||||
|
|
||||||
export function Sources() {
|
export function Sources() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setTitle, setAction } = useContext(NavBarContext);
|
const { setAction } = useContext(NavBarContext);
|
||||||
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
@@ -84,7 +84,6 @@ export function Sources() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('source.title'));
|
|
||||||
setAction(
|
setAction(
|
||||||
<>
|
<>
|
||||||
<Tooltip title={t('search.title.global_search')}>
|
<Tooltip title={t('search.title.global_search')}>
|
||||||
@@ -100,6 +99,10 @@ export function Sources() {
|
|||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, shownLangs, sources]);
|
}, [t, shownLangs, sources]);
|
||||||
|
|
||||||
if (isLoading) return <LoadingPlaceholder />;
|
if (isLoading) return <LoadingPlaceholder />;
|
||||||
|
|||||||
@@ -104,8 +104,12 @@ export const Updates: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('updates.title'));
|
setTitle(t('updates.title'));
|
||||||
|
|
||||||
setAction(<UpdateChecker />);
|
setAction(<UpdateChecker />);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t, lastUpdateTimestamp]);
|
}, [t, lastUpdateTimestamp]);
|
||||||
|
|
||||||
const downloadForChapter = (chapter: TChapter) => {
|
const downloadForChapter = (chapter: TChapter) => {
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ export function About() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('settings.about.title'));
|
setTitle(t('settings.about.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useSetDefaultBackTo('settings');
|
useSetDefaultBackTo('settings');
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ export function Backup() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('settings.backup.title'));
|
setTitle(t('settings.backup.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useSetDefaultBackTo('settings');
|
useSetDefaultBackTo('settings');
|
||||||
|
|||||||
@@ -48,8 +48,13 @@ export function Categories() {
|
|||||||
|
|
||||||
const { setTitle, setAction } = useContext(NavBarContext);
|
const { setTitle, setAction } = useContext(NavBarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('category.title.categories'));
|
setTitle(t('category.title.category_other'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { data } = requestManager.useGetCategories();
|
const { data } = requestManager.useGetCategories();
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export function DefaultReaderSettings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('reader.settings.title.default_reader_settings'));
|
setTitle(t('reader.settings.title.default_reader_settings'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { metadata, settings, loading } = useDefaultReaderSettings();
|
const { metadata, settings, loading } = useDefaultReaderSettings();
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export const DownloadSettings = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('download.settings.title'));
|
setTitle(t('download.settings.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { data } = requestManager.useGetServerSettings();
|
const { data } = requestManager.useGetServerSettings();
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ export function LibrarySettings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('library.settings.title'));
|
setTitle(t('library.settings.title'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useSetDefaultBackTo('settings');
|
useSetDefaultBackTo('settings');
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ export const ServerSettings = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('settings.server.title.settings'));
|
setTitle(t('settings.server.title.settings'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { data } = requestManager.useGetServerSettings();
|
const { data } = requestManager.useGetServerSettings();
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ export const WebUISettings = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(t('settings.webui.title.settings'));
|
setTitle(t('settings.webui.title.settings'));
|
||||||
setAction(null);
|
setAction(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setTitle('');
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const { data } = requestManager.useGetServerSettings();
|
const { data } = requestManager.useGetServerSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user