diff --git a/src/component/FileManager/ReadMe/ReadMeContent.tsx b/src/component/FileManager/ReadMe/ReadMeContent.tsx index 26593f8..b64e0e8 100644 --- a/src/component/FileManager/ReadMe/ReadMeContent.tsx +++ b/src/component/FileManager/ReadMe/ReadMeContent.tsx @@ -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 (
@@ -62,6 +70,7 @@ const ReadMeContent = () => { readOnly={true} onChange={() => {}} initialValue={value} + imagePreviewHandler={imagePreviewHandler} /> )} diff --git a/src/component/Uploader/Uploader.tsx b/src/component/Uploader/Uploader.tsx index bbab5d9..6da2df8 100644 --- a/src/component/Uploader/Uploader.tsx +++ b/src/component/Uploader/Uploader.tsx @@ -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) => [ diff --git a/src/component/Uploader/core/index.ts b/src/component/Uploader/core/index.ts index 4300e86..1b74b27 100644 --- a/src/component/Uploader/core/index.ts +++ b/src/component/Uploader/core/index.ts @@ -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 => { if (!this.currentPath) { - return; + return undefined; } const uploaders = await new Promise((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); }; diff --git a/src/component/Uploader/core/uploader/base.ts b/src/component/Uploader/core/uploader/base.ts index 77ce7af..b760c64 100644 --- a/src/component/Uploader/core/uploader/base.ts +++ b/src/component/Uploader/core/uploader/base.ts @@ -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) => 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); } diff --git a/src/component/Viewers/MarkdownEditor/Editor.tsx b/src/component/Viewers/MarkdownEditor/Editor.tsx index 4f2041f..7d53afb 100644 --- a/src/component/Viewers/MarkdownEditor/Editor.tsx +++ b/src/component/Viewers/MarkdownEditor/Editor.tsx @@ -56,6 +56,7 @@ export interface MarkdownEditorProps { onSaveShortcut?: () => void; imageAutocompleteSuggestions?: string[] | null; imagePreviewHandler?: (imageSource: string) => Promise; + imageUploadHandler?: ((image: File) => Promise) | 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, }), diff --git a/src/component/Viewers/MarkdownEditor/MarkdownViewer.tsx b/src/component/Viewers/MarkdownEditor/MarkdownViewer.tsx index b8a1768..e14b4fb 100644 --- a/src/component/Viewers/MarkdownEditor/MarkdownViewer.tsx +++ b/src/component/Viewers/MarkdownEditor/MarkdownViewer.tsx @@ -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 => { + return dispatch(uploadMarkdownImage(file)); + }, + [dispatch], + ); + return ( { onSaveShortcut={onSaveShortcut} imagePreviewHandler={imagePreviewHandler} imageAutocompleteSuggestions={imageAutocompleteSuggestions} + imageUploadHandler={onImageUpload} /> )} diff --git a/src/redux/globalStateSlice.ts b/src/redux/globalStateSlice.ts index b966982..1e46f1a 100644 --- a/src/redux/globalStateSlice.ts +++ b/src/redux/globalStateSlice.ts @@ -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) => { 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, diff --git a/src/redux/thunks/filemanager.ts b/src/redux/thunks/filemanager.ts index ea16b0c..64247e0 100644 --- a/src/redux/thunks/filemanager.ts +++ b/src/redux/thunks/filemanager.ts @@ -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> { + return async (dispatch, _getState) => { + const id = promiseId(); + return new Promise((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; diff --git a/src/redux/thunks/viewer.ts b/src/redux/thunks/viewer.ts index e5a1e79..4d75bee 100644 --- a/src/redux/thunks/viewer.ts +++ b/src/redux/thunks/viewer.ts @@ -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 f.name); }; } + +export function uploadMarkdownImage(file: File): AppThunk> { + return async (dispatch, getState) => { + const task = await dispatch(uploadRawFile(file)); + return task.name; + }; +}