mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(markdown): upload and insert images (cloudreve/cloudreve#2593)
This commit is contained in:
parent
0feed521f0
commit
b7bfbffda5
|
|
@ -1,7 +1,8 @@
|
|||
import { Box, Skeleton, useTheme } from "@mui/material";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { getEntityContent } from "../../../redux/thunks/file.ts";
|
||||
import { markdownImagePreviewHandler } from "../../../redux/thunks/viewer.ts";
|
||||
import Header from "../Sidebar/Header.tsx";
|
||||
|
||||
const MarkdownEditor = lazy(() => import("../../Viewers/MarkdownEditor/Editor.tsx"));
|
||||
|
|
@ -40,6 +41,13 @@ const ReadMeContent = () => {
|
|||
}
|
||||
}, [readMeTarget]);
|
||||
|
||||
const imagePreviewHandler = useCallback(
|
||||
async (imageSource: string) => {
|
||||
return dispatch(markdownImagePreviewHandler(imageSource, readMeTarget?.path ?? ""));
|
||||
},
|
||||
[dispatch, readMeTarget],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<Header target={readMeTarget} variant={"readme"} />
|
||||
|
|
@ -62,6 +70,7 @@ const ReadMeContent = () => {
|
|||
readOnly={true}
|
||||
onChange={() => {}}
|
||||
initialValue={value}
|
||||
imagePreviewHandler={imagePreviewHandler}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ import { useSnackbar } from "notistack";
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ContextMenuTypes } from "../../redux/fileManagerSlice.ts";
|
||||
import { closeUploadTaskList, openUploadTaskList, setUploadProgress } from "../../redux/globalStateSlice.ts";
|
||||
import {
|
||||
closeUploadTaskList,
|
||||
openUploadTaskList,
|
||||
setUploadProgress,
|
||||
setUploadRawFiles,
|
||||
} from "../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import { refreshFileList, updateUserCapacity } from "../../redux/thunks/filemanager.ts";
|
||||
import SessionManager, { UserSettings } from "../../session";
|
||||
|
|
@ -38,6 +43,8 @@ const Uploader = () => {
|
|||
const policy = useAppSelector((state) => state.fileManager[FileManagerIndex.main].list?.storage_policy);
|
||||
const selectFileSignal = useAppSelector((state) => state.globalState.uploadFileSignal);
|
||||
const selectFolderSignal = useAppSelector((state) => state.globalState.uploadFolderSignal);
|
||||
const uploadRawPromiseId = useAppSelector((state) => state.globalState.uploadRawPromiseId);
|
||||
const uploadRawFiles = useAppSelector((state) => state.globalState.uploadRawFiles);
|
||||
|
||||
const displayOpt = useActionDisplayOpt([], ContextMenuTypes.empty, parent, FileManagerIndex.main);
|
||||
|
||||
|
|
@ -160,6 +167,15 @@ const Uploader = () => {
|
|||
[uploadManager, taskAdded, handleUploaderError, dispatch, getClipboardFileName],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadRawFiles && uploadRawFiles.length > 0) {
|
||||
uploadManager.addRawFiles(uploadRawFiles, getClipboardFileName, uploadRawPromiseId).catch((e) => {
|
||||
handleUploaderError(e);
|
||||
});
|
||||
dispatch(setUploadRawFiles({ files: [], promiseId: [] }));
|
||||
}
|
||||
}, [uploadRawFiles, uploadRawPromiseId, handleUploaderError, uploadManager]);
|
||||
|
||||
useEffect(() => {
|
||||
const unfinished = uploadManager.resumeTasks();
|
||||
setUploaders((uploaders) => [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Logger, { LogLevel } from "./logger";
|
|||
import { Task, TaskType } from "./types";
|
||||
import Base, { MessageColor } from "./uploader/base";
|
||||
import COS from "./uploader/cos";
|
||||
import KS3 from "./uploader/ks3";
|
||||
import Local from "./uploader/local";
|
||||
import OBS from "./uploader/obs.ts";
|
||||
import OneDrive from "./uploader/onedrive";
|
||||
|
|
@ -13,7 +14,6 @@ import ResumeHint from "./uploader/placeholder";
|
|||
import Qiniu from "./uploader/qiniu";
|
||||
import Remote from "./uploader/remote";
|
||||
import S3 from "./uploader/s3";
|
||||
import KS3 from "./uploader/ks3";
|
||||
import Upyun from "./uploader/upyun";
|
||||
import {
|
||||
cleanupResumeCtx,
|
||||
|
|
@ -196,13 +196,24 @@ export default class UploadManager {
|
|||
cleanupResumeCtx(this.logger);
|
||||
};
|
||||
|
||||
public addRawFiles = async (files: File[], getName?: (file: File) => string) => {
|
||||
public addRawFiles = async (
|
||||
files: File[],
|
||||
getName?: (file: File) => string,
|
||||
promiseIds?: string[],
|
||||
): Promise<Base[] | undefined> => {
|
||||
if (!this.currentPath) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
const uploaders = await new Promise<Base[]>((resolve, reject) =>
|
||||
this.addFiles(files, this.currentPath ?? defaultPath, resolve, reject, getName),
|
||||
);
|
||||
if (promiseIds) {
|
||||
uploaders.forEach((u, i) => {
|
||||
if (promiseIds[i]) {
|
||||
u.promiseId = promiseIds[i];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.o.onProactiveFileAdded && this.o.onProactiveFileAdded(uploaders);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
// 所有 Uploader 的基类
|
||||
import { Task } from "../types";
|
||||
import UploadManager from "../index";
|
||||
import Logger from "../logger";
|
||||
import { validate } from "../utils/validator";
|
||||
import { CancelToken } from "../utils/request";
|
||||
import axios, { CanceledError, CancelTokenSource } from "axios";
|
||||
import { createUploadSession, deleteUploadSession } from "../api";
|
||||
import * as utils from "../utils";
|
||||
import { UploaderError } from "../errors";
|
||||
import { PolicyType } from "../../../../api/explorer.ts";
|
||||
import CrUri from "../../../../util/uri.ts";
|
||||
import { createUploadSession, deleteUploadSession } from "../api";
|
||||
import { UploaderError } from "../errors";
|
||||
import UploadManager from "../index";
|
||||
import Logger from "../logger";
|
||||
import { Task } from "../types";
|
||||
import * as utils from "../utils";
|
||||
import { CancelToken } from "../utils/request";
|
||||
import { validate } from "../utils/validator";
|
||||
|
||||
export enum Status {
|
||||
added,
|
||||
|
|
@ -65,6 +65,13 @@ const resumePolicy = [
|
|||
];
|
||||
const deleteUploadSessionDelay = 500;
|
||||
|
||||
export const uploadPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: Task | PromiseLike<Task>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export default abstract class Base {
|
||||
public child?: Base[];
|
||||
public status: Status = Status.added;
|
||||
|
|
@ -81,6 +88,7 @@ export default abstract class Base {
|
|||
|
||||
public lastTime = Date.now();
|
||||
public startTime = Date.now();
|
||||
public promiseId: string | undefined;
|
||||
|
||||
constructor(
|
||||
public task: Task,
|
||||
|
|
@ -229,6 +237,21 @@ export default abstract class Base {
|
|||
|
||||
protected transit(status: Status) {
|
||||
this.status = status;
|
||||
if (this.promiseId && status === Status.finished) {
|
||||
const promise = uploadPromisePool[this.promiseId];
|
||||
delete uploadPromisePool[this.promiseId];
|
||||
this.promiseId = undefined;
|
||||
if (promise) {
|
||||
switch (status) {
|
||||
case Status.finished:
|
||||
promise.resolve(this.task);
|
||||
break;
|
||||
default:
|
||||
promise.reject(this.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.subscriber.onTransition(status);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export interface MarkdownEditorProps {
|
|||
onSaveShortcut?: () => void;
|
||||
imageAutocompleteSuggestions?: string[] | null;
|
||||
imagePreviewHandler?: (imageSource: string) => Promise<string>;
|
||||
imageUploadHandler?: ((image: File) => Promise<string>) | null;
|
||||
}
|
||||
|
||||
function whenInAdmonition(editorInFocus: EditorInFocus | null) {
|
||||
|
|
@ -196,9 +197,7 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
|
|||
linkPlugin(),
|
||||
linkDialogPlugin(),
|
||||
imagePlugin({
|
||||
imageUploadHandler: () => {
|
||||
return Promise.resolve("https://picsum.photos/200/300");
|
||||
},
|
||||
imageUploadHandler: props.imageUploadHandler,
|
||||
imagePreviewHandler: props.imagePreviewHandler,
|
||||
imageAutocompleteSuggestions: props.imageAutocompleteSuggestions ?? undefined,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
markdownImageAutocompleteSuggestions,
|
||||
markdownImagePreviewHandler,
|
||||
saveMarkdown,
|
||||
uploadMarkdownImage,
|
||||
} from "../../../redux/thunks/viewer.ts";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
|
||||
import useActionDisplayOpt, { canUpdate } from "../../FileManager/ContextMenu/useActionDisplayOpt.ts";
|
||||
|
|
@ -124,6 +125,13 @@ const MarkdownViewer = () => {
|
|||
[dispatch, viewerState?.file?.path],
|
||||
);
|
||||
|
||||
const onImageUpload = useCallback(
|
||||
async (file: File): Promise<string> => {
|
||||
return dispatch(uploadMarkdownImage(file));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewerDialog
|
||||
file={viewerState?.file}
|
||||
|
|
@ -179,6 +187,7 @@ const MarkdownViewer = () => {
|
|||
onSaveShortcut={onSaveShortcut}
|
||||
imagePreviewHandler={imagePreviewHandler}
|
||||
imageAutocompleteSuggestions={imageAutocompleteSuggestions}
|
||||
imageUploadHandler={onImageUpload}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ export interface GlobalStateSlice {
|
|||
uploadTaskCount?: number;
|
||||
uploadTaskListOpen?: boolean;
|
||||
uploadFromClipboardDialogOpen?: boolean;
|
||||
uploadRawFiles?: File[];
|
||||
uploadRawPromiseId?: string[];
|
||||
|
||||
policyOptionCache?: StoragePolicy[];
|
||||
|
||||
|
|
@ -272,6 +274,16 @@ export const globalStateSlice = createSlice({
|
|||
name: "globalState",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUploadRawFiles: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
files: File[];
|
||||
promiseId: string[];
|
||||
}>,
|
||||
) => {
|
||||
state.uploadRawFiles = action.payload.files ?? [];
|
||||
state.uploadRawPromiseId = action.payload.promiseId ?? [];
|
||||
},
|
||||
setShareReadmeDetect: (state, action: PayloadAction<boolean>) => {
|
||||
state.shareReadmeDetect = action.payload ? (state.shareReadmeDetect ?? 0) + 1 : 0;
|
||||
},
|
||||
|
|
@ -737,6 +749,7 @@ export const globalStateSlice = createSlice({
|
|||
|
||||
export default globalStateSlice.reducer;
|
||||
export const {
|
||||
setUploadRawFiles,
|
||||
setMobileDrawerOpen,
|
||||
setDirectLinkDialog,
|
||||
closeDirectLinkDialog,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { getPaginationState } from "../../component/FileManager/Pagination/Pagin
|
|||
import { Condition, ConditionType } from "../../component/FileManager/Search/AdvanceSearch/ConditionBox.tsx";
|
||||
import { MinPageSize } from "../../component/FileManager/TopBar/ViewOptionPopover.tsx";
|
||||
import { SelectType } from "../../component/Uploader/core";
|
||||
import { Task } from "../../component/Uploader/core/types.ts";
|
||||
import { uploadPromisePool } from "../../component/Uploader/core/uploader/base.ts";
|
||||
import { defaultPath } from "../../hooks/useNavigation.tsx";
|
||||
import { router } from "../../router";
|
||||
import SessionManager, { UserSettings } from "../../session";
|
||||
|
|
@ -49,9 +51,11 @@ import {
|
|||
setSearchPopup,
|
||||
setShareReadmeDetect,
|
||||
setUploadFromClipboardDialog,
|
||||
setUploadRawFiles,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { Viewers, ViewersByID } from "../siteConfigSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { promiseId } from "./dialog.ts";
|
||||
import { deleteFile, openFileContextMenu } from "./file.ts";
|
||||
import { queueLoadShareInfo } from "./share.ts";
|
||||
import { openViewer } from "./viewer.ts";
|
||||
|
|
@ -785,6 +789,21 @@ export function applyGalleryWidth(index: number, width: number): AppThunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function uploadRawFile(files: File): AppThunk<Promise<Task>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<Task>((resolve, reject) => {
|
||||
uploadPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setUploadRawFiles({
|
||||
files: [files],
|
||||
promiseId: [id],
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function sortByLocalCompare(files: FileResponse[], mixed?: boolean, isDesc?: boolean): FileResponse[] {
|
||||
if (files.length === 0) {
|
||||
return files;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { Viewers, ViewersByID } from "../siteConfigSlice.ts";
|
|||
import { AppThunk } from "../store.ts";
|
||||
import { askSaveAs, askStaleVersionAction } from "./dialog.ts";
|
||||
import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks } from "./file.ts";
|
||||
import { uploadRawFile } from "./filemanager.ts";
|
||||
|
||||
export interface ExpandedViewerSetting {
|
||||
[key: string]: Viewer[];
|
||||
|
|
@ -718,8 +719,8 @@ export function markdownImagePreviewHandler(imageSource: string, mdFileUri: stri
|
|||
}
|
||||
|
||||
try {
|
||||
const file = await dispatch(getFileInfo({ uri: uri.toString() }));
|
||||
const fileUrl = await dispatch(getFileEntityUrl({ uris: [getFileLinkedUri(file)], entity: file.primary_entity }));
|
||||
const file = await dispatch(getFileInfo({ uri: uri.toString() }, true));
|
||||
const fileUrl = await dispatch(getFileEntityUrl({ uris: [getFileLinkedUri(file)] }));
|
||||
return fileUrl.urls[0].url;
|
||||
} catch (e) {
|
||||
return BROKEN_IMG_URI;
|
||||
|
|
@ -742,3 +743,10 @@ export function markdownImageAutocompleteSuggestions(): AppThunk<string[] | null
|
|||
return suggestions.map((f) => f.name);
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadMarkdownImage(file: File): AppThunk<Promise<string>> {
|
||||
return async (dispatch, getState) => {
|
||||
const task = await dispatch(uploadRawFile(file));
|
||||
return task.name;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue