206 lines
6.1 KiB
TypeScript
206 lines
6.1 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
Checkbox,
|
||
Typography,
|
||
List,
|
||
ListItem,
|
||
ListItemIcon,
|
||
ListItemText,
|
||
Collapse,
|
||
CircularProgress,
|
||
} from '@mui/material';
|
||
import FolderIcon from '@mui/icons-material/Folder';
|
||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||
|
||
interface FileNode {
|
||
name: string;
|
||
path: string;
|
||
isDirectory: boolean;
|
||
children: FileNode[];
|
||
}
|
||
|
||
interface FilesSelectorProps {
|
||
packName: string;
|
||
onSelectionChange: (selectedFiles: string[]) => void;
|
||
initialSelected?: string[]; // Добавляем этот параметр
|
||
}
|
||
|
||
export default function FilesSelector({
|
||
packName,
|
||
onSelectionChange,
|
||
initialSelected = [], // Значение по умолчанию
|
||
}: FilesSelectorProps) {
|
||
const [files, setFiles] = useState<FileNode[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
// Используем initialSelected для начального состояния
|
||
const [selectedFiles, setSelectedFiles] = useState<string[]>(initialSelected);
|
||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
||
new Set(),
|
||
);
|
||
|
||
useEffect(() => {
|
||
const fetchFiles = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const result = await window.electron.ipcRenderer.invoke(
|
||
'get-pack-files',
|
||
packName,
|
||
);
|
||
|
||
if (result.success) {
|
||
setFiles(result.files);
|
||
} else {
|
||
setError(result.error);
|
||
}
|
||
} catch (err) {
|
||
setError('Ошибка при загрузке файлов');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchFiles();
|
||
}, [packName]);
|
||
|
||
// Обработка выбора файла/папки
|
||
const handleToggle = (
|
||
path: string,
|
||
isDirectory: boolean,
|
||
children: FileNode[],
|
||
) => {
|
||
let newSelected = [...selectedFiles];
|
||
|
||
if (isDirectory) {
|
||
if (selectedFiles.includes(path)) {
|
||
// Если папка выбрана, убираем ее и все вложенные файлы
|
||
newSelected = newSelected.filter((p) => !p.startsWith(path));
|
||
} else {
|
||
// Если папка не выбрана, добавляем ее и все вложенные файлы
|
||
newSelected.push(path);
|
||
const addChildPaths = (nodes: FileNode[]) => {
|
||
for (const node of nodes) {
|
||
newSelected.push(node.path);
|
||
if (node.isDirectory) {
|
||
addChildPaths(node.children);
|
||
}
|
||
}
|
||
};
|
||
addChildPaths(children);
|
||
}
|
||
} else {
|
||
// Для обычного файла просто переключаем состояние
|
||
if (selectedFiles.includes(path)) {
|
||
newSelected = newSelected.filter((p) => p !== path);
|
||
} else {
|
||
newSelected.push(path);
|
||
}
|
||
}
|
||
|
||
setSelectedFiles(newSelected);
|
||
onSelectionChange(newSelected);
|
||
};
|
||
|
||
// Переключение раскрытия папки
|
||
const toggleFolder = (path: string) => {
|
||
const newExpanded = new Set(expandedFolders);
|
||
if (expandedFolders.has(path)) {
|
||
newExpanded.delete(path);
|
||
} else {
|
||
newExpanded.add(path);
|
||
}
|
||
setExpandedFolders(newExpanded);
|
||
};
|
||
|
||
// Рекурсивный компонент для отображения файлов и папок
|
||
const renderFileTree = (nodes: FileNode[]) => {
|
||
// Сортировка: сначала папки, потом файлы
|
||
const sortedNodes = [...nodes].sort((a, b) => {
|
||
// Если у элементов разные типы (папка/файл)
|
||
if (a.isDirectory !== b.isDirectory) {
|
||
return a.isDirectory ? -1 : 1; // Папки идут первыми
|
||
}
|
||
// Если оба элемента одного типа, сортируем по алфавиту
|
||
return a.name.localeCompare(b.name);
|
||
});
|
||
|
||
return (
|
||
<List dense>
|
||
{sortedNodes.map((node) => (
|
||
<div key={node.path}>
|
||
<ListItem
|
||
sx={{
|
||
borderRadius: '3vw',
|
||
backgroundColor: '#FFFFFF1A',
|
||
marginBottom: '1vh',
|
||
}}
|
||
>
|
||
<ListItemIcon>
|
||
<Checkbox
|
||
edge="start"
|
||
checked={selectedFiles.includes(node.path)}
|
||
onChange={() =>
|
||
handleToggle(node.path, node.isDirectory, node.children)
|
||
}
|
||
tabIndex={-1}
|
||
sx={{ color: 'white' }}
|
||
/>
|
||
</ListItemIcon>
|
||
|
||
{node.isDirectory && (
|
||
<ListItemIcon onClick={() => toggleFolder(node.path)}>
|
||
{expandedFolders.has(node.path) ? (
|
||
<ExpandLessIcon sx={{ color: 'white' }} />
|
||
) : (
|
||
<ExpandMoreIcon sx={{ color: 'white' }} />
|
||
)}
|
||
</ListItemIcon>
|
||
)}
|
||
|
||
<ListItemIcon>
|
||
{node.isDirectory ? (
|
||
<FolderIcon sx={{ color: 'white' }} />
|
||
) : (
|
||
<InsertDriveFileIcon sx={{ color: 'white' }} />
|
||
)}
|
||
</ListItemIcon>
|
||
|
||
<ListItemText
|
||
primary={node.name}
|
||
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
|
||
/>
|
||
</ListItem>
|
||
|
||
{node.isDirectory && node.children.length > 0 && (
|
||
<Collapse
|
||
in={expandedFolders.has(node.path)}
|
||
timeout="auto"
|
||
unmountOnExit
|
||
>
|
||
<Box sx={{ pl: 4 }}>{renderFileTree(node.children)}</Box>
|
||
</Collapse>
|
||
)}
|
||
</div>
|
||
))}
|
||
</List>
|
||
);
|
||
};
|
||
|
||
if (loading) {
|
||
return <CircularProgress />;
|
||
}
|
||
|
||
if (error) {
|
||
return <Typography color="error">{error}</Typography>;
|
||
}
|
||
|
||
return (
|
||
<Box sx={{ maxHeight: '300px', overflow: 'auto' }}>
|
||
{renderFileTree(files)}
|
||
</Box>
|
||
);
|
||
}
|