From 87f96de4f55a536ce3fdda10a62f26d53a7c86e7 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Thu, 8 Oct 2026 16:30:49 +0800 Subject: [PATCH] Fix Android note image preview generation --- .../src/components/LocalTiptapEditor.tsx | 34 +++-- .../src/screens/WorkspaceMemoDetail.tsx | 127 ++++++++++-------- 2 files changed, 89 insertions(+), 72 deletions(-) diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx index 05171f17..7afb41e4 100644 --- a/apps/mobile/src/components/LocalTiptapEditor.tsx +++ b/apps/mobile/src/components/LocalTiptapEditor.tsx @@ -87,6 +87,7 @@ import { generateCardCss, } from "@edgeever/shared/note-image-card"; import { useDOMImperativeHandle, type DOMImperativeFactory, type DOMProps } from "expo/dom"; +import { toCanvas } from "html-to-image"; import { createImageInsertTransaction, createNativeImageGalleryView, groupUploadedImages, NATIVE_IMAGE_GALLERY_CSS } from "@edgeever/shared/native-image-gallery"; import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type ReactNode, type Ref, type SetStateAction } from "react"; import { @@ -1239,18 +1240,22 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { const exportImage = useCallback((requestJsonValue: DOMValue) => { if (typeof requestJsonValue !== "string" || !editor || editor.isDestroyed || !onImageExportEventRef.current) return; + let request: ImageExportRequest; + try { + request = JSON.parse(requestJsonValue) as ImageExportRequest; + if (!request.requestId || (request.format !== "png" && request.format !== "jpeg")) return; + } catch { + return; + } + + const notify = (payload: Record) => + onImageExportEventRef.current?.(JSON.stringify({ requestId: request.requestId, ...payload })); + const reportProgress = (stage: "prepare" | "render" | "transfer") => { + void Promise.resolve(notify({ type: "progress", stage })).catch(() => {}); + }; + void (async () => { - let request: ImageExportRequest; - try { - request = JSON.parse(requestJsonValue) as ImageExportRequest; - if (!request.requestId || (request.format !== "png" && request.format !== "jpeg")) return; - } catch { - return; - } - - const notify = (payload: Record) => - onImageExportEventRef.current?.(JSON.stringify({ requestId: request.requestId, ...payload })); - + reportProgress("prepare"); const resolvedTheme = resolveTheme(request.background, request.theme); const fontStyle = request.fontStyle ?? "serif"; const fontSize = request.fontSize ?? "lg"; @@ -1326,7 +1331,7 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { } const backgroundColor = NOTE_IMAGE_BACKGROUND_COLORS[resolvedTheme] || themeCfg.canvasBg; - const { toCanvas } = await import("html-to-image"); + reportProgress("render"); const canvas = await toCanvas(captureRoot, { backgroundColor, cacheBust: false, @@ -1348,6 +1353,7 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { const filename = `${basename}.${extension}`; const mimeType = request.format === "jpeg" ? "image/jpeg" : "image/png"; + reportProgress("transfer"); for (let offset = 0; offset < blob.size; offset += IMAGE_EXPORT_CHUNK_BYTES) { const bytes = new Uint8Array(await blob.slice(offset, offset + IMAGE_EXPORT_CHUNK_BYTES).arrayBuffer()); await notify({ type: "chunk", chunk: bytesToBase64(bytes) }); @@ -1366,7 +1372,9 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { } finally { host.remove(); } - })(); + })().catch((error: unknown) => { + void notify({ type: "error", message: error instanceof Error ? error.message : "Image export failed" }); + }); }, [editor]); useDOMImperativeHandle( diff --git a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx index c40ab5da..72ad54bb 100644 --- a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx +++ b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx @@ -51,6 +51,7 @@ const IMAGE_EXPORT_TIMEOUT_MS = 60_000; type MobileImageExportEvent = | { type: "chunk"; requestId: string; chunk: string } + | { type: "progress"; requestId: string; stage: "prepare" | "render" | "transfer" } | { type: "complete"; requestId: string; @@ -484,6 +485,7 @@ export const MemoDetailModal = ({ const [viewerReady, setViewerReady] = useState(false); const [titleCollapsed, setTitleCollapsed] = useState(false); const [isExportingImage, setIsExportingImage] = useState(false); + const [imageExportStage, setImageExportStage] = useState<"prepare" | "render" | "transfer">("prepare"); const [imageShareOptionsOpen, setImageShareOptionsOpen] = useState(false); const [imageShareFormat, setImageShareFormat] = useState<"jpeg" | "png">("png"); const [imageShareTheme, setImageShareTheme] = useState("slate"); @@ -701,8 +703,8 @@ export const MemoDetailModal = ({ const closeImagePreview = () => { setPreparedNoteImage(null); + setImageShareOptionsOpen(false); if (imageShareFromList) { - setImageShareOptionsOpen(false); onClose(); } }; @@ -839,6 +841,10 @@ export const MemoDetailModal = ({ return; } if (!event.requestId || event.requestId !== imageExportRequestRef.current) return; + if (event.type === "progress") { + setImageExportStage(event.stage); + return; + } if (event.type === "chunk") { imageExportChunksRef.current.push(event.chunk); return; @@ -914,6 +920,7 @@ export const MemoDetailModal = ({ imageExportRequestRef.current = requestId; imageExportChunksRef.current = []; imageExportIntentRef.current = options.intent ?? "share"; + setImageExportStage("prepare"); setIsExportingImage(true); imageExportTimeoutRef.current = setTimeout(() => { if (imageExportRequestRef.current !== requestId) return; @@ -1464,7 +1471,57 @@ export const MemoDetailModal = ({ ) : null} - + + {preparedNoteImage ? ( + + + {resolvedLocale !== "zh-CN" ? "Image preview" : "图片预览"} + + + + + + 0 && preparedNoteImage.height > 0 + ? { aspectRatio: preparedNoteImage.width / preparedNoteImage.height } + : null, + ]} + /> + {preparedNoteImage.failedImages > 0 ? ( + + {resolvedLocale !== "zh-CN" + ? `${preparedNoteImage.failedImages} of ${preparedNoteImage.totalImages} note image(s) could not be included.` + : `笔记中的 ${preparedNoteImage.totalImages} 张图片有 ${preparedNoteImage.failedImages} 张未能包含。`} + + ) : null} + {preparedNoteImage.height > 12_000 ? ( + + {resolvedLocale !== "zh-CN" + ? "This is a long image. Some social apps may reduce its quality; keep the saved original." + : "图片较长,部分社交平台可能会压缩画质;建议保留保存的原图。"} + + ) : null} + + + void copyPreparedNoteImage(preparedNoteImage)} style={imageShareStyles.previewSecondaryButton}> + + {resolvedLocale !== "zh-CN" ? "Copy" : "复制图片"} + + void savePreparedNoteImage(preparedNoteImage)} style={imageShareStyles.previewSecondaryButton}> + + {resolvedLocale !== "zh-CN" ? "Save" : "保存图片"} + + void sharePreparedNoteImage(preparedNoteImage)} style={imageShareStyles.previewPrimaryButton}> + + {resolvedLocale !== "zh-CN" ? "Share" : "系统分享"} + + + + ) : ( { - setImageShareOptionsOpen(false); exportMemoImage(imageShareFormat, { theme: imageShareTheme, fontStyle: imageShareFontStyle, @@ -1596,9 +1652,15 @@ export const MemoDetailModal = ({ }} style={[imageShareStyles.shareButton, (isExportingImage || !viewerReady) && styles.buttonDisabled]} > - + {isExportingImage ? : } - {!viewerReady + {isExportingImage + ? (imageExportStage === "render" + ? (resolvedLocale !== "zh-CN" ? "Rendering image…" : "正在渲染图片…") + : imageExportStage === "transfer" + ? (resolvedLocale !== "zh-CN" ? "Saving preview…" : "正在保存预览…") + : (resolvedLocale !== "zh-CN" ? "Preparing image…" : "正在准备图片…")) + : !viewerReady ? (resolvedLocale !== "zh-CN" ? "Preparing note…" : "正在准备笔记…") : (resolvedLocale !== "zh-CN" ? "Generate preview" : "生成预览")} @@ -1606,60 +1668,7 @@ export const MemoDetailModal = ({ - - - - - {resolvedLocale !== "zh-CN" ? "Image preview" : "图片预览"} - - - - - {preparedNoteImage ? ( - <> - - 0 && preparedNoteImage.height > 0 - ? { aspectRatio: preparedNoteImage.width / preparedNoteImage.height } - : null, - ]} - /> - {preparedNoteImage.failedImages > 0 ? ( - - {resolvedLocale !== "zh-CN" - ? `${preparedNoteImage.failedImages} of ${preparedNoteImage.totalImages} note image(s) could not be included.` - : `笔记中的 ${preparedNoteImage.totalImages} 张图片有 ${preparedNoteImage.failedImages} 张未能包含。`} - - ) : null} - {preparedNoteImage.height > 12_000 ? ( - - {resolvedLocale !== "zh-CN" - ? "This is a long image. Some social apps may reduce its quality; keep the saved original." - : "图片较长,部分社交平台可能会压缩画质;建议保留保存的原图。"} - - ) : null} - - - void copyPreparedNoteImage(preparedNoteImage)} style={imageShareStyles.previewSecondaryButton}> - - {resolvedLocale !== "zh-CN" ? "Copy" : "复制图片"} - - void savePreparedNoteImage(preparedNoteImage)} style={imageShareStyles.previewSecondaryButton}> - - {resolvedLocale !== "zh-CN" ? "Save" : "保存图片"} - - void sharePreparedNoteImage(preparedNoteImage)} style={imageShareStyles.previewPrimaryButton}> - - {resolvedLocale !== "zh-CN" ? "Share" : "系统分享"} - - - - ) : null} - + )} {memo && !memo.isDeleted ? (