Skip to content

Commit

Permalink
Feature/manga migration (#536)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
schroda authored Jan 26, 2024
1 parent 5224ad1 commit e0c5e05
Show file tree
Hide file tree
Showing 39 changed files with 1,310 additions and 346 deletions.
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { ServerUpdateChecker } from '@/components/settings/ServerUpdateChecker.t
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ExtensionSettings } from '@/screens/settings/ExtensionSettings.tsx';
import { WebUISettings } from '@/screens/settings/WebUISettings.tsx';
import { Migrate } from '@/screens/Migrate.tsx';

if (process.env.NODE_ENV !== 'production') {
// Adds messages only in a dev environment
Expand Down Expand Up @@ -120,6 +121,10 @@ export const App: React.FC = () => (
<Route path="updates" element={<Updates />} />
<Route path="extensions" element={<Extensions />} />
<Route path="browse" element={<Browse />} />
<Route path="migrate/source/:sourceId">
<Route index element={<Migrate />} />
<Route path="manga/:mangaId/search" element={<SearchAll />} />
</Route>
</Routes>
</Container>
<Routes>
Expand Down
572 changes: 308 additions & 264 deletions src/components/MangaCard.tsx

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions src/components/MangaGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
import { useLocation, useNavigate } from 'react-router-dom';
import { EmptyView } from '@/components/util/EmptyView';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
import { MangaCard } from '@/components/MangaCard';
import { MangaCard, MangaCardProps } from '@/components/MangaCard';
import { GridLayout } from '@/components/context/LibraryOptionsContext';
import { useLocalStorage } from '@/util/useLocalStorage';
import { TManga, TPartialManga } from '@/typings.ts';
Expand Down Expand Up @@ -49,6 +49,7 @@ const createMangaCard = (
isSelectModeActive: boolean = false,
selectedMangaIds?: TManga['id'][],
handleSelection?: DefaultGridProps['handleSelection'],
mode?: MangaCardProps['mode'],
) => (
<MangaCard
key={manga.id}
Expand All @@ -57,10 +58,11 @@ const createMangaCard = (
inLibraryIndicator={inLibraryIndicator}
selected={isSelectModeActive ? selectedMangaIds?.includes(manga.id) : null}
handleSelection={handleSelection}
mode={mode}
/>
);

type DefaultGridProps = {
type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
isLoading: boolean;
mangas: TPartialManga[];
inLibraryIndicator?: boolean;
Expand All @@ -80,6 +82,7 @@ const HorizontalGrid = ({
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps) => (
<Grid
container
Expand All @@ -105,6 +108,7 @@ const HorizontalGrid = ({
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)}
</GridItemContainer>
))
Expand All @@ -123,6 +127,7 @@ const VerticalGrid = ({
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps & {
hasNextPage: boolean;
loadMore: () => void;
Expand Down Expand Up @@ -171,6 +176,7 @@ const VerticalGrid = ({
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)
}
/>
Expand Down Expand Up @@ -212,6 +218,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
} = props;

const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth);
Expand Down Expand Up @@ -287,6 +294,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
isSelectModeActive={isSelectModeActive}
selectedMangaIds={selectedMangaIds}
handleSelection={handleSelection}
mode={mode}
/>
) : (
<VerticalGrid
Expand All @@ -300,6 +308,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
isSelectModeActive={isSelectModeActive}
selectedMangaIds={selectedMangaIds}
handleSelection={handleSelection}
mode={mode}
/>
)}
</div>
Expand Down
97 changes: 97 additions & 0 deletions src/components/MigrateDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 60 additions & 0 deletions src/components/MigrationCard.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
23 changes: 22 additions & 1 deletion src/components/manga/MangaActionMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { useTranslation } from 'react-i18next';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import Label from '@mui/icons-material/Label';
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 { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
Expand All @@ -28,7 +30,7 @@ const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as
type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void };

export type SingleModeProps = {
manga: Pick<TManga, 'id'> & MangaDownloadInfo & MangaUnreadInfo;
manga: Pick<TManga, 'id' | 'title' | 'source'> & MangaDownloadInfo & MangaUnreadInfo;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
};

Expand All @@ -49,6 +51,8 @@ export const MangaActionMenuItems = ({
}: Props) => {
const { t } = useTranslation();

const navigate = useNavigate();

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

const isSingleMode = !!manga;
Expand Down Expand Up @@ -129,6 +133,23 @@ export const MangaActionMenuItems = ({
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
onClick={() => {
setIsCategorySelectOpen(true);
Expand Down
14 changes: 14 additions & 0 deletions src/components/manga/MangaToolbarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
} from '@mui/material';
import React, { useState } from 'react';
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 { TManga } from '@/typings.ts';

Expand All @@ -32,6 +34,7 @@ interface IProps {

export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
const { t } = useTranslation();

const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));

Expand All @@ -57,6 +60,17 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<Refresh />
</IconButton>
</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 && (
<Tooltip title={t('manga.label.edit_categories')}>
<IconButton
Expand Down
4 changes: 2 additions & 2 deletions src/components/settings/CategoriesInclusionSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('category.title.categories')}
primary={t('category.title.category_other')}
secondary={
<>
<span>
Expand All @@ -179,7 +179,7 @@ export const CategoriesInclusionSetting = (props: CategoriesInclusionSettingProp

<Dialog open={isDialogOpen} onClose={closeDialog}>
<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>}
<CheckboxContainer>
{dialogCategories.map((category) => (
Expand Down
Loading

0 comments on commit e0c5e05

Please sign in to comment.