feat(share): add option to automatically render and show README file (#2382)

This commit is contained in:
Aaron Liu 2025-07-04 14:40:30 +08:00
parent 27996dc3ea
commit 38f5114426
26 changed files with 1146 additions and 705 deletions

View File

@ -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"
]
}
}
}

View File

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

View File

@ -510,7 +510,9 @@
"unlinkOnly": "物理ファイルを保持",
"unlinkOnlyDes": "ファイル記録のみ削除、物理ファイルは削除されません",
"shareView": "共有ビュー設定",
"shareViewDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。"
"shareViewDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。",
"showReadme": "README ファイルを表示",
"showReadmeDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。"
},
"uploader": {
"fileCopyName": "コピー_",

View File

@ -513,6 +513,8 @@
"unlinkOnlyDes": "仅删除文件记录,物理文件不会被删除",
"shareView": "分享视图设置",
"shareViewDes": "勾选后,其他用户访问此共享文件夹时可以看到你保存在服务器的视图设置(布局、排序等)。",
"showReadme": "显示 README 文件",
"showReadmeDes": "勾选后,会自动为访问者展示目录下的 <0>README.md</0> (区分大小写) 文件。",
"viewSetting": "视图设置",
"saved": "已保存",
"notSet": "未设置",

View File

@ -509,6 +509,8 @@
"unlinkOnlyDes": "僅刪除檔案記錄,物理檔案不會被刪除",
"shareView": "分享視圖設定",
"shareViewDes": "勾選後,其他使用者存取此共享資料夾時可以看到你保存在服務器的視圖設定(佈局、排序等)。",
"showReadme": "顯示 README 文件",
"showReadmeDes": "勾選後,會自動為訪問者展示目錄下的 <0>README.md</0> (區分大小寫) 文件。",
"viewSetting": "視圖設定",
"saved": "已保存",
"notSet": "未設定",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1187
yarn.lock

File diff suppressed because it is too large Load Diff