diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 510473f1..64e3f19a 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -159,12 +159,20 @@ "!@mui/material/", "!@mui/icons-material/", "!@mui/x-date-pickers/" - ] + ], }, { "group": [ "@mui/*/*/*" ] + }, + { + "group": [ + "lodash", + "lodash/*", + "!lodash/fp/*" + ], + "message": "Use lodash-fp instead." } ] } @@ -411,6 +419,7 @@ "src/features/authentication/AuthManager.ts", "src/features/chapter/services/Chapters.ts", "src/features/metadata/services/MetadataMigrations.ts", + "src/features/migration/MigrationManager.ts", "src/features/reader/services/ReaderControls.ts", "src/features/reader/services/ReaderService.ts", "src/features/source/services/Sources.ts", diff --git a/CHANGELOG.md b/CHANGELOG.md index c61b43b4..ecfd1ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - (**WebUI Update**) Add an option to (partially) disable showing information when the webUI got updated - (**Server Update**) Add an option to disable showing information when the server got updated - (**Manga**) Add an option to include client data during migration +- (**Migrate**) Add bulk migration ### Changed diff --git a/package.json b/package.json index 463e70f0..5985b4e9 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "immer": "11.1.4", "jsonrepair": "3.13.2", "koration": "1.0.0", + "lodash": "4.18.1", "material-ui-popup-state": "5.3.6", "mui-nested-menu": "4.0.3", "node-vibrant": "4.0.4", @@ -96,6 +97,7 @@ "@lingui/swc-plugin": "5.11.0", "@lingui/vite-plugin": "5.9.2", "@tony.ganchev/eslint-plugin-header": "3.3.4", + "@types/lodash": "4.17.24", "@types/node": "24.10.1", "@types/react": "19.2.14", "@types/react-beautiful-dnd": "13.1.8", diff --git a/src/App.tsx b/src/App.tsx index 98dda23c..eb2241f5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,12 +32,17 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; import { ImageProcessingType } from '@/features/settings/Settings.types.ts'; +import { MigrationFABIndicator } from '@/features/migration/components/MigrationFABIndicator.tsx'; const { Browse } = loadable(() => import('@/features/browse/screens/Browse.tsx'), lazyLoadFallback); const { DownloadQueue } = loadable(() => import('@/features/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback); const { Library } = loadable(() => import('@/features/library/screens/Library.tsx'), lazyLoadFallback); const { Manga } = loadable(() => import('@/features/manga/screens/Manga.tsx'), lazyLoadFallback); const { SearchAll } = loadable(() => import('@/features/global-search/screens/SearchAll.tsx'), lazyLoadFallback); +const { MigrationManualSearch } = loadable( + () => import('@/features/migration/screens/MigrationManualSearch.tsx'), + lazyLoadFallback, +); const { Settings } = loadable(() => import('@/features/settings/screens/Settings.tsx'), lazyLoadFallback); const { About } = loadable(() => import('@/features/settings/screens/About.tsx'), lazyLoadFallback); const { Backup } = loadable(() => import('@/features/backup/screens/Backup.tsx'), lazyLoadFallback); @@ -69,7 +74,7 @@ const { ImageProcessingSetting } = loadable( const { ServerSettings } = loadable(() => import('@/features/settings/screens/ServerSettings.tsx'), lazyLoadFallback); const { BrowseSettings } = loadable(() => import('@/features/browse/screens/BrowseSettings.tsx'), lazyLoadFallback); const { WebUISettings } = loadable(() => import('@/features/settings/screens/WebUISettings.tsx'), lazyLoadFallback); -const { Migrate } = loadable(() => import('@/features/migration/screens/Migrate.tsx'), lazyLoadFallback); +const { Migration } = loadable(() => import('@/features/migration/screens/Migration.tsx'), lazyLoadFallback); const { DeviceSetting } = loadable(() => import('@/features/device/screens/DeviceSetting.tsx'), lazyLoadFallback); const { TrackingSettings } = loadable( () => import('@/features/tracker/screens/TrackingSettings.tsx'), @@ -293,9 +298,17 @@ const MainApp = () => { } /> {!hideHistory && } />} } /> + } /> - } /> - } /> + } /> + } + /> + } + /> } /> @@ -339,6 +352,7 @@ export const App: React.FC = () => ( } /> + ); diff --git a/src/UtilTypes.d.ts b/src/UtilTypes.d.ts index 993afeb4..9e293c99 100644 --- a/src/UtilTypes.d.ts +++ b/src/UtilTypes.d.ts @@ -24,8 +24,12 @@ type NullAndUndefined = T | null | undefined; type NonNullableProperties = { [P in keyof T]-?: NonNullable }; +type NonNullableProperty = Omit & NonNullableProperties>; + type OptionalProperty = Omit & Partial>; +type RequiredProperty = Omit & Required>; + type PropertiesNever = { [key in keyof T]?: never }; type OmitFirst = T extends [any, ...infer R] ? R : never; diff --git a/src/base/AppRoute.constants.ts b/src/base/AppRoute.constants.ts index b650cf8a..71289605 100644 --- a/src/base/AppRoute.constants.ts +++ b/src/base/AppRoute.constants.ts @@ -199,15 +199,19 @@ export const AppRoutes = { }), }, migrate: { - match: 'migrate/source/:sourceId', - path: (sourceId: SourceType['id']) => `/migrate/source/${sourceId}`, - + match: 'migrate/*', + path: '/migrate', childRoutes: { - search: { - match: 'manga/:mangaId/search', + singleMangaSearch: { + match: 'source/:sourceId/manga/:mangaId/search', path: (sourceId: SourceType['id'], mangaId: MangaIdInfo['id'], query?: string | null | undefined) => UrlUtil.addQueryParam(`/migrate/source/${sourceId}/manga/${mangaId}/search`, query), }, + manualSearch: { + match: 'manual-search/:mangaId', + path: (mangaId: MangaIdInfo['id'], query?: string | null | undefined) => + UrlUtil.addQueryParam(`/migrate/manual-search/${mangaId}`, query), + }, }, }, tracker: { diff --git a/src/base/collection/components/SelectableCollectionSelectMode.tsx b/src/base/collection/components/SelectableCollectionSelectMode.tsx index 06a46a2b..29a56770 100644 --- a/src/base/collection/components/SelectableCollectionSelectMode.tsx +++ b/src/base/collection/components/SelectableCollectionSelectMode.tsx @@ -18,12 +18,14 @@ export const SelectableCollectionSelectMode = ({ areNoItemsSelected, onSelectAll, onModeChange, + isCancelable = true, }: { isActive: boolean; areAllItemsSelected: boolean; areNoItemsSelected: boolean; onSelectAll: (selectAll: boolean) => void; onModeChange: (checked: boolean) => void; + isCancelable?: boolean; }) => { const { t } = useLingui(); @@ -36,20 +38,22 @@ export const SelectableCollectionSelectMode = ({ onChange={onSelectAll} /> )} - - } - sx={{ - padding: '8px', - color: 'inherit', - '&.Mui-checked': { + {isCancelable && ( + + } + sx={{ + padding: '8px', color: 'inherit', - }, - }} - checked={isActive} - onChange={(_, checked) => onModeChange(checked)} - /> - + '&.Mui-checked': { + color: 'inherit', + }, + }} + checked={isActive} + onChange={(_, checked) => onModeChange(checked)} + /> + + )} ); }; diff --git a/src/base/components/feedback/LoadingPlaceholder.tsx b/src/base/components/feedback/LoadingPlaceholder.tsx index 6fb40232..725d0b7b 100644 --- a/src/base/components/feedback/LoadingPlaceholder.tsx +++ b/src/base/components/feedback/LoadingPlaceholder.tsx @@ -10,17 +10,21 @@ import React, { type JSX } from 'react'; import CircularProgress from '@mui/material/CircularProgress'; import Box from '@mui/material/Box'; -interface IProps { +export function LoadingPlaceholder({ + children, + shouldRender, + component, + componentProps, + usePadding, + size, +}: { shouldRender?: boolean | (() => boolean); children?: React.ReactNode; component?: string | React.FunctionComponent | React.ComponentClass; componentProps?: any; usePadding?: boolean; -} - -export function LoadingPlaceholder(props: IProps) { - const { children, shouldRender, component, componentProps, usePadding } = props; - + size?: number; +}) { let condition = true; if (shouldRender !== undefined) { condition = shouldRender instanceof Function ? shouldRender() : shouldRender; @@ -47,7 +51,7 @@ export function LoadingPlaceholder(props: IProps) { justifyContent: 'center', }} > - + ); } diff --git a/src/base/components/feedback/SnackbarWithDescription.tsx b/src/base/components/feedback/SnackbarWithDescription.tsx index 9a0f3cef..eb22680c 100644 --- a/src/base/components/feedback/SnackbarWithDescription.tsx +++ b/src/base/components/feedback/SnackbarWithDescription.tsx @@ -83,7 +83,7 @@ export const SnackbarWithDescription = memo( > {message} {actualDescription} - {isDescriptionTooLong || (isGraphqlException && graphqlStackTrace) ? ( + {(isDescriptionTooLong || (isGraphqlException && graphqlStackTrace)) && ( - ) : ( - '' )} diff --git a/src/features/browse/screens/Browse.tsx b/src/features/browse/screens/Browse.tsx index 3f5a85c1..c9be09d5 100644 --- a/src/features/browse/screens/Browse.tsx +++ b/src/features/browse/screens/Browse.tsx @@ -15,12 +15,12 @@ import { Extensions } from '@/features/browse/extensions/Extensions.tsx'; import { TabPanel } from '@/base/components/tabs/TabPanel.tsx'; import { TabsWrapper } from '@/base/components/tabs/TabsWrapper.tsx'; import { TabsMenu } from '@/base/components/tabs/TabsMenu.tsx'; -import { Migration } from '@/features/migration/screens/Migration.tsx'; import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx'; import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; import { BrowseTab } from '@/features/browse/Browse.types.ts'; import { GROUPED_VIRTUOSO_Z_INDEX } from '@/lib/virtuoso/Virtuoso.constants.ts'; import { SearchParam } from '@/base/Base.types.ts'; +import { Migration } from '@/features/migration/screens/Migration.tsx'; export function Browse() { const { t } = useLingui(); diff --git a/src/features/global-search/screens/SearchAll.tsx b/src/features/global-search/screens/SearchAll.tsx index 96b274bf..eafdd40c 100644 --- a/src/features/global-search/screens/SearchAll.tsx +++ b/src/features/global-search/screens/SearchAll.tsx @@ -10,7 +10,7 @@ import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; import Typography from '@mui/material/Typography'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { StringParam, useQueryParam } from 'use-query-params'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; @@ -28,7 +28,7 @@ import { AppbarSearch } from '@/base/components/AppbarSearch.tsx'; import { useDebounce } from '@/base/hooks/useDebounce.ts'; import type { MangaCardProps } from '@/features/manga/Manga.types.ts'; import { EmptyView } from '@/base/components/feedback/EmptyView.tsx'; -import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts'; +import { STABLE_EMPTY_ARRAY, STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { BaseMangaGrid } from '@/features/manga/components/BaseMangaGrid.tsx'; @@ -56,6 +56,9 @@ import { MUIUtil } from '@/lib/mui/MUI.util.ts'; import type { MetadataBrowseSettings } from '@/features/browse/Browse.types.ts'; import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx'; import { SearchParam } from '@/base/Base.types.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { assertIsDefined } from '@/base/Asserts.ts'; +import { useBackButton } from '@/base/hooks/useBackButton.ts'; type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any }; type SourceToLoadingStateMap = Map; @@ -122,12 +125,13 @@ const SourceSearchPreview = React.memo( emptyQuery, mode, shouldShowOnlySourcesWithResults, + onMigrateSelect, }: { source: SourceIdInfo & SourceDisplayNameInfo & SourceNameInfo & SourceLanguageInfo; onSearchRequestFinished: (source: SourceIdInfo, state: SourceLoadingState) => void; searchString: string | null | undefined; emptyQuery: boolean; - } & Pick & + } & Pick & Pick) => { const { t } = useLingui(); @@ -230,6 +234,7 @@ const SourceSearchPreview = React.memo( message={errorMessage} inLibraryIndicator mode={mode} + onMigrateSelect={onMigrateSelect} /> )} @@ -237,15 +242,21 @@ const SourceSearchPreview = React.memo( }, ); -export const SearchAll: React.FC = () => { +export const SearchAll = ({ + migrationDestinationSourceIds, +}: { + migrationDestinationSourceIds?: SourceIdInfo['id'][]; +}) => { const { t } = useLingui(); const navigate = useNavigate(); + const handleBack = useBackButton(); const { pathname, state } = useLocation<{ mangaTitle?: string; shouldShowOnlyPinnedSources?: boolean }>(); const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize(); const shouldShowOnlyPinnedSources = state?.shouldShowOnlyPinnedSources ?? true; - const isMigrateMode = pathname.startsWith('/migrate/source'); + const isMigrateMode = pathname.startsWith('/migrate/source') || pathname.startsWith('/migrate/manual-search'); + const { mangaId } = useParams<{ mangaId?: string }>() ?? STABLE_EMPTY_OBJECT; const [query] = useQueryParam(SearchParam.QUERY, StringParam); const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD); @@ -255,7 +266,14 @@ export const SearchAll: React.FC = () => { } = useMetadataServerSettings(); const { data, loading, error, refetch } = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true }); - const sources = data?.sources.nodes ?? STABLE_EMPTY_ARRAY; + const tmpSources = data?.sources.nodes ?? STABLE_EMPTY_ARRAY; + const sources = useMemo( + () => + tmpSources.filter( + (source) => !migrationDestinationSourceIds || migrationDestinationSourceIds.includes(source.id), + ), + [tmpSources, migrationDestinationSourceIds], + ); const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState(new Map()); const debouncedSourceToLoadingStateMap = useDebounce(sourceToLoadingStateMap, 500); @@ -398,6 +416,19 @@ export const SearchAll: React.FC = () => { emptyQuery={!query} mode={isMigrateMode ? 'migrate.select' : 'source'} shouldShowOnlySourcesWithResults={shouldShowOnlySourcesWithResults} + onMigrateSelect={ + migrationDestinationSourceIds + ? (match) => { + assertIsDefined(mangaId); + MigrationManager.selectManualMatch(Number(mangaId), { + ...match, + sourceTitle: Sources.getFromCache(match.sourceId)?.displayName, + latestChapterNumber: undefined, + }); + handleBack(); + } + : undefined + } /> ))} diff --git a/src/features/manga/Manga.types.ts b/src/features/manga/Manga.types.ts index 0d2e2e2a..abb7d10e 100644 --- a/src/features/manga/Manga.types.ts +++ b/src/features/manga/Manga.types.ts @@ -22,6 +22,7 @@ import type { import type { SingleModeProps } from '@/features/manga/components/MangaActionMenuItems.tsx'; import type { GridLayout } from '@/base/Base.types.ts'; import type { ChapterListOptions } from '@/features/chapter/Chapter.types.ts'; +import type { MigrationMatch } from '@/features/migration/Migration.types.ts'; export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate'; @@ -33,6 +34,7 @@ type MangaCardBaseProps = Pick & export type MangaIdInfo = Pick; export type MangaChapterCountInfo = { chapters: Pick }; +export type MangaHighestChapterNumberInfo = { highestNumberedChapter?: Pick }; export type MangaInLibraryInfo = Pick; export type MangaDownloadInfo = Pick & MangaChapterCountInfo; export type MangaUnreadInfo = Pick & MangaChapterCountInfo; @@ -42,7 +44,7 @@ export type MangaTrackRecordInfo = MangaIdInfo & { }; export type MangaGenreInfo = Pick; export type MangaSourceIdInfo = Pick; -export type MangaSourceNameInfo = { source?: Maybe> }; +export type MangaSourceNameInfo = { source?: Maybe> }; export type MangaSourceLngInfo = { source?: Maybe> }; export type MangaArtistInfo = Pick; export type MangaAuthorInfo = Pick; @@ -58,6 +60,7 @@ export interface MangaCardProps { inLibraryIndicator?: boolean; selected?: boolean | null; handleSelection?: SelectableCollectionReturnType['handleSelection']; + onMigrateSelect?: (manga: Omit) => void; mode?: MangaCardMode; } diff --git a/src/features/manga/components/MangaActionMenuItems.tsx b/src/features/manga/components/MangaActionMenuItems.tsx index a1aafb15..8cc7ea47 100644 --- a/src/features/manga/components/MangaActionMenuItems.tsx +++ b/src/features/manga/components/MangaActionMenuItems.tsx @@ -150,7 +150,11 @@ export const MangaActionMenuItems = ({ )} {isSingleMode && ( diff --git a/src/features/manga/components/MangaGrid.tsx b/src/features/manga/components/MangaGrid.tsx index 20771838..7f8d64a6 100644 --- a/src/features/manga/components/MangaGrid.tsx +++ b/src/features/manga/components/MangaGrid.tsx @@ -25,7 +25,6 @@ import { useLingui } from '@lingui/react/macro'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { MangaCard } from '@/features/manga/components/cards/MangaCard.tsx'; -import type { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx'; import type { MangaCardProps } from '@/features/manga/Manga.types.ts'; import type { MangaType } from '@/lib/graphql/generated/graphql.ts'; @@ -68,6 +67,7 @@ const createMangaCard = ( selectedMangaIds?: MangaType['id'][], handleSelection?: DefaultGridProps['handleSelection'], mode?: MangaCardProps['mode'], + onMigrateSelect?: DefaultGridProps['onMigrateSelect'], ) => ( ); -type DefaultGridProps = Pick & { +type DefaultGridProps = Omit & { isLoading: boolean; mangas: TManga[]; inLibraryIndicator?: boolean; @@ -87,7 +88,6 @@ type DefaultGridProps = Pick & { gridLayout?: GridLayout; isSelectModeActive?: boolean; selectedMangaIds?: Required[]; - handleSelection?: SelectableCollectionReturnType['handleSelection']; ref?: ForwardedRef; }; @@ -102,6 +102,7 @@ const HorizontalGrid = ({ handleSelection, mode, ref, + onMigrateSelect, }: DefaultGridProps) => ( )) @@ -149,6 +151,7 @@ const VerticalGrid = ({ handleSelection, mode, ref, + onMigrateSelect, }: DefaultGridProps & { hasNextPage: boolean; loadMore: () => void; @@ -175,6 +178,7 @@ const VerticalGrid = ({ selectedMangaIds, handleSelection, mode, + onMigrateSelect, ) } /> @@ -220,6 +224,7 @@ export const MangaGrid: React.FC = ({ mode, retry, gridWrapperProps, + onMigrateSelect, }) => { const { t } = useLingui(); @@ -346,6 +351,7 @@ export const MangaGrid: React.FC = ({ selectedMangaIds={selectedMangaIds} handleSelection={handleSelection} mode={mode} + onMigrateSelect={onMigrateSelect} /> ) : ( = ({ selectedMangaIds={selectedMangaIds} handleSelection={handleSelection} mode={mode} + onMigrateSelect={onMigrateSelect} /> )} diff --git a/src/features/manga/components/MangaToolbarMenu.tsx b/src/features/manga/components/MangaToolbarMenu.tsx index 08945f38..c1c85044 100644 --- a/src/features/manga/components/MangaToolbarMenu.tsx +++ b/src/features/manga/components/MangaToolbarMenu.tsx @@ -84,7 +84,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { <> { diff --git a/src/features/manga/components/cards/MangaCard.tsx b/src/features/manga/components/cards/MangaCard.tsx index 8e693dea..07f1192f 100644 --- a/src/features/manga/components/cards/MangaCard.tsx +++ b/src/features/manga/components/cards/MangaCard.tsx @@ -7,12 +7,11 @@ */ import PopupState, { bindMenu } from 'material-ui-popup-state'; -import { memo, useCallback, useMemo, useState } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useLongPress } from 'use-long-press'; import type { SingleModeProps } from '@/features/manga/components/MangaActionMenuItems.tsx'; import { MangaActionMenuItems } from '@/features/manga/components/MangaActionMenuItems.tsx'; import { Menu } from '@/base/components/menu/Menu.tsx'; -import { MigrateDialog } from '@/features/migration/components/MigrateDialog.tsx'; import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx'; import { MangaGridCard } from '@/features/manga/components/cards/MangaGridCard.tsx'; import { MangaListCard } from '@/features/manga/components/cards/MangaListCard.tsx'; @@ -22,6 +21,14 @@ import { MangaBadges } from '@/features/manga/components/MangaBadges.tsx'; import { GridLayout } from '@/base/Base.types.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; +import { makeToast } from '@/base/utils/Toast.ts'; +import { Mangas } from '@/features/manga/services/Mangas.ts'; +import { t } from '@lingui/core/macro'; +import { useParams } from 'react-router-dom'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; +import { MigrationOptionsDialog } from '@/features/migration/components/MigrationOptionsDialog.tsx'; +import { AwaitableComponent } from 'awaitable-component'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler'; const getMangaLinkTo = ( mode: MangaCardMode, @@ -35,7 +42,7 @@ const getMangaLinkTo = ( case 'duplicate': return AppRoutes.manga.path(mangaId); case 'migrate.search': - return AppRoutes.migrate.childRoutes.search.path(sourceId ?? '-1', mangaId, mangaTitle); + return AppRoutes.migrate.childRoutes.singleMangaSearch.path(sourceId ?? '-1', mangaId, mangaTitle); case 'migrate.select': return ''; default: @@ -44,8 +51,20 @@ const getMangaLinkTo = ( }; export const MangaCard = memo((props: MangaCardProps) => { - const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props; + const { + manga, + gridLayout, + inLibraryIndicator, + selected, + handleSelection, + mode = 'default', + onMigrateSelect, + } = props; const { id, firstUnreadChapter, downloadCount, unreadCount } = manga; + + const { mangaId: mangaIdAsString } = useParams<{ mangaId: string }>(); + const migrationSourceMangaId = Number(mangaIdAsString); + const { settings: { showContinueReadingButton }, } = useMetadataServerSettings(); @@ -54,8 +73,6 @@ export const MangaCard = memo((props: MangaCardProps) => { const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title); - const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false); - const handleClick = useCallback( (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => { const isDefaultMode = mode === 'default'; @@ -88,10 +105,42 @@ export const MangaCard = memo((props: MangaCardProps) => { } if (isMigrateSelectMode) { - setIsMigrateDialogOpen(true); + const isBulkMigrationManualSearch = !!onMigrateSelect; + if (isBulkMigrationManualSearch) { + onMigrateSelect(manga); + return; + } + + const migrate = () => { + const optionsDialog = AwaitableComponent.showControlled( + MigrationOptionsDialog, + { + mangaIdToMigrateTo: id, + isMigrating: false, + startMigration: async (options) => { + makeToast(t`Migrating manga…`, 'info'); + + optionsDialog.update({ isMigrating: true }); + + try { + await Mangas.migrate(migrationSourceMangaId, id, options); + + optionsDialog.submit(options); + + ReactRouter.navigate(AppRoutes.manga.path(id), { replace: true }); + } catch (e) { + optionsDialog.update({ isMigrating: false, startMigration: () => migrate() }); + } + }, + }, + { id: `manga-migration-single-manga-${migrationSourceMangaId}-${id}` }, + ); + optionsDialog.promise.catch(defaultPromiseErrorHandler('MangaCard::migrate')); + }; + migrate(); } }, - [mode, selected, updateLibraryState, handleSelection], + [mode, selected, updateLibraryState, handleSelection, migrationSourceMangaId], ); const longPressBind = useLongPress( @@ -111,54 +160,49 @@ export const MangaCard = memo((props: MangaCardProps) => { ); return ( - <> - {isMigrateDialogOpen && ( - setIsMigrateDialogOpen(false)} /> + + {(popupState) => ( + <> + + } + mangaBadges={ + + } + /> + {!!handleSelection && popupState.isOpen && ( + + {(onClose, setHideMenu) => ( + + )} + + )} + )} - - {(popupState) => ( - <> - - } - mangaBadges={ - - } - /> - {!!handleSelection && popupState.isOpen && ( - - {(onClose, setHideMenu) => ( - - )} - - )} - - )} - - + ); }); diff --git a/src/features/migration/Migration.constants.ts b/src/features/migration/Migration.constants.ts index 73ac948d..180e0b79 100644 --- a/src/features/migration/Migration.constants.ts +++ b/src/features/migration/Migration.constants.ts @@ -8,7 +8,8 @@ import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; -import { SortBy, SortOrder } from '@/features/migration/Migration.types.ts'; +import { MigrationEntryStatus, MigrationPhase, SortBy, SortOrder } from '@/features/migration/Migration.types.ts'; +import type { MigrationState } from '@/features/migration/Migration.types.ts'; export const sortByToTranslation: Record = { [SortBy.SOURCE_NAME]: msg`By source name`, @@ -24,3 +25,70 @@ export const DEFAULT_SORT_SETTINGS = { sortBy: SortBy.SOURCE_NAME, sortOrder: SortOrder.ASC, }; + +export const MIGRATION_LOCAL_STORAGE_KEY = 'migration_state'; + +export const MAX_MANGAS_IN_PARALLEL = 5; + +export const MAX_SOURCES_IN_PARALLEL = 6; + +export const DEFAULT_MIGRATION_STATE: MigrationState = { + phase: MigrationPhase.IDLE, + sourceId: null, + entries: {}, + destinationSourceIds: [], + migrateOptions: null, + searchProgress: { total: 0, completed: 0, success: 0, failed: 0 }, + migrationProgress: { total: 0, completed: 0, success: 0, failed: 0 }, + startedAt: null, + lastUpdatedAt: null, + groupExpandState: {}, +}; + +export const ENTRY_STATUS_TRANSLATION: Record = { + [MigrationEntryStatus.PENDING]: msg`Pending…`, + [MigrationEntryStatus.SEARCHING]: msg`Searching…`, + [MigrationEntryStatus.SEARCH_COMPLETE]: msg`Match found`, + [MigrationEntryStatus.SEARCH_FAILED]: msg`Search failed`, + [MigrationEntryStatus.NO_MATCH]: msg`No match found`, + [MigrationEntryStatus.MIGRATING]: msg`Migrating…`, + [MigrationEntryStatus.MIGRATION_COMPLETE]: msg`Successfully migrated`, + [MigrationEntryStatus.MIGRATION_FAILED]: msg`Migration failed`, + [MigrationEntryStatus.EXCLUDED]: msg`Excluded`, +}; + +export const MIGRATE_SEARCH_ENTRY_GROUPS = [ + MigrationEntryStatus.SEARCHING, + MigrationEntryStatus.SEARCH_FAILED, + MigrationEntryStatus.NO_MATCH, + MigrationEntryStatus.SEARCH_COMPLETE, +] as const satisfies readonly MigrationEntryStatus[]; + +export const MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE: Record< + (typeof MIGRATE_SEARCH_ENTRY_GROUPS)[number], + boolean +> = { + [MigrationEntryStatus.SEARCHING]: true, + [MigrationEntryStatus.SEARCH_FAILED]: false, + [MigrationEntryStatus.NO_MATCH]: false, + [MigrationEntryStatus.SEARCH_COMPLETE]: false, +}; + +export const MIGRATE_EXECUTE_ENTRY_GROUPS = [ + MigrationEntryStatus.MIGRATING, + MigrationEntryStatus.MIGRATION_FAILED, + MigrationEntryStatus.NO_MATCH, + MigrationEntryStatus.EXCLUDED, + MigrationEntryStatus.MIGRATION_COMPLETE, +] as const satisfies readonly MigrationEntryStatus[]; + +export const MIGRATE_EXECUTE_ENTRY_GROUP_EXPAND_DEFAULT_STATE: Record< + (typeof MIGRATE_EXECUTE_ENTRY_GROUPS)[number], + boolean +> = { + [MigrationEntryStatus.MIGRATING]: true, + [MigrationEntryStatus.MIGRATION_FAILED]: false, + [MigrationEntryStatus.NO_MATCH]: false, + [MigrationEntryStatus.EXCLUDED]: false, + [MigrationEntryStatus.MIGRATION_COMPLETE]: false, +}; diff --git a/src/features/migration/Migration.types.ts b/src/features/migration/Migration.types.ts index 4b70a0f3..95f3bbd5 100644 --- a/src/features/migration/Migration.types.ts +++ b/src/features/migration/Migration.types.ts @@ -7,6 +7,23 @@ */ import type { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts'; +import type { + SourceDisplayNameInfo, + SourceIconInfo, + SourceIdInfo, + SourceLanguageInfo, + SourceMetaInfo, + SourceNameInfo, +} from '@/features/source/Source.types.ts'; +import type { + MangaArtistInfo, + MangaAuthorInfo, + MangaIdInfo, + MangaSourceIdInfo, + MangaThumbnailInfo, + MangaTitleInfo, +} from '@/features/manga/Manga.types.ts'; +import type { ChapterNumberInfo } from '@/features/chapter/Chapter.types.ts'; export enum SortBy { SOURCE_NAME, @@ -40,3 +57,69 @@ export type MetadataMigrationSettings = { migrateMetadata: boolean; migrateSortSettings: SortSettings; }; + +export enum MigrationPhase { + IDLE = 'idle', + SELECT_SOURCE = 'select_source', + SELECT_MANGAS = 'select_mangas', + SELECTING_SOURCES = 'selecting_sources', + SEARCHING = 'searching', + MIGRATING = 'migrating', +} + +export enum MigrationEntryStatus { + PENDING = 'pending', + SEARCHING = 'searching', + SEARCH_COMPLETE = 'search_complete', + SEARCH_FAILED = 'search_failed', + NO_MATCH = 'no_match', + MIGRATING = 'migrating', + MIGRATION_COMPLETE = 'migration_complete', + MIGRATION_FAILED = 'migration_failed', + EXCLUDED = 'excluded', +} + +export interface MigrationMatch + extends MangaIdInfo, MangaTitleInfo, MangaThumbnailInfo, MangaSourceIdInfo, MangaArtistInfo, MangaAuthorInfo { + sourceTitle: SourceDisplayNameInfo['displayName'] | undefined; + latestChapterNumber: ChapterNumberInfo['chapterNumber'] | undefined; +} + +export interface TMigrationEntry { + mangaId: MangaIdInfo['id']; + mangaTitle: MangaTitleInfo['title']; + mangaArtist: MangaArtistInfo['artist']; + mangaAuthor: MangaAuthorInfo['author']; + latestChapterNumber: ChapterNumberInfo['chapterNumber'] | undefined; + mangaThumbnailUrl: MangaThumbnailInfo['thumbnailUrl'] | undefined; + sourceId: SourceIdInfo['id']; + sourceTitle: SourceDisplayNameInfo['displayName'] | undefined; + status: MigrationEntryStatus; + searchMatches: MigrationMatch[]; + manualMatches: MigrationMatch[]; + selectedMatchMangaId: MangaIdInfo['id'] | null; + selectedMatchSourceId: SourceIdInfo['id'] | null; + destSourceIdToSearchState: Record; + error: string | undefined; + isExcluded: boolean; + areMatchesExpanded: boolean; +} + +export type MigratableEntry = NonNullableProperty; + +export type MigrationProgress = { total: number; completed: number; success: number; failed: number }; + +export interface MigrationState { + phase: MigrationPhase; + sourceId: SourceIdInfo['id'] | null; + entries: Record; + destinationSourceIds: SourceIdInfo['id'][]; + migrateOptions: Omit | null; + searchProgress: MigrationProgress; + migrationProgress: MigrationProgress; + startedAt: number | null; + lastUpdatedAt: number | null; + groupExpandState: Partial>; +} + +export interface SourceItem extends SourceIdInfo, SourceNameInfo, SourceLanguageInfo, SourceIconInfo, SourceMetaInfo {} diff --git a/src/features/migration/MigrationManager.ts b/src/features/migration/MigrationManager.ts new file mode 100644 index 00000000..18dda2ff --- /dev/null +++ b/src/features/migration/MigrationManager.ts @@ -0,0 +1,918 @@ +/* + * 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 type { LimitFunction } from 'p-limit'; +import pLimit from 'p-limit'; +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { devtools, persist } from 'zustand/middleware'; +import { + type MigratableEntry, + type MigrateOptions, + type TMigrationEntry, + MigrationEntryStatus, + MigrationPhase, + type MigrationMatch, + type MigrationState, +} from '@/features/migration/Migration.types.ts'; +import { + DEFAULT_MIGRATION_STATE, + MAX_MANGAS_IN_PARALLEL, + MAX_SOURCES_IN_PARALLEL, + MIGRATE_EXECUTE_ENTRY_GROUP_EXPAND_DEFAULT_STATE, + MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE, + MIGRATION_LOCAL_STORAGE_KEY, +} from '@/features/migration/Migration.constants.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { GET_MIGRATION_SOURCE_MANGAS_FETCH } from '@/lib/graphql/source/SourceMutation.ts'; +import type { + GetMigrationSourceMangasFetchMutation, + GetMigrationSourceMangasFetchMutationVariables, + GetServerSettingsQuery, + GetServerSettingsQueryVariables, + MangaMigrationFieldsFragment, +} from '@/lib/graphql/generated/graphql.ts'; +import { FetchSourceMangaType } from '@/lib/graphql/generated/graphql.ts'; +import { GET_SERVER_SETTINGS } from '@/lib/graphql/settings/SettingsQuery.ts'; +import { MangaMigration } from '@/features/migration/MangaMigration.ts'; +import type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; +import type { SourceIdInfo } from '@/features/source/Source.types.ts'; +import { assertIsDefined } from '@/base/Asserts.ts'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { Confirmation } from '@/base/AppAwaitableComponent.ts'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { t } from '@lingui/core/macro'; +import { enhancedCleanup } from '@/base/utils/Strings.ts'; +import { BrowseTab } from '@/features/browse/Browse.types.ts'; +import { Mangas } from '@/features/manga/services/Mangas.ts'; +import { MANGA_MIGRATION_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts'; +import { ZustandUtil } from '@/lib/zustand/ZustandUtil.ts'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import isEqual from 'lodash/fp/isEqual'; + +const RESUMABLE_PHASES: readonly MigrationPhase[] = [MigrationPhase.SEARCHING, MigrationPhase.MIGRATING]; + +const migrationStore = create()( + devtools( + persist( + immer(() => ({ ...DEFAULT_MIGRATION_STATE })), + { + name: MIGRATION_LOCAL_STORAGE_KEY, + merge: (persistedState, currentState) => { + const persisted = persistedState as MigrationState | undefined; + + if (!persisted || !RESUMABLE_PHASES.includes(persisted.phase)) { + return currentState; + } + + return { ...currentState, ...persisted }; + }, + }, + ), + ), +); + +const useMigrationStore = ZustandUtil.createStoreHook(migrationStore); + +export class MigrationManager { + private static abortController: AbortController | null = null; + + private static mangaProcessQueue = pLimit(MAX_MANGAS_IN_PARALLEL); + private static parallelSourcesQueue: LimitFunction | undefined; + private static queueBySource = new Map(); + + private static abortAndResetAbortController(reason: unknown): void { + MigrationManager.abortController?.abort(reason); + MigrationManager.abortController = null; + } + + private static abortAndCreateAbortController(abortReason: unknown): AbortController { + MigrationManager.abortController?.abort(abortReason); + MigrationManager.abortController = new AbortController(); + + return MigrationManager.abortController; + } + + private static getOrCreateAbortController(): AbortController { + if (!MigrationManager.abortController) { + MigrationManager.abortController = new AbortController(); + } + + return MigrationManager.abortController; + } + + private static getParallelSourceQueue(): LimitFunction { + if (MigrationManager.parallelSourcesQueue) { + return MigrationManager.parallelSourcesQueue; + } + + try { + const result = requestManager.graphQLClient.client.readQuery< + GetServerSettingsQuery, + GetServerSettingsQueryVariables + >({ + query: GET_SERVER_SETTINGS, + }); + + MigrationManager.parallelSourcesQueue = pLimit( + result?.settings.maxSourcesInParallel ?? MAX_SOURCES_IN_PARALLEL, + ); + } catch (error) { + MigrationManager.parallelSourcesQueue = pLimit(MAX_SOURCES_IN_PARALLEL); + } + + return MigrationManager.parallelSourcesQueue!; + } + + private static getOrCreateSourceQueue(sourceId: SourceIdInfo['id']): LimitFunction { + if (this.queueBySource.has(sourceId)) { + return this.queueBySource.get(sourceId)!; + } + + const queue = pLimit(1); + this.queueBySource.set(sourceId, queue); + + return queue; + } + + static async confirmAbort(): Promise { + try { + return await Confirmation.show({ + title: t`Abort migration`, + message: t`Are you sure you want to abort the migration?`, + }); + } catch (e) { + defaultPromiseErrorHandler('MigrationManager::abort')(e); + return false; + } + } + + static async goToPreviousPhase(): Promise { + switch (MigrationManager.getState().phase) { + case MigrationPhase.IDLE: + return true; + case MigrationPhase.SELECT_SOURCE: + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.IDLE; + }); + + return true; + case MigrationPhase.SELECT_MANGAS: + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.SELECT_SOURCE; + draft.entries = {}; + }); + + ReactRouter.navigate(AppRoutes.browse.path(BrowseTab.MIGRATE)); + return false; + case MigrationPhase.SELECTING_SOURCES: + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.SELECT_MANGAS; + draft.destinationSourceIds = []; + }); + + return false; + default: + try { + await MigrationManager.abort('goToPreviousPhase'); + return false; + } catch (e) { + return false; + } + } + } + + static isPhaseComplete(): boolean { + const { phase } = MigrationManager.getState(); + + switch (phase) { + case MigrationPhase.SEARCHING: + return ( + MigrationManager.getState().searchProgress.completed === + MigrationManager.getState().searchProgress.total + ); + case MigrationPhase.MIGRATING: + return ( + MigrationManager.getState().migrationProgress.completed === + MigrationManager.getState().migrationProgress.total + ); + default: + return false; + } + } + + private static getUpToDateSearchMatch(searchMatch: MigrationMatch): MigrationMatch { + const cachedEntry = Mangas.getFromCache( + searchMatch.id, + MANGA_MIGRATION_FIELDS, + 'MANGA_MIGRATION_FIELDS', + ); + + return { + id: cachedEntry?.id ?? searchMatch.id, + title: cachedEntry?.title ?? searchMatch.title, + thumbnailUrl: cachedEntry?.thumbnailUrl ?? searchMatch.thumbnailUrl, + thumbnailUrlLastFetched: cachedEntry?.thumbnailUrlLastFetched ?? searchMatch.thumbnailUrlLastFetched, + sourceId: cachedEntry?.sourceId ?? searchMatch.sourceId, + artist: cachedEntry?.artist ?? searchMatch.artist, + author: cachedEntry?.author ?? searchMatch.author, + sourceTitle: cachedEntry?.source?.displayName ?? searchMatch.sourceTitle, + latestChapterNumber: cachedEntry?.highestNumberedChapter?.chapterNumber ?? searchMatch.latestChapterNumber, + }; + } + + static getUpToDateMigrationEntry(entry: TMigrationEntry): TMigrationEntry { + const cachedEntry = Mangas.getFromCache( + entry.mangaId, + MANGA_MIGRATION_FIELDS, + 'MANGA_MIGRATION_FIELDS', + ); + + const updatedEntry = { + mangaId: cachedEntry?.id ?? entry.mangaId, + mangaTitle: cachedEntry?.title ?? entry.mangaTitle, + mangaArtist: cachedEntry?.artist ?? entry.mangaArtist, + mangaAuthor: cachedEntry?.author ?? entry.mangaAuthor, + latestChapterNumber: cachedEntry?.highestNumberedChapter?.chapterNumber ?? entry.latestChapterNumber, + mangaThumbnailUrl: cachedEntry?.thumbnailUrl ?? entry.mangaThumbnailUrl, + sourceId: cachedEntry?.sourceId ?? entry.sourceId, + sourceTitle: cachedEntry?.source?.displayName ?? entry.sourceTitle, + status: entry.status, + searchMatches: entry.searchMatches.map(MigrationManager.getUpToDateSearchMatch.bind(MigrationManager)), + manualMatches: entry.manualMatches.map(MigrationManager.getUpToDateSearchMatch.bind(MigrationManager)), + selectedMatchMangaId: entry.selectedMatchMangaId, + selectedMatchSourceId: entry.selectedMatchSourceId, + destSourceIdToSearchState: entry.destSourceIdToSearchState, + error: entry.error, + isExcluded: entry.isExcluded, + areMatchesExpanded: entry.areMatchesExpanded, + } satisfies TMigrationEntry; + + if (isEqual(entry, updatedEntry)) { + return entry; + } + + MigrationManager.updateState((draft) => { + draft.entries[entry.mangaId] = updatedEntry; + }); + + return updatedEntry; + } + + static selectSource(sourceId: SourceIdInfo['id']): void { + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.SELECT_MANGAS; + draft.sourceId = sourceId; + draft.entries = {}; + }); + ReactRouter.navigate(AppRoutes.migrate.path); + } + + static selectMangas(mangas: MangaMigrationFieldsFragment[]): void { + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.SELECTING_SOURCES; + draft.entries = Object.fromEntries( + mangas.map((manga) => [ + manga.id, + { + mangaId: manga.id, + mangaTitle: manga.title, + mangaArtist: manga.artist, + mangaAuthor: manga.author, + latestChapterNumber: manga.highestNumberedChapter?.chapterNumber, + mangaThumbnailUrl: manga.thumbnailUrl, + sourceId: manga.sourceId, + sourceTitle: manga.source?.displayName, + status: MigrationEntryStatus.PENDING, + searchMatches: [], + manualMatches: [], + selectedMatchMangaId: null, + selectedMatchSourceId: null, + destSourceIdToSearchState: {}, + isExcluded: false, + areMatchesExpanded: false, + error: undefined, + }, + ]), + ); + }); + } + + static async startSearch(destinationSourceIds: SourceIdInfo['id'][]): Promise { + MigrationManager.updateState((draft) => { + draft.destinationSourceIds = destinationSourceIds; + }); + + const state = MigrationManager.getState(); + const entryIds = Object.keys(state.entries).map(Number); + + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.SEARCHING; + draft.searchProgress = { total: entryIds.length, completed: 0, success: 0, failed: 0 }; + draft.startedAt = Date.now(); + draft.groupExpandState = MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE; + }); + + try { + await MigrationManager.search(Object.values(state.entries)); + } finally { + const { searchProgress } = MigrationManager.getState(); + + if (searchProgress.completed === searchProgress.total) { + const allSearchesFailed = searchProgress.failed === searchProgress.total; + + MigrationManager.updateState((draft) => { + draft.groupExpandState = { + ...MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE, + [MigrationEntryStatus.SEARCHING]: false, + [MigrationEntryStatus.NO_MATCH]: !allSearchesFailed && !searchProgress.success, + [MigrationEntryStatus.SEARCH_FAILED]: allSearchesFailed, + [MigrationEntryStatus.SEARCH_COMPLETE]: !!searchProgress.success, + }; + }); + } + } + } + + private static async search(entries: TMigrationEntry[]): Promise { + const { signal } = MigrationManager.abortAndCreateAbortController('search'); + + const searchPromises = entries.map((entry) => + MigrationManager.mangaProcessQueue(async () => { + if (signal.aborted) { + return; + } + + await MigrationManager.searchForManga(entry.mangaId, entry.mangaTitle, signal); + }), + ); + + await Promise.allSettled(searchPromises); + } + + static getMigratableEntries(): MigratableEntry[] { + const { entries } = MigrationManager.getState(); + + return Object.values(entries).filter( + (entry): entry is MigratableEntry => + entry.status === MigrationEntryStatus.SEARCH_COMPLETE && + !entry.isExcluded && + entry.selectedMatchMangaId != null && + entry.selectedMatchSourceId != null, + ); + } + + static async startMigration(options: Omit): Promise { + const migratableEntries = MigrationManager.getMigratableEntries(); + + await Confirmation.show({ + title: t`Migration information`, + message: t`The migration runs on the client on the current device, NOT the server.\nAs long as the client is open, the migration will run in the background.\nThe client can be closed. The migration will be resumed once it gets opened again on the same device it got started on.`, + actions: { + confirm: { + title: t`Understood`, + }, + }, + }); + + MigrationManager.updateState((draft) => { + draft.phase = MigrationPhase.MIGRATING; + draft.migrateOptions = options; + draft.migrationProgress = { total: migratableEntries.length, completed: 0, success: 0, failed: 0 }; + draft.groupExpandState = MIGRATE_EXECUTE_ENTRY_GROUP_EXPAND_DEFAULT_STATE; + }); + + try { + await MigrationManager.migrate(migratableEntries, options); + } finally { + const { migrationProgress } = MigrationManager.getState(); + + if (migrationProgress.completed === migrationProgress.total) { + MigrationManager.updateState((draft) => { + draft.groupExpandState = { + ...MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE, + [MigrationEntryStatus.MIGRATING]: false, + [MigrationEntryStatus.MIGRATION_FAILED]: !!migrationProgress.failed, + [MigrationEntryStatus.MIGRATION_COMPLETE]: !migrationProgress.failed, + }; + }); + } + } + } + + private static async migrate( + entries: MigratableEntry[], + options: Omit, + ): Promise { + const { signal } = MigrationManager.abortAndCreateAbortController('migrate'); + + const entriesBySource = Object.groupBy(entries, (entry) => entry.selectedMatchSourceId); + + const migrationPromises = Object.values(entriesBySource).map((sourceEntries = []) => + MigrationManager.mangaProcessQueue(async () => { + for (const entry of sourceEntries) { + if (signal.aborted) { + return; + } + + // oxlint-disable-next-line no-await-in-loop + await MigrationManager.migrateSingleEntry(entry.mangaId, options, signal); + } + }), + ); + await Promise.allSettled(migrationPromises); + + if (!signal.aborted) { + MigrationManager.updateState((draft) => { + draft.lastUpdatedAt = Date.now(); + }); + } + } + + static async abort(reason: unknown = 'abort'): Promise { + if (!(await MigrationManager.confirmAbort())) { + return false; + } + + MigrationManager.abortAndResetAbortController(reason); + + MigrationManager.reset(); + + ReactRouter.navigate(AppRoutes.browse.path(BrowseTab.MIGRATE)); + + return true; + } + + static async resume(): Promise { + const state = MigrationManager.getState(); + + const isResumeablePhase = RESUMABLE_PHASES.includes(state.phase); + if (!isResumeablePhase) { + return; + } + + const resumeMigrationPhase = state.phase === MigrationPhase.MIGRATING && state.migrateOptions; + if (resumeMigrationPhase) { + assertIsDefined(state.migrateOptions); + + const migratableEntries = MigrationManager.getMigratableEntries(); + + await MigrationManager.migrate(migratableEntries, state.migrateOptions); + + return; + } + + const pendingEntries = Object.values(state.entries).filter( + (entry) => entry.status === MigrationEntryStatus.PENDING, + ); + + await MigrationManager.search(pendingEntries); + } + + static reset(): void { + MigrationManager.abortAndResetAbortController('reset'); + migrationStore.setState({ ...DEFAULT_MIGRATION_STATE }); + } + + static excludeManga(mangaId: MangaIdInfo['id']): void { + MigrationManager.updateState((draft) => { + const entry = draft.entries[mangaId]; + + if (entry) { + entry.isExcluded = true; + } + }); + } + + static includeManga(mangaId: MangaIdInfo['id']): void { + MigrationManager.updateState((draft) => { + const entry = draft.entries[mangaId]; + + if (entry) { + entry.isExcluded = false; + } + }); + } + + static selectMatch( + mangaId: MangaIdInfo['id'], + targetMangaId: MangaIdInfo['id'], + targetSourceId: SourceIdInfo['id'], + ): void { + MigrationManager.updateState((draft) => { + const entry = draft.entries[mangaId]; + if (entry) { + entry.selectedMatchMangaId = targetMangaId; + entry.selectedMatchSourceId = targetSourceId; + entry.status = MigrationEntryStatus.SEARCH_COMPLETE; + } + }); + } + + static selectManualMatch(mangaId: MangaIdInfo['id'], match: MigrationMatch): void { + MigrationManager.updateState((draft) => { + const entry = draft.entries[mangaId]; + if (entry) { + const isExistingSearchMatch = draft.entries[mangaId].searchMatches.some( + (searchMatch) => searchMatch.id === match.id, + ); + const isExistingManualMatch = draft.entries[mangaId].manualMatches.some( + (manualMatch) => manualMatch.id === match.id, + ); + + const isExistingEntry = isExistingSearchMatch || isExistingManualMatch; + if (!isExistingEntry) { + draft.entries[mangaId].manualMatches = [...draft.entries[mangaId].manualMatches, match]; + } + + entry.selectedMatchMangaId = match.id; + entry.selectedMatchSourceId = match.sourceId; + entry.status = MigrationEntryStatus.SEARCH_COMPLETE; + } + }); + } + + static getState(): MigrationState { + return migrationStore.getState(); + } + + static isActive(): boolean { + const { phase } = migrationStore.getState(); + return phase === MigrationPhase.SEARCHING || phase === MigrationPhase.MIGRATING; + } + + static hasPausedMigration(): boolean { + return RESUMABLE_PHASES.includes(MigrationManager.getState().phase); + } + + private static getHigherPrioritySourceIds(sourceId: SourceIdInfo['id']): SourceIdInfo['id'][] { + const { destinationSourceIds } = MigrationManager.getState(); + + const sourceIdPriority = destinationSourceIds.indexOf(sourceId); + return destinationSourceIds.slice(0, Math.max(0, sourceIdPriority - 1)); + } + + private static isHigherPrioritySourceUnsettled(mangaId: MangaIdInfo['id'], sourceId: SourceIdInfo['id']): boolean { + const { entries } = MigrationManager.getState(); + const entry = entries[mangaId]; + + assertIsDefined(entry); + + return MigrationManager.getHigherPrioritySourceIds(sourceId).some( + (higherPrioritySourceId) => entry.destSourceIdToSearchState[higherPrioritySourceId] == null, + ); + } + + private static hasHigherSourcePriorityMatch(mangaId: MangaIdInfo['id'], sourceId: SourceIdInfo['id']): boolean { + const { entries } = MigrationManager.getState(); + const entry = entries[mangaId]; + + if (!entry || entry.selectedMatchSourceId == null) { + return false; + } + + return MigrationManager.getHigherPrioritySourceIds(sourceId).some( + (higherPrioritySourceId) => entry.destSourceIdToSearchState[higherPrioritySourceId], + ); + } + + private static async findMatchesForMangaInSource( + mangaId: MangaIdInfo['id'], + mangaTitle: string, + sourceId: SourceIdInfo['id'], + signal: AbortSignal, + ): Promise { + if (signal.aborted) { + throw new Error(signal.reason); + } + + return MigrationManager.getOrCreateSourceQueue(sourceId)(async () => { + if (signal.aborted) { + throw new Error(signal.reason); + } + + if (MigrationManager.hasHigherSourcePriorityMatch(mangaId, sourceId)) { + throw new Error('Entry already has a selected match from a higher priority source'); + } + + const searchResponse = await requestManager.graphQLClient.client.mutate< + GetMigrationSourceMangasFetchMutation, + GetMigrationSourceMangasFetchMutationVariables + >({ + mutation: GET_MIGRATION_SOURCE_MANGAS_FETCH, + variables: { + input: { + source: sourceId, + query: mangaTitle, + page: 1, + type: FetchSourceMangaType.Search, + }, + }, + context: { fetchOptions: { signal } }, + }); + + const searchMatches = searchResponse?.data?.fetchSourceManga?.mangas ?? []; + const matches = searchMatches.filter( + (searchMatch) => enhancedCleanup(searchMatch.title) === enhancedCleanup(mangaTitle), + ); + + const matchUpdatePromises = matches.map(async (match) => { + if (signal.aborted) { + throw new Error(signal.reason); + } + + const updatedMatch = await requestManager.getMangaFetch(match.id).response; + + return updatedMatch.data?.fetchManga?.manga ?? match; + }); + + const updatedMatches = await Promise.all(matchUpdatePromises); + + return updatedMatches; + }); + } + + private static async searchForManga( + mangaId: MangaIdInfo['id'], + mangaTitle: string, + mainSignal: AbortSignal, + ): Promise { + const state = MigrationManager.getState(); + const entry = state.entries[mangaId]; + + const searchController = new AbortController(); + const signal = AbortSignal.any([mainSignal, searchController.signal]); + + if (!entry) { + return; + } + + MigrationManager.updateState((draft) => { + draft.entries[mangaId].status = MigrationEntryStatus.SEARCHING; + }); + + try { + const searchPromises = state.destinationSourceIds.map((destSourceId) => + MigrationManager.getParallelSourceQueue()(async () => { + if (signal.aborted) { + return null; + } + + if (MigrationManager.hasHigherSourcePriorityMatch(mangaId, destSourceId)) { + return null; + } + + const foundMatches = await (async () => { + try { + return await MigrationManager.findMatchesForMangaInSource( + mangaId, + mangaTitle, + destSourceId, + signal, + ); + } catch (e) { + MigrationManager.updateState((draft) => { + const draftEntry = draft.entries[mangaId]; + + draftEntry.destSourceIdToSearchState[destSourceId] = false; + }); + + throw e; + } + })(); + + if (!foundMatches.length) { + MigrationManager.updateState((draft) => { + const draftEntry = draft.entries[mangaId]; + + draftEntry.destSourceIdToSearchState[destSourceId] = false; + }); + + return null; + } + + MigrationManager.updateState((draft) => { + const draftEntry = draft.entries[mangaId]; + + const matches = foundMatches.map((manga) => ({ + id: manga.id, + title: manga.title, + artist: manga.artist, + author: manga.author, + latestChapterNumber: manga.highestNumberedChapter?.chapterNumber, + thumbnailUrl: manga.thumbnailUrl, + sourceId: manga.sourceId, + sourceTitle: manga.source?.displayName, + })); + + draftEntry.destSourceIdToSearchState[destSourceId] = true; + + draftEntry.searchMatches = [...draftEntry.searchMatches, ...matches]; + + if (!MigrationManager.hasHigherSourcePriorityMatch(mangaId, destSourceId)) { + const matchesByChapterNumber = Object.groupBy( + matches, + (match) => match.latestChapterNumber ?? -1, + ); + const latestChapterNumber = Math.max( + ...Object.keys(matchesByChapterNumber).map((chapterNumber) => Number(chapterNumber)), + ); + const bestMatch = matchesByChapterNumber[latestChapterNumber]?.[0]; + + assertIsDefined(bestMatch); + + draftEntry.selectedMatchMangaId = bestMatch.id; + draftEntry.selectedMatchSourceId = destSourceId; + } + + if (!MigrationManager.isHigherPrioritySourceUnsettled(mangaId, destSourceId)) { + searchController.abort(`Found best match in source "${destSourceId}"`); + } + }); + }), + ); + + const searchMatchPromises = await Promise.allSettled(searchPromises); + + const hasFulfilledSearch = searchMatchPromises.some((result) => result.status === 'fulfilled'); + if (!hasFulfilledSearch) { + throw new Error('All source searches failed'); + } + + MigrationManager.updateState((draft) => { + const draftEntry = draft.entries[mangaId]; + + if (draftEntry.searchMatches.length) { + draft.searchProgress.success += 1; + draftEntry.status = MigrationEntryStatus.SEARCH_COMPLETE; + } else { + draftEntry.status = MigrationEntryStatus.NO_MATCH; + } + + draft.searchProgress.completed += 1; + }); + } catch (error) { + if (signal.aborted) { + return; + } + + MigrationManager.updateState((draft) => { + const draftEntry = draft.entries[mangaId]; + + draftEntry.status = MigrationEntryStatus.SEARCH_FAILED; + draftEntry.error = getErrorMessage(error); + draft.searchProgress.completed += 1; + draft.searchProgress.failed += 1; + }); + } + } + + private static async migrateSingleEntry( + mangaId: MangaIdInfo['id'], + options: Omit, + signal: AbortSignal, + ): Promise { + const state = MigrationManager.getState(); + const entry = state.entries[mangaId]; + + if (!entry || !entry.selectedMatchSourceId || entry.selectedMatchMangaId == null) { + return; + } + + MigrationManager.updateState((draft) => { + draft.entries[mangaId].status = MigrationEntryStatus.MIGRATING; + }); + + try { + if (signal.aborted) { + return; + } + + await MigrationManager.getParallelSourceQueue()(() => + MigrationManager.getOrCreateSourceQueue(entry.sourceId)(() => { + assertIsDefined(entry.selectedMatchSourceId); + + return MigrationManager.getOrCreateSourceQueue(entry.selectedMatchSourceId)(async () => { + if (signal.aborted) { + return; + } + + assertIsDefined(entry.selectedMatchMangaId); + await MangaMigration.migrate(mangaId, entry.selectedMatchMangaId, options); + }); + }), + ); + + MigrationManager.updateState((draft) => { + draft.entries[mangaId].status = MigrationEntryStatus.MIGRATION_COMPLETE; + draft.migrationProgress.success += 1; + draft.migrationProgress.completed += 1; + }); + } catch (error) { + if (signal.aborted) { + return; + } + + MigrationManager.updateState((draft) => { + draft.entries[mangaId].status = MigrationEntryStatus.MIGRATION_FAILED; + draft.entries[mangaId].error = error instanceof Error ? error.message : String(error); + draft.migrationProgress.failed += 1; + draft.migrationProgress.completed += 1; + }); + } + } + + static async retryEntry(id: MangaIdInfo['id']): Promise { + const entry = MigrationManager.getState().entries[id]; + + if (!entry) { + return; + } + + const { signal } = MigrationManager.getOrCreateAbortController(); + const { status } = entry; + + if (status === MigrationEntryStatus.SEARCH_FAILED) { + MigrationManager.updateState((draft) => { + draft.searchProgress.completed -= 1; + draft.searchProgress.failed -= 1; + }); + + await MigrationManager.searchForManga(id, entry.mangaTitle, signal); + return; + } + + if (status === MigrationEntryStatus.MIGRATION_FAILED) { + MigrationManager.updateState((draft) => { + draft.migrationProgress.completed -= 1; + draft.migrationProgress.failed -= 1; + }); + + const migrationOptions = MigrationManager.getState().migrateOptions; + assertIsDefined(migrationOptions); + + await MigrationManager.migrateSingleEntry(id, migrationOptions, signal); + } + } + + private static updateState(updater: (draft: MigrationState) => void): void { + migrationStore.setState(updater); + } + + static setEntryMatchesExpandState(id: MangaIdInfo['id'], expanded: boolean): void { + MigrationManager.updateState((draft) => { + const entry = draft.entries[id]; + + if (entry) { + entry.areMatchesExpanded = expanded; + } + }); + } + + static useEntryMatchesExpandState(id: MangaIdInfo['id']): boolean { + return useMigrationStore((state) => state.entries[id]?.areMatchesExpanded ?? false); + } + + static setGroupExpandState(status: MigrationEntryStatus, expanded: boolean): void { + MigrationManager.updateState((draft) => { + draft.groupExpandState[status] = expanded; + }); + } + + static useGroupExpandState(status: MigrationEntryStatus): boolean { + return useMigrationStore((state) => state.groupExpandState[status] ?? false); + } + + static usePhase(): MigrationPhase { + return useMigrationStore((state) => state.phase); + } + + static useSourceId(): SourceIdInfo['id'] | null { + return useMigrationStore((state) => state.sourceId); + } + + static useEntries(): Record { + return useMigrationStore((state) => state.entries); + } + + static useSearchProgress(): MigrationState['searchProgress'] { + return useMigrationStore((state) => state.searchProgress); + } + + static useMigrationProgress(): MigrationState['migrationProgress'] { + return useMigrationStore((state) => state.migrationProgress); + } + + static useIsActive(): boolean { + // Listen to phase changes + MigrationManager.usePhase(); + + return MigrationManager.isActive(); + } +} diff --git a/src/features/migration/components/MIgrationEntryGroup.tsx b/src/features/migration/components/MIgrationEntryGroup.tsx new file mode 100644 index 00000000..e17c151f --- /dev/null +++ b/src/features/migration/components/MIgrationEntryGroup.tsx @@ -0,0 +1,68 @@ +/* + * 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 type { MigrationEntryStatus, TMigrationEntry } from '@/features/migration/Migration.types.ts'; +import type { ButtonProps } from '@mui/material/Button'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import Stack from '@mui/material/Stack'; +import {} from 'react'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import { MigrationEntry } from '@/features/migration/components/migration-entry/MigrationEntry.tsx'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; + +export const MigrationEntryGroup = ({ + status, + title, + entries, + color, + isMigrating = false, +}: { + status: MigrationEntryStatus; + title: string; + entries: TMigrationEntry[]; + color: ButtonProps['color']; + isMigrating?: boolean; +}) => { + const isExpanded = MigrationManager.useGroupExpandState(status); + + if (!entries.length) { + return null; + } + + return ( + + + + + {entries.map((entry) => ( + + ))} + + + + ); +}; diff --git a/src/features/migration/components/MigrationCard.tsx b/src/features/migration/components/MigrationCard.tsx index 97bbade5..fb752611 100644 --- a/src/features/migration/components/MigrationCard.tsx +++ b/src/features/migration/components/MigrationCard.tsx @@ -11,12 +11,11 @@ import Box from '@mui/material/Box'; import CardActionArea from '@mui/material/CardActionArea'; import Chip from '@mui/material/Chip'; import Typography from '@mui/material/Typography'; -import { Link } from 'react-router-dom'; import { useLingui } from '@lingui/react/macro'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import type { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts'; import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts'; -import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx'; @@ -33,7 +32,7 @@ export const MigrationCard = ({ id, name, lang, iconUrl, mangaCount }: TMigratab return ( - + MigrationManager.selectSource(id)}> void; + isDisabled?: boolean; + title?: string; +}) => { + const { t } = useLingui(); + + return ( + theme.spacing(2), + right: (theme) => theme.spacing(2), + }} + disabled={isDisabled} + onClick={onClick} + > + {title ?? t`Continue`} + + ); +}; diff --git a/src/features/migration/components/MigrationFABIndicator.tsx b/src/features/migration/components/MigrationFABIndicator.tsx new file mode 100644 index 00000000..1cd2939d --- /dev/null +++ b/src/features/migration/components/MigrationFABIndicator.tsx @@ -0,0 +1,74 @@ +/* + * 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 Chip from '@mui/material/Chip'; +import CircularProgress from '@mui/material/CircularProgress'; +import { useNavigate, useLocation } from 'react-router-dom'; +import type { MigrationProgress } from '@/features/migration/Migration.types.ts'; +import { MigrationPhase } from '@/features/migration/Migration.types.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import DoneAllIcon from '@mui/icons-material/DoneAll'; +import { useLayoutEffect, useState } from 'react'; + +const getButtonText = (phase: MigrationPhase, progress: MigrationProgress) => { + switch (phase) { + case MigrationPhase.SEARCHING: + if (progress.total === progress.completed) { + return `Searching`; + } + + return `Searching (${progress.completed}/${progress.total})`; + + case MigrationPhase.MIGRATING: + if (progress.total === progress.completed) { + return `Migrating`; + } + + return `Migrating (${progress.completed}/${progress.total})`; + default: + return 'Migrating'; + } +}; + +export const MigrationFABIndicator = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const isActive = MigrationManager.useIsActive(); + const phase = MigrationManager.usePhase(); + const searchProgress = MigrationManager.useSearchProgress(); + const migrationProgress = MigrationManager.useMigrationProgress(); + + const [isVisible, setIsVisible] = useState(true); + + useLayoutEffect(() => { + setIsVisible(true); + }, [phase]); + + if (!isVisible || !isActive || location.pathname.startsWith(AppRoutes.migrate.path)) { + return null; + } + + return ( + : } + label={getButtonText(phase, phase === MigrationPhase.SEARCHING ? searchProgress : migrationProgress)} + color="primary" + onClick={() => navigate(AppRoutes.migrate.path)} + sx={{ + position: 'fixed', + bottom: (theme) => theme.spacing(2), + right: (theme) => theme.spacing(2), + zIndex: (theme) => theme.zIndex.fab, + cursor: 'pointer', + }} + onDelete={() => setIsVisible(false)} + /> + ); +}; diff --git a/src/features/migration/components/MigrateDialog.tsx b/src/features/migration/components/MigrationOptionsDialog.tsx similarity index 58% rename from src/features/migration/components/MigrateDialog.tsx rename to src/features/migration/components/MigrationOptionsDialog.tsx index bc69689f..0a692a07 100644 --- a/src/features/migration/components/MigrateDialog.tsx +++ b/src/features/migration/components/MigrationOptionsDialog.tsx @@ -11,96 +11,83 @@ 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 Stack from '@mui/material/Stack'; -import { Link, useNavigate, useParams } from 'react-router-dom'; -import { useState } from 'react'; import FormGroup from '@mui/material/FormGroup'; +import Stack from '@mui/material/Stack'; import { useLingui } from '@lingui/react/macro'; import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx'; -import { Mangas } from '@/features/manga/services/Mangas.ts'; -import { makeToast } from '@/base/utils/Toast.ts'; import { createUpdateMetadataServerSettings, useMetadataServerSettings, } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import type { MetadataMigrationSettings, MigrateMode } from '@/features/migration/Migration.types.ts'; +import type { MetadataMigrationSettings, MigrateOptions } from '@/features/migration/Migration.types.ts'; +import type { AwaitableComponentProps } from 'awaitable-component'; +import type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { Link } from 'react-router-dom'; -export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrateTo: number; onClose: () => void }) => { +export const MigrationOptionsDialog = ({ + isVisible, + onDismiss, + onSubmit, + onExitComplete, + isMigrating = false, + mangaIdToMigrateTo, + startMigration = onSubmit, +}: AwaitableComponentProps> & { + isMigrating?: boolean; + mangaIdToMigrateTo?: MangaIdInfo['id']; + startMigration?: (options: Omit) => void; +}) => { const { t } = useLingui(); - const navigate = useNavigate(); - - const { mangaId: mangaIdAsString } = useParams<{ mangaId: string }>(); - const mangaId = Number(mangaIdAsString); - const { settings: { migrateChapters, migrateCategories, migrateTracking, deleteChapters, migrateMetadata }, } = useMetadataServerSettings(); - const [isMigrationInProcess, setIsMigrationInProcess] = useState(false); - - const setMigrationFlag = createUpdateMetadataServerSettings( - defaultPromiseErrorHandler('MigrateDialog::updateSetting'), - ); - - const migrate = async (mode: MigrateMode) => { - if (mangaId == null) { - throw new Error(`MigrateDialog::migrate: unexpected mangaId "${mangaId}"`); - } - - makeToast(t`Migrating manga…`, 'info'); - - setIsMigrationInProcess(true); - - try { - await Mangas.migrate(mangaId, mangaIdToMigrateTo, { - mode, - migrateChapters, - migrateCategories, - migrateTracking, - deleteChapters, - migrateMetadata, - }); - - navigate(AppRoutes.manga.path(mangaIdToMigrateTo), { replace: true }); - } catch (e) { - setIsMigrationInProcess(false); - } + const options: Omit = { + migrateChapters, + migrateCategories, + migrateTracking, + deleteChapters, + migrateMetadata, }; + const setMigrationFlag = createUpdateMetadataServerSettings( + defaultPromiseErrorHandler('MigrationOptionsDialog::updateSetting'), + ); + return ( - - {t`Select data to include`} + + {t`Migration options`} setMigrationFlag('migrateChapters', checked)} /> setMigrationFlag('migrateCategories', checked)} /> setMigrationFlag('migrateTracking', checked)} /> setMigrationFlag('migrateMetadata', checked)} /> setMigrationFlag('deleteChapters', checked)} @@ -115,21 +102,28 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat width: '100%', }} > - + {mangaIdToMigrateTo !== undefined && ( + + )} - - - diff --git a/src/features/migration/components/MigrationProgressBar.tsx b/src/features/migration/components/MigrationProgressBar.tsx new file mode 100644 index 00000000..ea7a5419 --- /dev/null +++ b/src/features/migration/components/MigrationProgressBar.tsx @@ -0,0 +1,52 @@ +/* + * 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 Box from '@mui/material/Box'; +import LinearProgress from '@mui/material/LinearProgress'; +import Typography from '@mui/material/Typography'; +import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; +import type { MigrationProgress } from '@/features/migration/Migration.types.ts'; + +export const MigrationProgressBar = ({ + completed, + total, + label, +}: { + label: string; +} & MigrationProgress) => { + const { appBarHeight } = useNavBarContext(); + + const progress = total > 0 ? (completed / total) * 100 : 0; + + if (completed === total) { + return null; + } + + return ( + + + + + + {label} + + + ); +}; diff --git a/src/features/migration/components/MigrationSourceList.tsx b/src/features/migration/components/MigrationSourceList.tsx new file mode 100644 index 00000000..17311a29 --- /dev/null +++ b/src/features/migration/components/MigrationSourceList.tsx @@ -0,0 +1,242 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useMemo, useState } from 'react'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Chip from '@mui/material/Chip'; +import Typography from '@mui/material/Typography'; +import type { DragEndEvent } from '@dnd-kit/core'; +import { closestCenter, DndContext } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { useLingui } from '@lingui/react/macro'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { DndSortableItem } from '@/lib/dnd-kit/DndSortableItem.tsx'; +import { DndKitUtil } from '@/lib/dnd-kit/DndKitUtil.ts'; +import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; +import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx'; +import type { SourceItem } from '@/features/migration/Migration.types.ts'; +import type { SourceIdInfo } from '@/features/source/Source.types.ts'; +import type { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts'; +import { assertIsDefined } from '@/base/Asserts.ts'; +import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; +import { StyledGroupedVirtuoso } from '@/base/components/virtuoso/StyledGroupedVirtuoso.tsx'; +import { StyledGroupHeader } from '@/base/components/virtuoso/StyledGroupHeader.tsx'; +import { DndOverlayItem } from '@/lib/dnd-kit/DndOverlayItem'; +import { noOp } from '@/lib/HelperFunctions'; +import CardActionArea from '@mui/material/CardActionArea'; +import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx'; +import DragHandle from '@mui/icons-material/DragHandle'; +import { languageCodeToName } from '@/base/utils/Languages.ts'; +import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx'; +import Stack from '@mui/material/Stack'; + +const SourceCard = ({ + source, + onToggle, + isCurrentSource, + isSelected, + isDragging, +}: { + source: SourceItem; + onToggle: (id: SourceIdInfo['id']) => void; + isCurrentSource: boolean; + isSelected: boolean; + isDragging?: boolean; +}) => { + const { t } = useLingui(); + + return ( + + + onToggle(source.id)}> + + + + + + {source.name} + + {languageCodeToName(source.lang)} + + + + {isCurrentSource && ( + + )} + {isSelected && ( + + + + )} + + + + + + ); +}; + +export const MigrationSourceList = ({ + sources, + handleSelection, + selectedSourceIds, + handlePriorityChange, + currentSourceId, +}: { + sources: SourceItem[]; + handleSelection: SelectableCollectionReturnType['handleSelection']; + handlePriorityChange: (oldIndex: number, newIndex: number) => void; + selectedSourceIds: SourceIdInfo['id'][]; + currentSourceId: SourceIdInfo['id'] | null; +}) => { + const { t } = useLingui(); + const dndSensors = DndKitUtil.useSensorsForDevice(); + const [dndActiveSource, setDndActiveSource] = useState(null); + + const selectedSources = useMemo( + () => + selectedSourceIds.map((sourceId) => { + const selectedSource = sources.find((source) => source.id === sourceId); + + assertIsDefined(selectedSource); + return selectedSource; + }), + [sources, selectedSourceIds], + ); + const unselectedSources = useMemo( + () => sources.filter((source) => !selectedSourceIds.includes(source.id)), + [sources, selectedSourceIds], + ); + const allSources = useMemo(() => [...selectedSources, ...unselectedSources], [selectedSources, unselectedSources]); + const groupedSourcesBySelectionState = useMemo<[boolean, SourceItem[]][]>( + () => + [selectedSources.length ? [true, selectedSources] : undefined, [false, unselectedSources]].filter( + (entry) => entry !== undefined, + ) as [boolean, SourceItem[]][], + [selectedSources, unselectedSources], + ); + + const groupCounts = useMemo( + () => groupedSourcesBySelectionState.map(([, sourcesOfGroup]) => sourcesOfGroup.length), + [groupedSourcesBySelectionState], + ); + const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( + groupCounts, + useCallback( + (index) => String(groupedSourcesBySelectionState[index][VirtuosoUtil.GROUP]), + [groupedSourcesBySelectionState], + ), + useCallback((index) => allSources[index].id, [groupedSourcesBySelectionState]), + ); + + const handleToggle = useCallback( + (id: SourceIdInfo['id']) => { + handleSelection(id, !selectedSourceIds.includes(id)); + }, + [handleSelection, selectedSourceIds], + ); + + const onDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + setDndActiveSource(null); + + if (!over || active.id === over.id) { + return; + } + + const oldIndex = selectedSourceIds.indexOf(String(active.id)); + const newIndex = selectedSourceIds.indexOf(String(over.id)); + handlePriorityChange(oldIndex, newIndex); + }; + + return ( + setDndActiveSource(sources.find((source) => source.id === event.active.id) ?? null)} + onDragEnd={onDragEnd} + onDragCancel={() => setDndActiveSource(null)} + onDragAbort={() => setDndActiveSource(null)} + > + + { + const [group] = groupedSourcesBySelectionState[index]; + + return ( + + + {group ? t`Selected` : t`Available`} + + + {group && !!selectedSources.length && ( + + {t`Drag to prioritize`} + + )} + + ); + }} + itemContent={(index, groupIndex) => { + const [isSelected] = groupedSourcesBySelectionState[groupIndex]; + const source = allSources[index]; + + const sourceCard = ( + + ); + + if (isSelected) { + return ( + + {sourceCard} + + ); + } + + return sourceCard; + }} + /> + + + + + + ); +}; diff --git a/src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx b/src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx new file mode 100644 index 00000000..0162ed69 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx @@ -0,0 +1,292 @@ +/* + * 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 type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; +import type { MigrationMatch, TMigrationEntry } from '@/features/migration/Migration.types.ts'; +import { MigrationEntryStatus, MigrationPhase } from '@/features/migration/Migration.types.ts'; +import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; +import { MigrationEntryCard } from '@/features/migration/components/migration-entry/MigrationEntryCard.tsx'; +import { applyStyles } from '@/base/utils/ApplyStyles.ts'; +import { MigrationEntryCardContent } from '@/features/migration/components/migration-entry/MigrationEntryCardContent.tsx'; +import { ENTRY_STATUS_TRANSLATION } from '@/features/migration/Migration.constants.ts'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts'; +import { Confirmation } from '@/base/AppAwaitableComponent.ts'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; +import { Mangas } from '@/features/manga/services/Mangas.ts'; +import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; +import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx'; +import Typography from '@mui/material/Typography'; +import { useLingui } from '@lingui/react/macro'; +import Stack from '@mui/material/Stack'; +import SearchIcon from '@mui/icons-material/Search'; +import CardActions from '@mui/material/CardActions'; +import Button from '@mui/material/Button'; +import { plural } from '@lingui/core/macro'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import { alpha } from '@mui/material/styles'; +import ReplayIcon from '@mui/icons-material/Replay'; +import Link from '@mui/material/Link'; +import { Link as RouterLink } from 'react-router-dom'; + +const EntryStatus = ({ + sourceMangaId, + sourceMangaTitle, + status, + isMigrating, +}: { + sourceMangaId: MangaIdInfo['id']; + sourceMangaTitle: string; + status: MigrationEntryStatus; + isMigrating: boolean; +}) => { + const { t } = useLingui(); + + return ( + + + {t(ENTRY_STATUS_TRANSLATION[status])} + + {!isMigrating && status === MigrationEntryStatus.NO_MATCH && ( + + )} + + ); +}; + +const EntryError = ({ + id, + title, + isMigrating, + sourceMangaId, + error, +}: { + sourceMangaId: MangaIdInfo['id']; + isMigrating: boolean; +} & Pick & + Pick) => { + const { t } = useLingui(); + + const { phase } = MigrationManager.getState(); + + const MAX_ERROR_LENGTH = 100; + + const { isGraphqlException, graphqlError, graphqlStackTrace } = extractGraphqlExceptionInfo(error); + const tmpError = isGraphqlException ? graphqlError : error; + const isErrorTooLong = (tmpError?.length ?? 0) > MAX_ERROR_LENGTH; + const finalError = isErrorTooLong ? tmpError?.slice(0, MAX_ERROR_LENGTH).concat('…') : tmpError; + + const isSearchRetryable = status === MigrationEntryStatus.SEARCH_FAILED && phase === MigrationPhase.SEARCHING; + const isMigrationRetryable = status === MigrationEntryStatus.MIGRATION_FAILED && phase === MigrationPhase.MIGRATING; + + const isRetryable = isSearchRetryable || isMigrationRetryable; + + return ( + + + + {finalError} + + {(isErrorTooLong || (isGraphqlException && graphqlStackTrace)) && ( + + )} + + {isRetryable && ( + + )} + + ); +}; + +const EntryData = (entry: MigrationMatch) => { + const { id, title } = entry; + + const { t } = useLingui(); + + return ( + <> + + + + + {t`Destination - ${entry.sourceTitle}`} + + + {title} + + + + + + ); +}; + +export const MigrationDestinationEntry = ({ + sourceMangaId, + sourceMangaTitle, + entry, + error, + status, + otherResultsCount, + isExpanded, + setIsExpanded, + isMigrating, +}: { + sourceMangaId: MangaIdInfo['id']; + sourceMangaTitle: string; + entry: MigrationMatch | undefined; + error?: string; + status: MigrationEntryStatus; + otherResultsCount: number; + isExpanded: boolean; + setIsExpanded: (expanded: boolean) => void; + isMigrating: boolean; +}) => { + const isTabletWidth = MediaQuery.useIsTabletWidth(); + + return ( + + + {(() => { + if (!entry) { + return ( + + ); + } + + if (error) { + return ( + + ); + } + + return ; + })()} + + {!isTabletWidth && !isMigrating && !!otherResultsCount && ( + alpha(theme.palette.primary.dark, 0.8), + }, + }} + > + + + )} + + ); +}; diff --git a/src/features/migration/components/migration-entry/MigrationEntry.tsx b/src/features/migration/components/migration-entry/MigrationEntry.tsx new file mode 100644 index 00000000..d3cb52db --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationEntry.tsx @@ -0,0 +1,241 @@ +/* + * 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 type { MigrationMatch, TMigrationEntry } from '@/features/migration/Migration.types.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import Paper from '@mui/material/Paper'; +import { useMemo } from 'react'; +import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; +import { applyStyles } from '@/base/utils/ApplyStyles.ts'; +import { MigrationSourceEntry } from '@/features/migration/components/migration-entry/MigrationSourceEntry.tsx'; +import { MigrationDestinationEntry } from '@/features/migration/components/migration-entry/MigrationDestinationEntry.tsx'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { MigrationMatchedEntry } from '@/features/migration/components/migration-entry/MigrationMatchedEntry.tsx'; +import { MigrationEntrySearchExcludeActions } from '@/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx'; +import Typography from '@mui/material/Typography'; +import { useLingui } from '@lingui/react/macro'; +import Stack from '@mui/material/Stack'; +import SearchIcon from '@mui/icons-material/Search'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import Divider from '@mui/material/Divider'; +import { MigrationEntryStatusIndicator } from '@/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx'; +import Box from '@mui/material/Box'; + +const MigrationEntryMobile = ({ + entry, + entry: { mangaId, mangaTitle, status, error, isExcluded }, + destinationEntry, + otherSearchMatches, + isExpanded, + setIsExpanded, + isMigrating, +}: { + entry: TMigrationEntry; + destinationEntry: MigrationMatch | undefined; + otherSearchMatches: MigrationMatch[]; + isExpanded: boolean; + setIsExpanded: (expanded: boolean) => void; + isMigrating: boolean; +}) => { + const { t } = useLingui(); + + return ( + <> + + + + + + + {t`Matches`} + + + + {otherSearchMatches.map((searchMatch) => { + if (destinationEntry?.id === searchMatch.id) { + return null; + } + + return ( + + ); + })} + + + + {!isMigrating && ( + + )} + + ); +}; + +export const MigrationEntryDesktop = ({ + entry, + entry: { mangaId, mangaTitle, status, error }, + destinationEntry, + otherSearchMatches, + isExpanded, + setIsExpanded, + isMigrating, +}: { + entry: TMigrationEntry; + destinationEntry: MigrationMatch | undefined; + otherSearchMatches: MigrationMatch[]; + isExpanded: boolean; + setIsExpanded: (expanded: boolean) => void; + isMigrating: boolean; +}) => { + const { t } = useLingui(); + + return ( + <> + + + + + + + + + + {!isMigrating && ( + + )} + + + + + + {t`Matches`} + + {otherSearchMatches.map((searchMatch) => { + if (destinationEntry?.id === searchMatch.id) { + return null; + } + + return ( + + ); + })} + + + + ); +}; + +export const MigrationEntry = ({ entry: propEntry, isMigrating }: { entry: TMigrationEntry; isMigrating: boolean }) => { + const isTabletWidth = MediaQuery.useIsTabletWidth(); + + const entry = useMemo(() => MigrationManager.getUpToDateMigrationEntry(propEntry), [propEntry]); + + const destinationEntry = useMemo(() => { + const match = entry.searchMatches.find((matchEntry) => matchEntry.id === entry.selectedMatchMangaId); + const manualMatch = entry.manualMatches.find((matchEntry) => matchEntry.id === entry.selectedMatchMangaId); + + return match ?? manualMatch; + }, [entry.searchMatches, entry.selectedMatchMangaId]); + const otherMatches = useMemo( + () => + entry.searchMatches + .filter((searchMatch) => searchMatch.id !== entry.selectedMatchMangaId) + .sort((a, b) => (b.latestChapterNumber ?? 0) - (a.latestChapterNumber ?? 0)), + [entry.searchMatches, entry.selectedMatchMangaId], + ); + + const MigrationComponent = useMemo( + () => (isTabletWidth ? MigrationEntryMobile : MigrationEntryDesktop), + [isTabletWidth], + ); + + return ( + + + MigrationManager.setEntryMatchesExpandState(entry.mangaId, !entry.areMatchesExpanded) + } + otherSearchMatches={otherMatches} + isMigrating={isMigrating} + /> + + ); +}; diff --git a/src/features/migration/components/migration-entry/MigrationEntryCard.tsx b/src/features/migration/components/migration-entry/MigrationEntryCard.tsx new file mode 100644 index 00000000..58b62149 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationEntryCard.tsx @@ -0,0 +1,17 @@ +/* + * 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 { styled } from '@mui/material/styles'; + +export const MigrationEntryCard = styled(Card)(({ theme }) => ({ + backgroundColor: theme.palette.background.default, + borderStyle: 'solid', + borderWidth: 2, + borderColor: theme.palette.divider, +})); diff --git a/src/features/migration/components/migration-entry/MigrationEntryCardContent.tsx b/src/features/migration/components/migration-entry/MigrationEntryCardContent.tsx new file mode 100644 index 00000000..af32a2ac --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationEntryCardContent.tsx @@ -0,0 +1,21 @@ +/* + * 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 CardContent from '@mui/material/CardContent'; +import { styled } from '@mui/material/styles'; + +export const MigrationEntryCardContent = styled(CardContent)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: theme.spacing(2), + padding: theme.spacing(2), + '&:last-child': { + paddingBottom: theme.spacing(2), + }, +})); diff --git a/src/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx b/src/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx new file mode 100644 index 00000000..e9cdb471 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx @@ -0,0 +1,32 @@ +/* + * 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 type { MigrationMatch } from '@/features/migration/Migration.types.ts'; +import Typography from '@mui/material/Typography'; +import { useLingui } from '@lingui/react/macro'; + +export const MigrationEntryMetadataText = ( + entry: Pick, +) => { + const { t } = useLingui(); + + const latestChapterNumber = (entry.latestChapterNumber ?? 0) > 1 ? entry.latestChapterNumber : t`Unknown`; + const latestChapter = t`Latest: ${latestChapterNumber}`; + + const artist = entry.artist ? `${entry.artist} - ` : ''; + const author = entry.author ? `${entry.author} - ` : ''; + const isSameArtistAuthor = artist === author; + const artistAuthor = isSameArtistAuthor ? artist : `${artist}${author}`; + + return ( + + {artistAuthor} + {latestChapter} + + ); +}; diff --git a/src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx b/src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx new file mode 100644 index 00000000..e147c752 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx @@ -0,0 +1,124 @@ +/* + * 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 type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; +import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; +import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; +import { CustomButtonIcon } from '@/base/components/buttons/CustomButtonIcon.tsx'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import IconButton from '@mui/material/IconButton'; +import { useLingui } from '@lingui/react/macro'; +import Stack from '@mui/material/Stack'; +import SearchIcon from '@mui/icons-material/Search'; +import CloseIcon from '@mui/icons-material/Close'; +import AddIcon from '@mui/icons-material/Add'; +import Button from '@mui/material/Button'; +import { plural } from '@lingui/core/macro'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ButtonGroup from '@mui/material/ButtonGroup'; + +export const MigrationEntrySearchExcludeActions = ({ + hasResults, + otherResultsCount, + isExpanded, + setIsExpanded, + isExcluded, + mangaId, + mangaTitle, +}: { + hasResults: boolean; + otherResultsCount: number; + isExpanded: boolean; + setIsExpanded: (expanded: boolean) => void; + isExcluded: boolean; + mangaId: MangaIdInfo['id']; + mangaTitle: string; +}) => { + const { t } = useLingui(); + const isTabletWidth = MediaQuery.useIsTabletWidth(); + + if (isTabletWidth) { + if (!hasResults) { + return; + } + + return ( + + {!!otherResultsCount && ( + + )} + {!isExpanded && ( + + { + ReactRouter.navigate( + AppRoutes.migrate.childRoutes.manualSearch.path(mangaId, mangaTitle), + { + state: { mangaTitle: mangaTitle }, + }, + ); + }} + > + + + + )} + + + isExcluded ? MigrationManager.includeManga(mangaId) : MigrationManager.excludeManga(mangaId) + } + > + {isExcluded ? : } + + + + ); + } + + return ( + + + + isExcluded ? MigrationManager.includeManga(mangaId) : MigrationManager.excludeManga(mangaId) + } + > + {isExcluded ? : } + + + + { + ReactRouter.navigate(AppRoutes.migrate.childRoutes.manualSearch.path(mangaId, mangaTitle)); + }} + > + + + + + ); +}; diff --git a/src/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx b/src/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx new file mode 100644 index 00000000..ab3e1207 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx @@ -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 { MediaQuery } from '@/base/utils/MediaQuery.tsx'; +import { MigrationEntryStatus } from '@/features/migration/Migration.types.ts'; +import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; +import Box from '@mui/material/Box'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import type { ReactNode } from 'react'; +import type { Theme } from '@mui/material/styles'; +import type { SystemCssProperties } from '@mui/system/styleFunctionSx'; +import PriorityHighIcon from '@mui/icons-material/PriorityHigh'; +import CheckIcon from '@mui/icons-material/Check'; + +const StatusIndicatorWrapper = ({ + backgroundColor, + statusIcon, +}: { + backgroundColor: SystemCssProperties['backgroundColor']; + statusIcon: ReactNode; +}) => { + const isTabletWidth = MediaQuery.useIsTabletWidth(); + + return ( + + {statusIcon} + + ); +}; +export const MigrationEntryStatusIndicator = ({ + status, + hasResults, +}: { + status: MigrationEntryStatus; + hasResults: boolean; +}) => { + const isTabletWidth = MediaQuery.useIsTabletWidth(); + + if ([MigrationEntryStatus.SEARCHING, MigrationEntryStatus.MIGRATING].includes(status)) { + return ; + } + + if ([MigrationEntryStatus.SEARCH_FAILED, MigrationEntryStatus.MIGRATION_FAILED].includes(status)) { + return ( + theme.palette.error.main} + statusIcon={ + + } + /> + ); + } + + if (status === MigrationEntryStatus.NO_MATCH) { + return ( + theme.palette.warning.main} + statusIcon={ + + } + /> + ); + } + + if (status === MigrationEntryStatus.MIGRATION_COMPLETE) { + return ( + + } + /> + ); + } + + if (hasResults) { + return ( + + } + /> + ); + } + + return null; +}; diff --git a/src/features/migration/components/migration-entry/MigrationMatchedEntry.tsx b/src/features/migration/components/migration-entry/MigrationMatchedEntry.tsx new file mode 100644 index 00000000..3501a889 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationMatchedEntry.tsx @@ -0,0 +1,92 @@ +/* + * 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 type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; +import type { MigrationMatch } from '@/features/migration/Migration.types.ts'; +import { MigrationEntryCard } from '@/features/migration/components/migration-entry/MigrationEntryCard.tsx'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { MigrationEntryCardContent } from '@/features/migration/components/migration-entry/MigrationEntryCardContent.tsx'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; +import { Mangas } from '@/features/manga/services/Mangas.ts'; +import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; +import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx'; +import { MUIUtil } from '@/lib/mui/MUI.util.ts'; +import Typography from '@mui/material/Typography'; +import { useLingui } from '@lingui/react/macro'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import CardActionArea from '@mui/material/CardActionArea'; +import Link from '@mui/material/Link'; +import { Link as RouterLink } from 'react-router-dom'; + +export const MigrationMatchedEntry = ({ + sourceMangaId, + entry, +}: { + sourceMangaId: MangaIdInfo['id']; + entry: MigrationMatch; +}) => { + const { t } = useLingui(); + + return ( + + MigrationManager.selectMatch(sourceMangaId, entry.id, entry.sourceId)}> + + {(() => ( + <> + e.stopPropagation()} + > + + + + + {entry.sourceTitle} + + e.stopPropagation()} + > + + {entry.title} + + + + + + ))()} + {(() => ( + + ))()} + + + + ); +}; diff --git a/src/features/migration/components/migration-entry/MigrationSourceEntry.tsx b/src/features/migration/components/migration-entry/MigrationSourceEntry.tsx new file mode 100644 index 00000000..8c2ca2c2 --- /dev/null +++ b/src/features/migration/components/migration-entry/MigrationSourceEntry.tsx @@ -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 type { TMigrationEntry } from '@/features/migration/Migration.types.ts'; +import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; +import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; +import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx'; +import { MigrationEntryStatusIndicator } from '@/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; +import { Mangas } from '@/features/manga/services/Mangas.ts'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { useLingui } from '@lingui/react/macro'; +import Stack from '@mui/material/Stack'; +import Link from '@mui/material/Link'; +import { Link as RouterLink } from 'react-router-dom'; + +export const MigrationSourceEntry = (entry: TMigrationEntry) => { + const { + mangaId, + mangaThumbnailUrl, + mangaTitle, + sourceTitle, + mangaArtist, + mangaAuthor, + latestChapterNumber, + searchMatches, + status, + } = entry; + + const { t } = useLingui(); + const isTabletWidth = MediaQuery.useIsTabletWidth(); + + if (isTabletWidth) { + return ( + + + {t`Source entry - ${sourceTitle}`} + + {mangaTitle} + + + + + + + + ); + } + + return ( + + + + + + {t`Source entry - ${sourceTitle}`} + + + {mangaTitle} + + + + + + ); +}; diff --git a/src/features/migration/screens/Migrate.tsx b/src/features/migration/screens/Migrate.tsx deleted file mode 100644 index e1ea5af1..00000000 --- a/src/features/migration/screens/Migrate.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { useLingui } from '@lingui/react/macro'; -import { requestManager } from '@/lib/requests/RequestManager.ts'; -import type { TMigratableSource } from '@/features/migration/components/MigrationCard.tsx'; -import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; -import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; -import { GridLayouts } from '@/base/components/GridLayouts.tsx'; -import { useLocalStorage } from '@/base/hooks/useStorage.tsx'; -import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import type { GetSourceMigratableQuery, GetSourceMigratableQueryVariables } from '@/lib/graphql/generated/graphql.ts'; -import { GET_SOURCE_MIGRATABLE } from '@/lib/graphql/source/SourceQuery.ts'; -import { SOURCE_BASE_FIELDS } from '@/lib/graphql/source/SourceFragments.ts'; -import { BaseMangaGrid } from '@/features/manga/components/BaseMangaGrid.tsx'; -import { GridLayout } from '@/base/Base.types.ts'; -import { getErrorMessage } from '@/lib/HelperFunctions.ts'; -import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts'; - -export const Migrate = () => { - const { t } = useLingui(); - - const { sourceId: paramSourceId } = useParams<{ sourceId: string }>(); - - const [gridLayout, setGridLayout] = useLocalStorage('migrateGridLayout', GridLayout.List); - - const fragmentSource = requestManager.graphQLClient.client.cache.readFragment< - Pick - >({ - id: requestManager.graphQLClient.client.cache.identify({ __typename: 'SourceType', id: paramSourceId }), - fragment: SOURCE_BASE_FIELDS, - }); - - const [isKnownSource, setIsKnownSource] = useState(fragmentSource !== null ? true : undefined); - const { - data: migratableSourceData, - loading: isSourceLoading, - error: sourceError, - refetch: refetchSource, - } = requestManager.useGetSource( - GET_SOURCE_MIGRATABLE, - paramSourceId, - { skip: !!isKnownSource, notifyOnNetworkStatusChange: true }, - ); - - const { sourceId, name } = { - sourceId: paramSourceId, - name: paramSourceId, - ...fragmentSource, - ...migratableSourceData?.source, - }; - - const { - data: migratableSourceMangasData, - loading: areMangasLoading, - error: mangasError, - refetch: refetchMangas, - } = requestManager.useGetMigratableSourceMangas(sourceId, { - skip: !isKnownSource, - notifyOnNetworkStatusChange: true, - }); - - useAppTitleAndAction( - name ?? sourceId ?? t`Migrate`, - , - [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 ; - } - - const hasErrorSource = sourceError && isKnownSource === false; - const hasError = hasErrorSource || mangasError; - if (hasError) { - const error = (hasErrorSource ? sourceError : mangasError)!; - return ( - { - if (hasErrorSource) { - refetchSource().catch(defaultPromiseErrorHandler('Migrate::refetchSource')); - } - - if (mangasError) { - refetchMangas().catch(defaultPromiseErrorHandler('Migrate::refetchMangas')); - } - }} - /> - ); - } - - return ( - {}} - isLoading={areMangasLoading} - mangas={migratableSourceMangasData?.mangas.nodes ?? []} - gridLayout={gridLayout} - mode="migrate.search" - /> - ); -}; diff --git a/src/features/migration/screens/Migration.tsx b/src/features/migration/screens/Migration.tsx index d4c977cf..f3286f24 100644 --- a/src/features/migration/screens/Migration.tsx +++ b/src/features/migration/screens/Migration.tsx @@ -6,156 +6,59 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useMemo } from 'react'; -import List from '@mui/material/List'; -import Stack from '@mui/material/Stack'; -import IconButton from '@mui/material/IconButton'; -import SortByAlphaIcon from '@mui/icons-material/SortByAlpha'; -import TagIcon from '@mui/icons-material/Tag'; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; -import { useLingui } from '@lingui/react/macro'; -import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; -import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; -import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; -import type { TMigratableSource } from '@/features/migration/components/MigrationCard.tsx'; -import { MigrationCard } from '@/features/migration/components/MigrationCard.tsx'; -import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx'; -import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import type { SortSettings, TMigratableSourcesResult } from '@/features/migration/Migration.types.ts'; -import { SortBy, SortOrder } from '@/features/migration/Migration.types.ts'; -import { sortByToTranslation, sortOrderToTranslation } from '@/features/migration/Migration.constants.ts'; -import { - createUpdateMetadataServerSettings, - useMetadataServerSettings, -} from '@/features/settings/services/ServerSettingsMetadata.ts'; -import { makeToast } from '@/base/utils/Toast.ts'; -import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; -import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { Navigate } from 'react-router-dom'; +import { MigrationPhase } from '@/features/migration/Migration.types.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { MigrationSelectSource } from '@/features/migration/screens/MigrationSelectSource.tsx'; +import { MigrationSelectMangas } from '@/features/migration/screens/MigrationSelectMangas.tsx'; +import { MigrationSelectDestinationSources } from '@/features/migration/screens/MigrationSelectDestinationSources.tsx'; +import { MigrationSearch } from '@/features/migration/screens/MigrationSearch.tsx'; +import { MigrationExecute } from '@/features/migration/screens/MigrationExecute.tsx'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { BrowseTab } from '@/features/browse/Browse.types.ts'; +import { useAppPageHistoryContext } from '@/base/contexts/AppPageHistoryContext.tsx'; +import { useEffect } from 'react'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; -const getMigratableSources = ( - mangas: TMigratableSourcesResult | undefined, - { sortBy, sortOrder }: SortSettings, -): TMigratableSource[] => { - if (!mangas) { - return []; - } +export const Migration = ({ tabsMenuHeight = 0 }: { tabsMenuHeight?: number }) => { + const phase = MigrationManager.usePhase(); + const { setOnBack } = useAppPageHistoryContext(); - const sourceBySourceId: Record = {}; + useEffect(() => { + if (window.location.pathname !== AppRoutes.migrate.path) { + if (!MigrationManager.isActive()) { + MigrationManager.reset(); + } else { + ReactRouter.navigate(AppRoutes.migrate.path); + } - mangas.forEach(({ sourceId, source }) => { - const uniqueSource = sourceBySourceId[sourceId] ?? { - ...{ id: sourceId, name: sourceId, lang: 'unknown', iconUrl: null, mangaCount: 0, ...source }, - }; - - sourceBySourceId[sourceId] = { - ...uniqueSource, - mangaCount: uniqueSource.mangaCount + 1, - }; - }); - - const sourcesSortedBy = Object.values(sourceBySourceId).toSorted((a, b) => { - switch (sortBy) { - case SortBy.SOURCE_NAME: - return a.name.localeCompare(b.name); - case SortBy.MANGA_COUNT: - return a.mangaCount - b.mangaCount; - default: - throw new Error(`Unexpected "sortBy" "${sortBy}"`); + return; } - }); - switch (sortOrder) { - case SortOrder.ASC: - return sourcesSortedBy; - case SortOrder.DESC: - return sourcesSortedBy.toReversed(); + setOnBack(() => MigrationManager.goToPreviousPhase()); + + return () => { + setOnBack(null); + }; + }, []); + + switch (phase) { + case MigrationPhase.IDLE: + case MigrationPhase.SELECT_SOURCE: + return ; + case MigrationPhase.SELECT_MANGAS: + return ; + case MigrationPhase.SELECTING_SOURCES: + return ; + case MigrationPhase.SEARCHING: + return ; + case MigrationPhase.MIGRATING: + return ; + // @ts-ignore - fall through + case MigrationPhase.ABORTED: + MigrationManager.reset(); + // fall through default: - throw new Error(`Unexpected "sortOrder" "${sortOrder}"`); + return ; } }; - -export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => { - const { t } = useLingui(); - const { appBarHeight } = useNavBarContext(); - - const { - settings: { migrateSortSettings }, - } = useMetadataServerSettings(); - const updateMetadataServerSettings = createUpdateMetadataServerSettings<'migrateSortSettings'>((e) => - makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)), - ); - const { sortBy, sortOrder } = migrateSortSettings; - - const { data, loading, error, refetch } = requestManager.useGetMigratableSources({ - notifyOnNetworkStatusChange: true, - }); - const migratableSources = useMemo( - () => getMigratableSources(data?.mangas.nodes, migrateSortSettings), - [data?.mangas.nodes, migrateSortSettings], - ); - - if (loading) { - return ; - } - - if (error) { - return ( - refetch().catch(defaultPromiseErrorHandler('Migration::refetch'))} - /> - ); - } - - return ( - <> - - - - updateMetadataServerSettings('migrateSortSettings', { sortBy: (sortBy + 1) % 2, sortOrder }) - } - > - {sortBy ? : } - - - - - updateMetadataServerSettings('migrateSortSettings', { - sortBy, - sortOrder: (sortOrder + 1) % 2, - }) - } - > - {sortOrder ? : } - - - - - {migratableSources.map((migratableSource) => ( - - - - ))} - - - ); -}; diff --git a/src/features/migration/screens/MigrationExecute.tsx b/src/features/migration/screens/MigrationExecute.tsx new file mode 100644 index 00000000..67df9f18 --- /dev/null +++ b/src/features/migration/screens/MigrationExecute.tsx @@ -0,0 +1,123 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { MigrationProgressBar } from '@/features/migration/components/MigrationProgressBar.tsx'; +import { useLingui } from '@lingui/react/macro'; +import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { MigrationEntryStatus } from '@/features/migration/Migration.types.ts'; +import { useMemo } from 'react'; +import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx'; +import { MigrationContinueButton } from '@/features/migration/components/MigrationContinueButton.tsx'; +import { plural } from '@lingui/core/macro'; +import { MigrationEntryGroup } from '@/features/migration/components/MIgrationEntryGroup.tsx'; + +export const MigrationExecute = () => { + const { t } = useLingui(); + const entries = MigrationManager.useEntries(); + const progress = MigrationManager.useMigrationProgress(); + + useAppTitleAndAction(MigrationManager.isPhaseComplete() ? t`Migration complete` : t`Migrating`, undefined, [ + MigrationManager.isPhaseComplete(), + ]); + + const entryList = useMemo(() => Object.values(entries), [entries]); + const migratingEntries = useMemo( + () => + entryList.filter((entry) => + [MigrationEntryStatus.PENDING, MigrationEntryStatus.MIGRATING].includes(entry.status), + ), + [entryList], + ); + const migratedEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.MIGRATION_COMPLETE), + [entryList], + ); + const failedEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.MIGRATION_FAILED), + [entryList], + ); + const excludedEntries = useMemo(() => entryList.filter((entry) => entry.isExcluded), [entryList]); + const noMatchEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.NO_MATCH), + [entryList], + ); + + return ( + <> + 0 ? ` (${progress.failed} failed)` : ''}`} + /> + + + + + + + + + + + MigrationManager.isPhaseComplete() ? MigrationManager.reset() : MigrationManager.abort() + } + /> + + ); +}; diff --git a/src/features/migration/screens/MigrationManualSearch.tsx b/src/features/migration/screens/MigrationManualSearch.tsx new file mode 100644 index 00000000..b2aebca8 --- /dev/null +++ b/src/features/migration/screens/MigrationManualSearch.tsx @@ -0,0 +1,14 @@ +/* + * 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 { SearchAll } from '@/features/global-search/screens/SearchAll.tsx'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; + +export const MigrationManualSearch = () => ( + +); diff --git a/src/features/migration/screens/MigrationSearch.tsx b/src/features/migration/screens/MigrationSearch.tsx new file mode 100644 index 00000000..0aa4d4bd --- /dev/null +++ b/src/features/migration/screens/MigrationSearch.tsx @@ -0,0 +1,115 @@ +/* + * 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 { useLingui } from '@lingui/react/macro'; +import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { MigrationProgressBar } from '@/features/migration/components/MigrationProgressBar.tsx'; +import { MigrationOptionsDialog } from '@/features/migration/components/MigrationOptionsDialog.tsx'; +import { MigrationContinueButton } from '@/features/migration/components/MigrationContinueButton.tsx'; +import { AwaitableComponent } from 'awaitable-component'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx'; +import { useMemo } from 'react'; +import Stack from '@mui/material/Stack'; +import { MigrationEntryStatus } from '@/features/migration/Migration.types.ts'; +import { MigrationEntryGroup } from '@/features/migration/components/MIgrationEntryGroup.tsx'; +import { plural } from '@lingui/core/macro'; + +export const MigrationSearch = () => { + const { t } = useLingui(); + const entries = MigrationManager.useEntries(); + const searchProgress = MigrationManager.useSearchProgress(); + + const isSearchComplete = searchProgress.completed === searchProgress.total && searchProgress.total > 0; + + useAppTitleAndAction(t`Search results`, undefined, []); + + const entryList = useMemo(() => Object.values(entries), [entries]); + const searchingEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.SEARCHING), + [entryList], + ); + const failedEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.SEARCH_FAILED), + [entryList], + ); + const noMatchEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.NO_MATCH), + [entryList], + ); + const matchedEntries = useMemo( + () => entryList.filter((entry) => entry.status === MigrationEntryStatus.SEARCH_COMPLETE), + [entryList], + ); + + const hasMigratableEntries = useMemo(() => !!MigrationManager.getMigratableEntries().length, [entryList]); + + return ( + <> + + + + + + + + + { + try { + const options = await AwaitableComponent.show(MigrationOptionsDialog); + await MigrationManager.startMigration(options); + } catch (e) { + defaultPromiseErrorHandler('MigrationSearch')(e); + } + }} + /> + + ); +}; diff --git a/src/features/migration/screens/MigrationSelectDestinationSources.tsx b/src/features/migration/screens/MigrationSelectDestinationSources.tsx new file mode 100644 index 00000000..89db7757 --- /dev/null +++ b/src/features/migration/screens/MigrationSelectDestinationSources.tsx @@ -0,0 +1,175 @@ +/* + * 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 { useLingui } from '@lingui/react/macro'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; +import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { MigrationSourceList } from '@/features/migration/components/MigrationSourceList.tsx'; +import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts'; +import type { SourceIdInfo } from '@/features/source/Source.types.ts'; +import { useCallback, useMemo } from 'react'; +import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts'; +import { arrayMove } from '@dnd-kit/sortable'; +import Fab from '@mui/material/Fab'; +import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import IconButton from '@mui/material/IconButton'; +import { SelectableCollectionSelectMode } from '@/base/collection/components/SelectableCollectionSelectMode.tsx'; +import { Sources } from '@/features/source/services/Sources.ts'; +import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; +import ToggleOnIcon from '@mui/icons-material/ToggleOn'; +import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; + +export const MigrationSelectDestinationSources = () => { + const { t } = useLingui(); + const currentSourceId = MigrationManager.useSourceId(); + + const { + settings: { browseLanguages, showNsfw }, + loading: areSettingsLoading, + request: { error: settingsError, refetch: refetchSettings }, + } = useMetadataServerSettings(); + + const { + data, + loading: areSourcesLoading, + error: sourceError, + refetch: refetchSources, + } = requestManager.useGetSourceList({ + notifyOnNetworkStatusChange: true, + }); + + const allSources = data?.sources.nodes ?? STABLE_EMPTY_ARRAY; + const sources = useMemo( + () => + Sources.filter(allSources, { + languages: browseLanguages, + isNsfw: showNsfw ? undefined : false, + }), + [allSources, browseLanguages, showNsfw], + ); + const sourceIds = useMemo(() => Sources.getIds(sources), [sources]); + const pinnedSourceIds = useMemo(() => Sources.getIds(Sources.filter(sources, { pinned: true })), [sources]); + const enabledSourceIds = useMemo(() => Sources.getIds(Sources.filter(sources, { enabled: true })), [sources]); + + const { + selectedItemIds, + areAllItemsSelected, + areNoItemsSelected, + handleSelectAll, + setSelectionForKey, + handleSelection, + } = useSelectableCollection(sources.length ?? 0, { + currentKey: 'default', + initialState: useMemo( + () => ({ + default: pinnedSourceIds, + }), + [pinnedSourceIds], + ), + }); + + useAppTitleAndAction( + t`Select destination sources`, + <> + + setSelectionForKey('default', pinnedSourceIds)}> + + + + + setSelectionForKey('default', enabledSourceIds)}> + + + + + handleSelectAll(selectAll, [...new Set([...selectedItemIds, ...sourceIds])]) + } + onModeChange={(checked) => { + handleSelectAll(checked, [...new Set([...selectedItemIds, ...sourceIds])]); + }} + /> + , + [ + setSelectionForKey, + selectedItemIds, + pinnedSourceIds, + enabledSourceIds, + areAllItemsSelected, + areNoItemsSelected, + handleSelectAll, + sourceIds, + ], + ); + + const handlePriorityChange = useCallback( + (oldIndex: number, newIndex: number) => { + setSelectionForKey('default', arrayMove(selectedItemIds, oldIndex, newIndex)); + }, + [selectedItemIds, setSelectionForKey], + ); + + const loading = areSourcesLoading || areSettingsLoading; + if (loading) { + return ; + } + + const error = settingsError ?? sourceError; + if (error) { + return ( + { + if (settingsError) { + refetchSettings().catch( + defaultPromiseErrorHandler('MigrationSelectingSources::refetchSettings'), + ); + } + + if (sourceError) { + refetchSources().catch(defaultPromiseErrorHandler('MigrationSelectingSources::refetchSources')); + } + }} + /> + ); + } + + return ( + <> + + theme.spacing(2), + right: (theme) => theme.spacing(2), + }} + onClick={() => MigrationManager.startSearch(selectedItemIds)} + > + {t`Start Search`} + + + ); +}; diff --git a/src/features/migration/screens/MigrationSelectMangas.tsx b/src/features/migration/screens/MigrationSelectMangas.tsx new file mode 100644 index 00000000..0756d941 --- /dev/null +++ b/src/features/migration/screens/MigrationSelectMangas.tsx @@ -0,0 +1,171 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useMemo } from 'react'; +import { useLingui } from '@lingui/react/macro'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; +import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; +import { BaseMangaGrid } from '@/features/manga/components/BaseMangaGrid.tsx'; +import { GridLayout } from '@/base/Base.types.ts'; +import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts'; +import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts'; +import { GridLayouts } from '@/base/components/GridLayouts.tsx'; +import { useLocalStorage } from '@/base/hooks/useStorage.tsx'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { MigrationManager } from '@/features/migration/MigrationManager.ts'; +import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts'; +import type { GetSourceMigratableQuery, GetSourceMigratableQueryVariables } from '@/lib/graphql/generated/graphql.ts'; +import { GET_SOURCE_MIGRATABLE } from '@/lib/graphql/source/SourceQuery.ts'; +import { SelectableCollectionSelectMode } from '@/base/collection/components/SelectableCollectionSelectMode.tsx'; +import { Mangas } from '@/features/manga/services/Mangas.ts'; +import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts'; +import { MigrationContinueButton } from '@/features/migration/components/MigrationContinueButton.tsx'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; + +const getSourceError = (error: unknown): unknown => { + const message = getErrorMessage(error); + + if ( + message.includes( + "The field at path '/source' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value", + ) + ) { + return null; + } + + return error; +}; + +export const MigrationSelectMangas = () => { + const { t } = useLingui(); + const sourceId = MigrationManager.useSourceId(); + const selectedMangas = MigrationManager.useEntries(); + + const [gridLayout, setGridLayout] = useLocalStorage('migrateGridLayout', GridLayout.List); + + const { + data: migratableSourceData, + loading: isSourceLoading, + error: sourceError, + refetch: refetchSource, + } = requestManager.useGetSource( + GET_SOURCE_MIGRATABLE, + sourceId ?? '', + { skip: !sourceId, notifyOnNetworkStatusChange: true }, + ); + + const { + data: migratableSourceMangasData, + loading: areMangasLoading, + error: mangasError, + refetch: refetchMangas, + } = requestManager.useGetMigratableSourceMangas(sourceId!, { + skip: !sourceId, + notifyOnNetworkStatusChange: true, + }); + + const mangas = migratableSourceMangasData?.mangas.nodes ?? STABLE_EMPTY_ARRAY; + const mangaIds = useMemo(() => Mangas.getIds(mangas), [mangas]); + + const { selectedItemIds, handleSelection, handleSelectAll, areAllItemsSelected, areNoItemsSelected } = + useSelectableCollection(mangas.length, { + itemIds: mangaIds, + currentKey: 'default', + initialState: useMemo( + () => ({ + default: Object.keys(selectedMangas).map(Number), + }), + [selectedMangas], + ), + }); + + const sourceName = migratableSourceData?.source?.displayName ?? sourceId ?? t`Migrate`; + useAppTitleAndAction( + sourceName, + <> + + handleSelectAll(selectAll, [...new Set([...selectedItemIds, ...mangaIds])])} + onModeChange={(checked) => { + handleSelectAll(checked, [...new Set([...selectedItemIds, ...mangaIds])]); + }} + /> + , + [gridLayout, setGridLayout, areAllItemsSelected, areNoItemsSelected, handleSelectAll, mangaIds], + ); + + const handleContinue = () => { + const selected = mangas.filter((manga) => selectedItemIds.includes(manga.id)); + + const [entry] = selected; + const isSingleManga = selected.length === 1; + + if (isSingleManga && entry) { + ReactRouter.navigate( + AppRoutes.migrate.childRoutes.singleMangaSearch.path(entry.sourceId, entry.id, entry.title), + { + state: { mangaTitle: entry.title }, + }, + ); + + MigrationManager.reset(); + + return; + } + + MigrationManager.selectMangas(selected); + }; + + const isLoading = isSourceLoading || areMangasLoading; + if (isLoading) { + return ; + } + + const hasError = getSourceError(sourceError) || mangasError; + if (hasError) { + const error = getSourceError(sourceError) ?? mangasError; + + return ( + { + if (getSourceError(sourceError)) { + refetchSource().catch(defaultPromiseErrorHandler('MigrationSelectMangas::refetchSource')); + } + if (mangasError) { + refetchMangas().catch(defaultPromiseErrorHandler('MigrationSelectMangas::refetchMangas')); + } + }} + /> + ); + } + + return ( + <> + + + + ); +}; diff --git a/src/features/migration/screens/MigrationSelectSource.tsx b/src/features/migration/screens/MigrationSelectSource.tsx new file mode 100644 index 00000000..1810308f --- /dev/null +++ b/src/features/migration/screens/MigrationSelectSource.tsx @@ -0,0 +1,161 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useMemo } from 'react'; +import List from '@mui/material/List'; +import Stack from '@mui/material/Stack'; +import IconButton from '@mui/material/IconButton'; +import SortByAlphaIcon from '@mui/icons-material/SortByAlpha'; +import TagIcon from '@mui/icons-material/Tag'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import { useLingui } from '@lingui/react/macro'; +import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; +import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; +import type { TMigratableSource } from '@/features/migration/components/MigrationCard.tsx'; +import { MigrationCard } from '@/features/migration/components/MigrationCard.tsx'; +import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import type { SortSettings, TMigratableSourcesResult } from '@/features/migration/Migration.types.ts'; +import { SortBy, SortOrder } from '@/features/migration/Migration.types.ts'; +import { sortByToTranslation, sortOrderToTranslation } from '@/features/migration/Migration.constants.ts'; +import { + createUpdateMetadataServerSettings, + useMetadataServerSettings, +} from '@/features/settings/services/ServerSettingsMetadata.ts'; +import { makeToast } from '@/base/utils/Toast.ts'; +import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; + +const getMigratableSources = ( + mangas: TMigratableSourcesResult | undefined, + { sortBy, sortOrder }: SortSettings, +): TMigratableSource[] => { + if (!mangas) { + return []; + } + + const sourceBySourceId: Record = {}; + + mangas.forEach(({ sourceId, source }) => { + const uniqueSource = sourceBySourceId[sourceId] ?? { + ...{ id: sourceId, name: sourceId, lang: 'unknown', iconUrl: null, mangaCount: 0, ...source }, + }; + + sourceBySourceId[sourceId] = { + ...uniqueSource, + mangaCount: uniqueSource.mangaCount + 1, + }; + }); + + const sourcesSortedBy = Object.values(sourceBySourceId).toSorted((a, b) => { + switch (sortBy) { + case SortBy.SOURCE_NAME: + return a.name.localeCompare(b.name); + case SortBy.MANGA_COUNT: + return a.mangaCount - b.mangaCount; + default: + throw new Error(`Unexpected "sortBy" "${sortBy}"`); + } + }); + + switch (sortOrder) { + case SortOrder.ASC: + return sourcesSortedBy; + case SortOrder.DESC: + return sourcesSortedBy.toReversed(); + default: + throw new Error(`Unexpected "sortOrder" "${sortOrder}"`); + } +}; + +export const MigrationSelectSource = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => { + const { t } = useLingui(); + const { appBarHeight } = useNavBarContext(); + + const { + settings: { migrateSortSettings }, + } = useMetadataServerSettings(); + const updateMetadataServerSettings = createUpdateMetadataServerSettings<'migrateSortSettings'>((e) => + makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)), + ); + const { sortBy, sortOrder } = migrateSortSettings; + + const { data, loading, error, refetch } = requestManager.useGetMigratableSources({ + notifyOnNetworkStatusChange: true, + }); + const migratableSources = useMemo( + () => getMigratableSources(data?.mangas.nodes, migrateSortSettings), + [data?.mangas.nodes, migrateSortSettings], + ); + + if (loading) { + return ; + } + + if (error) { + return ( + refetch().catch(defaultPromiseErrorHandler('Migration::refetch'))} + /> + ); + } + + return ( + <> + + + + updateMetadataServerSettings('migrateSortSettings', { sortBy: (sortBy + 1) % 2, sortOrder }) + } + > + {sortBy ? : } + + + + + updateMetadataServerSettings('migrateSortSettings', { + sortBy, + sortOrder: (sortOrder + 1) % 2, + }) + } + > + {sortOrder ? : } + + + + + {migratableSources.map((migratableSource) => ( + + + + ))} + + + ); +}; diff --git a/src/features/settings/screens/ServerSettings.tsx b/src/features/settings/screens/ServerSettings.tsx index 3433ae03..a543bebc 100644 --- a/src/features/settings/screens/ServerSettings.tsx +++ b/src/features/settings/screens/ServerSettings.tsx @@ -401,7 +401,7 @@ export const ServerSettings = () => { ), actions: { confirm: { - title: t`I understand`, + title: t`Understood`, }, }, }); diff --git a/src/features/source/services/Sources.ts b/src/features/source/services/Sources.ts index 3380d215..acff06c5 100644 --- a/src/features/source/services/Sources.ts +++ b/src/features/source/services/Sources.ts @@ -30,10 +30,33 @@ import { } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import type { SourceBaseFieldsFragment } from '@/lib/graphql/generated/graphql.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import type { DocumentNode, Unmasked } from '@apollo/client'; +import { SOURCE_BASE_FIELDS } from '@/lib/graphql/source/SourceFragments.ts'; export class Sources { static readonly LOCAL_SOURCE_ID = '0'; + static getIds(sources: SourceIdInfo[]): SourceIdInfo['id'][] { + return sources.map((source) => source.id); + } + + static getFromCache( + id: SourceIdInfo['id'], + fragment: DocumentNode = SOURCE_BASE_FIELDS, + fragmentName: string = 'SOURCE_BASE_FIELDS', + ): Unmasked | null { + return requestManager.graphQLClient.client.cache.readFragment({ + id: requestManager.graphQLClient.client.cache.identify({ + __typename: 'SourceType', + id, + }), + fragment, + fragmentName, + }); + } + static isLocalSource(source: SourceIdInfo): boolean { return source.id === Sources.LOCAL_SOURCE_ID; } diff --git a/src/i18n/locales/ar.po b/src/i18n/locales/ar.po index e89d5093..286b24d0 100644 --- a/src/i18n/locales/ar.po +++ b/src/i18n/locales/ar.po @@ -287,7 +287,7 @@ msgstr "حسب تاريخ التحميل" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -419,7 +419,7 @@ msgid "Copied to clipboard" msgstr "تم النسخ إلى الحافظة" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "نسخ" @@ -584,7 +584,7 @@ msgstr "احذف الفصل بعد وضع علامة عليه يدويًا كم msgid "Delete chapters" msgstr "حذف الفصول" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "حذف ما تم تنزيله" @@ -1134,7 +1134,7 @@ msgstr "القائمة" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrate" msgstr "يهاجر" @@ -1144,7 +1144,7 @@ msgstr "يهاجر" msgid "Migrate" msgstr "نقل" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "جاري نقل المانجا…" @@ -1503,7 +1503,7 @@ msgstr "تحديد" msgid "Select all" msgstr "تحديد الكل" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "اختر البيانات لتضمينها" @@ -1559,7 +1559,7 @@ msgid "Show continue reading button" msgstr "إظهار زر مواصلة القراءة" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "إظهار الإدخال" @@ -2855,7 +2855,7 @@ msgstr "الفئات" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "الفئة" @@ -2870,7 +2870,7 @@ msgstr "يغيّر ألوان السمة في صفحة المانغا بناءً #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx @@ -2896,7 +2896,7 @@ msgid "Clear" msgstr "مسح" #: src/features/backup/Backup.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Client data" msgstr "بيانات المستخدم" diff --git a/src/i18n/locales/de.po b/src/i18n/locales/de.po index 79f8eeb8..f23471a3 100644 --- a/src/i18n/locales/de.po +++ b/src/i18n/locales/de.po @@ -374,7 +374,7 @@ msgstr "Aufruf Timeout" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -589,7 +589,7 @@ msgid "Copied to clipboard" msgstr "In Zwischenablage kopiert" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Kopieren" @@ -876,7 +876,7 @@ msgstr "Lösche Kapitel, nach sie manuell als gelesen markiert wurden" msgid "Delete chapters" msgstr "Lösche Kapitel" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Heruntergeladene Löschen" @@ -1814,12 +1814,12 @@ msgstr "Menü" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migrieren" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Manga werden migriert…" @@ -2464,7 +2464,7 @@ msgstr "Wähle ein Gerät, um dessen am Server gespeicherten UI-Einstellungen zu msgid "Select all" msgstr "Alle auswählen" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Wähle Daten zu inkludieren" @@ -2544,7 +2544,7 @@ msgid "Show continue reading button" msgstr "Weiterlesen-Knopf anzeigen" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Eintrag anzeigen" @@ -2947,7 +2947,7 @@ msgstr "Tracker" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -3551,14 +3551,14 @@ msgstr "Auch von {0} entfernen" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "Kategorie" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx diff --git a/src/i18n/locales/en.po b/src/i18n/locales/en.po index 2f794352..6e4d239c 100644 --- a/src/i18n/locales/en.po +++ b/src/i18n/locales/en.po @@ -58,11 +58,61 @@ msgstr "{0, plural, one {# Source} other {# Sources}}" msgid "{0, plural, one {# Tracker} other {# Tracker}}" msgstr "{0, plural, one {# Tracker} other {# Tracker}}" +#. placeholder {0}: noMatchEntries.length +#: src/features/migration/screens/MigrationExecute.tsx +#: src/features/migration/screens/MigrationSearch.tsx +msgid "{0, plural, one {1 entry with no match} other {# entries with no match}}" +msgstr "{0, plural, one {1 entry with no match} other {# entries with no match}}" + +#. placeholder {0}: excludedEntries.length +#: src/features/migration/screens/MigrationExecute.tsx +msgid "{0, plural, one {1 excluded entry} other {# excluded entries}}" +msgstr "{0, plural, one {1 excluded entry} other {# excluded entries}}" + +#. placeholder {0}: failedEntries.length +#: src/features/migration/screens/MigrationExecute.tsx +#: src/features/migration/screens/MigrationSearch.tsx +msgid "{0, plural, one {1 failed entry} other {# failed entries}}" +msgstr "{0, plural, one {1 failed entry} other {# failed entries}}" + +#. placeholder {0}: matchedEntries.length +#: src/features/migration/screens/MigrationSearch.tsx +msgid "{0, plural, one {1 matched entry} other {# matched entries}}" +msgstr "{0, plural, one {1 matched entry} other {# matched entries}}" + +#. placeholder {0}: migratedEntries.length +#: src/features/migration/screens/MigrationExecute.tsx +msgid "{0, plural, one {1 migrated entry} other {# migrated entries}}" +msgstr "{0, plural, one {1 migrated entry} other {# migrated entries}}" + +#. placeholder {0}: migratingEntries.length +#: src/features/migration/screens/MigrationExecute.tsx +msgid "{0, plural, one {1 migrating entry} other {# migrating entries}}" +msgstr "{0, plural, one {1 migrating entry} other {# migrating entries}}" + +#. placeholder {0}: searchingEntries.length +#: src/features/migration/screens/MigrationSearch.tsx +msgid "{0, plural, one {1 searching} other {# searching}}" +msgstr "{0, plural, one {1 searching} other {# searching}}" + #. placeholder {0}: autoScroll.value #: src/features/reader/auto-scroll/settings/quick-setting/ReaderNavBarDesktopAutoScroll.tsx msgid "{0, plural, one {Second} other {Seconds}}" msgstr "{0, plural, one {Second} other {Seconds}}" +#. placeholder {0}: searchProgress.completed +#. placeholder {1}: searchProgress.total +#: src/features/migration/screens/MigrationSearch.tsx +msgid "{0} / {1}" +msgstr "{0} / {1}" + +#. placeholder {0}: progress.completed +#. placeholder {1}: progress.total +#. placeholder {2}: progress.failed > 0 ? ` (${progress.failed} failed)` : '' +#: src/features/migration/screens/MigrationExecute.tsx +msgid "{0} / {1}{2}" +msgstr "{0} / {1}{2}" + #. placeholder {0}: updateStatus.progress #: src/features/app-updates/components/WebUIUpdateChecker.tsx msgid "{0}% | Updating…" @@ -236,6 +286,11 @@ msgstr "{currentDownloadAheadLimit, plural, one {# Chapter} other {# Chapters}}" msgid "{missingChapterCount, plural, one {Missing # chapter} other {Missing # chapters}}" msgstr "{missingChapterCount, plural, one {Missing # chapter} other {Missing # chapters}}" +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +msgid "{otherResultsCount, plural, one {# more match} other {# more matches}}" +msgstr "{otherResultsCount, plural, one {# more match} other {# more matches}}" + #: src/features/app-updates/components/VersionInfo.tsx msgid "{progress}% | Updating…" msgstr "{progress}% | Updating…" @@ -264,6 +319,14 @@ msgstr "A preview focused web frontend built with svelte" msgid "A-Z" msgstr "A-Z" +#: src/features/migration/screens/MigrationExecute.tsx +msgid "Abort" +msgstr "Abort" + +#: src/features/migration/MigrationManager.ts +msgid "Abort migration" +msgstr "Abort migration" + #: src/features/navigation-bar/NavigationBar.constants.ts #: src/features/settings/screens/About.tsx msgid "About" @@ -359,6 +422,10 @@ msgstr "Also remove from {0}" msgid "Appearance" msgstr "Appearance" +#: src/features/migration/MigrationManager.ts +msgid "Are you sure you want to abort the migration?" +msgstr "Are you sure you want to abort the migration?" + #: src/features/chapter/services/Chapters.ts #: src/features/manga/hooks/useManageMangaLibraryState.tsx #: src/features/manga/hooks/useManageMangaLibraryState.tsx @@ -442,6 +509,10 @@ msgstr "Automatically refresh metadata" msgid "Automatically use webtoon mode for entries that are detected to likely use the long strip format" msgstr "Automatically use webtoon mode for entries that are detected to likely use the long strip format" +#: src/features/migration/components/MigrationSourceList.tsx +msgid "Available" +msgstr "Available" + #: src/features/backup/screens/Backup.tsx msgid "Back up library as a Tachiyomi backup" msgstr "Back up library as a Tachiyomi backup" @@ -604,7 +675,7 @@ msgstr "Call timeout" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -630,7 +701,7 @@ msgstr "Categories" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx msgid "Category" msgstr "Category" @@ -671,7 +742,7 @@ msgstr "Channel" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx @@ -753,12 +824,13 @@ msgid "Client" msgstr "Client" #: src/features/backup/Backup.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx msgid "Client data" msgstr "Client data" #: src/base/components/feedback/SnackbarWithDescription.tsx #: src/features/app-updates/components/VersionUpdateInfoDialog.tsx +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx msgid "Close" msgstr "Close" @@ -812,6 +884,10 @@ msgstr "Connected as {currentUsername} to {currentServerAddress}" msgid "Connected as {initialUsername} to {initialServerAddress}" msgstr "Connected as {initialUsername} to {initialServerAddress}" +#: src/features/migration/components/MigrationContinueButton.tsx +msgid "Continue" +msgstr "Continue" + #: src/features/manga/components/ContinueReadingTooltip.tsx msgid "" "Continue reading\n" @@ -853,7 +929,7 @@ msgid "Copied to clipboard" msgstr "Copied to clipboard" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx msgid "Copy" msgstr "Copy" @@ -1067,6 +1143,10 @@ msgstr "Creating theme…" msgid "Crimson" msgstr "Crimson" +#: src/features/migration/components/MigrationSourceList.tsx +msgid "Current source" +msgstr "Current source" + #: src/features/reader/viewer/components/ReaderTransitionPage.tsx msgid "Current:" msgstr "Current:" @@ -1193,7 +1273,7 @@ msgstr "Delete chapter after manually marking it as read" msgid "Delete chapters" msgstr "Delete chapters" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx msgid "Delete downloaded" msgstr "Delete downloaded" @@ -1233,6 +1313,11 @@ msgstr "Deselect" msgid "Desktop" msgstr "Desktop" +#. placeholder {0}: entry.sourceTitle +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +msgid "Destination - {0}" +msgstr "Destination - {0}" + #: src/features/device/screens/DeviceSetting.tsx #: src/features/settings/screens/Settings.tsx msgid "Device" @@ -1292,6 +1377,10 @@ msgstr "Does not work when marking multiple manga as read at once" msgid "Don't show this dialog again" msgstr "Don't show this dialog again" +#: src/features/migration/screens/MigrationExecute.tsx +msgid "Done" +msgstr "Done" + #: src/features/reader/settings/ReaderSettings.constants.tsx msgid "Double page" msgstr "Double page" @@ -1352,6 +1441,10 @@ msgstr "Downloading" msgid "Downloads" msgstr "Downloads" +#: src/features/migration/components/MigrationSourceList.tsx +msgid "Drag to prioritize" +msgstr "Drag to prioritize" + #: src/features/theme/Themes.ts msgid "Dune" msgstr "Dune" @@ -1431,6 +1524,11 @@ msgstr "Error" msgid "Example for possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)" msgstr "Example for possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)" +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +msgid "Exclude" +msgstr "Exclude" + #: src/features/chapter/components/ChapterExcludeSanlatorsFilter.tsx msgid "Exclude scanlators" msgstr "Exclude scanlators" @@ -1440,6 +1538,10 @@ msgstr "Exclude scanlators" msgid "Exclude: {excludedCategoriesText}" msgstr "Exclude: {excludedCategoriesText}" +#: src/features/migration/Migration.constants.ts +msgid "Excluded" +msgstr "Excluded" + #: src/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx #: src/features/reader/overlay/navigation/components/ReaderExitButton.tsx msgid "Exit reader" @@ -1495,7 +1597,7 @@ msgstr "Failed to disconnect from KOReader Sync server." #: src/features/global-search/screens/SearchAll.tsx #: src/features/history/screens/HistorySettings.tsx #: src/features/library/components/LibraryOptionsPanel.tsx -#: src/features/migration/screens/Migration.tsx +#: src/features/migration/screens/MigrationSelectSource.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettings.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/settings/components/webUI/WebUIUpdateIntervalSetting.tsx @@ -1742,10 +1844,6 @@ msgstr "How many chapters should get downloaded while reading." msgid "How much time FlareSolverr has to handle the request" msgstr "How much time FlareSolverr has to handle the request" -#: src/features/settings/screens/ServerSettings.tsx -msgid "I understand" -msgstr "I understand" - #: src/features/app-updates/components/VersionUpdateInfoDialog.tsx msgid "Ignore" msgstr "Ignore" @@ -1803,6 +1901,11 @@ msgstr "" msgid "In Library" msgstr "In Library" +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +msgid "Include" +msgstr "Include" + #: src/features/backup/screens/Backup.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx msgid "Include: {includedCategoriesText}" @@ -1988,6 +2091,10 @@ msgstr "Latest fetched chapter" msgid "Latest uploaded chapter" msgstr "Latest uploaded chapter" +#: src/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx +msgid "Latest: {latestChapterNumber}" +msgstr "Latest: {latestChapterNumber}" + #: src/features/theme/Themes.ts msgid "Lavender" msgstr "Lavender" @@ -2150,6 +2257,13 @@ msgstr "Manhua" msgid "Manhwa" msgstr "Manhwa" +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +#: src/features/migration/components/migration-entry/MigrationEntry.tsx +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +#: src/features/migration/components/migration-entry/MigrationEntrySearchExcludeActions.tsx +msgid "Manual search" +msgstr "Manual search" + #: src/features/chapter/components/ChaptersToolbarMenu.tsx msgid "Mark all as read" msgstr "Mark all as read" @@ -2178,6 +2292,15 @@ msgstr "Mark selected as read" msgid "Mark selected as unread" msgstr "Mark selected as unread" +#: src/features/migration/Migration.constants.ts +msgid "Match found" +msgstr "Match found" + +#: src/features/migration/components/migration-entry/MigrationEntry.tsx +#: src/features/migration/components/migration-entry/MigrationEntry.tsx +msgid "Matches" +msgstr "Matches" + #: src/features/settings/screens/ServerSettings.tsx msgid "Maximum log file size" msgstr "Maximum log file size" @@ -2199,8 +2322,8 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx -#: src/features/migration/screens/Migrate.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx +#: src/features/migration/screens/MigrationSelectMangas.tsx msgid "Migrate" msgstr "Migrate" @@ -2209,10 +2332,39 @@ msgstr "Migrate" msgid "Migrate \"{0}\"" msgstr "Migrate \"{0}\"" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/screens/MigrationExecute.tsx +msgid "Migrating" +msgstr "Migrating" + +#: src/features/manga/components/cards/MangaCard.tsx msgid "Migrating manga…" msgstr "Migrating manga…" +#: src/features/migration/Migration.constants.ts +msgid "Migrating…" +msgstr "Migrating…" + +#: src/features/migration/screens/MigrationExecute.tsx +msgid "Migration complete" +msgstr "Migration complete" + +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +#: src/features/migration/Migration.constants.ts +msgid "Migration failed" +msgstr "Migration failed" + +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +msgid "Migration for \"{title}\" failed with error:" +msgstr "Migration for \"{title}\" failed with error:" + +#: src/features/migration/MigrationManager.ts +msgid "Migration information" +msgstr "Migration information" + +#: src/features/migration/components/MigrationOptionsDialog.tsx +msgid "Migration options" +msgstr "Migration options" + #: src/features/settings/components/images/Processing.tsx msgid "MIME-Type" msgstr "MIME-Type" @@ -2314,6 +2466,10 @@ msgstr "No manga found" msgid "No manga matches this filter" msgstr "No manga matches this filter" +#: src/features/migration/Migration.constants.ts +msgid "No match found" +msgstr "No match found" + #: src/features/reader/viewer/ReaderChapterViewer.tsx msgid "No pages found" msgstr "No pages found" @@ -2497,6 +2653,10 @@ msgstr "Password" msgid "Paused — {count} remaining" msgstr "Paused — {count} remaining" +#: src/features/migration/Migration.constants.ts +msgid "Pending…" +msgstr "Pending…" + #: src/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx msgid "Percentage Tolerance" msgstr "Percentage Tolerance" @@ -2735,6 +2895,7 @@ msgstr "Resume" #: src/base/components/SpinnerImage.tsx #: src/features/chapter/components/buttons/ChapterDownloadRetryButton.tsx #: src/features/manga/hooks/useManageMangaLibraryState.tsx +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx msgid "Retry" msgstr "Retry" @@ -2825,10 +2986,19 @@ msgstr "Scroll forward" msgid "Search" msgstr "Search" +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +#: src/features/migration/Migration.constants.ts +msgid "Search failed" +msgstr "Search failed" + #: src/features/library/screens/Library.tsx msgid "Search for \"{query}\" globally" msgstr "Search for \"{query}\" globally" +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx +msgid "Search for \"{title}\" failed with error:" +msgstr "Search for \"{title}\" failed with error:" + #: src/features/tracker/components/TrackerSearch.tsx msgid "" "Search for a ID via \"id:\" (e.g. \"id:13\")\n" @@ -2846,10 +3016,18 @@ msgstr "Search parameters" msgid "Search parameters ({0})" msgstr "Search parameters ({0})" +#: src/features/migration/screens/MigrationSearch.tsx +msgid "Search results" +msgstr "Search results" + #: src/features/library/screens/LibrarySettings.tsx msgid "Search results will include manga that do not match the current filters" msgstr "Search results will include manga that do not match the current filters" +#: src/features/migration/Migration.constants.ts +msgid "Searching…" +msgstr "Searching…" + #: src/features/settings/components/images/Processing.tsx #: src/features/settings/components/images/Processing.tsx #: src/features/settings/screens/ServerSettings.tsx @@ -2872,6 +3050,7 @@ msgstr "See the official <0>MUI documentation for how to customize the theme #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/manga/components/MangaActionMenuItems.tsx #: src/features/manga/components/MangaOptionButton.tsx +#: src/features/migration/components/migration-entry/MigrationMatchedEntry.tsx msgid "Select" msgstr "Select" @@ -2885,9 +3064,21 @@ msgstr "Select a device to use its server stored UI settings" msgid "Select all" msgstr "Select all" -#: src/features/migration/components/MigrateDialog.tsx -msgid "Select data to include" -msgstr "Select data to include" +#: src/features/migration/screens/MigrationSelectDestinationSources.tsx +msgid "Select destination sources" +msgstr "Select destination sources" + +#: src/features/migration/screens/MigrationSelectDestinationSources.tsx +msgid "Select enabled sources" +msgstr "Select enabled sources" + +#: src/features/migration/screens/MigrationSelectDestinationSources.tsx +msgid "Select pinned sources" +msgstr "Select pinned sources" + +#: src/features/migration/components/MigrationSourceList.tsx +msgid "Selected" +msgstr "Selected" #: src/features/reader/filters/settings/components/ReaderSettingSepia.tsx msgid "Sepia" @@ -2976,7 +3167,7 @@ msgid "Show continue reading button" msgstr "Show continue reading button" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx msgid "Show entry" msgstr "Show entry" @@ -2992,6 +3183,7 @@ msgstr "Show less" #: src/base/components/feedback/EmptyView.tsx #: src/base/components/feedback/SnackbarWithDescription.tsx #: src/features/global-search/screens/SearchAll.tsx +#: src/features/migration/components/migration-entry/MigrationDestinationEntry.tsx #: src/features/tracker/components/cards/TrackerMangaCard.tsx msgid "Show more" msgstr "Show more" @@ -3126,6 +3318,11 @@ msgstr "Source" msgid "Source Configuration" msgstr "Source Configuration" +#: src/features/migration/components/migration-entry/MigrationSourceEntry.tsx +#: src/features/migration/components/migration-entry/MigrationSourceEntry.tsx +msgid "Source entry - {sourceTitle}" +msgstr "Source entry - {sourceTitle}" + #: src/features/chapter/components/ChapterOptions.tsx msgid "Source title" msgstr "Source title" @@ -3147,6 +3344,10 @@ msgstr "Start" msgid "Start date" msgstr "Start date" +#: src/features/migration/screens/MigrationSearch.tsx +msgid "Start migration" +msgstr "Start migration" + #: src/features/manga/components/ContinueReadingTooltip.tsx msgid "" "Start reading\n" @@ -3157,6 +3358,10 @@ msgstr "" "#{chapterNumber} — {name}\n" "{scanlator}" +#: src/features/migration/screens/MigrationSelectDestinationSources.tsx +msgid "Start Search" +msgstr "Start Search" + #: src/features/library/components/LibraryOptionsPanel.tsx #: src/features/tracker/components/cards/TrackerMangaCard.tsx msgid "Started" @@ -3195,6 +3400,10 @@ msgstr "Submit" msgid "Success" msgstr "Success" +#: src/features/migration/Migration.constants.ts +msgid "Successfully migrated" +msgstr "Successfully migrated" + #: src/features/manga/Manga.constants.ts msgid "Successfully migrated manga" msgstr "Successfully migrated manga" @@ -3264,6 +3473,16 @@ msgstr "The login will be handled by the client." msgid "The login will be handled by the server." msgstr "The login will be handled by the server." +#: src/features/migration/MigrationManager.ts +msgid "" +"The migration runs on the client on the current device, NOT the server.\n" +"As long as the client is open, the migration will run in the background.\n" +"The client can be closed. The migration will be resumed once it gets opened again on the same device it got started on." +msgstr "" +"The migration runs on the client on the current device, NOT the server.\n" +"As long as the client is open, the migration will run in the background.\n" +"The client can be closed. The migration will be resumed once it gets opened again on the same device it got started on." + #: src/features/reader/services/ReaderControls.ts msgid "" "The next chapter has a different scanlator then the current one.\n" @@ -3408,7 +3627,7 @@ msgstr "Trackers" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationOptionsDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -3444,8 +3663,8 @@ msgstr "UI Login" #: src/features/library/screens/LibrarySettings.tsx #: src/features/manga/hooks/useManageMangaLibraryState.tsx #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/screens/Migrate.tsx -#: src/features/migration/screens/Migration.tsx +#: src/features/migration/screens/MigrationSelectMangas.tsx +#: src/features/migration/screens/MigrationSelectSource.tsx #: src/features/reader/screens/Reader.tsx #: src/features/reader/settings/screens/GlobalReaderSettings.tsx #: src/features/reader/viewer/ReaderChapterViewer.tsx @@ -3465,6 +3684,15 @@ msgstr "UI Login" msgid "Unable to load data" msgstr "Unable to load data" +#: src/features/migration/screens/MigrationSelectDestinationSources.tsx +msgid "Unable to load sources" +msgstr "Unable to load sources" + +#: src/features/migration/MigrationManager.ts +#: src/features/settings/screens/ServerSettings.tsx +msgid "Understood" +msgstr "Understood" + #: src/features/extension/Extensions.constants.ts msgid "Uninstall" msgstr "Uninstall" @@ -3475,6 +3703,7 @@ msgstr "Uninstalling" #: src/features/manga/components/details/MangaDetails.tsx #: src/features/manga/Manga.constants.ts +#: src/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx #: src/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx #: src/features/tracker/Tracker.constants.ts msgid "Unknown" diff --git a/src/i18n/locales/es.po b/src/i18n/locales/es.po index 1398f47b..6557101e 100644 --- a/src/i18n/locales/es.po +++ b/src/i18n/locales/es.po @@ -370,7 +370,7 @@ msgstr "Por fecha de subida" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -562,7 +562,7 @@ msgid "Copied to clipboard" msgstr "Copiado al portapapeles" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Copiar" @@ -848,7 +848,7 @@ msgstr "Borrar el capítulo después de marcarlo manualmente como leído" msgid "Delete chapters" msgstr "Borrar los capítulos" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Eliminar descargados" @@ -1770,12 +1770,12 @@ msgstr "Menú" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migración" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Migrando datos…" @@ -2418,7 +2418,7 @@ msgstr "Selecciona un dispositivo para usar su configuración de interfaz almace msgid "Select all" msgstr "Seleccionar todo" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Seleccionar datos a incluir" @@ -2498,7 +2498,7 @@ msgid "Show continue reading button" msgstr "Mostrar botón de seguir leyendo" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Mostrar entrada" @@ -2899,7 +2899,7 @@ msgstr "Rastreadores" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/fa.po b/src/i18n/locales/fa.po index 5cdc343c..b7040bbf 100644 --- a/src/i18n/locales/fa.po +++ b/src/i18n/locales/fa.po @@ -373,7 +373,7 @@ msgstr "محدودیت زمانی فراخوانی" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -586,7 +586,7 @@ msgid "Copied to clipboard" msgstr "در کلیپ‌ بورد کپی شد" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "کپی" @@ -872,7 +872,7 @@ msgstr "حذف چپتر بعد از علامت‌ گذاری دستی به‌ ع msgid "Delete chapters" msgstr "حذف چپتر ها" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "حذف دانلودشده‌ها" @@ -1807,12 +1807,12 @@ msgstr "منو" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "انتقال" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "در حال انتقال مانگا…" @@ -2439,7 +2439,7 @@ msgstr "دستگاهی را برای استفاده از تنظیمات رابط msgid "Select all" msgstr "انتخاب همه" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "انتخاب داده‌ برای لحاظ شدن" @@ -2519,7 +2519,7 @@ msgid "Show continue reading button" msgstr "نمایش دکمه ادامه خواندن" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "نمایش محتوا" @@ -2925,7 +2925,7 @@ msgstr "ردیاب‌ها" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/fil.po b/src/i18n/locales/fil.po index 0ef23e97..da6842af 100644 --- a/src/i18n/locales/fil.po +++ b/src/i18n/locales/fil.po @@ -90,7 +90,7 @@ msgstr "Naka-bookmark" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -139,7 +139,7 @@ msgid "Convert images to different formats" msgstr "I-convert ang mga larawan sa iba't ibang mga format" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Kopyahin" @@ -498,7 +498,7 @@ msgstr "Markahan ang napili bilang hindi pa nababasa" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrate" msgstr "Mag-migrate" diff --git a/src/i18n/locales/fr.po b/src/i18n/locales/fr.po index 1002afad..c52ef4d5 100644 --- a/src/i18n/locales/fr.po +++ b/src/i18n/locales/fr.po @@ -365,7 +365,7 @@ msgstr "Par date de mise en ligne" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -534,7 +534,7 @@ msgid "Copied to clipboard" msgstr "Copié dans le presse-papiers" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Copier" @@ -811,7 +811,7 @@ msgstr "Supprimer les chapitres après les avoir manuellement marqués comme lus msgid "Delete chapters" msgstr "Supprimer les chapitres" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Supprimer les téléchargements" @@ -1658,12 +1658,12 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migrer" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Migration des mangas…" @@ -2287,7 +2287,7 @@ msgstr "Sélectionnez un appareil pour utiliser ses paramètres d'interface util msgid "Select all" msgstr "Tout sélectionner" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Sélectionner les données à inclure" @@ -2344,7 +2344,7 @@ msgid "Show continue reading button" msgstr "Afficher le bouton de reprise de lecture" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Afficher l'entrée" diff --git a/src/i18n/locales/he.po b/src/i18n/locales/he.po index 04fc78db..5bae3fe4 100644 --- a/src/i18n/locales/he.po +++ b/src/i18n/locales/he.po @@ -643,7 +643,7 @@ msgstr "הבקשה נכשלה (Timeout)" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -669,7 +669,7 @@ msgstr "קטגוריות" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "קטגורייה" @@ -706,7 +706,7 @@ msgstr "ערוץ" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx @@ -894,7 +894,7 @@ msgid "Copied to clipboard" msgstr "הועתק ללוח" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "העתק" @@ -1225,7 +1225,7 @@ msgstr "שרתי מעקב" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -1277,7 +1277,7 @@ msgstr "בחירה" msgid "Select all" msgstr "בחירת הכל" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "מחיקת פרקים שהורדו" diff --git a/src/i18n/locales/hu.po b/src/i18n/locales/hu.po index 457b9117..66e1a5f5 100644 --- a/src/i18n/locales/hu.po +++ b/src/i18n/locales/hu.po @@ -657,7 +657,7 @@ msgstr "Feltöltés ideje szerint" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -683,7 +683,7 @@ msgstr "Kategóriák" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "Kategória" @@ -720,7 +720,7 @@ msgstr "Csatorna" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx @@ -877,7 +877,7 @@ msgid "Copied to clipboard" msgstr "Másolva a vágólapra" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Másolás" @@ -1202,7 +1202,7 @@ msgstr "Fejezet törlése olvasottnak jelölés után" msgid "Delete chapters" msgstr "Fejezetek törlése" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Letöltöttek törlése" @@ -2058,7 +2058,7 @@ msgstr "Menü" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Átmozgatás" @@ -2068,7 +2068,7 @@ msgstr "Átmozgatás" msgid "Migrate \"{0}\"" msgstr "\"{0}\" átmozgatása" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Manga átmozgatás alatt…" @@ -2781,7 +2781,7 @@ msgid "Show only downloaded chapters" msgstr "Csak a letöltött fejezetek megjelenítése" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Elem megjelenítése" @@ -3150,7 +3150,7 @@ msgstr "Követők" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/id.po b/src/i18n/locales/id.po index 9c9b01ac..82692d71 100644 --- a/src/i18n/locales/id.po +++ b/src/i18n/locales/id.po @@ -235,7 +235,7 @@ msgstr "Berdasarkan tanggal diunggah" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -351,7 +351,7 @@ msgid "Copied to clipboard" msgstr "Disalin ke papan klip" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Salin" @@ -497,7 +497,7 @@ msgstr "Hapus bab setelah menandainya secara manual sebagai telah dibaca" msgid "Delete chapters" msgstr "Hapus bab" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Hapus yang diunduh" @@ -1014,7 +1014,7 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrate" msgstr "Bermigasi" @@ -1024,7 +1024,7 @@ msgstr "Bermigasi" msgid "Migrate" msgstr "Migrasi" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Memindahkan manga…" @@ -1388,7 +1388,7 @@ msgstr "Pilih" msgid "Select all" msgstr "Pilih Semua" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Pilih data untuk disertakan" @@ -1409,7 +1409,7 @@ msgid "Show continue reading button" msgstr "Tampilkan tombol lanjutkan membaca" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Tampilkan entri" diff --git a/src/i18n/locales/it.po b/src/i18n/locales/it.po index 491131e7..48ef34a5 100644 --- a/src/i18n/locales/it.po +++ b/src/i18n/locales/it.po @@ -368,7 +368,7 @@ msgstr "Timeout della chiamata" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -560,7 +560,7 @@ msgid "Copied to clipboard" msgstr "Copiato negli appunti" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Copia" @@ -846,7 +846,7 @@ msgstr "Cancella un capitolo dopo averlo manualmente segnato come letto" msgid "Delete chapters" msgstr "Cancella capitoli" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Cancellare gli scaricati" @@ -1731,12 +1731,12 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migrare" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Migrazione dei manga…" @@ -2369,7 +2369,7 @@ msgstr "Selezionare un dispositivo per utilizzare le impostazioni dell'interfacc msgid "Select all" msgstr "Seleziona tutto" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Selezionare i dati da includere" @@ -2445,7 +2445,7 @@ msgid "Show continue reading button" msgstr "Mostra il pulsante Continua a leggere" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Mostra la voce" @@ -2839,7 +2839,7 @@ msgstr "Trackers" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/ja.po b/src/i18n/locales/ja.po index 74908faa..d278a835 100644 --- a/src/i18n/locales/ja.po +++ b/src/i18n/locales/ja.po @@ -361,7 +361,7 @@ msgstr "アップロード日順" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -526,7 +526,7 @@ msgid "Copied to clipboard" msgstr "クリップボードにコピーしました" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "コピー" @@ -795,7 +795,7 @@ msgstr "手動で既読にした後に章を削除" msgid "Delete chapters" msgstr "章を削除" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "ダウンロード済を削除" @@ -1608,12 +1608,12 @@ msgstr "メニュー" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "移行" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "マンガを移行中…" @@ -2229,7 +2229,7 @@ msgstr "サーバーに保存された UI 設定を使用するデバイスを msgid "Select all" msgstr "すべて選択" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "含めるデータを選択" @@ -2286,7 +2286,7 @@ msgid "Show continue reading button" msgstr "続きを読むボタンを表示" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "エントリーを表示" @@ -3020,14 +3020,14 @@ msgstr "カテゴリー" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "カテゴリー" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx @@ -3038,7 +3038,7 @@ msgstr "章" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/ko.po b/src/i18n/locales/ko.po index 088c04d4..9524153f 100644 --- a/src/i18n/locales/ko.po +++ b/src/i18n/locales/ko.po @@ -373,7 +373,7 @@ msgstr "호출 시간 초과" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -586,7 +586,7 @@ msgid "Copied to clipboard" msgstr "클립보드에 복사됨" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "복사" @@ -872,7 +872,7 @@ msgstr "읽음으로 표시하면 챕터 삭제" msgid "Delete chapters" msgstr "챕터 삭제" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "다운로드된 항목 삭제" @@ -1807,12 +1807,12 @@ msgstr "메뉴" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "마이그레이션" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "만화 마이그레이션 중…" @@ -2449,7 +2449,7 @@ msgstr "서버에 저장된 UI 설정을 사용할 기기를 선택하세요" msgid "Select all" msgstr "모두 선택" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "포함할 데이터 선택" @@ -2525,7 +2525,7 @@ msgid "Show continue reading button" msgstr "계속 읽기 버튼 표시" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "항목 표시" @@ -2931,7 +2931,7 @@ msgstr "추적" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/nl.po b/src/i18n/locales/nl.po index e1e78c9e..4b805c5a 100644 --- a/src/i18n/locales/nl.po +++ b/src/i18n/locales/nl.po @@ -201,7 +201,7 @@ msgstr "Op uploaddatum" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -285,7 +285,7 @@ msgid "Copied to clipboard" msgstr "Gekopieerd naar klembord" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Kopiëren" @@ -413,7 +413,7 @@ msgstr "Verwijder het hoofdstuk nadat hij handmatig als gelezen is gemarkeerd" msgid "Delete chapters" msgstr "Verwijder hoofdstukken" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Verwijder downloads" @@ -877,7 +877,7 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrate" msgstr "Migreren" @@ -887,7 +887,7 @@ msgstr "Migreren" msgid "Migrate" msgstr "Migreren" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Manga migreren…" @@ -1197,7 +1197,7 @@ msgstr "Selecteer" msgid "Select all" msgstr "Alles selecteren" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Selecteer gegevens die u wilt opnemen" @@ -1218,7 +1218,7 @@ msgid "Show continue reading button" msgstr "Knop doorgaan met lezen tonen" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Invoer tonen" diff --git a/src/i18n/locales/pl.po b/src/i18n/locales/pl.po index e2fdb690..dc85fd69 100644 --- a/src/i18n/locales/pl.po +++ b/src/i18n/locales/pl.po @@ -362,7 +362,7 @@ msgstr "Według daty przesłania" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -527,7 +527,7 @@ msgid "Copied to clipboard" msgstr "Skopiowano do schowka" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Kopiuj" @@ -796,7 +796,7 @@ msgstr "Usuń rozdział po ręcznym oznaczeniu go jako przeczytany" msgid "Delete chapters" msgstr "Usuń rozdziały" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Usuń pobrane" @@ -1617,12 +1617,12 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migruj" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Migruję mangę…" @@ -2238,7 +2238,7 @@ msgstr "Wybierz urządzenie, aby użyć jego ustawień interfejsu użytkownika z msgid "Select all" msgstr "Wybierz wszystko" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Wybierz dane do uwzględnienia" @@ -2295,7 +2295,7 @@ msgid "Show continue reading button" msgstr "Pokaż przycisk kontynuowania czytania" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Pokaż pozycję" @@ -3152,7 +3152,7 @@ msgstr "" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -3192,14 +3192,14 @@ msgstr "Kategorie" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "Kategoria" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx @@ -3217,7 +3217,7 @@ msgid "Clear" msgstr "Wyczyść" #: src/features/backup/Backup.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Client data" msgstr "Dane klienta" diff --git a/src/i18n/locales/pt-BR.po b/src/i18n/locales/pt-BR.po index 594ab2ed..338c7357 100644 --- a/src/i18n/locales/pt-BR.po +++ b/src/i18n/locales/pt-BR.po @@ -325,7 +325,7 @@ msgstr "Por data de upload" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -474,7 +474,7 @@ msgid "Copied to clipboard" msgstr "Copiado para área de transferencia" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Copiar" @@ -723,7 +723,7 @@ msgstr "Apagar capítulo depois de marcar manualmente como lido" msgid "Delete chapters" msgstr "Apagar capítulos" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Remover baixados" @@ -1443,12 +1443,12 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migrar" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Migrando mangá…" @@ -1994,7 +1994,7 @@ msgstr "Selecione um dispositivo para usar suas configurações de interface do msgid "Select all" msgstr "Selecionar todos" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Selecionar dados para incluir" @@ -2044,7 +2044,7 @@ msgid "Show continue reading button" msgstr "Mostrar botão de continuar leitura" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Mostrar entrada" diff --git a/src/i18n/locales/pt.po b/src/i18n/locales/pt.po index ed491192..34c6c03f 100644 --- a/src/i18n/locales/pt.po +++ b/src/i18n/locales/pt.po @@ -344,7 +344,7 @@ msgstr "Por data do envio" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -497,7 +497,7 @@ msgid "Copied to clipboard" msgstr "Copiado para a área de transferência" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Copiar" @@ -766,7 +766,7 @@ msgstr "Deletar o capítulo após marcá-lo manualmente como lido" msgid "Delete chapters" msgstr "Deletar capítulos" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Excluir baixado" @@ -1536,12 +1536,12 @@ msgstr "Menu" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Migrar" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Migrando manga…" @@ -2129,7 +2129,7 @@ msgstr "Selecione um dispositivo para usar suas configurações de interface do msgid "Select all" msgstr "Selecionar todos" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Selecione dados para incluir" @@ -2182,7 +2182,7 @@ msgid "Show continue reading button" msgstr "Mostrar o botão de continue lendo" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Mostrar entrada" diff --git a/src/i18n/locales/ru.po b/src/i18n/locales/ru.po index f4a76bab..3fff768d 100644 --- a/src/i18n/locales/ru.po +++ b/src/i18n/locales/ru.po @@ -379,7 +379,7 @@ msgstr "Тайм-аут запроса" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -595,7 +595,7 @@ msgid "Copied to clipboard" msgstr "Скопированно в буфер обмена" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Скопировать" @@ -881,7 +881,7 @@ msgstr "Удаление главы после ручной пометки ее msgid "Delete chapters" msgstr "Удалять главы" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Удалить загруженное" @@ -1823,12 +1823,12 @@ msgstr "Меню" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Мигрировать" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Миграция манги…" @@ -2474,7 +2474,7 @@ msgstr "Выберите устройство, чтобы использоват msgid "Select all" msgstr "Выбрать все" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Выберите данные для включения" @@ -2554,7 +2554,7 @@ msgid "Show continue reading button" msgstr "Кнопка продолжить чтение" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Показать записи" @@ -2959,7 +2959,7 @@ msgstr "Отслеживания" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -3531,14 +3531,14 @@ msgstr "" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "Категория" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx diff --git a/src/i18n/locales/ta.po b/src/i18n/locales/ta.po index 11af269b..c22f8f61 100644 --- a/src/i18n/locales/ta.po +++ b/src/i18n/locales/ta.po @@ -360,7 +360,7 @@ msgstr "பதிவேற்றுவதன் மூலம்" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -525,7 +525,7 @@ msgid "Copied to clipboard" msgstr "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "நகலெடு" @@ -794,7 +794,7 @@ msgstr "அத்தியாயத்தை கைமுறையாக வா msgid "Delete chapters" msgstr "அத்தியாயங்களை நீக்கு" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "பதிவிறக்கம் செய்யப்பட்டதை நீக்கு" @@ -1607,12 +1607,12 @@ msgstr "பட்டியல்" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "இடம்பெயர்வு" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "இடம்பெயரும் மங்கா…" @@ -2228,7 +2228,7 @@ msgstr "அதன் சேவையக சேமிக்கப்பட்ட msgid "Select all" msgstr "அனைத்தையும் தெரிவுசெய்" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "சேர்க்க தரவைத் தேர்ந்தெடுக்கவும்" @@ -2285,7 +2285,7 @@ msgid "Show continue reading button" msgstr "தொடர்ந்து வாசிப்பு பொத்தானைக் காட்டு" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "நுழைவு காட்டு" diff --git a/src/i18n/locales/th.po b/src/i18n/locales/th.po index 45df0397..3b01a764 100644 --- a/src/i18n/locales/th.po +++ b/src/i18n/locales/th.po @@ -128,7 +128,7 @@ msgstr "ค้นหา" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -184,7 +184,7 @@ msgid "Copied to clipboard" msgstr "คัดลอกไปยังคลิปบอร์ดแล้ว" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "คัดลอก" @@ -591,7 +591,7 @@ msgstr "เมนู" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrate" msgstr "ย้าย" diff --git a/src/i18n/locales/tr.po b/src/i18n/locales/tr.po index 296d233c..dc29c78e 100644 --- a/src/i18n/locales/tr.po +++ b/src/i18n/locales/tr.po @@ -329,7 +329,7 @@ msgstr "Çağrı zaman aşımı" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -487,7 +487,7 @@ msgid "Copied to clipboard" msgstr "Panoya kopyalandı" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Kopyala" @@ -720,7 +720,7 @@ msgstr "Okundu olarak işaretlendikten sonra bölümü sil" msgid "Delete chapters" msgstr "Bölümleri sil" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "İndirilenleri sil" @@ -1464,12 +1464,12 @@ msgstr "Menü" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Taşı" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Manga taşınıyor…" @@ -2004,7 +2004,7 @@ msgstr "Sunucusunda depolanan kullanıcı arayüzü ayarlarını kullanmak için msgid "Select all" msgstr "Hepsini seç" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Dahil edilecek verileri seçin" @@ -2049,7 +2049,7 @@ msgid "Show continue reading button" msgstr "Okumaya devam et düğmesini göster" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Öğeyi göster" @@ -2800,14 +2800,14 @@ msgstr "Kategoriler" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "Kategori" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx diff --git a/src/i18n/locales/uk.po b/src/i18n/locales/uk.po index 00e6d81b..fdb243c9 100644 --- a/src/i18n/locales/uk.po +++ b/src/i18n/locales/uk.po @@ -224,7 +224,7 @@ msgstr "За джерелом" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -317,7 +317,7 @@ msgid "Continuous vertical" msgstr "Вертикальний безперервний" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Скопіювати" @@ -910,7 +910,7 @@ msgstr "Позначити вибране як непрочитане" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrate" msgstr "Мігрувати" @@ -920,7 +920,7 @@ msgstr "Мігрувати" msgid "Migrate" msgstr "Мігрувати" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Міграція манґи…" @@ -1198,7 +1198,7 @@ msgstr "Виберіть пристрій для використання йог msgid "Select all" msgstr "Вибрати все" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Вибрати дані, які потрібно включити" @@ -1246,7 +1246,7 @@ msgid "Show continue reading button" msgstr "Показувати кнопку продовження читання" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Показати елемент" diff --git a/src/i18n/locales/vi.po b/src/i18n/locales/vi.po index bda75d7a..f615225d 100644 --- a/src/i18n/locales/vi.po +++ b/src/i18n/locales/vi.po @@ -374,7 +374,7 @@ msgstr "Hết thời gian chờ" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -587,7 +587,7 @@ msgid "Copied to clipboard" msgstr "Đã sao chép vào bảng nhớ tạm" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "Sao chép" @@ -873,7 +873,7 @@ msgstr "Xóa chương sau khi được đánh dấu là đã đọc" msgid "Delete chapters" msgstr "Xóa chương" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "Xóa tải về" @@ -1808,12 +1808,12 @@ msgstr "Danh sách" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "Di chuyển" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "Di chuyển truyện…" @@ -2460,7 +2460,7 @@ msgstr "Chọn một thiết bị để sử dụng các cài đặt cá nhân msgid "Select all" msgstr "Chọn tất cả" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "Lụa chọn dữ liệu để thêm vào" @@ -2540,7 +2540,7 @@ msgid "Show continue reading button" msgstr "Hiển thị nút tiếp tục đọc" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "Hiển thị mục" @@ -2946,7 +2946,7 @@ msgstr "Dịch vụ" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" diff --git a/src/i18n/locales/zh-Hans.po b/src/i18n/locales/zh-Hans.po index 01bd6655..f2449ced 100644 --- a/src/i18n/locales/zh-Hans.po +++ b/src/i18n/locales/zh-Hans.po @@ -390,7 +390,7 @@ msgstr "调用超时" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -604,7 +604,7 @@ msgid "Copied to clipboard" msgstr "已复制到剪贴板" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "复制" @@ -890,7 +890,7 @@ msgstr "手动标记为已读后删除章节" msgid "Delete chapters" msgstr "删除章节" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "删除已下载项" @@ -1822,12 +1822,12 @@ msgstr "菜单" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "迁移" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "正在迁移漫画…" @@ -2464,7 +2464,7 @@ msgstr "选择一个设备以使用其存储在服务器中的UI设置" msgid "Select all" msgstr "全选" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "选择要包含的数据" @@ -2540,7 +2540,7 @@ msgid "Show continue reading button" msgstr "显示继续阅读按钮" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "显示条目" @@ -2943,7 +2943,7 @@ msgstr "追踪列表" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -3262,14 +3262,14 @@ msgstr "{0}% | 更新中……" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "分类" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx diff --git a/src/i18n/locales/zh-Hant.po b/src/i18n/locales/zh-Hant.po index 503ecb62..560e86ee 100644 --- a/src/i18n/locales/zh-Hant.po +++ b/src/i18n/locales/zh-Hant.po @@ -365,7 +365,7 @@ msgstr "依照更新日期" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategorySelect.tsx #: src/features/category/components/CreateOrEditCategoryDialog.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx #: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx #: src/features/source/browse/components/SourceOptions.tsx @@ -548,7 +548,7 @@ msgid "Copied to clipboard" msgstr "已複製到剪貼簿" #: src/features/manga/components/details/MangaDetails.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Copy" msgstr "複製" @@ -834,7 +834,7 @@ msgstr "手動標記為已讀後刪除章節" msgid "Delete chapters" msgstr "刪除章節" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Delete downloaded" msgstr "刪除已下載項目" @@ -1702,12 +1702,12 @@ msgstr "選單" #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts #: src/features/manga/Manga.constants.ts -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/migration/screens/Migrate.tsx msgid "Migrate" msgstr "遷移" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Migrating manga…" msgstr "正在遷移漫畫…" @@ -2319,7 +2319,7 @@ msgstr "選擇一個裝置以使用它儲存在伺服器中的 UI 設定" msgid "Select all" msgstr "全選" -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Select data to include" msgstr "選擇要包含的資料" @@ -2395,7 +2395,7 @@ msgid "Show continue reading button" msgstr "顯示繼續閱讀按鈕" #: src/features/manga/hooks/useManageMangaLibraryState.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Show entry" msgstr "顯示條目" @@ -2788,7 +2788,7 @@ msgstr "追蹤器" #: src/features/backup/Backup.constants.ts #: src/features/manga/components/TrackMangaButton.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/settings/screens/Settings.tsx #: src/features/tracker/screens/TrackingSettings.tsx msgid "Tracking" @@ -3108,14 +3108,14 @@ msgstr "呼叫逾時" #: src/features/category/components/CategoriesInclusionSetting.tsx #: src/features/category/components/CategoriesInclusionSetting.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx msgid "Category" msgstr "分類" #: src/features/chapter/components/cards/ChapterCard.tsx #: src/features/downloads/components/DownloadAheadSetting.tsx #: src/features/downloads/screens/DownloadSettings.tsx -#: src/features/migration/components/MigrateDialog.tsx +#: src/features/migration/components/MigrationSingleMangaDialog.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx #: src/features/tracker/components/cards/TrackerActiveCard.tsx diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index 6eff06ab..f755de2f 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -3599,7 +3599,7 @@ export type GetCategoryMangasQueryVariables = Exact<{ }>; -export type GetCategoryMangasQuery = { __typename?: 'Query', category: { __typename?: 'CategoryType', id: number, mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } } }; +export type GetCategoryMangasQuery = { __typename?: 'Query', category: { __typename?: 'CategoryType', id: number, mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } } }; export type ChapterMetaFieldsFragment = { __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string }; @@ -3627,7 +3627,7 @@ export type GetMangaChaptersFetchMutationVariables = Exact<{ }>; -export type GetMangaChaptersFetchMutation = { __typename?: 'Mutation', fetchChapters?: { __typename?: 'FetchChaptersPayload', chapters: Array<{ __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, lastReadAt: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }> } | null }; +export type GetMangaChaptersFetchMutation = { __typename?: 'Mutation', fetchChapters?: { __typename?: 'FetchChaptersPayload', chapters: Array<{ __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, lastReadAt: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean, manga: { __typename?: 'MangaType', id: number, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }> } | null }; export type UpdateChapterMutationVariables = Exact<{ input: UpdateChapterInput; @@ -3914,13 +3914,15 @@ export type MangaBaseFieldsFragment = { __typename?: 'MangaType', id: number, ti export type MangaChapterStatFieldsFragment = { __typename?: 'MangaType', id: number, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number } }; -export type MangaChapterNodeFieldsFragment = { __typename?: 'MangaType', firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null }; +export type MangaChapterNodeFieldsFragment = { __typename?: 'MangaType', firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }; export type MangaReaderFieldsFragment = { __typename?: 'MangaType', genre: Array, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string, lang: string } | null, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number } }; -export type MangaLibraryFieldsFragment = { __typename?: 'MangaType', genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null }; +export type MangaLibraryFieldsFragment = { __typename?: 'MangaType', genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }; -export type MangaScreenFieldsFragment = { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null }; +export type MangaMigrationFieldsFragment = { __typename?: 'MangaType', artist?: string | null, author?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string } | null, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }; + +export type MangaScreenFieldsFragment = { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null, chapters: { __typename?: 'ChapterNodeList', totalCount: number } }; export type MangaLibraryDuplicateScreenFieldsFragment = { __typename?: 'MangaType', description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number } }; @@ -3929,7 +3931,7 @@ export type GetMangaFetchMutationVariables = Exact<{ }>; -export type GetMangaFetchMutation = { __typename?: 'Mutation', fetchManga?: { __typename?: 'FetchMangaPayload', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } } | null }; +export type GetMangaFetchMutation = { __typename?: 'Mutation', fetchManga?: { __typename?: 'FetchMangaPayload', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null, chapters: { __typename?: 'ChapterNodeList', totalCount: number } } } | null }; export type GetMangaToMigrateToFetchMutationVariables = Exact<{ id: Scalars['Int']['input']; @@ -3992,7 +3994,7 @@ export type GetMangaScreenQueryVariables = Exact<{ }>; -export type GetMangaScreenQuery = { __typename?: 'Query', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }; +export type GetMangaScreenQuery = { __typename?: 'Query', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null, chapters: { __typename?: 'ChapterNodeList', totalCount: number } } }; export type GetMangaReaderQueryVariables = Exact<{ id: Scalars['Int']['input']; @@ -4052,7 +4054,7 @@ export type GetMangasLibraryQueryVariables = Exact<{ }>; -export type GetMangasLibraryQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; +export type GetMangasLibraryQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', totalCount: number, nodes: Array<{ __typename?: 'MangaType', genre: Array, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; export type GetMangasDuplicatesQueryVariables = Exact<{ after?: InputMaybe; @@ -4073,7 +4075,7 @@ export type GetMigratableSourceMangasQueryVariables = Exact<{ }>; -export type GetMigratableSourceMangasQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', nodes: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, sourceId: string, categories: { __typename?: 'CategoryNodeList', nodes: Array<{ __typename?: 'CategoryType', id: number }> } }> } }; +export type GetMigratableSourceMangasQuery = { __typename?: 'Query', mangas: { __typename?: 'MangaNodeList', nodes: Array<{ __typename?: 'MangaType', artist?: string | null, author?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string } | null, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }> } }; export type GetLibraryMangaCountQueryVariables = Exact<{ [key: string]: never; }>; @@ -4224,6 +4226,13 @@ export type GetSourceMangasFetchMutationVariables = Exact<{ export type GetSourceMangasFetchMutation = { __typename?: 'Mutation', fetchSourceManga?: { __typename?: 'FetchSourceMangaPayload', hasNextPage: boolean, mangas: Array<{ __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string }> } | null }; +export type GetMigrationSourceMangasFetchMutationVariables = Exact<{ + input: FetchSourceMangaInput; +}>; + + +export type GetMigrationSourceMangasFetchMutation = { __typename?: 'Mutation', fetchSourceManga?: { __typename?: 'FetchSourceMangaPayload', hasNextPage: boolean, mangas: Array<{ __typename?: 'MangaType', artist?: string | null, author?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, source?: { __typename?: 'SourceType', id: string, name: string, displayName: string } | null, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null }> } | null }; + export type UpdateSourcePreferencesMutationVariables = Exact<{ input: UpdateSourcePreferenceInput; }>; @@ -4383,15 +4392,15 @@ export type TrackerSearchQueryVariables = Exact<{ export type TrackerSearchQuery = { __typename?: 'Query', searchTracker: { __typename?: 'SearchTrackerPayload', trackSearches: Array<{ __typename?: 'TrackSearchType', id: number, remoteId: string, title: string, trackingUrl: string, coverUrl: string, publishingType: string, startDate: string, publishingStatus: string, summary: string, score: number, totalChapters: number }> } }; -export type UpdaterMangaFieldsFragment = { __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }; +export type UpdaterMangaFieldsFragment = { __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }; export type UpdaterCategoryFieldsFragment = { __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }; export type UpdaterJobInfoFieldsFragment = { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }; -export type UpdaterStatusFieldsFragment = { __typename?: 'LibraryUpdateStatus', jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }> }; +export type UpdaterStatusFieldsFragment = { __typename?: 'LibraryUpdateStatus', jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }> }; -export type UpdaterSubscriptionFieldsFragment = { __typename?: 'UpdaterUpdates', omittedUpdates: boolean, jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }> }; +export type UpdaterSubscriptionFieldsFragment = { __typename?: 'UpdaterUpdates', omittedUpdates: boolean, jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }> }; export type UpdaterStartStopFieldsFragment = { __typename?: 'UpdateStatus', isRunning: boolean }; @@ -4400,7 +4409,7 @@ export type UpdateLibraryMutationVariables = Exact<{ }>; -export type UpdateLibraryMutation = { __typename?: 'Mutation', updateLibrary?: { __typename?: 'UpdateLibraryPayload', updateStatus: { __typename?: 'LibraryUpdateStatus', jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }> } } | null }; +export type UpdateLibraryMutation = { __typename?: 'Mutation', updateLibrary?: { __typename?: 'UpdateLibraryPayload', updateStatus: { __typename?: 'LibraryUpdateStatus', jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }> } } | null }; export type StopUpdaterMutationVariables = Exact<{ input?: InputMaybe; @@ -4412,7 +4421,7 @@ export type StopUpdaterMutation = { __typename?: 'Mutation', updateStop: { __typ export type GetUpdateStatusQueryVariables = Exact<{ [key: string]: never; }>; -export type GetUpdateStatusQuery = { __typename?: 'Query', libraryUpdateStatus: { __typename?: 'LibraryUpdateStatus', jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }> } }; +export type GetUpdateStatusQuery = { __typename?: 'Query', libraryUpdateStatus: { __typename?: 'LibraryUpdateStatus', jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }> } }; export type GetLastUpdateTimestampQueryVariables = Exact<{ [key: string]: never; }>; @@ -4424,7 +4433,7 @@ export type UpdaterSubscriptionVariables = Exact<{ }>; -export type UpdaterSubscription = { __typename?: 'Subscription', libraryUpdateStatusChanged: { __typename?: 'UpdaterUpdates', omittedUpdates: boolean, jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } }> } }; +export type UpdaterSubscription = { __typename?: 'Subscription', libraryUpdateStatusChanged: { __typename?: 'UpdaterUpdates', omittedUpdates: boolean, jobsInfo: { __typename?: 'UpdaterJobsInfoType', isRunning: boolean, totalJobs: number, finishedJobs: number, skippedCategoriesCount: number, skippedMangasCount: number }, categoryUpdates: Array<{ __typename?: 'CategoryUpdateType', status: CategoryJobStatus, category: { __typename?: 'CategoryType', id: number, name: string } }>, mangaUpdates: Array<{ __typename?: 'MangaUpdateType', status: MangaJobStatus, manga: { __typename?: 'MangaType', id: number, title: string, thumbnailUrl?: string | null, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number, chapterNumber: number, name: string, scanlator?: string | null } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null, highestNumberedChapter?: { __typename?: 'ChapterType', id: number, chapterNumber: number } | null } }> } }; export type UserLoginMutationVariables = Exact<{ password: Scalars['String']['input']; diff --git a/src/lib/graphql/manga/MangaFragments.ts b/src/lib/graphql/manga/MangaFragments.ts index 180647c6..96df665d 100644 --- a/src/lib/graphql/manga/MangaFragments.ts +++ b/src/lib/graphql/manga/MangaFragments.ts @@ -73,6 +73,10 @@ export const MANGA_CHAPTER_NODE_FIELDS = gql` id uploadDate } + highestNumberedChapter { + id + chapterNumber + } } `; @@ -143,12 +147,33 @@ export const MANGA_LIBRARY_FIELDS = gql` } `; +export const MANGA_MIGRATION_FIELDS = gql` + ${MANGA_BASE_FIELDS} + ${MANGA_CHAPTER_NODE_FIELDS} + + fragment MANGA_MIGRATION_FIELDS on MangaType { + ...MANGA_BASE_FIELDS + ...MANGA_CHAPTER_NODE_FIELDS + artist + author + source { + id + name + displayName + } + } +`; + export const MANGA_SCREEN_FIELDS = gql` ${MANGA_LIBRARY_FIELDS} ${MANGA_META_FIELDS} + ${MANGA_CHAPTER_NODE_FIELDS} + ${MANGA_MIGRATION_FIELDS} fragment MANGA_SCREEN_FIELDS on MangaType { ...MANGA_LIBRARY_FIELDS + ...MANGA_CHAPTER_NODE_FIELDS + ...MANGA_MIGRATION_FIELDS artist author @@ -164,6 +189,7 @@ export const MANGA_SCREEN_FIELDS = gql` sourceId source { id + name displayName } diff --git a/src/lib/graphql/manga/MangaQuery.ts b/src/lib/graphql/manga/MangaQuery.ts index d17bfe9e..f845b5ca 100644 --- a/src/lib/graphql/manga/MangaQuery.ts +++ b/src/lib/graphql/manga/MangaQuery.ts @@ -13,6 +13,7 @@ import { MANGA_LIBRARY_DUPLICATE_SCREEN_FIELDS, MANGA_LIBRARY_FIELDS, MANGA_META_FIELDS, + MANGA_MIGRATION_FIELDS, MANGA_READER_FIELDS, MANGA_SCREEN_FIELDS, } from '@/lib/graphql/manga/MangaFragments.ts'; @@ -236,18 +237,12 @@ export const GET_MANGAS_DUPLICATES = gql` `; export const GET_MIGRATABLE_SOURCE_MANGAS = gql` + ${MANGA_MIGRATION_FIELDS} + query GET_MIGRATABLE_SOURCE_MANGAS($sourceId: LongString!) { mangas(condition: { sourceId: $sourceId, inLibrary: true }) { nodes { - id - title - thumbnailUrl - sourceId - categories { - nodes { - id - } - } + ...MANGA_MIGRATION_FIELDS } } } diff --git a/src/lib/graphql/source/SourceMutation.ts b/src/lib/graphql/source/SourceMutation.ts index 43fa1a0e..5d6a46f4 100644 --- a/src/lib/graphql/source/SourceMutation.ts +++ b/src/lib/graphql/source/SourceMutation.ts @@ -8,7 +8,7 @@ import gql from 'graphql-tag'; import { SOURCE_META_FIELDS, SOURCE_SETTING_FIELDS } from '@/lib/graphql/source/SourceFragments.ts'; -import { MANGA_BASE_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts'; +import { MANGA_BASE_FIELDS, MANGA_MIGRATION_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts'; export const GET_SOURCE_MANGAS_FETCH = gql` ${MANGA_BASE_FIELDS} @@ -23,6 +23,19 @@ export const GET_SOURCE_MANGAS_FETCH = gql` } `; +export const GET_MIGRATION_SOURCE_MANGAS_FETCH = gql` + ${MANGA_MIGRATION_FIELDS} + + mutation GET_MIGRATION_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) { + fetchSourceManga(input: $input) { + hasNextPage + mangas { + ...MANGA_MIGRATION_FIELDS + } + } + } +`; + export const UPDATE_SOURCE_PREFERENCES = gql` ${SOURCE_SETTING_FIELDS} diff --git a/yarn.lock b/yarn.lock index 35f4b4cf..535a9493 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3587,6 +3587,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/lodash@4.17.24": + version "4.17.24" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f" + integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ== + "@types/node@*": version "20.12.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e" @@ -7034,6 +7039,11 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== +lodash@4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + lodash@^4.17.20, lodash@~4.17.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"