/* * 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; }} /> ); };