diff --git a/apps/desktop/src/main/desktop-menu.mjs b/apps/desktop/src/main/desktop-menu.mjs
index 6db1ba8c..a828a136 100644
--- a/apps/desktop/src/main/desktop-menu.mjs
+++ b/apps/desktop/src/main/desktop-menu.mjs
@@ -17,7 +17,7 @@ const menuCopy = {
resetZoom: "Reset Zoom",
restartToUpdate: "Restart to update",
screenshotFailed: "Couldn't capture a screenshot. If this is the first time, allow Screen Recording for EdgeEver in System Settings.",
- screenshotToNote: "Save screenshot to note",
+ screenshotToNote: "Capture screen to note",
selectAll: "Select All",
show: "Show EdgeEver",
toggleFocusMode: "Toggle Focus Mode",
@@ -48,7 +48,7 @@ const menuCopy = {
resetZoom: "重置缩放",
restartToUpdate: "重启以更新",
screenshotFailed: "截图失败。若是第一次使用,请在系统设置中允许 EdgeEver 使用屏幕录制。",
- screenshotToNote: "截图并保存到笔记",
+ screenshotToNote: "截取屏幕到笔记",
selectAll: "全选",
show: "显示 EdgeEver",
toggleFocusMode: "切换专注模式",
diff --git a/apps/desktop/src/main/desktop-menu.test.mjs b/apps/desktop/src/main/desktop-menu.test.mjs
index 5aa91fff..0214f408 100644
--- a/apps/desktop/src/main/desktop-menu.test.mjs
+++ b/apps/desktop/src/main/desktop-menu.test.mjs
@@ -9,7 +9,7 @@ describe("desktop native menu localization", () => {
expect(copy.view).toBe("视图");
expect(copy.window).toBe("窗口");
expect(copy.restartToUpdate).toBe("重启以更新");
- expect(copy.screenshotToNote).toBe("截图并保存到笔记");
+ expect(copy.screenshotToNote).toBe("截取屏幕到笔记");
});
test("recognizes other Chinese locale variants", () => {
diff --git a/apps/desktop/src/main/index.mjs b/apps/desktop/src/main/index.mjs
index 530f1d43..ca7320d8 100644
--- a/apps/desktop/src/main/index.mjs
+++ b/apps/desktop/src/main/index.mjs
@@ -29,7 +29,7 @@ import { isAllowedPrintPreviewUrl } from "./window-open-policy.mjs";
import { showWindow } from "./window-visibility.mjs";
import { trayIconPath } from "./tray-icon.mjs";
import { writeRichClipboard } from "./clipboard-write.mjs";
-import { captureInteractiveScreenshot, writeScreenshotTempPath } from "./screenshot-capture.mjs";
+import { captureScreenToNote, screenshotImportIpcPayload, writeScreenshotTempPath } from "./screenshot-capture.mjs";
import { LocalDataResetError, scheduleMacLocalDataReset } from "./local-data-reset.mjs";
import { buildDesktopDiagnosticIssueUrl, normalizeDesktopDiagnostic } from "./desktop-diagnostics.mjs";
import { createRendererStartupGuard } from "./renderer-startup-guard.mjs";
@@ -519,17 +519,20 @@ const flushPendingMarkdownImport = () => {
};
const sendScreenshotImport = (payload) => {
+ const ipcPayload = screenshotImportIpcPayload(payload);
+ if (!ipcPayload.bytes.byteLength) return;
if (!mainWindow || mainWindow.isDestroyed() || mainWindow.webContents.isLoading() || !rendererReady) {
- pendingScreenshotImport = payload;
+ pendingScreenshotImport = ipcPayload;
return;
}
- mainWindow.webContents.send("desktop:import-screenshot", payload);
+ mainWindow.webContents.send("desktop:import-screenshot", ipcPayload);
};
const flushPendingScreenshotImport = () => {
if (!pendingScreenshotImport || !rendererReady || !mainWindow || mainWindow.isDestroyed()) return;
const payload = pendingScreenshotImport;
pendingScreenshotImport = null;
+ if (!payload.bytes?.byteLength) return;
mainWindow.webContents.send("desktop:import-screenshot", payload);
};
@@ -546,16 +549,12 @@ const captureScreenshotToNote = async () => {
if (process.platform === "darwin") app.hide();
else if (wasVisible) mainWindow.hide();
await new Promise((resolve) => setTimeout(resolve, 250));
- const captured = await captureInteractiveScreenshot({
+ const captured = await captureScreenToNote({
platform: process.platform,
locale: app.getLocale(),
outputPath: process.platform === "darwin" ? writeScreenshotTempPath(app.getPath("temp")) : undefined,
- BrowserWindow,
desktopCapturer,
screen,
- ipcMain,
- overlayHtmlPath: join(currentDirectory, "../screenshot/overlay.html"),
- overlayPreloadPath: join(currentDirectory, "../screenshot/overlay-preload.cjs"),
});
if (!captured) {
if (wasVisible) revealWindow();
diff --git a/apps/desktop/src/main/screenshot-capture.mjs b/apps/desktop/src/main/screenshot-capture.mjs
index 14f6551d..ae3f6306 100644
--- a/apps/desktop/src/main/screenshot-capture.mjs
+++ b/apps/desktop/src/main/screenshot-capture.mjs
@@ -6,9 +6,36 @@ import { promisify } from "node:util";
const execFileAsync = promisify(execFileCallback);
const MAC_SCREENCAPTURE = "/usr/sbin/screencapture";
-const MIN_CROP_EDGE = 4;
-export const macScreencaptureArgs = (outputPath) => ["-i", "-x", "-t", "png", outputPath];
+export const macScreencaptureArgs = (outputPath) => ["-x", "-t", "png", outputPath];
+
+export const normalizeIpcBytes = (value) => {
+ if (value instanceof Uint8Array) {
+ const copy = new Uint8Array(value.byteLength);
+ copy.set(value);
+ return copy;
+ }
+ if (value instanceof ArrayBuffer) {
+ return new Uint8Array(value.slice(0));
+ }
+ if (ArrayBuffer.isView(value)) {
+ return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
+ }
+ if (Array.isArray(value)) {
+ return Uint8Array.from(value);
+ }
+ if (value && typeof value === "object" && value.type === "Buffer") {
+ return normalizeIpcBytes(value.data);
+ }
+ return new Uint8Array();
+};
+
+export const screenshotImportIpcPayload = (captured) => ({
+ name: captured.name,
+ type: captured.type,
+ title: captured.title,
+ bytes: normalizeIpcBytes(captured.bytes),
+});
export const isChineseDesktopLocale = (locale) =>
typeof locale === "string" && locale.toLowerCase().startsWith("zh");
@@ -29,24 +56,6 @@ export const isScreenshotCancelledExit = (error) => {
return code === 1 || code === 2;
};
-export const clampCropRect = (rect, imageSize) => {
- const x = Math.max(0, Math.round(Number(rect?.x) || 0));
- const y = Math.max(0, Math.round(Number(rect?.y) || 0));
- const width = Math.max(0, Math.round(Number(rect?.width) || 0));
- const height = Math.max(0, Math.round(Number(rect?.height) || 0));
- const maxWidth = Math.max(0, Math.round(Number(imageSize?.width) || 0) - x);
- const maxHeight = Math.max(0, Math.round(Number(imageSize?.height) || 0) - y);
- return {
- x,
- y,
- width: Math.min(width, maxWidth),
- height: Math.min(height, maxHeight),
- };
-};
-
-export const isUsableCropRect = (rect) =>
- Boolean(rect) && rect.width >= MIN_CROP_EDGE && rect.height >= MIN_CROP_EDGE;
-
export const readCapturedScreenshot = async (outputPath, io = { readFile, unlink, stat }) => {
try {
const info = await io.stat(outputPath);
@@ -63,7 +72,7 @@ export const readCapturedScreenshot = async (outputPath, io = { readFile, unlink
}
};
-export const captureMacInteractiveScreenshot = async ({
+export const captureMacScreenScreenshot = async ({
execFile = execFileAsync,
outputPath,
readFile: readCapturedFile = readFile,
@@ -101,26 +110,7 @@ const matchCaptureSource = (sources, display) => {
return sources.find((source) => source && String(source.display_id) === displayId) || sources[0];
};
-export const cropCapturedImage = (image, rect, scaleFactor = 1) => {
- const size = image?.getSize?.() || { width: 0, height: 0 };
- const crop = clampCropRect({
- x: (Number(rect?.x) || 0) * scaleFactor,
- y: (Number(rect?.y) || 0) * scaleFactor,
- width: (Number(rect?.width) || 0) * scaleFactor,
- height: (Number(rect?.height) || 0) * scaleFactor,
- }, size);
- if (!isUsableCropRect(crop)) return null;
- return toPngBytes(image.crop(crop));
-};
-
-export const captureRegionScreenshot = async ({
- BrowserWindow,
- desktopCapturer,
- screen,
- ipcMain,
- overlayHtmlPath,
- overlayPreloadPath,
-}) => {
+export const captureDisplayScreenshot = async ({ desktopCapturer, screen }) => {
const point = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(point);
const thumbnailSize = displayThumbnailSize(display);
@@ -136,76 +126,17 @@ export const captureRegionScreenshot = async ({
const screenshotBytes = toPngBytes(thumbnail);
if (!screenshotBytes) throw new Error("Screen capture is unavailable");
-
- return await new Promise((resolve, reject) => {
- const completeChannel = "desktop:screenshot-overlay-complete";
- const cancelChannel = "desktop:screenshot-overlay-cancel";
- let settled = false;
- const overlay = new BrowserWindow({
- x: display.bounds.x,
- y: display.bounds.y,
- width: display.bounds.width,
- height: display.bounds.height,
- frame: false,
- resizable: false,
- movable: false,
- minimizable: false,
- maximizable: false,
- fullscreenable: false,
- skipTaskbar: true,
- alwaysOnTop: true,
- hasShadow: false,
- show: false,
- hiddenInMissionControl: true,
- webPreferences: {
- preload: overlayPreloadPath,
- contextIsolation: true,
- nodeIntegration: false,
- sandbox: true,
- },
- });
- overlay.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
- overlay.setAlwaysOnTop(true, "screen-saver");
-
- const finish = (result, error) => {
- if (settled) return;
- settled = true;
- ipcMain.removeListener(completeChannel, onComplete);
- ipcMain.removeListener(cancelChannel, onCancel);
- if (!overlay.isDestroyed()) overlay.close();
- if (error) reject(error);
- else resolve(result);
- };
-
- const onComplete = (_event, rect) => {
- try {
- finish(cropCapturedImage(thumbnail, rect, display.scaleFactor || 1));
- } catch (error) {
- finish(null, error);
- }
- };
- const onCancel = () => finish(null);
-
- ipcMain.on(completeChannel, onComplete);
- ipcMain.on(cancelChannel, onCancel);
- overlay.on("closed", () => finish(null));
- overlay.webContents.once("did-finish-load", () => {
- overlay.webContents.send("desktop:screenshot-overlay-init", { bytes: screenshotBytes });
- overlay.show();
- overlay.focus();
- });
- overlay.loadFile(overlayHtmlPath).catch((error) => finish(null, error));
- });
+ return screenshotBytes;
};
-export const captureInteractiveScreenshot = async (input) => {
+export const captureScreenToNote = async (input) => {
const bytes = input.platform === "darwin"
- ? await captureMacInteractiveScreenshot(input)
- : await captureRegionScreenshot(input);
- if (!bytes) return null;
+ ? await captureMacScreenScreenshot(input)
+ : await captureDisplayScreenshot(input);
+ if (!bytes || bytes.byteLength === 0) return null;
const capturedAt = input.now ? new Date(input.now) : new Date();
return {
- bytes,
+ bytes: normalizeIpcBytes(bytes),
name: screenshotFileName(capturedAt),
type: "image/png",
title: screenshotNoteTitle(input.locale, capturedAt),
diff --git a/apps/desktop/src/main/screenshot-capture.test.mjs b/apps/desktop/src/main/screenshot-capture.test.mjs
index 1586a5bc..90860e55 100644
--- a/apps/desktop/src/main/screenshot-capture.test.mjs
+++ b/apps/desktop/src/main/screenshot-capture.test.mjs
@@ -1,13 +1,13 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import {
- clampCropRect,
- cropCapturedImage,
+ captureDisplayScreenshot,
isScreenshotCancelledExit,
- isUsableCropRect,
macScreencaptureArgs,
+ normalizeIpcBytes,
readCapturedScreenshot,
screenshotFileName,
+ screenshotImportIpcPayload,
screenshotNoteTitle,
} from "./screenshot-capture.mjs";
@@ -16,8 +16,8 @@ const preloadSource = readFileSync(new URL("../preload/index.cjs", import.meta.u
const workspaceSource = readFileSync(new URL("../../../web/src/components/WorkspaceApp.tsx", import.meta.url), "utf8");
describe("screenshot capture helpers", () => {
- test("uses macOS interactive capture without a shutter sound", () => {
- expect(macScreencaptureArgs("/tmp/capture.png")).toEqual(["-i", "-x", "-t", "png", "/tmp/capture.png"]);
+ test("captures the full screen on macOS without an interactive selection", () => {
+ expect(macScreencaptureArgs("/tmp/capture.png")).toEqual(["-x", "-t", "png", "/tmp/capture.png"]);
});
test("treats screencapture cancel exit codes as a quiet abort", () => {
@@ -26,30 +26,30 @@ describe("screenshot capture helpers", () => {
expect(isScreenshotCancelledExit({ code: "ENOENT" })).toBe(false);
});
- test("clamps and rejects tiny crop rectangles", () => {
- expect(clampCropRect({ x: -8, y: 2.8, width: 40, height: 12 }, { width: 20, height: 10 })).toEqual({
- x: 0,
- y: 3,
- width: 20,
- height: 7,
- });
- expect(isUsableCropRect({ width: 3, height: 40 })).toBe(false);
- expect(isUsableCropRect({ width: 8, height: 8 })).toBe(true);
- });
-
- test("crops a captured image in physical pixels", () => {
- const cropped = [];
- const image = {
- getSize: () => ({ width: 200, height: 100 }),
- crop: (rect) => {
- cropped.push(rect);
- return { toPNG: () => Buffer.from("png") };
+ test("captures the display under the cursor without a crop overlay", async () => {
+ const bytes = await captureDisplayScreenshot({
+ screen: {
+ getCursorScreenPoint: () => ({ x: 10, y: 10 }),
+ getDisplayNearestPoint: () => ({
+ id: 2,
+ size: { width: 100, height: 50 },
+ scaleFactor: 2,
+ }),
},
- };
-
- expect(cropCapturedImage(image, { x: 10, y: 5, width: 20, height: 10 }, 2)).toEqual(Buffer.from("png"));
- expect(cropped).toEqual([{ x: 20, y: 10, width: 40, height: 20 }]);
- expect(cropCapturedImage(image, { x: 0, y: 0, width: 1, height: 1 }, 1)).toBeNull();
+ desktopCapturer: {
+ getSources: async (options) => {
+ expect(options).toEqual({ types: ["screen"], thumbnailSize: { width: 200, height: 100 } });
+ return [{
+ display_id: "2",
+ thumbnail: {
+ isEmpty: () => false,
+ toPNG: () => Buffer.from("png"),
+ },
+ }];
+ },
+ },
+ });
+ expect(bytes).toEqual(Buffer.from("png"));
});
test("returns null when the user cancelled before a file was written", async () => {
@@ -71,16 +71,33 @@ describe("screenshot capture helpers", () => {
expect(screenshotNoteTitle("en-US", date)).toBe("Screenshot 2026-09-14 11:08");
expect(screenshotFileName(date)).toBe("screenshot-20260914-110803.png");
});
+
+ test("restores Electron Buffer JSON instead of constructing an empty typed array", () => {
+ const encoded = { type: "Buffer", data: [137, 80, 78, 71] };
+ expect(new Uint8Array(encoded).byteLength).toBe(0);
+ expect(Array.from(normalizeIpcBytes(encoded))).toEqual([137, 80, 78, 71]);
+ expect(Array.from(screenshotImportIpcPayload({
+ name: "screenshot.png",
+ type: "image/png",
+ title: "截图",
+ bytes: Buffer.from([1, 2, 3]),
+ }).bytes)).toEqual([1, 2, 3]);
+ });
});
describe("desktop screenshot to note wiring", () => {
test("replaces the tray sync and backup actions with screenshot capture", () => {
expect(mainSource).toContain("captureScreenshotToNote");
+ expect(mainSource).toContain("captureScreenToNote");
+ expect(mainSource).not.toContain("overlay.html");
expect(mainSource).toContain("copy.screenshotToNote");
expect(mainSource).not.toContain('label: copy.syncNow, click: () => sendDesktopCommand("sync-now")');
expect(mainSource).not.toContain('label: copy.backupNow, click: () => sendDesktopCommand("backup-now")');
expect(preloadSource).toContain("onImportScreenshot");
+ expect(preloadSource).toContain('value.type === "Buffer"');
expect(workspaceSource).toContain("onImportScreenshot");
- expect(workspaceSource).toContain("setPendingEditorInsert");
+ expect(workspaceSource).toContain("createScreenshotMemo");
+ expect(workspaceSource).toContain("screenshotFileFromImportPayload");
+ expect(workspaceSource).not.toContain("setPendingEditorInsert");
});
});
diff --git a/apps/desktop/src/preload/index.cjs b/apps/desktop/src/preload/index.cjs
index 8e800017..f42520f8 100644
--- a/apps/desktop/src/preload/index.cjs
+++ b/apps/desktop/src/preload/index.cjs
@@ -1,5 +1,26 @@
const { contextBridge, ipcRenderer } = require("electron");
+const normalizeIpcBytes = (value) => {
+ if (value instanceof Uint8Array) {
+ const copy = new Uint8Array(value.byteLength);
+ copy.set(value);
+ return copy;
+ }
+ if (value instanceof ArrayBuffer) {
+ return new Uint8Array(value.slice(0));
+ }
+ if (ArrayBuffer.isView(value)) {
+ return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
+ }
+ if (Array.isArray(value)) {
+ return Uint8Array.from(value);
+ }
+ if (value && typeof value === "object" && value.type === "Buffer") {
+ return normalizeIpcBytes(value.data);
+ }
+ return new Uint8Array();
+};
+
contextBridge.exposeInMainWorld("edgeeverDesktop", Object.freeze({
isAvailable: true,
canClearLocalData: ipcRenderer.sendSync("desktop:local-data-reset-available-sync"),
@@ -66,10 +87,7 @@ contextBridge.exposeInMainWorld("edgeeverDesktop", Object.freeze({
},
onImportScreenshot: (callback) => {
const listener = (_event, payload) => {
- const bytes = payload?.bytes instanceof Uint8Array
- ? payload.bytes
- : new Uint8Array(payload?.bytes || []);
- callback({ ...payload, bytes });
+ callback({ ...payload, bytes: normalizeIpcBytes(payload?.bytes) });
};
ipcRenderer.on("desktop:import-screenshot", listener);
return () => ipcRenderer.removeListener("desktop:import-screenshot", listener);
diff --git a/apps/desktop/src/screenshot/overlay-preload.cjs b/apps/desktop/src/screenshot/overlay-preload.cjs
deleted file mode 100644
index 015709bb..00000000
--- a/apps/desktop/src/screenshot/overlay-preload.cjs
+++ /dev/null
@@ -1,11 +0,0 @@
-const { contextBridge, ipcRenderer } = require("electron");
-
-contextBridge.exposeInMainWorld("edgeeverScreenshot", Object.freeze({
- onInit: (callback) => {
- const listener = (_event, payload) => callback(payload);
- ipcRenderer.on("desktop:screenshot-overlay-init", listener);
- return () => ipcRenderer.removeListener("desktop:screenshot-overlay-init", listener);
- },
- complete: (rect) => ipcRenderer.send("desktop:screenshot-overlay-complete", rect),
- cancel: () => ipcRenderer.send("desktop:screenshot-overlay-cancel"),
-}));
diff --git a/apps/desktop/src/screenshot/overlay.html b/apps/desktop/src/screenshot/overlay.html
deleted file mode 100644
index 3a6b6867..00000000
--- a/apps/desktop/src/screenshot/overlay.html
+++ /dev/null
@@ -1,107 +0,0 @@
-
-
-
-
- EdgeEver Screenshot
-
-
-
-
-
-
-
-
diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx
index 06d4276b..e9209ef9 100644
--- a/apps/web/src/components/EditorPane.tsx
+++ b/apps/web/src/components/EditorPane.tsx
@@ -246,6 +246,11 @@ import {
resolveEditorDraftState,
shouldReplaceEditorDocument,
} from "./editor/editor-draft-state";
+import {
+ pendingEditorInsertMatchesMemo,
+ shouldInsertPendingEditorFiles,
+ usablePendingInsertFiles,
+} from "./editor/editor-pending-insert";
import type { EdgeEverPluginHost, PluginEditorAdapter } from "@/lib/plugins/plugin-host";
import {
useEditorResourceActions,
@@ -997,7 +1002,7 @@ const RichEditorPane = ({
const currentEditor = editorRef.current;
if (!currentMemo || currentMemo.isDeleted || !currentEditor || !currentEditor.isEditable || files.length === 0) {
- return;
+ return false;
}
const targetMemoId = currentMemo.id;
@@ -1171,6 +1176,7 @@ const RichEditorPane = ({
removeImageUploadPlaceholder(placeholderEditor, placeholder);
});
});
+ return true;
}, [queryClient, repository, resourceInsertionLimit, t]);
const pluginEmbedExtension = useMemo(() => createPluginEmbedExtension(pluginHost), [pluginHost]);
@@ -1757,12 +1763,40 @@ const RichEditorPane = ({
}, [editor]);
useEffect(() => {
- if (!pendingInsertFiles || pendingInsertFiles.memoId !== memo?.id) return;
- if (hydratedEditorMemoId !== memo.id) return;
- if (!isEditorReady(editor) || !editor.isEditable || pendingInsertFiles.files.length === 0) return;
- insertResourceFiles(pendingInsertFiles.files);
+ if (!pendingInsertFiles || !pendingEditorInsertMatchesMemo(
+ pendingInsertFiles,
+ memo?.id,
+ editorInstanceMemoIdentityRef.current.aliases,
+ )) return;
+ const files = usablePendingInsertFiles(pendingInsertFiles);
+ if (files.length === 0) {
+ onPendingInsertFilesConsumed?.();
+ return;
+ }
+ if (!shouldInsertPendingEditorFiles({
+ pendingInsertFiles,
+ memoId: memo?.id,
+ memoAliases: editorInstanceMemoIdentityRef.current.aliases,
+ editorHydratedForMemo: editorIsHydratedForCurrentMemo,
+ editorReady: isEditorReady(editor),
+ readOnly: effectiveReadOnly,
+ }) || !isEditorReady(editor)) return;
+ // Hydration leaves the editor non-editable until a later effect calls
+ // setEditable. That later effect does not re-render, so waiting on
+ // editor.isEditable here would drop the screenshot forever.
+ if (!editor.isEditable) editor.setEditable(true);
+ if (!insertResourceFiles(files)) return;
onPendingInsertFilesConsumed?.();
- }, [editor, hydratedEditorMemoId, insertResourceFiles, memo?.id, onPendingInsertFilesConsumed, pendingInsertFiles]);
+ }, [
+ editor,
+ editorIsHydratedForCurrentMemo,
+ effectiveReadOnly,
+ hydratedEditorMemoId,
+ insertResourceFiles,
+ memo?.id,
+ onPendingInsertFilesConsumed,
+ pendingInsertFiles,
+ ]);
useEffect(() => {
if (!isEditorReady(editor)) {
diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx
index 1f575398..6559e93f 100644
--- a/apps/web/src/components/WorkspaceApp.tsx
+++ b/apps/web/src/components/WorkspaceApp.tsx
@@ -117,6 +117,9 @@ import { createPublicNetworkAdapter } from "@/lib/plugins/public-network-adapter
import { clearRendererRecoveryRequired, isRendererRecoveryRequired } from "@/lib/renderer-recovery";
import { EditorPaneErrorBoundary, EditorRecoveryPane } from "./EditorPaneErrorBoundary";
import { isMarkdownFile, readMarkdownFile } from "@/lib/markdown-file-import";
+import { compressImageForUpload } from "@/lib/image-compression";
+import { createScreenshotMemo, screenshotFileFromImportPayload } from "@/lib/screenshot-import";
+import { isDesktopResourceRuntime, stageDesktopResource, toDesktopResourceUrl } from "@/lib/desktop-resources";
const isDesktopViewport = () => window.matchMedia("(min-width: 1024px)").matches;
const PULL_TO_REFRESH_TRIGGER_PX = 72;
@@ -752,7 +755,6 @@ export const WorkspaceApp = ({
const [selectedTag, setSelectedTag] = useState(null);
const autoSelectedDemoNotebookRef = useRef(false);
const [createdMemoEditId, setCreatedMemoEditId] = useState(null);
- const [pendingEditorInsert, setPendingEditorInsert] = useState<{ memoId: string; files: File[] } | null>(null);
const pendingCreatedMemoIdRef = useRef(null);
const pendingQuickSwitcherMemoIdRef = useRef(null);
const creatingMemoSelectionRef = useRef(false);
@@ -1729,6 +1731,39 @@ export const WorkspaceApp = ({
},
});
+ const revealCreatedMemo = (memo: MemoDetail) => {
+ const targetNotebookId = memo.notebookId;
+ const isDiagram = Boolean(parseDiagramDocument(memo.contentMarkdown));
+
+ setMemoView("notebook");
+ setSearch("");
+ setSelectedTag(null);
+ // A newly created memo is not pinned or otherwise guaranteed to match
+ // the active list filter. Leave filtered views so the selected memo
+ // remains visible instead of the list effect falling back to its first
+ // item (for example, the currently pinned memo).
+ setMemoFilterMode("all");
+ if (targetNotebookId !== selectedNotebookId) {
+ setSelectedNotebookId(targetNotebookId);
+ }
+ cacheMemoDetail(queryClient, memo, "notebook");
+ updateMemoSummaryInLists(queryClient, memoToSummary(memo));
+ void Promise.all([
+ queryClient.invalidateQueries({ queryKey: ["memos"], refetchType: "inactive" }),
+ queryClient.invalidateQueries({ queryKey: ["notebooks"], refetchType: "inactive" }),
+ ]);
+ navigateWorkspaceHome();
+ setRightView("editor");
+ pendingCreatedMemoIdRef.current = memo.id;
+ setCreatedMemoEditId(isDiagram ? null : memo.id);
+ setSelectedMemoId(memo.id);
+ setActivePane("editor");
+
+ if (!isDesktopViewport() && !isDiagram) {
+ openStandaloneMobileEditor(memo.id);
+ }
+ };
+
const createMemoMutation = useMutation({
mutationFn: async (input: Parameters[0]) => {
const requiresRemoteMemo = requiresRemoteMemoForStandaloneMobileEditor({
@@ -1741,36 +1776,7 @@ export const WorkspaceApp = ({
return data;
},
onSuccess: (data) => {
- const targetNotebookId = data.memo.notebookId;
- const isDiagram = Boolean(parseDiagramDocument(data.memo.contentMarkdown));
-
- setMemoView("notebook");
- setSearch("");
- setSelectedTag(null);
- // A newly created memo is not pinned or otherwise guaranteed to match
- // the active list filter. Leave filtered views so the selected memo
- // remains visible instead of the list effect falling back to its first
- // item (for example, the currently pinned memo).
- setMemoFilterMode("all");
- if (targetNotebookId !== selectedNotebookId) {
- setSelectedNotebookId(targetNotebookId);
- }
- cacheMemoDetail(queryClient, data.memo, "notebook");
- updateMemoSummaryInLists(queryClient, memoToSummary(data.memo));
- void Promise.all([
- queryClient.invalidateQueries({ queryKey: ["memos"], refetchType: "inactive" }),
- queryClient.invalidateQueries({ queryKey: ["notebooks"], refetchType: "inactive" }),
- ]);
- navigateWorkspaceHome();
- setRightView("editor");
- pendingCreatedMemoIdRef.current = data.memo.id;
- setCreatedMemoEditId(isDiagram ? null : data.memo.id);
- setSelectedMemoId(data.memo.id);
- setActivePane("editor");
-
- if (!isDesktopViewport() && !isDiagram) {
- openStandaloneMobileEditor(data.memo.id);
- }
+ revealCreatedMemo(data.memo);
},
onError: () => {
clearPendingCreatedMemo();
@@ -2188,25 +2194,56 @@ export const WorkspaceApp = ({
: defaultMemoNotebookId;
if (!notebookId) return;
- const source = payload.bytes instanceof Uint8Array ? payload.bytes : new Uint8Array(payload.bytes);
- const bytes = new Uint8Array(source.byteLength);
- bytes.set(source);
- const file = new File([bytes], payload.name || "screenshot.png", { type: payload.type || "image/png" });
+ const file = screenshotFileFromImportPayload(payload);
+ if (file.size === 0) {
+ setAppNoticeDialog({
+ title: t("memoList.importScreenshotFailedTitle"),
+ description: t("memoList.importScreenshotEmpty"),
+ });
+ return;
+ }
setTemplatesOpen(false);
setMobileBottomNavActive("home");
creatingMemoSelectionRef.current = true;
try {
- const data = await createMemoMutation.mutateAsync({
+ const preparedFile = imageCompressionEnabled ? (await compressImageForUpload(file)).file : file;
+ const memo = await createScreenshotMemo({
notebookId,
title: payload.title?.trim() || "",
- contentMarkdown: "",
- tags: [],
+ file: preparedFile,
+ createMemo: (input) => repository.createMemo(input),
+ uploadResource: async (memoId, uploadFile) => {
+ try {
+ const { resource } = await repository.uploadMemoResource(memoId, uploadFile);
+ return { url: toDesktopResourceUrl(resource.url), filename: resource.filename };
+ } catch (error) {
+ if (!isDesktopResourceRuntime()) throw error;
+ const staged = await stageDesktopResource(memoId, uploadFile);
+ if (!staged) throw error;
+ return { url: `edgeever-staged://${staged.id}`, filename: uploadFile.name };
+ }
+ },
+ updateMemo: (created, content) => repository.updateMemo(created, {
+ expectedRevision: created.revision,
+ expectedContentHash: created.contentHash,
+ editSessionId: `screenshot:${created.id}`,
+ title: created.title ?? "",
+ contentJson: content.contentJson,
+ contentMarkdown: content.contentMarkdown,
+ tags: created.tags,
+ }),
+ deleteMemo: (memoId) => repository.deleteMemo(memoId, true),
});
- setPendingEditorInsert({ memoId: data.memo.id, files: [file] });
+ await putLocalMemo(localDataScope, memo);
+ revealCreatedMemo(memo);
} catch {
creatingMemoSelectionRef.current = false;
+ setAppNoticeDialog({
+ title: t("memoList.importScreenshotFailedTitle"),
+ description: t("memoList.importScreenshotFailed"),
+ });
}
- }, [createMemoMutation, defaultMemoNotebookId, memoView, notebooks, selectedNotebookId]);
+ }, [defaultMemoNotebookId, imageCompressionEnabled, localDataScope, memoView, notebooks, repository, selectedNotebookId, t]);
const handleCreateMemo = (kind?: DiagramKind) => {
const targetNotebookId = createMemoNotebookId;
@@ -3699,8 +3736,6 @@ export const WorkspaceApp = ({
onToggleDesktopFocusMode={toggleDesktopFocusMode}
editorContentAlignment={editorContentAlignment}
mobileDefaultEditMemoId={createdMemoEditId}
- pendingInsertFiles={pendingEditorInsert}
- onPendingInsertFilesConsumed={() => setPendingEditorInsert(null)}
isTrashView={memoView === "trash"}
notebooks={notebooks}
isLoading={memoQuery.isLoading}
diff --git a/apps/web/src/components/editor/editor-pending-insert.test.mjs b/apps/web/src/components/editor/editor-pending-insert.test.mjs
new file mode 100644
index 00000000..406f40ab
--- /dev/null
+++ b/apps/web/src/components/editor/editor-pending-insert.test.mjs
@@ -0,0 +1,53 @@
+import { describe, expect, test } from "bun:test";
+import {
+ pendingEditorInsertMatchesMemo,
+ shouldInsertPendingEditorFiles,
+ usablePendingInsertFiles,
+} from "./editor-pending-insert.ts";
+
+const file = (size) => ({ size });
+
+describe("pending editor screenshot insert", () => {
+ test("matches a remapped memo id through aliases", () => {
+ expect(pendingEditorInsertMatchesMemo(
+ { memoId: "local_memo", files: [file(12)] },
+ "memo_remote",
+ new Set(["local_memo", "memo_remote"]),
+ )).toBe(true);
+ expect(pendingEditorInsertMatchesMemo(
+ { memoId: "local_memo", files: [file(12)] },
+ "memo_other",
+ new Set(["memo_other"]),
+ )).toBe(false);
+ });
+
+ test("waits until the editor is hydrated for the new memo", () => {
+ const pendingInsertFiles = { memoId: "memo_1", files: [file(24)] };
+ expect(shouldInsertPendingEditorFiles({
+ pendingInsertFiles,
+ memoId: "memo_1",
+ editorHydratedForMemo: false,
+ editorReady: true,
+ readOnly: false,
+ })).toBe(false);
+ expect(shouldInsertPendingEditorFiles({
+ pendingInsertFiles,
+ memoId: "memo_1",
+ editorHydratedForMemo: true,
+ editorReady: true,
+ readOnly: false,
+ })).toBe(true);
+ });
+
+ test("ignores empty screenshot files instead of inserting a blank image", () => {
+ const pendingInsertFiles = { memoId: "memo_1", files: [file(0)] };
+ expect(usablePendingInsertFiles(pendingInsertFiles)).toEqual([]);
+ expect(shouldInsertPendingEditorFiles({
+ pendingInsertFiles,
+ memoId: "memo_1",
+ editorHydratedForMemo: true,
+ editorReady: true,
+ readOnly: false,
+ })).toBe(false);
+ });
+});
diff --git a/apps/web/src/components/editor/editor-pending-insert.ts b/apps/web/src/components/editor/editor-pending-insert.ts
new file mode 100644
index 00000000..791e86af
--- /dev/null
+++ b/apps/web/src/components/editor/editor-pending-insert.ts
@@ -0,0 +1,43 @@
+export type PendingEditorInsertFiles = {
+ memoId: string;
+ files: File[];
+};
+
+export const pendingEditorInsertMatchesMemo = (
+ pendingInsertFiles: PendingEditorInsertFiles | null | undefined,
+ memoId: string | null | undefined,
+ memoAliases?: ReadonlySet,
+) => Boolean(
+ pendingInsertFiles
+ && memoId
+ && (
+ pendingInsertFiles.memoId === memoId
+ || Boolean(memoAliases?.has(pendingInsertFiles.memoId))
+ ),
+);
+
+export const usablePendingInsertFiles = (
+ pendingInsertFiles: PendingEditorInsertFiles | null | undefined,
+) => (pendingInsertFiles?.files ?? []).filter((file) => file.size > 0);
+
+export const shouldInsertPendingEditorFiles = ({
+ pendingInsertFiles,
+ memoId,
+ memoAliases,
+ editorHydratedForMemo,
+ editorReady,
+ readOnly,
+}: {
+ pendingInsertFiles?: PendingEditorInsertFiles | null;
+ memoId?: string | null;
+ memoAliases?: ReadonlySet;
+ editorHydratedForMemo: boolean;
+ editorReady: boolean;
+ readOnly: boolean;
+}) => Boolean(
+ pendingEditorInsertMatchesMemo(pendingInsertFiles, memoId, memoAliases)
+ && editorHydratedForMemo
+ && editorReady
+ && !readOnly
+ && usablePendingInsertFiles(pendingInsertFiles).length > 0
+);
diff --git a/apps/web/src/lib/screenshot-import.test.mjs b/apps/web/src/lib/screenshot-import.test.mjs
new file mode 100644
index 00000000..573fff16
--- /dev/null
+++ b/apps/web/src/lib/screenshot-import.test.mjs
@@ -0,0 +1,122 @@
+import { describe, expect, test } from "bun:test";
+import {
+ createScreenshotMemo,
+ normalizeScreenshotBytes,
+ screenshotFileFromImportPayload,
+ screenshotNoteContent,
+} from "./screenshot-import.ts";
+
+describe("screenshot import bytes", () => {
+ test("does not treat Electron Buffer JSON as an empty typed array", () => {
+ const encoded = { type: "Buffer", data: [137, 80, 78, 71] };
+ expect(new Uint8Array(encoded).byteLength).toBe(0);
+ expect(Array.from(normalizeScreenshotBytes(encoded))).toEqual([137, 80, 78, 71]);
+ });
+
+ test("copies Uint8Array payloads so later IPC buffer reuse cannot empty the file", () => {
+ const source = new Uint8Array([1, 2, 3]);
+ const copy = normalizeScreenshotBytes(source);
+ source[0] = 9;
+ expect(Array.from(copy)).toEqual([1, 2, 3]);
+ });
+
+ test("skips creating a screenshot file when the payload has no image bytes", () => {
+ const file = screenshotFileFromImportPayload({
+ name: "screenshot.png",
+ type: "image/png",
+ bytes: { type: "Buffer" },
+ });
+ expect(file.size).toBe(0);
+ });
+
+ test("creates a file with the restored screenshot bytes", async () => {
+ const file = screenshotFileFromImportPayload({
+ name: "screenshot.png",
+ type: "image/png",
+ bytes: { type: "Buffer", data: [137, 80, 78, 71] },
+ });
+ expect(file.size).toBe(4);
+ expect(Array.from(new Uint8Array(await file.arrayBuffer()))).toEqual([137, 80, 78, 71]);
+ });
+});
+
+describe("screenshot note content", () => {
+ test("stores the image in the note body with room to type underneath", () => {
+ const content = screenshotNoteContent("screenshot.png", "/api/v1/resources/res_shot/blob");
+ expect(content.contentJson.content?.some((node) => node.type === "image")).toBe(true);
+ expect(content.contentJson.content?.at(-1)?.type).toBe("paragraph");
+ expect(content.contentMarkdown).toContain("/api/v1/resources/res_shot/blob");
+ expect(content.contentMarkdown).toContain("screenshot.png");
+ });
+
+ test("creates the memo only after the screenshot is attached", async () => {
+ const created = {
+ id: "memo_local",
+ revision: 0,
+ contentHash: "empty",
+ title: "截图 2026-09-15 09:28",
+ tags: [],
+ contentMarkdown: "",
+ };
+ const saved = {
+ ...created,
+ revision: 1,
+ contentHash: "shot",
+ contentMarkdown: "",
+ };
+ const file = screenshotFileFromImportPayload({
+ name: "screenshot.png",
+ type: "image/png",
+ bytes: { type: "Buffer", data: [137, 80, 78, 71] },
+ });
+ const memo = await createScreenshotMemo({
+ notebookId: "nb_inbox",
+ title: created.title,
+ file,
+ createMemo: async () => ({ memo: created }),
+ uploadResource: async (memoId, uploaded) => {
+ expect(memoId).toBe(created.id);
+ expect(uploaded.size).toBe(4);
+ return { url: "/api/v1/resources/res_shot/blob", filename: uploaded.name };
+ },
+ updateMemo: async (memoToUpdate, content) => {
+ expect(memoToUpdate.id).toBe(created.id);
+ expect(content.contentMarkdown).toContain("/api/v1/resources/res_shot/blob");
+ return { memo: { ...saved, contentMarkdown: content.contentMarkdown } };
+ },
+ });
+ expect(memo.contentMarkdown).toContain("/api/v1/resources/res_shot/blob");
+ });
+
+ test("deletes the empty memo if the screenshot cannot be attached", async () => {
+ const deleted = [];
+ const file = screenshotFileFromImportPayload({
+ name: "screenshot.png",
+ type: "image/png",
+ bytes: { type: "Buffer", data: [137, 80, 78, 71] },
+ });
+ await expect(createScreenshotMemo({
+ notebookId: "nb_inbox",
+ title: "截图",
+ file,
+ createMemo: async () => ({
+ memo: {
+ id: "memo_failed",
+ revision: 0,
+ contentHash: "empty",
+ title: "截图",
+ tags: [],
+ contentMarkdown: "",
+ },
+ }),
+ uploadResource: async () => {
+ throw new Error("offline");
+ },
+ updateMemo: async (memo) => ({ memo }),
+ deleteMemo: async (memoId) => {
+ deleted.push(memoId);
+ },
+ })).rejects.toThrow("offline");
+ expect(deleted).toEqual(["memo_failed"]);
+ });
+});
diff --git a/apps/web/src/lib/screenshot-import.ts b/apps/web/src/lib/screenshot-import.ts
new file mode 100644
index 00000000..567c6940
--- /dev/null
+++ b/apps/web/src/lib/screenshot-import.ts
@@ -0,0 +1,89 @@
+import { docToMarkdown, markdownToDoc, type TiptapDoc } from "@edgeever/shared";
+
+export const normalizeScreenshotBytes = (value: unknown): Uint8Array => {
+ if (value instanceof Uint8Array) {
+ const copy = new Uint8Array(value.byteLength);
+ copy.set(value);
+ return copy;
+ }
+ if (value instanceof ArrayBuffer) {
+ return new Uint8Array(value.slice(0));
+ }
+ if (ArrayBuffer.isView(value)) {
+ return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
+ }
+ if (Array.isArray(value) && value.every((item) => typeof item === "number")) {
+ return Uint8Array.from(value);
+ }
+ if (value && typeof value === "object" && "type" in value && (value as { type?: unknown }).type === "Buffer") {
+ return normalizeScreenshotBytes((value as { data?: unknown }).data);
+ }
+ return new Uint8Array();
+};
+
+export const screenshotFileFromImportPayload = (payload: {
+ name?: string;
+ type?: string;
+ bytes: unknown;
+}) => {
+ const bytes = normalizeScreenshotBytes(payload.bytes);
+ const blobPart = new ArrayBuffer(bytes.byteLength);
+ new Uint8Array(blobPart).set(bytes);
+ return new File([blobPart], payload.name || "screenshot.png", { type: payload.type || "image/png" });
+};
+
+const escapeMarkdownImageAlt = (value: string) => value.replaceAll("\\", "\\\\").replaceAll("]", "\\]");
+
+export const screenshotNoteContent = (filename: string, url: string) => {
+ const alt = escapeMarkdownImageAlt(filename || "screenshot.png");
+ const contentMarkdown = ``;
+ const parsed = markdownToDoc(contentMarkdown);
+ const nodes = parsed.content ?? [];
+ const contentJson: TiptapDoc = {
+ type: "doc",
+ content: nodes.at(-1)?.type === "paragraph" ? nodes : [...nodes, { type: "paragraph" }],
+ };
+ return { contentJson, contentMarkdown: docToMarkdown(contentJson) };
+};
+
+type ScreenshotMemo = {
+ id: string;
+ revision: number;
+ contentHash: string;
+ title: string | null;
+ tags: string[];
+ contentMarkdown: string;
+};
+
+export const createScreenshotMemo = async (input: {
+ notebookId: string;
+ title: string;
+ file: File;
+ createMemo: (payload: {
+ notebookId: string;
+ title: string;
+ contentMarkdown: string;
+ tags: string[];
+ }) => Promise<{ memo: TMemo }>;
+ uploadResource: (memoId: string, file: File) => Promise<{ url: string; filename?: string | null }>;
+ updateMemo: (
+ memo: TMemo,
+ content: { contentJson: TiptapDoc; contentMarkdown: string },
+ ) => Promise<{ memo: TMemo }>;
+ deleteMemo?: (memoId: string) => Promise;
+}) => {
+ const created = await input.createMemo({
+ notebookId: input.notebookId,
+ title: input.title,
+ contentMarkdown: "",
+ tags: [],
+ });
+ try {
+ const resource = await input.uploadResource(created.memo.id, input.file);
+ const content = screenshotNoteContent(resource.filename || input.file.name, resource.url);
+ return (await input.updateMemo(created.memo, content)).memo;
+ } catch (error) {
+ await input.deleteMemo?.(created.memo.id).catch(() => undefined);
+ throw error;
+ }
+};
diff --git a/package.json b/package.json
index b0146684..e6c72305 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"release": "bun scripts/release.mjs",
"publish:stores": "bun scripts/store-delivery.mjs",
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
- "test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/ai-direct.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/screenshot-capture.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
+ "test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/ai-direct.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/screenshot-capture.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/screenshot-import.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
"verify:desktop-renderer": "bun scripts/verify-desktop-renderer.mjs",
"verify:renderer-origin-migration": "bun run --cwd apps/desktop verify-origin-migration",
"verify:desktop-cross-version-startup": "bun scripts/verify-desktop-cross-version-startup.mjs",
diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts
index e64101f1..e0ca0a9c 100644
--- a/packages/shared/src/i18n/en-US.ts
+++ b/packages/shared/src/i18n/en-US.ts
@@ -1436,6 +1436,9 @@ export const enUS = {
importMarkdownSingleFile: "Drag in one Markdown file at a time.",
importMarkdownUnsupported: "Only .md and .markdown files are supported.",
importMarkdownReadFailed: "The file could not be read or the note could not be created. Try again.",
+ importScreenshotFailedTitle: "Could not save screenshot to note",
+ importScreenshotEmpty: "The screenshot image was empty. Try capturing again, and allow Screen Recording for EdgeEver in System Settings if this is the first time.",
+ importScreenshotFailed: "Couldn't save the screenshot as a note. Check the connection and try again.",
loadingMore: "Loading more notes",
openMemo: "Open note",
unpin: "Unpin",
diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts
index 7ff5ca12..6ca9cd78 100644
--- a/packages/shared/src/i18n/ja.ts
+++ b/packages/shared/src/i18n/ja.ts
@@ -1436,6 +1436,9 @@ export const ja = {
importMarkdownSingleFile: "Markdown ファイルは一度に 1 つだけドラッグしてください。",
importMarkdownUnsupported: ".md と .markdown ファイルのみ対応しています。",
importMarkdownReadFailed: "ファイルを読み取れなかったか、ノートを作成できませんでした。再試行してください。",
+ importScreenshotFailedTitle: "スクリーンショットをノートに保存できませんでした",
+ importScreenshotEmpty: "スクリーンショット画像が空でした。もう一度撮影してください。初めて使う場合は、システム設定で EdgeEver の画面収録を許可してください。",
+ importScreenshotFailed: "スクリーンショットをノートとして保存できませんでした。接続を確認して再試行してください。",
loadingMore: "さらにノートを読み込み中",
openMemo: "ノートを開く",
unpin: "ピン留めを外す",
diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts
index 9ac48658..c6939fe3 100644
--- a/packages/shared/src/i18n/zh-CN.ts
+++ b/packages/shared/src/i18n/zh-CN.ts
@@ -1436,6 +1436,9 @@ export const zhCN = {
importMarkdownSingleFile: "请一次只拖入一个 Markdown 文件。",
importMarkdownUnsupported: "仅支持 .md 或 .markdown 文件。",
importMarkdownReadFailed: "文件读取或笔记创建失败,请重试。",
+ importScreenshotFailedTitle: "无法将截图保存到笔记",
+ importScreenshotEmpty: "截图图片为空。请重新截取;若是第一次使用,请在系统设置中允许 EdgeEver 使用屏幕录制。",
+ importScreenshotFailed: "无法将截图保存为笔记。请检查网络后重试。",
loadingMore: "正在加载更多笔记",
openMemo: "打开笔记",
unpin: "取消置顶",
diff --git a/release-summary.json b/release-summary.json
index b3046f09..0258fd94 100644
--- a/release-summary.json
+++ b/release-summary.json
@@ -2,19 +2,19 @@
"version": "1.73.0",
"changes": {
"en-US": [
- "Capture a screenshot from the desktop tray or menu bar and save it as a new note. The macOS menu-bar icon now uses the cat brand mark.",
+ "Click the desktop tray or menu bar to capture the current screen and save it as a new note. The macOS menu-bar icon now uses the cat brand mark.",
"Give desktop notes even Evernote-like reading gutters, including when a scrollbar is visible.",
"Tighten the notebook sidebar so sync status, create, templates, all notes, and the notebook list sit closer together.",
"Keep the web app installable without downloading the full interface up front; pages cache as you use them."
],
"zh-CN": [
- "桌面端可从托盘或菜单栏截图并保存为新笔记。macOS 菜单栏图标改为猫咪品牌图形。",
+ "桌面端可从托盘或菜单栏一键截取当前屏幕并保存为新笔记。macOS 菜单栏图标改为猫咪品牌图形。",
"桌面笔记左右留白更接近印象笔记,出现滚动条时两侧仍保持对称。",
"收紧笔记本侧栏间距,同步状态、新建、模板、全部笔记和笔记本列表更紧凑。",
"网页版安装为应用时不再预下载整套界面,改为使用各页面时再缓存。"
],
"ja": [
- "デスクトップのトレイまたはメニューバーからスクリーンショットを撮り、新しいノートとして保存できます。macOS のメニューバーアイコンは猫のブランドマークになりました。",
+ "デスクトップのトレイまたはメニューバーをクリックすると、現在の画面を撮って新しいノートとして保存できます。macOS のメニューバーアイコンは猫のブランドマークになりました。",
"デスクトップのノートに Evernote に近い左右余白を付け、スクロールバーが出ても左右が揃うようにしました。",
"ノートブックサイドバーの余白を詰め、同期状態・作成・テンプレート・すべてのノート・ノートブック一覧を近づけました。",
"インストール可能な Web アプリは導入時に全画面を先読みしなくなり、使うページからキャッシュします。"