mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(share): add option to automatically render and show README file (#2382)
This commit is contained in:
parent
27996dc3ea
commit
38f5114426
|
|
@ -20,7 +20,7 @@
|
|||
"@fontsource/roboto": "^5.0.8",
|
||||
"@giscus/react": "^3.1.0",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@mdxeditor/editor": "^3.4.0",
|
||||
"@mdxeditor/editor": "^3.39.0",
|
||||
"@mui/icons-material": "^6.0.0",
|
||||
"@mui/lab": "^6.0.0-beta.30",
|
||||
"@mui/material": "^6.4.6",
|
||||
|
|
@ -114,4 +114,4 @@
|
|||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -479,6 +479,8 @@
|
|||
"useCustomPassword": "Custom share link password",
|
||||
"shareView": "Share view setting",
|
||||
"shareViewDes": "If selected, other users can see your view setting (layout, sorting, etc.) saved on the server server when accessing this shared folder.",
|
||||
"showReadme": "Show README file",
|
||||
"showReadmeDes": "If selected, the <0>README.md</0> file (case-sensitive) in the directory will be automatically displayed for visitors.",
|
||||
"expireAfterDownload": "Expire after being downloaded",
|
||||
"sharePassword": "Share password",
|
||||
"randomlyGenerate": "Random",
|
||||
|
|
|
|||
|
|
@ -510,7 +510,9 @@
|
|||
"unlinkOnly": "物理ファイルを保持",
|
||||
"unlinkOnlyDes": "ファイル記録のみ削除、物理ファイルは削除されません",
|
||||
"shareView": "共有ビュー設定",
|
||||
"shareViewDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。"
|
||||
"shareViewDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。",
|
||||
"showReadme": "README ファイルを表示",
|
||||
"showReadmeDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。"
|
||||
},
|
||||
"uploader": {
|
||||
"fileCopyName": "コピー_",
|
||||
|
|
|
|||
|
|
@ -513,6 +513,8 @@
|
|||
"unlinkOnlyDes": "仅删除文件记录,物理文件不会被删除",
|
||||
"shareView": "分享视图设置",
|
||||
"shareViewDes": "勾选后,其他用户访问此共享文件夹时可以看到你保存在服务器的视图设置(布局、排序等)。",
|
||||
"showReadme": "显示 README 文件",
|
||||
"showReadmeDes": "勾选后,会自动为访问者展示目录下的 <0>README.md</0> (区分大小写) 文件。",
|
||||
"viewSetting": "视图设置",
|
||||
"saved": "已保存",
|
||||
"notSet": "未设置",
|
||||
|
|
|
|||
|
|
@ -509,6 +509,8 @@
|
|||
"unlinkOnlyDes": "僅刪除檔案記錄,物理檔案不會被刪除",
|
||||
"shareView": "分享視圖設定",
|
||||
"shareViewDes": "勾選後,其他使用者存取此共享資料夾時可以看到你保存在服務器的視圖設定(佈局、排序等)。",
|
||||
"showReadme": "顯示 README 文件",
|
||||
"showReadmeDes": "勾選後,會自動為訪問者展示目錄下的 <0>README.md</0> (區分大小寫) 文件。",
|
||||
"viewSetting": "視圖設定",
|
||||
"saved": "已保存",
|
||||
"notSet": "未設定",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Suspense, useEffect, useMemo } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useRegisterSW } from "virtual:pwa-register/react";
|
||||
import FileIconSnackbar from "./component/Common/Snackbar/FileIconSnackbar.tsx";
|
||||
import LoadingSnackbar from "./component/Common/Snackbar/LoadingSnackbar.tsx";
|
||||
import GlobalDialogs from "./component/Dialogs/GlobalDialogs.tsx";
|
||||
import { GrowDialogTransition } from "./component/FileManager/Search/SearchPopup.tsx";
|
||||
|
|
@ -344,6 +345,7 @@ const AppContent = () => {
|
|||
warning: StyledMaterialDesignContent,
|
||||
loading: LoadingSnackbar,
|
||||
default: StyledMaterialDesignContent,
|
||||
file: FileIconSnackbar,
|
||||
}}
|
||||
>
|
||||
<GlobalDialogs />
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export interface Share {
|
|||
owner: User;
|
||||
source_uri?: string;
|
||||
password?: string;
|
||||
show_readme?: boolean;
|
||||
}
|
||||
|
||||
export enum PolicyType {
|
||||
|
|
@ -288,6 +289,7 @@ export interface ShareCreateService {
|
|||
password?: string;
|
||||
expire?: number;
|
||||
share_view?: boolean;
|
||||
show_readme?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateFileService {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { Box } from "@mui/material";
|
||||
import MuiSnackbarContent from "@mui/material/SnackbarContent";
|
||||
import { CustomContentProps } from "notistack";
|
||||
import * as React from "react";
|
||||
import { forwardRef, useState } from "react";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx";
|
||||
|
||||
declare module "notistack" {
|
||||
interface VariantOverrides {
|
||||
file: {
|
||||
file: FileResponse;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface FileIconSnackbarProps extends CustomContentProps {
|
||||
file: FileResponse;
|
||||
}
|
||||
|
||||
const FileIconSnackbar = forwardRef<HTMLDivElement, FileIconSnackbarProps>((props, ref) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const {
|
||||
// You have access to notistack props and options 👇🏼
|
||||
message,
|
||||
action,
|
||||
id,
|
||||
file,
|
||||
// as well as your own custom props 👇🏼
|
||||
...other
|
||||
} = props;
|
||||
|
||||
let componentOrFunctionAction: React.ReactNode = undefined;
|
||||
if (typeof action === "function") {
|
||||
componentOrFunctionAction = action(id);
|
||||
} else {
|
||||
componentOrFunctionAction = action;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiSnackbarContent
|
||||
ref={ref}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
width: "100%",
|
||||
"& .MuiSnackbarContent-message": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
message={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", flexGrow: 1, gap: 1 }}>
|
||||
<FileTypeIcon sx={{ width: 20, height: 20 }} reverseDarkMode name={file.name} fileType={file.type} />
|
||||
<Box>{message}</Box>
|
||||
</Box>
|
||||
|
||||
{componentOrFunctionAction && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginLeft: "auto",
|
||||
paddingLeft: "16px",
|
||||
marginRight: "-8px",
|
||||
}}
|
||||
>
|
||||
{componentOrFunctionAction}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default FileIconSnackbar;
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { closeSnackbar, SnackbarKey } from "notistack";
|
||||
import { Button } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Response } from "../../../api/request.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { closeSnackbar, SnackbarKey } from "notistack";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { Response } from "../../../api/request.ts";
|
||||
import { setBatchDownloadLogDialog, setShareReadmeOpen } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { showAggregatedErrorDialog } from "../../../redux/thunks/dialog.ts";
|
||||
import { navigateToPath } from "../../../redux/thunks/filemanager.ts";
|
||||
import { FileManagerIndex } from "../../FileManager/FileManager.tsx";
|
||||
import { setBatchDownloadLogDialog } from "../../../redux/globalStateSlice.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const DefaultCloseAction = (snackbarId: SnackbarKey | undefined) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -42,6 +43,28 @@ export const ErrorListDetailAction = (error: Response<any>) => (snackbarId: Snac
|
|||
);
|
||||
};
|
||||
|
||||
export const OpenReadMeAction = (file: FileResponse) => (snackbarId: SnackbarKey | undefined) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const Close = DefaultCloseAction(snackbarId);
|
||||
|
||||
const openReadMe = useCallback(() => {
|
||||
dispatch(setShareReadmeOpen({ open: true, target: file }));
|
||||
closeSnackbar(snackbarId);
|
||||
}, [dispatch, file, snackbarId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={openReadMe} color="inherit" size="small">
|
||||
{t("application:modals.view")}
|
||||
</Button>
|
||||
{Close}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewDstAction = (dst: string) => (snackbarId: SnackbarKey | undefined) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const shareToSetting = (share: ShareModel, t: TFunction): ShareSetting => {
|
|||
password: share.password,
|
||||
use_custom_password: true,
|
||||
share_view: share.share_view,
|
||||
show_readme: share.show_readme,
|
||||
downloads: share.remain_downloads != undefined && share.remain_downloads > 0,
|
||||
|
||||
expires_val: expireOptions[2],
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import MuiAccordion from "@mui/material/Accordion";
|
|||
import MuiAccordionDetails from "@mui/material/AccordionDetails";
|
||||
import MuiAccordionSummary from "@mui/material/AccordionSummary";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FileResponse, FileType } from "../../../../api/explorer.ts";
|
||||
import { Code } from "../../../Admin/Common/Code.tsx";
|
||||
import { FilledTextField, SmallFormControlLabel } from "../../../Common/StyledComponents.tsx";
|
||||
import BookInformation from "../../../Icons/BookInformation.tsx";
|
||||
import ClockArrowDownload from "../../../Icons/ClockArrowDownload.tsx";
|
||||
import Eye from "../../../Icons/Eye.tsx";
|
||||
import TableSettingsOutlined from "../../../Icons/TableSettings.tsx";
|
||||
|
|
@ -76,6 +78,7 @@ export interface ShareSetting {
|
|||
use_custom_password?: boolean;
|
||||
password?: string;
|
||||
share_view?: boolean;
|
||||
show_readme?: boolean;
|
||||
downloads?: boolean;
|
||||
expires?: boolean;
|
||||
|
||||
|
|
@ -129,7 +132,7 @@ const ShareSettingContent = ({ setting, file, editing, onSettingChange }: ShareS
|
|||
setExpanded(isExpanded ? panel : undefined);
|
||||
};
|
||||
|
||||
const handleCheck = (prop: "is_private" | "share_view" | "expires" | "downloads") => () => {
|
||||
const handleCheck = (prop: "is_private" | "share_view" | "show_readme" | "expires" | "downloads") => () => {
|
||||
if (!setting[prop]) {
|
||||
handleExpand(prop)(null, true);
|
||||
}
|
||||
|
|
@ -198,20 +201,38 @@ const ShareSettingContent = ({ setting, file, editing, onSettingChange }: ShareS
|
|||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{file?.type == FileType.folder && (
|
||||
<Accordion expanded={expanded === "share_view"} onChange={handleExpand("share_view")}>
|
||||
<AccordionSummary aria-controls="panel1a-content" id="panel1a-header">
|
||||
<StyledListItemButton>
|
||||
<ListItemIcon>
|
||||
<TableSettingsOutlined />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t("application:modals.shareView")} />
|
||||
<ListItemSecondaryAction>
|
||||
<Checkbox checked={setting.share_view} onChange={handleCheck("share_view")} />
|
||||
</ListItemSecondaryAction>
|
||||
</StyledListItemButton>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>{t("application:modals.shareViewDes")}</AccordionDetails>
|
||||
</Accordion>
|
||||
<>
|
||||
<Accordion expanded={expanded === "share_view"} onChange={handleExpand("share_view")}>
|
||||
<AccordionSummary aria-controls="panel1a-content" id="panel1a-header">
|
||||
<StyledListItemButton>
|
||||
<ListItemIcon>
|
||||
<TableSettingsOutlined />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t("application:modals.shareView")} />
|
||||
<ListItemSecondaryAction>
|
||||
<Checkbox checked={setting.share_view} onChange={handleCheck("share_view")} />
|
||||
</ListItemSecondaryAction>
|
||||
</StyledListItemButton>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>{t("application:modals.shareViewDes")}</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion expanded={expanded === "show_readme"} onChange={handleExpand("show_readme")}>
|
||||
<AccordionSummary aria-controls="panel1a-content" id="panel1a-header">
|
||||
<StyledListItemButton>
|
||||
<ListItemIcon>
|
||||
<BookInformation />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t("application:modals.showReadme")} />
|
||||
<ListItemSecondaryAction>
|
||||
<Checkbox checked={setting.show_readme} onChange={handleCheck("show_readme")} />
|
||||
</ListItemSecondaryAction>
|
||||
</StyledListItemButton>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Trans i18nKey="application:modals.showReadmeDes" components={[<Code />]} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
<Accordion expanded={expanded === "expires"} onChange={handleExpand("expires")}>
|
||||
<AccordionSummary aria-controls="panel1a-content" id="panel1a-header">
|
||||
|
|
|
|||
|
|
@ -84,9 +84,19 @@ interface TypeIcon {
|
|||
color_dark?: string;
|
||||
img?: string;
|
||||
hideUnknown?: boolean;
|
||||
reverseDarkMode?: boolean;
|
||||
}
|
||||
|
||||
const FileTypeIcon = ({ name, fileType, notLoaded, sx, hideUnknown, customizedColor, ...rest }: FileTypeIconProps) => {
|
||||
const FileTypeIcon = ({
|
||||
name,
|
||||
fileType,
|
||||
notLoaded,
|
||||
sx,
|
||||
hideUnknown,
|
||||
customizedColor,
|
||||
reverseDarkMode,
|
||||
...rest
|
||||
}: FileTypeIconProps) => {
|
||||
const theme = useTheme();
|
||||
const iconOptions = useAppSelector((state) => state.siteConfig.explorer.typed?.icons) as ExpandedIconSettings;
|
||||
const IconComponent = useMemo(() => {
|
||||
|
|
@ -122,7 +132,7 @@ const FileTypeIcon = ({ name, fileType, notLoaded, sx, hideUnknown, customizedCo
|
|||
if (customizedColor) {
|
||||
return customizedColor;
|
||||
}
|
||||
if (theme.palette.mode == "dark") {
|
||||
if (theme.palette.mode == (reverseDarkMode ? "light" : "dark")) {
|
||||
return IconComponent.color_dark ?? IconComponent.color ?? theme.palette.action.active;
|
||||
} else {
|
||||
return IconComponent.color ?? theme.palette.action.active;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ImageViewer from "../Viewers/ImageViewer/ImageViewer.tsx";
|
|||
import Explorer from "./Explorer/Explorer.tsx";
|
||||
import { FmIndexContext } from "./FmIndexContext.tsx";
|
||||
import PaginationFooter from "./Pagination/PaginationFooter.tsx";
|
||||
import { ReadMe } from "./ReadMe/ReadMe.tsx";
|
||||
import Sidebar from "./Sidebar/Sidebar.tsx";
|
||||
import SidebarDialog from "./Sidebar/SidebarDialog.tsx";
|
||||
import NavHeader from "./TopBar/NavHeader.tsx";
|
||||
|
|
@ -97,6 +98,7 @@ export const FileManager = ({ index = 0, initialPath, skipRender }: FileManagerP
|
|||
<Box sx={{ display: "flex", flexGrow: 1, overflowY: "auto" }}>
|
||||
<Explorer />
|
||||
{index == FileManagerIndex.main && (isTablet ? <SidebarDialog /> : <Sidebar />)}
|
||||
{index == FileManagerIndex.main && <ReadMe />}
|
||||
</Box>
|
||||
<PaginationFooter />
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { RadiusFrame } from "../../Frame/RadiusFrame.tsx";
|
||||
import { Box, Pagination, Slide, styled, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { forwardRef, useContext } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { MinPageSize } from "../TopBar/ViewOptionPopover.tsx";
|
||||
import { changePage } from "../../../redux/thunks/filemanager.ts";
|
||||
import { RadiusFrame } from "../../Frame/RadiusFrame.tsx";
|
||||
import { MinPageSize } from "../TopBar/ViewOptionPopover.tsx";
|
||||
import PaginationItem from "./PaginationItem.tsx";
|
||||
|
||||
import { PaginationResults } from "../../../api/explorer.ts";
|
||||
import { FmIndexContext } from "../FmIndexContext.tsx";
|
||||
|
||||
const PaginationFrame = styled(RadiusFrame)(({ theme }) => ({
|
||||
|
|
@ -23,6 +24,10 @@ export interface PaginationState {
|
|||
|
||||
export const usePaginationState = (fmIndex: number) => {
|
||||
const pagination = useAppSelector((state) => state.fileManager[fmIndex].list?.pagination);
|
||||
return getPaginationState(pagination);
|
||||
};
|
||||
|
||||
export const getPaginationState = (pagination?: PaginationResults) => {
|
||||
const totalItems = pagination?.total_items;
|
||||
const page = pagination?.page;
|
||||
const pageSize = pagination?.page_size;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { closeShareReadme } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { detectReadMe } from "../../../redux/thunks/share.ts";
|
||||
import { FmIndexContext } from "../FmIndexContext.tsx";
|
||||
import ReadMeDialog from "./ReadMeDialog.tsx";
|
||||
import ReadMeSideBar from "./ReadMeSideBar.tsx";
|
||||
|
||||
export const ReadMe = () => {
|
||||
const fmIndex = useContext(FmIndexContext);
|
||||
const dispatch = useAppDispatch();
|
||||
const detect = useAppSelector((state) => state.globalState.shareReadmeDetect);
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
useEffect(() => {
|
||||
if (detect) {
|
||||
dispatch(detectReadMe(fmIndex, isTablet));
|
||||
}
|
||||
}, [detect, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (detect === 0) {
|
||||
setTimeout(() => {
|
||||
dispatch(closeShareReadme());
|
||||
}, 500);
|
||||
}
|
||||
}, [detect]);
|
||||
|
||||
if (isTablet) {
|
||||
return <ReadMeDialog />;
|
||||
}
|
||||
|
||||
return <ReadMeSideBar />;
|
||||
};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { Box, Skeleton, useTheme } from "@mui/material";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { getEntityContent } from "../../../redux/thunks/file.ts";
|
||||
import Header from "../Sidebar/Header.tsx";
|
||||
|
||||
const MarkdownEditor = lazy(() => import("../../Viewers/MarkdownEditor/Editor.tsx"));
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Skeleton variant="text" width="100%" height={24} />
|
||||
<Skeleton variant="text" width="40%" height={24} />
|
||||
<Skeleton variant="text" width="75%" height={24} />
|
||||
<Skeleton variant="text" width="85%" height={24} />
|
||||
<Skeleton variant="text" width="20%" height={24} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ReadMeContent = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const readMeTarget = useAppSelector((state) => state.globalState.shareReadmeTarget);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (readMeTarget) {
|
||||
setLoading(true);
|
||||
dispatch(getEntityContent(readMeTarget))
|
||||
.then((res) => {
|
||||
const content = new TextDecoder().decode(res);
|
||||
setValue(content);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [readMeTarget]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<Header target={readMeTarget} variant={"readme"} />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
bgcolor: "background.paper",
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{loading && <Loading />}
|
||||
{!loading && (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<MarkdownEditor
|
||||
displayOnly
|
||||
value={value}
|
||||
darkMode={theme.palette.mode === "dark"}
|
||||
readOnly={true}
|
||||
onChange={() => {}}
|
||||
initialValue={value}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadMeContent;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { Dialog, Slide } from "@mui/material";
|
||||
import { TransitionProps } from "@mui/material/transitions";
|
||||
import { forwardRef } from "react";
|
||||
import { closeShareReadme } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import ReadMeContent from "./ReadMeContent.tsx";
|
||||
|
||||
const Transition = forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement<unknown>;
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
const ReadMeDialog = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const readMeOpen = useAppSelector((state) => state.globalState.shareReadmeOpen);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
fullScreen
|
||||
TransitionComponent={Transition}
|
||||
open={!!readMeOpen}
|
||||
onClose={() => {
|
||||
dispatch(closeShareReadme());
|
||||
}}
|
||||
>
|
||||
<ReadMeContent />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadMeDialog;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { Box, Collapse } from "@mui/material";
|
||||
import { useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { RadiusFrame } from "../../Frame/RadiusFrame.tsx";
|
||||
import ReadMeContent from "./ReadMeContent.tsx";
|
||||
|
||||
const ReadMeSideBar = () => {
|
||||
const readMeOpen = useAppSelector((state) => state.globalState.shareReadmeOpen);
|
||||
return (
|
||||
<Box>
|
||||
<Collapse in={readMeOpen} sx={{ height: "100%" }} orientation={"horizontal"} unmountOnExit timeout={"auto"}>
|
||||
<RadiusFrame
|
||||
sx={{
|
||||
width: "400px",
|
||||
height: "100%",
|
||||
ml: 1,
|
||||
overflow: "hidden",
|
||||
borderRadius: (theme) => theme.shape.borderRadius / 8,
|
||||
}}
|
||||
withBorder={true}
|
||||
>
|
||||
<ReadMeContent />
|
||||
</RadiusFrame>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadMeSideBar;
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { Box, IconButton, Skeleton, Typography } from "@mui/material";
|
||||
import FileIcon from "../Explorer/FileIcon.tsx";
|
||||
import Dismiss from "../../Icons/Dismiss.tsx";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { closeShareReadme, closeSidebar } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { closeSidebar } from "../../../redux/globalStateSlice.ts";
|
||||
import Dismiss from "../../Icons/Dismiss.tsx";
|
||||
import FileIcon from "../Explorer/FileIcon.tsx";
|
||||
|
||||
export interface HeaderProps {
|
||||
target: FileResponse | undefined | null;
|
||||
variant?: "readme";
|
||||
}
|
||||
const Header = ({ target }: HeaderProps) => {
|
||||
const Header = ({ target, variant }: HeaderProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<Box sx={{ display: "flex", p: 2 }}>
|
||||
|
|
@ -23,7 +24,7 @@ const Header = ({ target }: HeaderProps) => {
|
|||
)}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
dispatch(closeSidebar());
|
||||
dispatch(variant == "readme" ? closeShareReadme() : closeSidebar());
|
||||
}}
|
||||
sx={{
|
||||
ml: 1,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { SvgIcon, SvgIconProps } from "@mui/material";
|
||||
|
||||
export default function BookInformation(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d="M13.25 7a1 1 0 1 1-2 0a1 1 0 0 1 2 0M11.5 9.75v5a.75.75 0 0 0 1.5 0v-5a.75.75 0 0 0-1.5 0M4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5zM19 18V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1V18z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ export interface MarkdownEditorProps {
|
|||
initialValue: string;
|
||||
onChange: (value: string) => void;
|
||||
readOnly?: boolean;
|
||||
displayOnly?: boolean;
|
||||
onSaveShortcut?: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
|
|||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "calc(100vh - 200px)",
|
||||
minHeight: props.displayOnly ? "100%" : "calc(100vh - 200px)",
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
|
|
@ -112,77 +113,81 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
|
|||
diffMarkdown: props.initialValue,
|
||||
viewMode: "rich-text",
|
||||
}),
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<ConditionalContents
|
||||
options={[
|
||||
{
|
||||
when: (editor) => editor?.editorType === "codeblock",
|
||||
contents: () => <ChangeCodeMirrorLanguage />,
|
||||
},
|
||||
{
|
||||
when: (editor) => editor?.editorType === "sandpack",
|
||||
contents: () => <ShowSandpackInfo />,
|
||||
},
|
||||
{
|
||||
fallback: () => (
|
||||
<DiffSourceToggleWrapper>
|
||||
<UndoRedo />
|
||||
<Separator />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<CodeToggle />
|
||||
<Separator />
|
||||
<StrikeThroughSupSubToggles />
|
||||
<Separator />
|
||||
<ListsToggle />
|
||||
<Separator />
|
||||
...(props.displayOnly
|
||||
? []
|
||||
: [
|
||||
toolbarPlugin({
|
||||
toolbarContents: () => (
|
||||
<ConditionalContents
|
||||
options={[
|
||||
{
|
||||
when: (editor) => editor?.editorType === "codeblock",
|
||||
contents: () => <ChangeCodeMirrorLanguage />,
|
||||
},
|
||||
{
|
||||
when: (editor) => editor?.editorType === "sandpack",
|
||||
contents: () => <ShowSandpackInfo />,
|
||||
},
|
||||
{
|
||||
fallback: () => (
|
||||
<DiffSourceToggleWrapper>
|
||||
<UndoRedo />
|
||||
<Separator />
|
||||
<BoldItalicUnderlineToggles />
|
||||
<CodeToggle />
|
||||
<Separator />
|
||||
<StrikeThroughSupSubToggles />
|
||||
<Separator />
|
||||
<ListsToggle />
|
||||
<Separator />
|
||||
|
||||
<ConditionalContents
|
||||
options={[
|
||||
{
|
||||
when: whenInAdmonition,
|
||||
contents: () => <ChangeAdmonitionType />,
|
||||
},
|
||||
{ fallback: () => <BlockTypeSelect /> },
|
||||
]}
|
||||
/>
|
||||
<ConditionalContents
|
||||
options={[
|
||||
{
|
||||
when: whenInAdmonition,
|
||||
contents: () => <ChangeAdmonitionType />,
|
||||
},
|
||||
{ fallback: () => <BlockTypeSelect /> },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
<CreateLink />
|
||||
<InsertImage />
|
||||
<CreateLink />
|
||||
<InsertImage />
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
<InsertTable />
|
||||
<InsertThematicBreak />
|
||||
<InsertTable />
|
||||
<InsertThematicBreak />
|
||||
|
||||
<Separator />
|
||||
<InsertCodeBlock />
|
||||
<Separator />
|
||||
<InsertCodeBlock />
|
||||
|
||||
<ConditionalContents
|
||||
options={[
|
||||
{
|
||||
when: (editorInFocus) => !whenInAdmonition(editorInFocus),
|
||||
contents: () => (
|
||||
<>
|
||||
<Separator />
|
||||
<InsertAdmonition />
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ConditionalContents
|
||||
options={[
|
||||
{
|
||||
when: (editorInFocus) => !whenInAdmonition(editorInFocus),
|
||||
contents: () => (
|
||||
<>
|
||||
<Separator />
|
||||
<InsertAdmonition />
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
<InsertFrontmatter />
|
||||
</DiffSourceToggleWrapper>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
<Separator />
|
||||
<InsertFrontmatter />
|
||||
</DiffSourceToggleWrapper>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
]),
|
||||
listsPlugin(),
|
||||
quotePlugin(),
|
||||
headingsPlugin({ allowedHeadingLevels: [1, 2, 3] }),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect } from "react";
|
||||
import { useQuery } from "../util";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks.ts";
|
||||
import { beforePathChange, navigateReconcile, setTargetPath } from "../redux/thunks/filemanager.ts";
|
||||
import { Filesystem } from "../util/uri.ts";
|
||||
import { FileManagerIndex } from "../component/FileManager/FileManager.tsx";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks.ts";
|
||||
import { beforePathChange, checkReadMeEnabled, navigateReconcile, setTargetPath } from "../redux/thunks/filemanager.ts";
|
||||
import { useQuery } from "../util";
|
||||
import { Filesystem } from "../util/uri.ts";
|
||||
|
||||
const pathQueryKey = "path";
|
||||
export const defaultPath = "cloudreve://my";
|
||||
|
|
@ -30,7 +30,9 @@ const useNavigation = (index: number, initialPath?: string) => {
|
|||
// When path state changed, dispatch to load file list
|
||||
useEffect(() => {
|
||||
if (path) {
|
||||
dispatch(navigateReconcile(index));
|
||||
dispatch(navigateReconcile(index)).then(() => {
|
||||
dispatch(checkReadMeEnabled(index));
|
||||
});
|
||||
dispatch(beforePathChange(index));
|
||||
}
|
||||
}, [path]);
|
||||
|
|
|
|||
|
|
@ -242,6 +242,11 @@ export interface GlobalStateSlice {
|
|||
advanceSearchOpen?: boolean;
|
||||
advanceSearchBasePath?: string;
|
||||
advanceSearchInitialNameCondition?: string[];
|
||||
|
||||
// Share README
|
||||
shareReadmeDetect?: number;
|
||||
shareReadmeOpen?: boolean;
|
||||
shareReadmeTarget?: FileResponse;
|
||||
}
|
||||
|
||||
let preferred_theme: string | undefined = undefined;
|
||||
|
|
@ -267,6 +272,16 @@ export const globalStateSlice = createSlice({
|
|||
name: "globalState",
|
||||
initialState,
|
||||
reducers: {
|
||||
setShareReadmeDetect: (state, action: PayloadAction<boolean>) => {
|
||||
state.shareReadmeDetect = action.payload ? (state.shareReadmeDetect ?? 0) + 1 : 0;
|
||||
},
|
||||
setShareReadmeOpen: (state, action: PayloadAction<{ open: boolean; target?: FileResponse }>) => {
|
||||
state.shareReadmeOpen = action.payload.open;
|
||||
state.shareReadmeTarget = action.payload.target;
|
||||
},
|
||||
closeShareReadme: (state) => {
|
||||
state.shareReadmeOpen = false;
|
||||
},
|
||||
setDirectLinkManagementDialog: (
|
||||
state,
|
||||
action: PayloadAction<{ open: boolean; file?: FileResponse; highlight?: string }>,
|
||||
|
|
@ -815,4 +830,7 @@ export const {
|
|||
closeExcalidrawViewer,
|
||||
setDirectLinkManagementDialog,
|
||||
closeDirectLinkManagementDialog,
|
||||
setShareReadmeDetect,
|
||||
closeShareReadme,
|
||||
setShareReadmeOpen,
|
||||
} = globalStateSlice.actions;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import dayjs from "dayjs";
|
||||
import { getFileInfo, getFileList, getUserCapacity, sendPatchViewSync } from "../../api/api.ts";
|
||||
import { ExplorerView, FileResponse, ListResponse, Metadata } from "../../api/explorer.ts";
|
||||
import { ExplorerView, FileResponse, FileType, ListResponse, Metadata } from "../../api/explorer.ts";
|
||||
import { getActionOpt } from "../../component/FileManager/ContextMenu/useActionDisplayOpt.ts";
|
||||
import { ListViewColumnSetting } from "../../component/FileManager/Explorer/ListView/Column.tsx";
|
||||
import { FileManagerIndex } from "../../component/FileManager/FileManager.tsx";
|
||||
|
|
@ -46,11 +46,13 @@ import {
|
|||
setAdvanceSearch,
|
||||
setPinFileDialog,
|
||||
setSearchPopup,
|
||||
setShareReadmeDetect,
|
||||
setUploadFromClipboardDialog,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { Viewers } from "../siteConfigSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { deleteFile, openFileContextMenu } from "./file.ts";
|
||||
import { queueLoadShareInfo } from "./share.ts";
|
||||
|
||||
export function setTargetPath(index: number, path: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
|
|
@ -110,7 +112,23 @@ export function beforePathChange(index: number): AppThunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function navigateReconcile(index: number, opt?: NavigateReconcileOptions): AppThunk {
|
||||
export function checkReadMeEnabled(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const { path, current_fs } = getState().fileManager[index];
|
||||
if (path && current_fs == Filesystem.share) {
|
||||
try {
|
||||
const info = await dispatch(queueLoadShareInfo(new CrUri(path), false));
|
||||
dispatch(setShareReadmeDetect(info?.show_readme && info.source_type == FileType.folder));
|
||||
} catch (e) {
|
||||
dispatch(setShareReadmeDetect(false));
|
||||
}
|
||||
} else {
|
||||
dispatch(setShareReadmeDetect(false));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function navigateReconcile(index: number, opt?: NavigateReconcileOptions): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const timeNow = dayjs().valueOf();
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import i18next from "i18next";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { closeSnackbar, enqueueSnackbar, SnackbarKey } from "notistack";
|
||||
import { getFileInfo, getFileList, getShareInfo, sendCreateShare, sendUpdateShare } from "../../api/api.ts";
|
||||
import { FileResponse, Share, ShareCreateService } from "../../api/explorer.ts";
|
||||
import { DefaultCloseAction } from "../../component/Common/Snackbar/snackbar.tsx";
|
||||
import { DefaultCloseAction, OpenReadMeAction } from "../../component/Common/Snackbar/snackbar.tsx";
|
||||
import { ShareSetting } from "../../component/FileManager/Dialogs/Share/ShareSetting.tsx";
|
||||
import { getPaginationState } from "../../component/FileManager/Pagination/PaginationFooter.tsx";
|
||||
import CrUri from "../../util/uri.ts";
|
||||
import { fileUpdated } from "../fileManagerSlice.ts";
|
||||
import { addShareInfo, setManageShareDialog, setShareLinkDialog } from "../globalStateSlice.ts";
|
||||
import {
|
||||
addShareInfo,
|
||||
closeShareReadme,
|
||||
setManageShareDialog,
|
||||
setShareLinkDialog,
|
||||
setShareReadmeOpen,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { longRunningTaskWithSnackbar } from "./file.ts";
|
||||
|
||||
|
|
@ -22,6 +29,7 @@ export function createOrUpdateShareLink(
|
|||
is_private: setting.is_private,
|
||||
password: setting.password,
|
||||
share_view: setting.share_view,
|
||||
show_readme: setting.show_readme,
|
||||
downloads: setting.downloads && setting.downloads_val.value > 0 ? setting.downloads_val.value : undefined,
|
||||
expire: setting.expires && setting.expires_val.value > 0 ? setting.expires_val.value : undefined,
|
||||
};
|
||||
|
|
@ -127,6 +135,63 @@ export function openShareEditByID(shareId: string, password?: string, singleFile
|
|||
};
|
||||
}
|
||||
|
||||
// Priority from high to low
|
||||
const supportedReadMeFiles = ["README.md", "README.txt"];
|
||||
|
||||
export function detectReadMe(index: number, isTablet: boolean): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const { files: list, pagination } = getState().fileManager[index]?.list ?? {};
|
||||
if (list) {
|
||||
// Find readme file from highest to lowest priority
|
||||
for (const readmeFile of supportedReadMeFiles) {
|
||||
const found = list.find((file) => file.name === readmeFile);
|
||||
if (found) {
|
||||
dispatch(tryOpenReadMe(found, isTablet));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in current file list, try to get file directly
|
||||
const path = getState().fileManager[index]?.pure_path;
|
||||
const hasMorePages = getPaginationState(pagination).moreItems;
|
||||
if (path && hasMorePages) {
|
||||
const uri = new CrUri(path);
|
||||
for (const readmeFile of supportedReadMeFiles) {
|
||||
try {
|
||||
const file = await dispatch(getFileInfo({ uri: uri.join(readmeFile).toString() }, true));
|
||||
if (file) {
|
||||
dispatch(tryOpenReadMe(file, isTablet));
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
dispatch(closeShareReadme());
|
||||
};
|
||||
}
|
||||
|
||||
let snackbarId: SnackbarKey | undefined = undefined;
|
||||
|
||||
function tryOpenReadMe(file: FileResponse, askForConfirmation?: boolean): AppThunk<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
if (askForConfirmation) {
|
||||
dispatch(setShareReadmeOpen({ open: false, target: file }));
|
||||
if (snackbarId) {
|
||||
closeSnackbar(snackbarId);
|
||||
}
|
||||
snackbarId = enqueueSnackbar({
|
||||
message: "README.md",
|
||||
variant: "file",
|
||||
file,
|
||||
action: OpenReadMeAction(file),
|
||||
});
|
||||
} else {
|
||||
dispatch(setShareReadmeOpen({ open: true, target: file }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getFileAndShareById(
|
||||
shareId: string,
|
||||
password?: string,
|
||||
|
|
|
|||
Loading…
Reference in New Issue