diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx index ce08114c..0017faa7 100644 --- a/apps/mobile/src/components/LocalTiptapEditor.tsx +++ b/apps/mobile/src/components/LocalTiptapEditor.tsx @@ -6,7 +6,7 @@ import { EditorContent, useEditor, useEditorState } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import type { TiptapDoc } from "@edgeever/shared"; import { useDOMImperativeHandle, type DOMImperativeFactory, type DOMProps } from "expo/dom"; -import { useCallback, useEffect, useRef, type ReactNode, type Ref } from "react"; +import { useCallback, useEffect, useMemo, useRef, type ReactNode, type Ref } from "react"; type EditorDoc = TiptapDoc; @@ -17,10 +17,7 @@ type PickedImage = { export interface LocalTiptapEditorRef extends DOMImperativeFactory { flush: () => void; - focus: () => void; focusEnd: () => void; - replaceAll: DOMImperativeFactory[string]; - search: DOMImperativeFactory[string]; } type LocalTiptapEditorProps = { @@ -28,9 +25,9 @@ type LocalTiptapEditorProps = { content: EditorDoc; dom?: DOMProps; onChange: (content: EditorDoc) => Promise; + onLoadResource: (source: string) => Promise; onPickImage: () => Promise; onReady: (startupMs: number) => Promise; - onSearchState?: (count: number, activeIndex: number) => Promise; ref: Ref; locale: "zh-CN" | "en-US"; theme: "light" | "dark"; @@ -42,22 +39,23 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) { const startedAtRef = useRef(performance.now()); const changeTimerRef = useRef(null); const onChangeRef = useRef(props.onChange); + const onLoadResourceRef = useRef(props.onLoadResource); const onPickImageRef = useRef(props.onPickImage); const onReadyRef = useRef(props.onReady); - const onSearchStateRef = useRef(props.onSearchState); onChangeRef.current = props.onChange; + onLoadResourceRef.current = props.onLoadResource; onPickImageRef.current = props.onPickImage; onReadyRef.current = props.onReady; - onSearchStateRef.current = props.onSearchState; + const protectedImageExtension = useMemo( + () => createProtectedImageExtension(props.baseUrl, (source) => onLoadResourceRef.current(source)), + [props.baseUrl] + ); const editor = useEditor({ extensions: [ StarterKit, - Image.configure({ - allowBase64: false, - inline: false, - }), + protectedImageExtension, Placeholder.configure({ placeholder: props.locale === "en-US" ? "Start writing..." : "开始记录...", }), @@ -95,80 +93,13 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) { void onChangeRef.current(normalizeImageSources(editor.getJSON() as EditorDoc, props.baseUrl)); }, [editor, props.baseUrl]); - const findSearchMatches = useCallback((query: string) => { - const needle = query.trim().toLocaleLowerCase(); - if (!editor || editor.isDestroyed || needle.length === 0) { - return []; - } - - const characters: Array<{ char: string; pos: number }> = []; - let previousTextEnd: number | null = null; - editor.state.doc.descendants((node, pos) => { - if (!node.isText || !node.text) { - return; - } - if (previousTextEnd !== null && pos > previousTextEnd) { - characters.push({ char: "\u0000", pos: -1 }); - } - for (let index = 0; index < node.text.length; index += 1) { - characters.push({ char: node.text[index] ?? "", pos: pos + index }); - } - previousTextEnd = pos + node.text.length; - }); - - const haystack = characters.map((item) => item.char).join("").toLocaleLowerCase(); - const matches: Array<{ from: number; to: number }> = []; - let index = haystack.indexOf(needle); - while (index !== -1) { - const start = characters[index]; - const end = characters[index + needle.length - 1]; - if (start && end && start.pos >= 0 && end.pos >= 0) { - matches.push({ from: start.pos, to: end.pos + 1 }); - } - index = haystack.indexOf(needle, index + needle.length); - } - return matches; - }, [editor]); - - const search = useCallback((query: string, requestedIndex: number) => { - const matches = findSearchMatches(query); - const activeIndex = matches.length > 0 - ? ((Math.trunc(requestedIndex) % matches.length) + matches.length) % matches.length - : 0; - const match = matches[activeIndex]; - if (match && editor && !editor.isDestroyed) { - editor.chain().setTextSelection(match).scrollIntoView().run(); - } - void onSearchStateRef.current?.(matches.length, activeIndex); - }, [editor, findSearchMatches]); - - const replaceAll = useCallback((query: string, replacement: string) => { - const matches = findSearchMatches(query); - if (!editor || editor.isDestroyed || matches.length === 0) { - void onSearchStateRef.current?.(0, 0); - return; - } - - editor.chain().command(({ tr, dispatch }) => { - for (const match of [...matches].reverse()) { - tr.insertText(replacement, match.from, match.to); - } - dispatch?.(tr); - return true; - }).run(); - window.requestAnimationFrame(() => search(query, 0)); - }, [editor, findSearchMatches, search]); - useDOMImperativeHandle( props.ref, () => ({ flush, - focus: () => editor?.commands.focus(), focusEnd: () => editor?.commands.focus("end"), - replaceAll: (...args: Parameters) => replaceAll(String(args[0] ?? ""), String(args[1] ?? "")), - search: (...args: Parameters) => search(String(args[0] ?? ""), Number(args[1] ?? 0)), }), - [editor, flush, replaceAll, search] + [editor, flush] ); useEffect(() => { @@ -313,6 +244,12 @@ const normalizeImageSources = (doc: EditorDoc, baseUrl: string) => { return mapImageSources(doc, (source) => source.startsWith(`${normalizedBaseUrl}/`) ? source.slice(normalizedBaseUrl.length) : source); }; +const normalizeProtectedResourceSource = (source: string, baseUrl: string) => { + const normalizedBaseUrl = baseUrl.replace(/\/+$/, ""); + const relativeSource = source.startsWith(`${normalizedBaseUrl}/`) ? source.slice(normalizedBaseUrl.length) : source; + return relativeSource.startsWith("/api/v1/resources/") ? relativeSource : null; +}; + const resolveUrl = (source: string, baseUrl: string) => { if (!source.startsWith("/")) { return source; @@ -320,6 +257,70 @@ const resolveUrl = (source: string, baseUrl: string) => { return `${baseUrl.replace(/\/+$/, "")}${source}`; }; +const createProtectedImageExtension = (baseUrl: string, loadResource: (source: string) => Promise) => Image.extend({ + addNodeView() { + return ({ node }) => { + const image = document.createElement("img"); + const imageType = node.type; + let requestId = 0; + + const clearRequest = () => { + requestId += 1; + }; + + const renderNode = (attributes: Record) => { + clearRequest(); + const source = String(attributes.src ?? ""); + const alt = String(attributes.alt ?? ""); + const title = String(attributes.title ?? ""); + image.alt = alt; + if (title) { + image.title = title; + } else { + image.removeAttribute("title"); + } + + const protectedSource = normalizeProtectedResourceSource(source, baseUrl); + if (!protectedSource) { + image.src = resolveUrl(source, baseUrl); + return; + } + + image.removeAttribute("src"); + const activeRequestId = requestId; + void loadResource(protectedSource) + .then((dataUrl) => { + if (activeRequestId === requestId) { + image.src = dataUrl ?? resolveUrl(source, baseUrl); + } + }) + .catch(() => { + if (activeRequestId === requestId) { + image.src = resolveUrl(source, baseUrl); + } + }); + }; + + renderNode(node.attrs); + + return { + dom: image, + update: (updatedNode) => { + if (updatedNode.type !== imageType) { + return false; + } + renderNode(updatedNode.attrs); + return true; + }, + destroy: clearRequest, + }; + }; + }, +}).configure({ + allowBase64: false, + inline: false, +}); + const getEditorStyles = (theme: "light" | "dark") => ` :root { color-scheme: ${theme}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } diff --git a/apps/mobile/src/screens/WorkspaceScreen.tsx b/apps/mobile/src/screens/WorkspaceScreen.tsx index 2cedb786..a47679b0 100644 --- a/apps/mobile/src/screens/WorkspaceScreen.tsx +++ b/apps/mobile/src/screens/WorkspaceScreen.tsx @@ -65,6 +65,7 @@ import { type AppStateStatus, FlatList, Image as RNImage, + type ImageStyle, InteractionManager, Linking, Modal, @@ -72,12 +73,14 @@ import { RefreshControl, ScrollView, StyleSheet, + type StyleProp, Switch, View, } from "react-native"; import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; import { Alert, Pressable, Text, TextInput } from "../components/LocalizedText"; -import Markdown from "react-native-markdown-display"; +import Markdown, { type RenderRules } from "react-native-markdown-display"; +import { SvgXml } from "react-native-svg"; import { buildRevisionDiffRows, createExcerpt, docToMarkdown, docToText, getNotebookDescendantIds, markdownToDoc, type ApiToken, type AuthUser, type MemoDetail, type MemoRevision, type MemoSummary, type Notebook, type ResourceListItem, type RevisionDiffRow, type TagSummary, type TiptapDoc } from "@edgeever/shared"; import { clearMobileMemoDraft, clearMobileNewMemoDraft, readMobileMemoDraft, readMobileNewMemoDraft, writeMobileMemoDraft, writeMobileNewMemoDraft } from "../lib/mobile-drafts"; import { @@ -1231,15 +1234,9 @@ export const WorkspaceScreen = () => { {richEditingMemo ? { - setRichEditingMemo(null); - setSelectedMemoId(memo.id); - }} updateMutation={localUpdateMemoMutation} /> : null} {editorRuntimeWarm ? ( @@ -2274,6 +2271,7 @@ const CreateMemoModal = ({ const { resolvedTheme } = useMobileTheme(); const fallbackNotebookId = activeNotebookId !== ALL_NOTES_ID ? activeNotebookId : notebooks[0]?.id ?? ""; const editorRef = useRef(null); + const resourceDataUrlCacheRef = useRef(new Map>()); const contentJsonRef = useRef(markdownToDoc("")); const contentMarkdownRef = useRef(""); const dirtyRef = useRef(false); @@ -2565,6 +2563,19 @@ const CreateMemoModal = ({ ]); }; + const loadEditorResource = useCallback((source: string) => { + if (!client) { + return Promise.resolve(null); + } + const cached = resourceDataUrlCacheRef.current.get(source); + if (cached) { + return cached; + } + const pending = client.getResourceBlob(source).then(blobToDataUrl).catch(() => null); + resourceDataUrlCacheRef.current.set(source, pending); + return pending; + }, [client]); + const editorElement = useMemo(() => draftLoaded && baseUrl ? ( { setEditorReady(true); @@ -2594,7 +2606,7 @@ const CreateMemoModal = ({ locale={resolvedLocale} theme={resolvedTheme} /> - ) : null, [baseUrl, draftLoaded, resolvedLocale, resolvedTheme]); + ) : null, [baseUrl, draftLoaded, loadEditorResource, resolvedLocale, resolvedTheme]); return ( void requestClose()} presentationStyle="fullScreen" visible={visible}> @@ -4071,6 +4083,7 @@ const ResourceCard = ({ onPreview: () => void; resource: ResourceListItem; }) => { + const { session } = useSession(); const source = resource.memoDeleted ? "已删除笔记" : resource.memoTitle || resource.memoExcerpt || resource.memoId; const isImage = resource.kind === "image"; const localePreference = useMobileLocalePreference(); @@ -4079,7 +4092,11 @@ const ResourceCard = ({ {isImage ? ( - + ) : ( {getResourceIcon(resource)} )} @@ -4134,8 +4151,10 @@ const ImagePreviewModal = ({ resource: ResourceListItem | null; resourceCount: number; resourceIndex: number; -}) => ( - +}) => { + const { session } = useSession(); + + return @@ -4150,7 +4169,14 @@ const ImagePreviewModal = ({ - {resource ? : null} + {resource ? ( + + ) : null} {resourceCount > 1 ? ( @@ -4168,8 +4194,8 @@ const ImagePreviewModal = ({ ) : null} - -); + ; +}; const RevisionHistoryModal = ({ memo, @@ -4349,6 +4375,96 @@ const RevisionDiffCell = ({ row, tone }: { row: RevisionDiffRow; tone: "history" ); +const getAuthenticatedResourceSource = ( + source: string, + session: { baseUrl: string; token: string } | null +) => { + const baseUrl = session?.baseUrl.replace(/\/+$/, "") ?? ""; + const uri = source.startsWith("/") && baseUrl ? `${baseUrl}${source}` : source; + const isProtectedResource = source.startsWith("/api/v1/resources/") || Boolean(baseUrl && uri.startsWith(`${baseUrl}/api/v1/resources/`)); + + return { + uri, + ...(session?.token && isProtectedResource ? { headers: { Authorization: `Bearer ${session.token}` } } : {}), + }; +}; + +const AuthenticatedResourceImage = ({ + alt, + fitAspect = false, + resizeMode = "cover", + source, + style, +}: { + alt: string; + fitAspect?: boolean; + resizeMode?: "center" | "contain" | "cover" | "repeat" | "stretch"; + source: ReturnType; + style: StyleProp; +}) => { + const [aspectRatio, setAspectRatio] = useState(16 / 9); + const [svgXml, setSvgXml] = useState(null); + const svgRequestStartedRef = useRef(false); + const imageStyle = fitAspect ? [style, { aspectRatio, height: undefined, width: "100%" as const }] : style; + + useEffect(() => { + setSvgXml(null); + svgRequestStartedRef.current = false; + }, [source.uri]); + + const loadSvgFallback = () => { + if (svgRequestStartedRef.current) { + return; + } + svgRequestStartedRef.current = true; + void fetch(source.uri, { headers: source.headers }) + .then(async (response) => { + if (!response.ok || !response.headers.get("Content-Type")?.toLowerCase().includes("svg")) { + return null; + } + return response.text(); + }) + .then((xml) => { + if (!xml) { + return; + } + const viewBox = xml.match(/viewBox=["']\s*[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)\s*["']/i); + const width = Number(viewBox?.[1]); + const height = Number(viewBox?.[2]); + if (width > 0 && height > 0) { + setAspectRatio(width / height); + } + setSvgXml(xml); + }) + .catch(() => undefined); + }; + + if (svgXml) { + return ( + + + + ); + } + + return ( + { + const { height, width } = event.nativeEvent.source; + if (height > 0 && width > 0) { + setAspectRatio(width / height); + } + }} + onError={loadSvgFallback} + resizeMode={resizeMode} + source={source} + style={imageStyle} + /> + ); +}; + const MemoDetailModal = ({ isDeleting, isLoading, @@ -4382,6 +4498,7 @@ const MemoDetailModal = ({ onTogglePin: (memo: MemoDetail) => void; visible: boolean; }) => { + const { session } = useSession(); const { resolvedTheme, toggleTheme } = useMobileTheme(); const [actionsOpen, setActionsOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); @@ -4392,6 +4509,18 @@ const MemoDetailModal = ({ () => resolveMobileThemeStyles(detailMarkdownStyles, resolvedTheme), [resolvedTheme] ); + const detailMarkdownRules = useMemo(() => ({ + image: (node, _children, _parents, markdownStyles) => ( + + ), + }), [session]); const detailText = memo?.contentMarkdown || memo?.contentText || "没有正文内容"; const searchMatches = useMemo(() => getTextSearchMatches(detailText, searchQuery), [detailText, searchQuery]); const searchMatchLabel = searchQuery.trim() ? `${searchMatches.length > 0 ? activeMatchIndex + 1 : 0}/${searchMatches.length}` : "0/0"; @@ -4479,7 +4608,7 @@ const MemoDetailModal = ({ {searchOpen && searchQuery.trim() ? ( ) : ( - {detailText} + {detailText} )} ) : ( @@ -4566,29 +4695,23 @@ const HighlightedDetailText = ({ const RichEditorModal = ({ baseUrl, imageCompressionEnabled, - isDeleting, memo, notebooks, onClose, - onDelete, - onRestored, updateMutation, }: { baseUrl: string; imageCompressionEnabled: boolean; - isDeleting: boolean; memo: MemoDetail | null; notebooks: Notebook[]; onClose: () => void; - onDelete: (memo: MemoDetail) => void; - onRestored: (memo: MemoDetail) => void; updateMutation: MobileMemoUpdateMutation; }) => { const { client } = useSession(); const { resolvedLocale } = useMobileLocale(); - const { resolvedTheme, toggleTheme } = useMobileTheme(); - const insets = useSafeAreaInsets(); + const { resolvedTheme } = useMobileTheme(); const editorRef = useRef(null); + const resourceDataUrlCacheRef = useRef(new Map>()); const initialFocusTimerRef = useRef | null>(null); const contentJsonRef = useRef(memo?.contentJson ?? markdownToDoc(memo?.contentMarkdown ?? "")); const contentMarkdownRef = useRef(memo?.contentMarkdown ?? ""); @@ -4601,14 +4724,6 @@ const RichEditorModal = ({ const [tagsText, setTagsText] = useState(memo?.tags.join(", ") ?? ""); const [notebookId, setNotebookId] = useState(memo?.notebookId ?? ""); const [notebookPickerOpen, setNotebookPickerOpen] = useState(false); - const [actionsOpen, setActionsOpen] = useState(false); - const [historyMemo, setHistoryMemo] = useState(null); - const [noteSearchOpen, setNoteSearchOpen] = useState(false); - const [noteSearchReplaceOpen, setNoteSearchReplaceOpen] = useState(false); - const [noteSearchQuery, setNoteSearchQuery] = useState(""); - const [noteSearchReplacement, setNoteSearchReplacement] = useState(""); - const [noteSearchCount, setNoteSearchCount] = useState(0); - const [noteSearchIndex, setNoteSearchIndex] = useState(0); const [draftLoaded, setDraftLoaded] = useState(false); const [draftRestored, setDraftRestored] = useState(false); const [ready, setReady] = useState(false); @@ -4618,7 +4733,7 @@ const RichEditorModal = ({ const [error, setError] = useState(null); const [startupMs, setStartupMs] = useState(null); const notebookLabel = notebooks.find((notebook) => notebook.id === notebookId)?.name ?? "未分类"; - const saveLabel = error ? "保存失败" : saving ? "正在保存" : uploading ? "正在上传" : dirty ? "本地草稿" : ready ? "已保存" : "正在启动"; + const saveLabel = error ? "保存失败" : saving ? "保存中" : uploading ? "上传中" : dirty ? (draftRestored ? "本地草稿" : "未保存") : ready ? "已保存" : "加载中"; const titleRef = useRef(title); const tagsTextRef = useRef(tagsText); const notebookIdRef = useRef(notebookId); @@ -4640,14 +4755,6 @@ const RichEditorModal = ({ setReady(false); setSaving(false); setUploading(false); - setActionsOpen(false); - setHistoryMemo(null); - setNoteSearchOpen(false); - setNoteSearchReplaceOpen(false); - setNoteSearchQuery(""); - setNoteSearchReplacement(""); - setNoteSearchCount(0); - setNoteSearchIndex(0); uploadingRef.current = false; setError(null); setStartupMs(null); @@ -4734,6 +4841,7 @@ const RichEditorModal = ({ await clearMobileMemoDraft(memo.id); dirtyRef.current = false; setDirty(false); + setDraftRestored(false); return savedMemo; } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "保存失败"); @@ -4764,62 +4872,6 @@ const RichEditorModal = ({ }); }; - const handleEditorSearchState = useCallback(async (count: number, activeIndex: number) => { - setNoteSearchCount(count); - setNoteSearchIndex(activeIndex); - }, []); - - const openNoteSearch = (showReplace = false) => { - setActionsOpen(false); - setNoteSearchOpen(true); - setNoteSearchReplaceOpen(showReplace); - }; - - const closeNoteSearch = () => { - setNoteSearchOpen(false); - setNoteSearchReplaceOpen(false); - setNoteSearchCount(0); - setNoteSearchIndex(0); - editorRef.current?.focus(); - }; - - const moveNoteSearchMatch = (direction: 1 | -1) => { - if (noteSearchCount === 0) { - return; - } - const nextIndex = (noteSearchIndex + direction + noteSearchCount) % noteSearchCount; - setNoteSearchIndex(nextIndex); - editorRef.current?.search(noteSearchQuery, nextIndex); - }; - - const replaceAllNoteSearchMatches = () => { - if (!noteSearchQuery.trim() || noteSearchCount === 0) { - return; - } - editorRef.current?.replaceAll(noteSearchQuery, noteSearchReplacement); - }; - - const openRevisionHistory = async () => { - setActionsOpen(false); - await flushEditor(); - if (!memo) { - return; - } - setHistoryMemo({ - ...memo, - contentJson: contentJsonRef.current, - contentMarkdown: contentMarkdownRef.current, - title: titleRef.current.trim() || DEFAULT_MEMO_TITLE, - }); - }; - - useEffect(() => { - if (!noteSearchOpen || !ready) { - return; - } - editorRef.current?.search(noteSearchQuery, 0); - }, [noteSearchOpen, noteSearchQuery, ready]); - const requestClose = async () => { if (savingRef.current || uploadingRef.current) { return; @@ -4875,6 +4927,19 @@ const RichEditorModal = ({ } }; + const loadEditorResource = useCallback((source: string) => { + if (!client) { + return Promise.resolve(null); + } + const cached = resourceDataUrlCacheRef.current.get(source); + if (cached) { + return cached; + } + const pending = client.getResourceBlob(source).then(blobToDataUrl).catch(() => null); + resourceDataUrlCacheRef.current.set(source, pending); + return pending; + }, [client]); + const editorElement = useMemo( () => memo && baseUrl && draftLoaded ? ( { setStartupMs(elapsedMs); @@ -4901,13 +4967,12 @@ const RichEditorModal = ({ editorRef.current?.focusEnd(); }, 160); }} - onSearchState={handleEditorSearchState} ref={editorRef} locale={resolvedLocale} theme={resolvedTheme} /> ) : null, - [baseUrl, draftLoaded, handleEditorSearchState, memo?.id, resolvedLocale, resolvedTheme] + [baseUrl, draftLoaded, loadEditorResource, memo?.id, resolvedLocale, resolvedTheme] ); useEffect(() => { @@ -4956,12 +5021,6 @@ const RichEditorModal = ({ > {saving ? : 完成} - - {resolvedTheme === "dark" ? : } - - setActionsOpen(true)} style={styles.richEditorHeaderIconButton}> - - @@ -4996,54 +5055,6 @@ const RichEditorModal = ({ value={tagsText} /> - {noteSearchOpen ? ( - - - - - - {`${noteSearchCount > 0 ? noteSearchIndex + 1 : 0}/${noteSearchCount}`} - - - - - - {noteSearchReplaceOpen ? ( - - - - - ) : null} - - moveNoteSearchMatch(-1)}> - - - moveNoteSearchMatch(1)}> - - - {noteSearchReplaceOpen ? ( - - - - ) : null} - - - ) : null} {draftRestored ? 已恢复上次未完成的本地草稿 : null} {!ready || !draftLoaded ? ( @@ -5075,39 +5086,6 @@ const RichEditorModal = ({ }} visible={notebookPickerOpen} /> - setActionsOpen(false)} transparent visible={actionsOpen}> - setActionsOpen(false)} style={styles.richEditorMoreBackdrop}> - - } label="搜索当前笔记" onPress={() => openNoteSearch(false)} /> - } label="替换当前笔记" onPress={() => openNoteSearch(true)} /> - } label="版本历史" onPress={() => void openRevisionHistory()} /> - - } - label={isDeleting ? "删除中" : "删除"} - onPress={() => { - setActionsOpen(false); - if (memo) { - onDelete(memo); - } - }} - /> - - - - {historyMemo ? ( - setHistoryMemo(null)} - onRestored={async (restoredMemo) => { - await clearMobileMemoDraft(restoredMemo.id); - setHistoryMemo(null); - onRestored(restoredMemo); - }} - /> - ) : null} ); @@ -6602,6 +6580,19 @@ const openResource = (resource: ResourceListItem) => { }); }; +const blobToDataUrl = (blob: Blob) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(reader.error ?? new Error("资源读取失败")); + reader.onloadend = () => { + if (typeof reader.result === "string") { + resolve(reader.result); + return; + } + reject(new Error("资源读取失败")); + }; + reader.readAsDataURL(blob); +}); + const appendResourceMarkdown = ( currentMarkdown: string, resource: { @@ -8150,13 +8141,6 @@ const baseWorkspaceStyles = StyleSheet.create({ richEditorHeaderStatus: { maxWidth: 76, }, - richEditorHeaderIconButton: { - alignItems: "center", - borderRadius: 999, - height: 36, - justifyContent: "center", - width: 36, - }, richEditorContainer: { backgroundColor: "#ffffff", flex: 1, @@ -8240,45 +8224,6 @@ const baseWorkspaceStyles = StyleSheet.create({ richStandaloneTagsInput: { minHeight: 30, }, - richEditorSearchPanel: { - backgroundColor: "#f8fafc", - borderBottomColor: "#e2e8f0", - borderBottomWidth: 1, - gap: 8, - marginHorizontal: -12, - paddingHorizontal: 12, - paddingVertical: 10, - }, - richEditorSearchActions: { - flexDirection: "row", - flexWrap: "wrap", - gap: 8, - }, - richEditorSearchIconButton: { - alignItems: "center", - borderRadius: 999, - height: 32, - justifyContent: "center", - width: 32, - }, - richEditorMoreBackdrop: { - flex: 1, - }, - richEditorMoreMenu: { - backgroundColor: "#ffffff", - borderColor: "#e2e8f0", - borderRadius: 9, - borderWidth: 1, - elevation: 8, - padding: 6, - position: "absolute", - right: 12, - shadowColor: "#0f172a", - shadowOffset: { width: 0, height: 5 }, - shadowOpacity: 0.18, - shadowRadius: 12, - width: 228, - }, richEditorLoading: { alignItems: "center", backgroundColor: "#ffffff",