mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(archive viewer): option to select text encoding for zip files
This commit is contained in:
parent
2db256607a
commit
dece1c7098
|
|
@ -564,4 +564,5 @@ export interface ArchiveListFilesResponse {
|
|||
export interface ArchiveListFilesService {
|
||||
uri: string;
|
||||
entity?: string;
|
||||
text_encoding?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
FormControl,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectProps,
|
||||
styled,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NoLabelFilledSelect } from "../../FileManager/Sidebar/CustomProps/MultiSelectPropsContent.tsx";
|
||||
import Translate from "../../Icons/Translate.tsx";
|
||||
|
||||
const encodings = [
|
||||
"ibm866",
|
||||
"iso8859_2",
|
||||
"iso8859_3",
|
||||
"iso8859_4",
|
||||
"iso8859_5",
|
||||
"iso8859_6",
|
||||
"iso8859_7",
|
||||
"iso8859_8",
|
||||
"iso8859_8I",
|
||||
"iso8859_10",
|
||||
"iso8859_13",
|
||||
"iso8859_14",
|
||||
"iso8859_15",
|
||||
"iso8859_16",
|
||||
"koi8r",
|
||||
"koi8u",
|
||||
"macintosh",
|
||||
"windows874",
|
||||
"windows1250",
|
||||
"windows1251",
|
||||
"windows1252",
|
||||
"windows1253",
|
||||
"windows1254",
|
||||
"windows1255",
|
||||
"windows1256",
|
||||
"windows1257",
|
||||
"windows1258",
|
||||
"macintoshcyrillic",
|
||||
"gbk",
|
||||
"gb18030",
|
||||
"big5",
|
||||
"eucjp",
|
||||
"iso2022jp",
|
||||
"shiftjis",
|
||||
"euckr",
|
||||
"utf16be",
|
||||
"utf16le",
|
||||
];
|
||||
|
||||
const defaultEncodingValue = " ";
|
||||
|
||||
export interface EncodingSelectorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
size?: "small" | "medium";
|
||||
variant?: "outlined" | "standard" | "filled";
|
||||
fullWidth?: boolean;
|
||||
showIcon?: boolean;
|
||||
SelectProps?: Partial<SelectProps>;
|
||||
}
|
||||
|
||||
export const StyledInputAdornment = styled(InputAdornment)(({ theme }) => ({
|
||||
"&.MuiInputAdornment-positionStart": {
|
||||
marginTop: "0!important",
|
||||
},
|
||||
}));
|
||||
|
||||
const EncodingSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
size = "medium",
|
||||
variant = "outlined",
|
||||
fullWidth = false,
|
||||
showIcon = true,
|
||||
SelectProps,
|
||||
}: EncodingSelectorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const displayLabel = label || t("modals.selectEncoding");
|
||||
|
||||
const SelectComponent = size == "small" ? NoLabelFilledSelect : Select;
|
||||
const InputAdornmentComponent = size == "small" ? StyledInputAdornment : InputAdornment;
|
||||
|
||||
return (
|
||||
<FormControl variant={variant} fullWidth={fullWidth} size={size}>
|
||||
{size != "small" && <InputLabel>{displayLabel}</InputLabel>}
|
||||
<SelectComponent
|
||||
variant={variant}
|
||||
size={size}
|
||||
startAdornment={
|
||||
showIcon &&
|
||||
!isMobile && (
|
||||
<InputAdornmentComponent position="start" sx={{ mt: 0 }}>
|
||||
<Translate />
|
||||
</InputAdornmentComponent>
|
||||
)
|
||||
}
|
||||
label={displayLabel}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value as string)}
|
||||
{...SelectProps}
|
||||
>
|
||||
<MenuItem value={defaultEncodingValue}>
|
||||
<em>{t("modals.defaultEncoding")}</em>
|
||||
</MenuItem>
|
||||
{encodings.map((enc) => (
|
||||
<MenuItem key={enc} value={enc}>
|
||||
{enc}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SelectComponent>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export { defaultEncodingValue };
|
||||
export default EncodingSelector;
|
||||
|
|
@ -1,15 +1,4 @@
|
|||
import {
|
||||
DialogContent,
|
||||
FormControl,
|
||||
Grid2,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { DialogContent, Grid2, InputAdornment, TextField, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -17,56 +6,14 @@ import { sendExtractArchive } from "../../../api/api.ts";
|
|||
import { closeExtractArchiveDialog } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { fileExtension, getFileLinkedUri } from "../../../util";
|
||||
import EncodingSelector, { defaultEncodingValue } from "../../Common/Form/EncodingSelector.tsx";
|
||||
import { FileDisplayForm } from "../../Common/Form/FileDisplayForm.tsx";
|
||||
import { PathSelectorForm } from "../../Common/Form/PathSelectorForm.tsx";
|
||||
import { ViewTaskAction } from "../../Common/Snackbar/snackbar.tsx";
|
||||
import DraggableDialog from "../../Dialogs/DraggableDialog.tsx";
|
||||
import Password from "../../Icons/Password.tsx";
|
||||
import Translate from "../../Icons/Translate.tsx";
|
||||
import { FileManagerIndex } from "../FileManager.tsx";
|
||||
|
||||
const encodings = [
|
||||
"ibm866",
|
||||
"iso8859_2",
|
||||
"iso8859_3",
|
||||
"iso8859_4",
|
||||
"iso8859_5",
|
||||
"iso8859_6",
|
||||
"iso8859_7",
|
||||
"iso8859_8",
|
||||
"iso8859_8I",
|
||||
"iso8859_10",
|
||||
"iso8859_13",
|
||||
"iso8859_14",
|
||||
"iso8859_15",
|
||||
"iso8859_16",
|
||||
"koi8r",
|
||||
"koi8u",
|
||||
"macintosh",
|
||||
"windows874",
|
||||
"windows1250",
|
||||
"windows1251",
|
||||
"windows1252",
|
||||
"windows1253",
|
||||
"windows1254",
|
||||
"windows1255",
|
||||
"windows1256",
|
||||
"windows1257",
|
||||
"windows1258",
|
||||
"macintoshcyrillic",
|
||||
"gbk",
|
||||
"gb18030",
|
||||
"big5",
|
||||
"eucjp",
|
||||
"iso2022jp",
|
||||
"shiftjis",
|
||||
"euckr",
|
||||
"utf16be",
|
||||
"utf16le",
|
||||
];
|
||||
|
||||
const defaultEncodingValue = " ";
|
||||
|
||||
const ExtractArchive = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
|
@ -84,6 +31,11 @@ const ExtractArchive = () => {
|
|||
const target = useAppSelector((state) => state.globalState.extractArchiveDialogFile);
|
||||
const current = useAppSelector((state) => state.fileManager[FileManagerIndex.main].pure_path);
|
||||
const mask = useAppSelector((state) => state.globalState.extractArchiveDialogMask);
|
||||
const predefinedEncoding = useAppSelector((state) => state.globalState.extractArchiveDialogEncoding);
|
||||
|
||||
useEffect(() => {
|
||||
setEncoding(predefinedEncoding ?? defaultEncodingValue);
|
||||
}, [predefinedEncoding]);
|
||||
|
||||
const showEncodingOption = useMemo(() => {
|
||||
const ext = fileExtension(target?.name ?? "");
|
||||
|
|
@ -167,31 +119,13 @@ const ExtractArchive = () => {
|
|||
md: 6,
|
||||
}}
|
||||
>
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
<InputLabel>{t("modals.selectEncoding")}</InputLabel>
|
||||
<Select
|
||||
variant="outlined"
|
||||
startAdornment={
|
||||
!isMobile && (
|
||||
<InputAdornment position="start">
|
||||
<Translate />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
label={t("modals.selectEncoding")}
|
||||
value={encoding}
|
||||
onChange={(e) => setEncoding(e.target.value)}
|
||||
>
|
||||
<MenuItem value={defaultEncodingValue}>
|
||||
<em>{t("modals.defaultEncoding")}</em>
|
||||
</MenuItem>
|
||||
{encodings.map((enc) => (
|
||||
<MenuItem key={enc} value={enc}>
|
||||
{enc}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<EncodingSelector
|
||||
value={encoding}
|
||||
onChange={setEncoding}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
showIcon={!isMobile}
|
||||
/>
|
||||
</Grid2>
|
||||
)}
|
||||
<Grid2
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { PropsContentProps } from "./CustomPropsItem.tsx";
|
||||
|
||||
const NoLabelFilledSelect = styled(Select)(({ theme }) => ({
|
||||
export const NoLabelFilledSelect = styled(Select)(({ theme }) => ({
|
||||
"& .MuiSelect-select": {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { getArchiveListFiles } from "../../../api/api.ts";
|
|||
import { ArchivedFile, FileType } from "../../../api/explorer.ts";
|
||||
import { closeArchiveViewer, setExtractArchiveDialog } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { fileBase, getFileLinkedUri, sizeToString } from "../../../util";
|
||||
import { fileBase, fileExtension, getFileLinkedUri, sizeToString } from "../../../util";
|
||||
import AutoHeight from "../../Common/AutoHeight.tsx";
|
||||
import EncodingSelector, { defaultEncodingValue } from "../../Common/Form/EncodingSelector.tsx";
|
||||
import { SecondaryButton, StyledCheckbox, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx";
|
||||
import TimeBadge from "../../Common/TimeBadge.tsx";
|
||||
import FileIcon from "../../FileManager/Explorer/FileIcon.tsx";
|
||||
|
|
@ -29,6 +31,11 @@ const ArchivePreview = () => {
|
|||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [height, setHeight] = useState(33);
|
||||
const [encoding, setEncoding] = useState(defaultEncodingValue);
|
||||
|
||||
const isZip = useMemo(() => {
|
||||
return fileExtension(viewerState?.file?.name ?? "") === "zip";
|
||||
}, [viewerState?.file?.name]);
|
||||
|
||||
const currentFiles = useMemo(() => {
|
||||
if (!files) return [];
|
||||
|
|
@ -88,6 +95,7 @@ const ArchivePreview = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!viewerState || !viewerState.open) {
|
||||
setEncoding(defaultEncodingValue);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +109,7 @@ const ArchivePreview = () => {
|
|||
getArchiveListFiles({
|
||||
uri: getFileLinkedUri(viewerState.file),
|
||||
entity: viewerState.version,
|
||||
text_encoding: encoding !== defaultEncodingValue ? encoding : undefined,
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
|
|
@ -110,29 +119,33 @@ const ArchivePreview = () => {
|
|||
const allDirs = new Set<string>();
|
||||
|
||||
// 目录项
|
||||
res.files.filter(item => item.is_directory).forEach(item => {
|
||||
allDirs.add(item.name);
|
||||
allItems.push(item);
|
||||
});
|
||||
res.files
|
||||
.filter((item) => item.is_directory)
|
||||
.forEach((item) => {
|
||||
allDirs.add(item.name);
|
||||
allItems.push(item);
|
||||
});
|
||||
|
||||
// 文件项,并补齐缺失目录
|
||||
res.files.filter(item => !item.is_directory).forEach(item => {
|
||||
allItems.push(item);
|
||||
res.files
|
||||
.filter((item) => !item.is_directory)
|
||||
.forEach((item) => {
|
||||
allItems.push(item);
|
||||
|
||||
const dirElements = item.name.split("/");
|
||||
for (let i = 1; i < dirElements.length; i++) {
|
||||
const dirName = dirElements.slice(0, i).join("/");
|
||||
if (!allDirs.has(dirName)) {
|
||||
allDirs.add(dirName);
|
||||
allItems.push({
|
||||
name: dirName,
|
||||
size: 0,
|
||||
updated_at: "1970-01-01T00:00:00Z",
|
||||
is_directory: true,
|
||||
});
|
||||
const dirElements = item.name.split("/");
|
||||
for (let i = 1; i < dirElements.length; i++) {
|
||||
const dirName = dirElements.slice(0, i).join("/");
|
||||
if (!allDirs.has(dirName)) {
|
||||
allDirs.add(dirName);
|
||||
allItems.push({
|
||||
name: dirName,
|
||||
size: 0,
|
||||
updated_at: "1970-01-01T00:00:00Z",
|
||||
is_directory: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 排序文件
|
||||
// 先目录,后文件,分别按名称排序
|
||||
|
|
@ -151,7 +164,7 @@ const ArchivePreview = () => {
|
|||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [viewerState]);
|
||||
}, [viewerState, encoding]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeArchiveViewer());
|
||||
|
|
@ -214,15 +227,15 @@ const ArchivePreview = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
dispatch(setExtractArchiveDialog({ open: true, file: viewerState?.file, mask: selectedFiles }));
|
||||
}, [selectedFiles, t, enqueueSnackbar]);
|
||||
dispatch(setExtractArchiveDialog({ open: true, file: viewerState?.file, mask: selectedFiles, encoding }));
|
||||
}, [selectedFiles, t, enqueueSnackbar, encoding]);
|
||||
|
||||
const extractArchive = useCallback(() => {
|
||||
if (!viewerState?.file) {
|
||||
return;
|
||||
}
|
||||
dispatch(setExtractArchiveDialog({ open: true, file: viewerState?.file }));
|
||||
}, [viewerState?.file]);
|
||||
dispatch(setExtractArchiveDialog({ open: true, file: viewerState?.file, encoding }));
|
||||
}, [viewerState?.file, encoding]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -236,45 +249,18 @@ const ArchivePreview = () => {
|
|||
maxWidth: "lg",
|
||||
}}
|
||||
>
|
||||
{loading && <ViewerLoading />}
|
||||
{!loading && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Breadcrumbs separator={<ChevronRight fontSize="small" />}>
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
onClick={() => navigateToBreadcrumb(-1)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
<Home fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{t("fileManager.rootFolder")}
|
||||
</Link>
|
||||
{breadcrumbPaths.map((path, index) => {
|
||||
const isLast = index === breadcrumbPaths.length - 1;
|
||||
return isLast ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
key={index}
|
||||
color="text.primary"
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<Folder fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{path}
|
||||
</Typography>
|
||||
) : (
|
||||
<AutoHeight>
|
||||
<div>
|
||||
{loading && <ViewerLoading />}
|
||||
{!loading && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Breadcrumbs separator={<ChevronRight fontSize="small" />}>
|
||||
<Link
|
||||
key={index}
|
||||
component="button"
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
onClick={() => navigateToBreadcrumb(index)}
|
||||
onClick={() => navigateToBreadcrumb(-1)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
|
@ -282,125 +268,180 @@ const ArchivePreview = () => {
|
|||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
<Folder fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{path}
|
||||
<Home fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{t("fileManager.rootFolder")}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
</Box>
|
||||
|
||||
{filteredFiles.length > 0 ? (
|
||||
<TableContainer component={StyledTableContainerPaper}>
|
||||
<TableVirtuoso
|
||||
style={{
|
||||
height: Math.min(height, 400),
|
||||
overflow: "auto",
|
||||
}}
|
||||
totalListHeightChanged={(h) => {
|
||||
setHeight(h + 0.5);
|
||||
}}
|
||||
components={{
|
||||
// eslint-disable-next-line react/display-name
|
||||
Table: (props) => <Table {...props} size="small" />,
|
||||
}}
|
||||
data={filteredFiles}
|
||||
itemContent={(_index, file) => {
|
||||
const fullPath = currentPath ? currentPath + "/" + file.name : file.name;
|
||||
const isSelected = selectedFiles.includes(fullPath);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableCell sx={{ width: 50, padding: "4px 8px" }}>
|
||||
<StyledCheckbox
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFileSelection(file.name)}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
{breadcrumbPaths.map((path, index) => {
|
||||
const isLast = index === breadcrumbPaths.length - 1;
|
||||
return isLast ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
key={index}
|
||||
color="text.primary"
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<Folder fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{path}
|
||||
</Typography>
|
||||
) : (
|
||||
<Link
|
||||
key={index}
|
||||
component="button"
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
onClick={() => navigateToBreadcrumb(index)}
|
||||
sx={{
|
||||
minWidth: 300,
|
||||
width: "100%",
|
||||
padding: "4px 8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ display: "flex", alignItems: "center" }}>
|
||||
<FileIcon
|
||||
sx={{ px: 0, py: 0, mr: 1, height: "20px" }}
|
||||
variant="small"
|
||||
iconProps={{ fontSize: "small" }}
|
||||
file={{
|
||||
type: file.is_directory ? FileType.folder : FileType.file,
|
||||
name: file.name,
|
||||
}}
|
||||
/>
|
||||
{file.is_directory ? (
|
||||
<Typography
|
||||
component="button"
|
||||
variant="inherit"
|
||||
onClick={() => navigateToDirectory(file.name)}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
fontWeight: 500,
|
||||
textDecoration: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{fileBase(file.name)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="inherit"
|
||||
sx={{
|
||||
color: "inherit",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{fileBase(file.name)}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 100, padding: "4px 8px" }}>
|
||||
<Typography variant="body2" noWrap>
|
||||
{file.is_directory ? "-" : sizeToString(file.size)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 120, padding: "4px 8px" }}>
|
||||
<Typography variant="body2" noWrap>
|
||||
{file.updated_at ? <TimeBadge variant="inherit" datetime={file.updated_at} /> : "-"}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TableContainer>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" align="center" sx={{ py: 4 }}>
|
||||
{t("fileManager.nothingFound")}
|
||||
</Typography>
|
||||
)}
|
||||
<Folder fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{path}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
</Box>
|
||||
|
||||
{!viewerState?.version && (
|
||||
<Box sx={{ mt: 2, display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||
<Button variant="contained" onClick={extractArchive} color="primary">
|
||||
{t("fileManager.extractArchive")}
|
||||
</Button>
|
||||
{selectedFiles.length > 0 && (
|
||||
<SecondaryButton variant={"contained"} onClick={extractSelectedFiles}>
|
||||
{t("fileManager.extractSelected")}
|
||||
</SecondaryButton>
|
||||
{filteredFiles.length > 0 ? (
|
||||
<TableContainer component={StyledTableContainerPaper}>
|
||||
<TableVirtuoso
|
||||
style={{
|
||||
height: Math.min(height, 400),
|
||||
overflow: "auto",
|
||||
}}
|
||||
totalListHeightChanged={(h) => {
|
||||
setHeight(h + 0.5);
|
||||
}}
|
||||
components={{
|
||||
// eslint-disable-next-line react/display-name
|
||||
Table: (props) => <Table {...props} size="small" />,
|
||||
}}
|
||||
data={filteredFiles}
|
||||
itemContent={(_index, file) => {
|
||||
const fullPath = currentPath ? currentPath + "/" + file.name : file.name;
|
||||
const isSelected = selectedFiles.includes(fullPath);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableCell sx={{ width: 50, padding: "4px 8px" }}>
|
||||
<StyledCheckbox
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFileSelection(file.name)}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
minWidth: 300,
|
||||
width: "100%",
|
||||
padding: "4px 8px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ display: "flex", alignItems: "center" }}>
|
||||
<FileIcon
|
||||
sx={{ px: 0, py: 0, mr: 1, height: "20px" }}
|
||||
variant="small"
|
||||
iconProps={{ fontSize: "small" }}
|
||||
file={{
|
||||
type: file.is_directory ? FileType.folder : FileType.file,
|
||||
name: file.name,
|
||||
}}
|
||||
/>
|
||||
{file.is_directory ? (
|
||||
<Typography
|
||||
component="button"
|
||||
variant="inherit"
|
||||
onClick={() => navigateToDirectory(file.name)}
|
||||
sx={{
|
||||
color: "primary.main",
|
||||
fontWeight: 500,
|
||||
textDecoration: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{fileBase(file.name)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="inherit"
|
||||
sx={{
|
||||
color: "inherit",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{fileBase(file.name)}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 100, padding: "4px 8px" }}>
|
||||
<Typography variant="body2" noWrap>
|
||||
{file.is_directory ? "-" : sizeToString(file.size)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 120, padding: "4px 8px" }}>
|
||||
<Typography variant="body2" noWrap>
|
||||
{file.updated_at ? <TimeBadge variant="inherit" datetime={file.updated_at} /> : "-"}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TableContainer>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" align="center" sx={{ py: 4 }}>
|
||||
{t("fileManager.nothingFound")}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{!viewerState?.version && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 1,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||
<Button variant="contained" onClick={extractArchive} color="primary">
|
||||
{t("fileManager.extractArchive")}
|
||||
</Button>
|
||||
{selectedFiles.length > 0 && (
|
||||
<SecondaryButton variant={"contained"} onClick={extractSelectedFiles}>
|
||||
{t("fileManager.extractSelected")}
|
||||
</SecondaryButton>
|
||||
)}
|
||||
</Box>
|
||||
{isZip && (
|
||||
<Box>
|
||||
<EncodingSelector
|
||||
value={encoding}
|
||||
onChange={setEncoding}
|
||||
size="small"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
showIcon
|
||||
label={t("modals.selectEncoding")}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</AutoHeight>
|
||||
</ViewerDialog>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ export interface GlobalStateSlice {
|
|||
extractArchiveDialogOpen?: boolean;
|
||||
extractArchiveDialogFile?: FileResponse;
|
||||
extractArchiveDialogMask?: string[];
|
||||
extractArchiveDialogEncoding?: string;
|
||||
|
||||
// Remote download dialog
|
||||
remoteDownloadDialogOpen?: boolean;
|
||||
|
|
@ -381,15 +382,17 @@ export const globalStateSlice = createSlice({
|
|||
},
|
||||
setExtractArchiveDialog: (
|
||||
state,
|
||||
action: PayloadAction<{ open: boolean; file?: FileResponse; mask?: string[] }>,
|
||||
action: PayloadAction<{ open: boolean; file?: FileResponse; mask?: string[]; encoding?: string }>,
|
||||
) => {
|
||||
state.extractArchiveDialogOpen = action.payload.open;
|
||||
state.extractArchiveDialogFile = action.payload.file;
|
||||
state.extractArchiveDialogMask = action.payload.mask;
|
||||
state.extractArchiveDialogEncoding = action.payload.encoding;
|
||||
},
|
||||
closeExtractArchiveDialog: (state) => {
|
||||
state.extractArchiveDialogOpen = false;
|
||||
state.extractArchiveDialogMask = undefined;
|
||||
state.extractArchiveDialogEncoding = undefined;
|
||||
},
|
||||
setCreateArchiveDialog: (
|
||||
state,
|
||||
|
|
|
|||
Loading…
Reference in New Issue