feat: preview archive file and extract selected files

This commit is contained in:
Aaron Liu 2025-09-02 11:51:15 +08:00
parent 64b34b280f
commit 463794a71e
23 changed files with 483 additions and 5 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -171,6 +171,8 @@
"googledocs": "Googleドキュメント オンラインリーダー",
"m365viewer": "Microsoft Office オンラインリーダー",
"pdfViewer": "PDFリーダー",
"archivePreview": "アーカイブプレビュー",
"extractSelected": "選択したファイルを展開",
"viewerFileSizeWarning": "ファイルサイズ({{file_size}})が{{app}}の制限({{max}})を超えているため、正常に動作しない可能性があります。",
"testSubtitleStyle": "字幕スタイルのテスト AaBbCc",
"color": "色",

View File

@ -171,6 +171,8 @@
"googledocs": "Google Docs 온라인 뷰어",
"m365viewer": "Microsoft Office 온라인 뷰어",
"pdfViewer": "PDF 뷰어",
"archivePreview": "아카이브 미리보기",
"extractSelected": "선택한 파일 압축 해제",
"viewerFileSizeWarning": "열린 파일 크기({{file_size}})가 {{app}}의 제한({{max}})을 초과하여 정상적으로 작동하지 않을 수 있습니다.",
"testSubtitleStyle": "자막 스타일 테스트 AaBbCc",
"color": "색상",

View File

@ -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",

View File

@ -171,6 +171,8 @@
"googledocs": "Онлайн-просмотрщик Google Docs",
"m365viewer": "Онлайн-просмотрщик Microsoft Office",
"pdfViewer": "Просмотрщик PDF",
"archivePreview": "Предварительный просмотр архива",
"extractSelected": "Извлечь выбранные файлы",
"viewerFileSizeWarning": "Размер открываемого файла ({{file_size}}) превышает лимит {{app}} ({{max}}), возможно, он не будет работать корректно.",
"testSubtitleStyle": "Тест стиля субтитров AaBbCc",
"color": "Цвет",

View File

@ -171,6 +171,8 @@
"googledocs": "Google Docs 在线阅读器",
"m365viewer": "Microsoft Office 在线阅读器",
"pdfViewer": "PDF 阅读器",
"archivePreview": "压缩包预览",
"extractSelected": "解压缩选中的文件",
"viewerFileSizeWarning": "打开的文件大小 ({{file_size}}) 超过了 {{app}} 的限制 ({{max}}),可能无法正常工作。",
"testSubtitleStyle": "测试字幕样式 AaBbCc",
"color": "颜色",

View File

@ -1116,7 +1116,7 @@
"allowWabDAVDes": "关闭后,用户无法通过 WebDAV 协议连接至网盘。",
"allowWabDAVProxy": "WebDAV 代理",
"allowWabDAVProxyDes": "启用后,用户可以配置 WebDAV 下载经由 Cloudreve 中转。",
"allowCompressTask": "压缩/解压缩任务",
"compressTask": "压缩/解压缩任务",
"compressTaskDes": "开启后,用户可以在线压缩/解压缩文件。",
"compressSize": "待压缩文件最大大小",
"compressSizeDes": "用户可创建的压缩任务的文件最大总大小,填写为 0 表示不限制。这一限制在创建压缩任务时不会检查,当执行时已处理原始文件总大小超过此限制时,任务会失败。",

View File

@ -171,6 +171,8 @@
"googledocs": "Google Docs 線上閱讀器",
"m365viewer": "Microsoft Office 線上閱讀器",
"pdfViewer": "PDF 閱讀器",
"archivePreview": "壓縮包預覽",
"extractSelected": "解壓縮選中的檔案",
"viewerFileSizeWarning": "開啟的檔案大小 ({{file_size}}) 超過了 {{app}} 的限制 ({{max}}),可能無法正常工作。",
"testSubtitleStyle": "測試字幕樣式 AaBbCc",
"color": "顏色",

View File

@ -1116,7 +1116,7 @@
"allowWabDAVDes": "關閉後,使用者無法通過 WebDAV 協議連線至網盤。",
"allowWabDAVProxy": "WebDAV 代理",
"allowWabDAVProxyDes": "啟用後, 使用者可以配置 WebDAV 下載經由 Cloudreve 中轉。",
"allowCompressTask": "壓縮/解壓縮任務",
"compressTask": "壓縮/解壓縮任務",
"compressTaskDes": "開啟後,使用者可以線上壓縮/解壓縮檔案。",
"compressSize": "待壓縮檔案最大大小",
"compressSizeDes": "使用者可建立的壓縮任務的檔案最大總大小,填寫為 0 表示不限制。這一限制在建立壓縮任務時不會檢查,當執行時已處理原始檔案總大小超過此限制時,任務會失敗。",

View File

@ -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,
},
),
);
};
}

View File

@ -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;
}

View File

@ -5,6 +5,7 @@ export interface ArchiveWorkflowService {
dst: string;
encoding?: string;
password?: string;
file_mask?: string[];
}
export interface TaskListResponse {

View File

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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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];