mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat: preview archive file and extract selected files
This commit is contained in:
parent
64b34b280f
commit
463794a71e
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Google Docs Online-Reader",
|
||||
"m365viewer": "Microsoft Office Online-Reader",
|
||||
"pdfViewer": "PDF-Reader",
|
||||
"archivePreview": "Archiv-Vorschau",
|
||||
"extractSelected": "Ausgewählte Dateien extrahieren",
|
||||
"viewerFileSizeWarning": "Die geöffnete Dateigröße ({{file_size}}) überschreitet das Limit von {{app}} ({{max}}) und funktioniert möglicherweise nicht ordnungsgemäß.",
|
||||
"testSubtitleStyle": "Untertitel-Stil testen AaBbCc",
|
||||
"color": "Farbe",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Google Docs Viewer",
|
||||
"m365viewer": "Microsoft Office Online Viewer",
|
||||
"pdfViewer": "PDF Viewer",
|
||||
"archivePreview": "Archive Preview",
|
||||
"extractSelected": "Extract Selected Files",
|
||||
"viewerFileSizeWarning": "Size of opened file ({{file_size}}) exceed limit ({{max}}) of {{app}}, it might not work properly.",
|
||||
"testSubtitleStyle": "Test subtitle style AaBbCc",
|
||||
"color": "Color",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Visor de Google Docs",
|
||||
"m365viewer": "Visor en línea de Microsoft Office",
|
||||
"pdfViewer": "Visor PDF",
|
||||
"archivePreview": "Vista previa del archivo",
|
||||
"extractSelected": "Extraer archivos seleccionados",
|
||||
"viewerFileSizeWarning": "El tamaño del archivo abierto ({{file_size}}) excede el límite ({{max}}) de {{app}}, podría no funcionar correctamente.",
|
||||
"testSubtitleStyle": "Probar estilo de subtítulos AaBbCc",
|
||||
"color": "Color",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Visionneuse Google Docs",
|
||||
"m365viewer": "Visionneuse Microsoft Office Online",
|
||||
"pdfViewer": "Visionneuse PDF",
|
||||
"archivePreview": "Aperçu de l'archive",
|
||||
"extractSelected": "Extraire les fichiers sélectionnés",
|
||||
"viewerFileSizeWarning": "La taille du fichier ouvert ({{file_size}}) dépasse la limite ({{max}}) de {{app}}, il pourrait ne pas fonctionner correctement.",
|
||||
"testSubtitleStyle": "Tester le style de sous-titres AaBbCc",
|
||||
"color": "Couleur",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Visualizzatore Google Docs",
|
||||
"m365viewer": "Visualizzatore Microsoft Office Online",
|
||||
"pdfViewer": "Visualizzatore PDF",
|
||||
"archivePreview": "Anteprima archivio",
|
||||
"extractSelected": "Estrai file selezionati",
|
||||
"viewerFileSizeWarning": "La dimensione del file aperto ({{file_size}}) supera il limite ({{max}}) di {{app}}, potrebbe non funzionare correttamente.",
|
||||
"testSubtitleStyle": "Testa stile sottotitoli AaBbCc",
|
||||
"color": "Colore",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Googleドキュメント オンラインリーダー",
|
||||
"m365viewer": "Microsoft Office オンラインリーダー",
|
||||
"pdfViewer": "PDFリーダー",
|
||||
"archivePreview": "アーカイブプレビュー",
|
||||
"extractSelected": "選択したファイルを展開",
|
||||
"viewerFileSizeWarning": "ファイルサイズ({{file_size}})が{{app}}の制限({{max}})を超えているため、正常に動作しない可能性があります。",
|
||||
"testSubtitleStyle": "字幕スタイルのテスト AaBbCc",
|
||||
"color": "色",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Google Docs 온라인 뷰어",
|
||||
"m365viewer": "Microsoft Office 온라인 뷰어",
|
||||
"pdfViewer": "PDF 뷰어",
|
||||
"archivePreview": "아카이브 미리보기",
|
||||
"extractSelected": "선택한 파일 압축 해제",
|
||||
"viewerFileSizeWarning": "열린 파일 크기({{file_size}})가 {{app}}의 제한({{max}})을 초과하여 정상적으로 작동하지 않을 수 있습니다.",
|
||||
"testSubtitleStyle": "자막 스타일 테스트 AaBbCc",
|
||||
"color": "색상",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Visualizador Google Docs",
|
||||
"m365viewer": "Visualizador Microsoft Office Online",
|
||||
"pdfViewer": "Visualizador PDF",
|
||||
"archivePreview": "Visualização de arquivo",
|
||||
"extractSelected": "Extrair arquivos selecionados",
|
||||
"viewerFileSizeWarning": "Tamanho do arquivo aberto ({{file_size}}) excede o limite ({{max}}) do {{app}}, pode não funcionar corretamente.",
|
||||
"testSubtitleStyle": "Testar estilo de legenda AaBbCc",
|
||||
"color": "Cor",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Онлайн-просмотрщик Google Docs",
|
||||
"m365viewer": "Онлайн-просмотрщик Microsoft Office",
|
||||
"pdfViewer": "Просмотрщик PDF",
|
||||
"archivePreview": "Предварительный просмотр архива",
|
||||
"extractSelected": "Извлечь выбранные файлы",
|
||||
"viewerFileSizeWarning": "Размер открываемого файла ({{file_size}}) превышает лимит {{app}} ({{max}}), возможно, он не будет работать корректно.",
|
||||
"testSubtitleStyle": "Тест стиля субтитров AaBbCc",
|
||||
"color": "Цвет",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Google Docs 在线阅读器",
|
||||
"m365viewer": "Microsoft Office 在线阅读器",
|
||||
"pdfViewer": "PDF 阅读器",
|
||||
"archivePreview": "压缩包预览",
|
||||
"extractSelected": "解压缩选中的文件",
|
||||
"viewerFileSizeWarning": "打开的文件大小 ({{file_size}}) 超过了 {{app}} 的限制 ({{max}}),可能无法正常工作。",
|
||||
"testSubtitleStyle": "测试字幕样式 AaBbCc",
|
||||
"color": "颜色",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,7 @@
|
|||
"allowWabDAVDes": "关闭后,用户无法通过 WebDAV 协议连接至网盘。",
|
||||
"allowWabDAVProxy": "WebDAV 代理",
|
||||
"allowWabDAVProxyDes": "启用后,用户可以配置 WebDAV 下载经由 Cloudreve 中转。",
|
||||
"allowCompressTask": "压缩/解压缩任务",
|
||||
"compressTask": "压缩/解压缩任务",
|
||||
"compressTaskDes": "开启后,用户可以在线压缩/解压缩文件。",
|
||||
"compressSize": "待压缩文件最大大小",
|
||||
"compressSizeDes": "用户可创建的压缩任务的文件最大总大小,填写为 0 表示不限制。这一限制在创建压缩任务时不会检查,当执行时已处理原始文件总大小超过此限制时,任务会失败。",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@
|
|||
"googledocs": "Google Docs 線上閱讀器",
|
||||
"m365viewer": "Microsoft Office 線上閱讀器",
|
||||
"pdfViewer": "PDF 閱讀器",
|
||||
"archivePreview": "壓縮包預覽",
|
||||
"extractSelected": "解壓縮選中的檔案",
|
||||
"viewerFileSizeWarning": "開啟的檔案大小 ({{file_size}}) 超過了 {{app}} 的限制 ({{max}}),可能無法正常工作。",
|
||||
"testSubtitleStyle": "測試字幕樣式 AaBbCc",
|
||||
"color": "顏色",
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,7 @@
|
|||
"allowWabDAVDes": "關閉後,使用者無法通過 WebDAV 協議連線至網盤。",
|
||||
"allowWabDAVProxy": "WebDAV 代理",
|
||||
"allowWabDAVProxyDes": "啟用後, 使用者可以配置 WebDAV 下載經由 Cloudreve 中轉。",
|
||||
"allowCompressTask": "壓縮/解壓縮任務",
|
||||
"compressTask": "壓縮/解壓縮任務",
|
||||
"compressTaskDes": "開啟後,使用者可以線上壓縮/解壓縮檔案。",
|
||||
"compressSize": "待壓縮檔案最大大小",
|
||||
"compressSizeDes": "使用者可建立的壓縮任務的檔案最大總大小,填寫為 0 表示不限制。這一限制在建立壓縮任務時不會檢查,當執行時已處理原始檔案總大小超過此限制時,任務會失敗。",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import {
|
|||
User as UserEnt,
|
||||
} from "./dashboard.ts";
|
||||
import {
|
||||
ArchiveListFilesResponse,
|
||||
ArchiveListFilesService,
|
||||
CreateFileService,
|
||||
CreateViewerSessionService,
|
||||
DeleteFileService,
|
||||
|
|
@ -2007,3 +2009,17 @@ export function sendCleanupTask(args: CleanupTaskService): ThunkResponse<void> {
|
|||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function getArchiveListFiles(args: ArchiveListFilesService): ThunkResponse<ArchiveListFilesResponse> {
|
||||
return async (dispatch, _getState) => {
|
||||
return await dispatch(
|
||||
send(
|
||||
`/file/archive`,
|
||||
{ method: "GET", params: args },
|
||||
{
|
||||
...defaultOpts,
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -447,6 +447,7 @@ export interface Viewer {
|
|||
};
|
||||
templates?: NewFileTemplate[];
|
||||
platform?: ViewerPlatform;
|
||||
required_group_permission?: number[];
|
||||
}
|
||||
|
||||
export interface NewFileTemplate {
|
||||
|
|
@ -548,3 +549,19 @@ export enum CustomPropsType {
|
|||
link = "link",
|
||||
rating = "rating",
|
||||
}
|
||||
|
||||
export interface ArchivedFile {
|
||||
name: string;
|
||||
size: number;
|
||||
updated_at?: string;
|
||||
is_directory: boolean;
|
||||
}
|
||||
|
||||
export interface ArchiveListFilesResponse {
|
||||
files: ArchivedFile[];
|
||||
}
|
||||
|
||||
export interface ArchiveListFilesService {
|
||||
uri: string;
|
||||
entity?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface ArchiveWorkflowService {
|
|||
dst: string;
|
||||
encoding?: string;
|
||||
password?: string;
|
||||
file_mask?: string[];
|
||||
}
|
||||
|
||||
export interface TaskListResponse {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import SaveAs from "./SaveAs.tsx";
|
|||
import Photopea from "../../Viewers/Photopea/Photopea.tsx";
|
||||
import OpenWith from "./OpenWith.tsx";
|
||||
import Wopi from "../../Viewers/Wopi.tsx";
|
||||
import ArchivePreview from "../../Viewers/ArchivePreview/ArchivePreview.tsx";
|
||||
import CodeViewer from "../../Viewers/CodeViewer/CodeViewer.tsx";
|
||||
import DrawIOViewer from "../../Viewers/DrawIO/DrawIOViewer.tsx";
|
||||
import MarkdownViewer from "../../Viewers/MarkdownEditor/MarkdownViewer.tsx";
|
||||
|
|
@ -41,6 +42,7 @@ const Dialogs = () => {
|
|||
const directLink = useAppSelector((state) => state.globalState.directLinkDialogOpen);
|
||||
const excalidrawViewer = useAppSelector((state) => state.globalState.excalidrawViewer);
|
||||
const directLinkManagement = useAppSelector((state) => state.globalState.directLinkManagementDialogOpen);
|
||||
const archivePreview = useAppSelector((state) => state.globalState.archiveViewer);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -75,6 +77,7 @@ const Dialogs = () => {
|
|||
{directLink != undefined && <DirectLinks />}
|
||||
{excalidrawViewer != undefined && <ExcalidrawViewer />}
|
||||
{directLinkManagement != undefined && <DirectLinksControl />}
|
||||
{archivePreview != undefined && <ArchivePreview />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const ExtractArchive = () => {
|
|||
const open = useAppSelector((state) => state.globalState.extractArchiveDialogOpen);
|
||||
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 showEncodingOption = useMemo(() => {
|
||||
const ext = fileExtension(target?.name ?? "");
|
||||
|
|
@ -116,6 +117,7 @@ const ExtractArchive = () => {
|
|||
dst: path,
|
||||
encoding: showEncodingOption && encoding != defaultEncodingValue ? encoding : undefined,
|
||||
password: showPasswordOption && password ? password : undefined,
|
||||
file_mask: mask ?? undefined,
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
|
|
@ -129,7 +131,7 @@ const ExtractArchive = () => {
|
|||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [target, encoding, path, showPasswordOption, showEncodingOption, password]);
|
||||
}, [target, encoding, path, showPasswordOption, showEncodingOption, password, mask]);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { SecondaryButton } from "../../Common/StyledComponents.tsx";
|
|||
import DraggableDialog, { StyledDialogContentText } from "../../Dialogs/DraggableDialog.tsx";
|
||||
import Book from "../../Icons/Book.tsx";
|
||||
import DocumentPDF from "../../Icons/DocumentPDF.tsx";
|
||||
import FolderZip from "../../Icons/FolderZip.tsx";
|
||||
import Image from "../../Icons/Image.tsx";
|
||||
import Markdown from "../../Icons/Markdown.tsx";
|
||||
import MoreHorizontal from "../../Icons/MoreHorizontal.tsx";
|
||||
|
|
@ -44,6 +45,7 @@ export const ViewerIDWithDefaultIcons = [
|
|||
builtInViewers.epub,
|
||||
builtInViewers.music,
|
||||
builtInViewers.markdown,
|
||||
builtInViewers.archive,
|
||||
];
|
||||
|
||||
export const ViewerIcon = ({ viewer, size = 32, py = 0.5 }: ViewerIconProps) => {
|
||||
|
|
@ -62,6 +64,8 @@ export const ViewerIcon = ({ viewer, size = 32, py = 0.5 }: ViewerIconProps) =>
|
|||
return <Book sx={{ width: size, height: size, color: "#81b315" }} />;
|
||||
case builtInViewers.music:
|
||||
return <MusicNote1 sx={{ width: size, height: size, color: "#651fff" }} />;
|
||||
case builtInViewers.archive:
|
||||
return <FolderZip sx={{ width: size, height: size, color: "#f9a825" }} />;
|
||||
case builtInViewers.markdown:
|
||||
return (
|
||||
<Markdown
|
||||
|
|
|
|||
|
|
@ -0,0 +1,372 @@
|
|||
import { Box, Breadcrumbs, Button, Link, Table, TableCell, TableContainer, Typography, useTheme } from "@mui/material";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableVirtuoso } from "react-virtuoso";
|
||||
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 { SecondaryButton, StyledCheckbox, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx";
|
||||
import TimeBadge from "../../Common/TimeBadge.tsx";
|
||||
import FileIcon from "../../FileManager/Explorer/FileIcon.tsx";
|
||||
import ChevronRight from "../../Icons/ChevronRight.tsx";
|
||||
import Folder from "../../Icons/Folder.tsx";
|
||||
import Home from "../../Icons/Home.tsx";
|
||||
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
|
||||
|
||||
const ArchivePreview = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const viewerState = useAppSelector((state) => state.globalState.archiveViewer);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [files, setFiles] = useState<ArchivedFile[]>([]);
|
||||
const [currentPath, setCurrentPath] = useState<string>("");
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [height, setHeight] = useState(33);
|
||||
|
||||
const currentFiles = useMemo(() => {
|
||||
if (!files) return [];
|
||||
|
||||
if (!currentPath) {
|
||||
return files.filter((file) => !file.name.includes("/"));
|
||||
}
|
||||
|
||||
// 如果在子目录,显示该目录下的文件和文件夹
|
||||
const pathPrefix = currentPath.endsWith("/") ? currentPath : currentPath + "/";
|
||||
const pathFiles = files.filter((file) => file.name.startsWith(pathPrefix) && file.name !== currentPath);
|
||||
|
||||
// 去重并转换为相对路径
|
||||
const relativePaths = new Set<string>();
|
||||
const result: ArchivedFile[] = [];
|
||||
|
||||
pathFiles.forEach((file) => {
|
||||
const relativePath = file.name.substring(pathPrefix.length);
|
||||
const firstSlash = relativePath.indexOf("/");
|
||||
|
||||
if (firstSlash === -1) {
|
||||
if (!relativePaths.has(relativePath)) {
|
||||
relativePaths.add(relativePath);
|
||||
result.push({
|
||||
...file,
|
||||
name: relativePath,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const dirName = relativePath.substring(0, firstSlash);
|
||||
if (!relativePaths.has(dirName)) {
|
||||
relativePaths.add(dirName);
|
||||
result.push({
|
||||
name: dirName,
|
||||
size: 0,
|
||||
updated_at: file.updated_at,
|
||||
is_directory: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [files, currentPath]);
|
||||
|
||||
// 过滤文件
|
||||
const filteredFiles = useMemo(() => {
|
||||
if (!filterText) return currentFiles;
|
||||
return currentFiles.filter((file) => file.name.toLowerCase().includes(filterText.toLowerCase()));
|
||||
}, [currentFiles, filterText]);
|
||||
|
||||
// 面包屑路径
|
||||
const breadcrumbPaths = useMemo(() => {
|
||||
if (!currentPath) return [];
|
||||
return currentPath.split("/").filter(Boolean);
|
||||
}, [currentPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewerState || !viewerState.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setFiles([]);
|
||||
setCurrentPath("");
|
||||
setSelectedFiles([]);
|
||||
setFilterText("");
|
||||
|
||||
dispatch(
|
||||
getArchiveListFiles({
|
||||
uri: getFileLinkedUri(viewerState.file),
|
||||
entity: viewerState.version,
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.files) {
|
||||
setFiles(res.files);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
onClose();
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [viewerState]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeArchiveViewer());
|
||||
}, [dispatch]);
|
||||
|
||||
const navigateToDirectory = useCallback(
|
||||
(dirName: string) => {
|
||||
if (!currentPath) {
|
||||
setCurrentPath(dirName);
|
||||
} else {
|
||||
setCurrentPath(currentPath + "/" + dirName);
|
||||
}
|
||||
setSelectedFiles([]);
|
||||
},
|
||||
[currentPath],
|
||||
);
|
||||
|
||||
const navigateToBreadcrumb = useCallback(
|
||||
(index: number) => {
|
||||
if (index === -1) {
|
||||
setCurrentPath("");
|
||||
} else {
|
||||
const newPath = breadcrumbPaths.slice(0, index + 1).join("/");
|
||||
setCurrentPath(newPath);
|
||||
}
|
||||
setSelectedFiles([]);
|
||||
},
|
||||
[breadcrumbPaths],
|
||||
);
|
||||
|
||||
const toggleFileSelection = useCallback(
|
||||
(fileName: string) => {
|
||||
const fullPath = currentPath ? currentPath + "/" + fileName : fileName;
|
||||
setSelectedFiles((prev) => {
|
||||
if (prev.includes(fullPath)) {
|
||||
return prev.filter((f) => f !== fullPath);
|
||||
} else {
|
||||
return [...prev, fullPath];
|
||||
}
|
||||
});
|
||||
},
|
||||
[currentPath],
|
||||
);
|
||||
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
const allFiles = filteredFiles.map((file) => (currentPath ? currentPath + "/" + file.name : file.name));
|
||||
|
||||
const allSelected = allFiles.every((file) => selectedFiles.includes(file));
|
||||
|
||||
if (allSelected) {
|
||||
setSelectedFiles((prev) => prev.filter((file) => !allFiles.includes(file)));
|
||||
} else {
|
||||
setSelectedFiles((prev) => [...new Set([...prev, ...allFiles])]);
|
||||
}
|
||||
}, [filteredFiles, selectedFiles, currentPath]);
|
||||
|
||||
// 解压选中的文件
|
||||
const extractSelectedFiles = useCallback(() => {
|
||||
if (selectedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setExtractArchiveDialog({ open: true, file: viewerState?.file, mask: selectedFiles }));
|
||||
}, [selectedFiles, t, enqueueSnackbar]);
|
||||
|
||||
const extractArchive = useCallback(() => {
|
||||
if (!viewerState?.file) {
|
||||
return;
|
||||
}
|
||||
dispatch(setExtractArchiveDialog({ open: true, file: viewerState?.file }));
|
||||
}, [viewerState?.file]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewerDialog
|
||||
file={viewerState?.file}
|
||||
loading={loading}
|
||||
dialogProps={{
|
||||
open: !!(viewerState && viewerState.open),
|
||||
onClose: onClose,
|
||||
fullWidth: true,
|
||||
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>
|
||||
) : (
|
||||
<Link
|
||||
key={index}
|
||||
component="button"
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
onClick={() => navigateToBreadcrumb(index)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
<Folder fontSize="small" sx={{ mr: 0.5 }} />
|
||||
{path}
|
||||
</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
|
||||
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 }}>
|
||||
<Button variant="contained" onClick={extractArchive} color="primary">
|
||||
{t("fileManager.extractArchive")}
|
||||
</Button>
|
||||
{selectedFiles.length > 0 && (
|
||||
<SecondaryButton variant={"contained"} onClick={extractSelectedFiles}>
|
||||
{t("fileManager.extractSelected")}
|
||||
</SecondaryButton>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</ViewerDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArchivePreview;
|
||||
|
|
@ -178,6 +178,7 @@ export interface GlobalStateSlice {
|
|||
// Extract archive dialog
|
||||
extractArchiveDialogOpen?: boolean;
|
||||
extractArchiveDialogFile?: FileResponse;
|
||||
extractArchiveDialogMask?: string[];
|
||||
|
||||
// Remote download dialog
|
||||
remoteDownloadDialogOpen?: boolean;
|
||||
|
|
@ -221,6 +222,7 @@ export interface GlobalStateSlice {
|
|||
epubViewer?: GeneralViewerState;
|
||||
musicPlayer?: MusicPlayerState;
|
||||
excalidrawViewer?: GeneralViewerState;
|
||||
archiveViewer?: GeneralViewerState;
|
||||
|
||||
// Viewer selector
|
||||
viewerSelector?: ViewerSelectorState;
|
||||
|
|
@ -375,13 +377,19 @@ export const globalStateSlice = createSlice({
|
|||
state.customViewer = undefined;
|
||||
state.epubViewer = undefined;
|
||||
state.excalidrawViewer = undefined;
|
||||
state.archiveViewer = undefined;
|
||||
},
|
||||
setExtractArchiveDialog: (state, action: PayloadAction<{ open: boolean; file?: FileResponse }>) => {
|
||||
setExtractArchiveDialog: (
|
||||
state,
|
||||
action: PayloadAction<{ open: boolean; file?: FileResponse; mask?: string[] }>,
|
||||
) => {
|
||||
state.extractArchiveDialogOpen = action.payload.open;
|
||||
state.extractArchiveDialogFile = action.payload.file;
|
||||
state.extractArchiveDialogMask = action.payload.mask;
|
||||
},
|
||||
closeExtractArchiveDialog: (state) => {
|
||||
state.extractArchiveDialogOpen = false;
|
||||
state.extractArchiveDialogMask = undefined;
|
||||
},
|
||||
setCreateArchiveDialog: (
|
||||
state,
|
||||
|
|
@ -519,6 +527,12 @@ export const globalStateSlice = createSlice({
|
|||
closeExcalidrawViewer: (state) => {
|
||||
state.excalidrawViewer && (state.excalidrawViewer.open = false);
|
||||
},
|
||||
setArchiveViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.archiveViewer = action.payload;
|
||||
},
|
||||
closeArchiveViewer: (state) => {
|
||||
state.archiveViewer && (state.archiveViewer.open = false);
|
||||
},
|
||||
addShareInfo: (state, action: PayloadAction<{ info: Share; id: string }>) => {
|
||||
state.shareInfo[action.payload.id] = action.payload.info;
|
||||
},
|
||||
|
|
@ -749,6 +763,8 @@ export const globalStateSlice = createSlice({
|
|||
|
||||
export default globalStateSlice.reducer;
|
||||
export const {
|
||||
setArchiveViewer,
|
||||
closeArchiveViewer,
|
||||
setUploadRawFiles,
|
||||
setMobileDrawerOpen,
|
||||
setDirectLinkDialog,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Viewer, ViewerPlatform } from "../api/explorer.ts";
|
||||
import { SiteConfig } from "../api/site.ts";
|
||||
import { ExpandedIconSettings, FileTypeIconSetting } from "../component/FileManager/Explorer/FileTypeIcon.tsx";
|
||||
import SessionManager from "../session/index.ts";
|
||||
import Boolset from "../util/boolset.ts";
|
||||
import { ExpandedViewerSetting } from "./thunks/viewer.ts";
|
||||
import { Viewer, ViewerPlatform } from "../api/explorer.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -75,6 +77,18 @@ const preProcessors: {
|
|||
return;
|
||||
}
|
||||
|
||||
if (viewer.required_group_permission) {
|
||||
const group = SessionManager.currentUserGroup();
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupBs = new Boolset(group.permission);
|
||||
if (viewer.required_group_permission.some((p) => !groupBs.enabled(p))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const platform = viewer.platform || ViewerPlatform.all;
|
||||
if (platform !== ViewerPlatform.all && platform !== (isMobile ? ViewerPlatform.mobile : ViewerPlatform.pc)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import CrUri, { CrUriPrefix } from "../../util/uri.ts";
|
|||
import { closeContextMenu, ContextMenuTypes, fileUpdated } from "../fileManagerSlice.ts";
|
||||
import {
|
||||
closeImageEditor,
|
||||
setArchiveViewer,
|
||||
setCodeViewer,
|
||||
setCustomViewer,
|
||||
setDrawIOViewer,
|
||||
|
|
@ -52,6 +53,7 @@ export const builtInViewers = {
|
|||
epub: "epub",
|
||||
music: "music",
|
||||
excalidraw: "excalidraw",
|
||||
archive: "archive",
|
||||
};
|
||||
|
||||
export function openViewers(
|
||||
|
|
@ -233,6 +235,15 @@ export function openViewer(
|
|||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.archive:
|
||||
dispatch(
|
||||
setArchiveViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.music: {
|
||||
// open image viewer
|
||||
const fm = getState().fileManager[FileManagerIndex.main];
|
||||
|
|
|
|||
Loading…
Reference in New Issue