diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 14c7a8c..f3e9968 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,7 +1,8 @@ // main 进程入口:创建窗口 + 初始化库 + 注册 IPC + 迁移检测 -import { app, BrowserWindow, ipcMain, protocol, net } from 'electron'; +import { app, BrowserWindow, ipcMain, protocol, net, dialog } from 'electron'; import nodePath from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { promises as fs } from 'node:fs'; import { VaultService } from './services/vault-service.js'; import { registerIpc } from './ipc/handlers.js'; import { IPC } from '@hunter/shared'; @@ -14,35 +15,56 @@ protocol.registerSchemesAsPrivileged([ ]); let win: BrowserWindow | null = null; +// 多窗口支持:窗口控制作用于当前聚焦窗口,其次主窗口 +const focusedWin = () => BrowserWindow.getFocusedWindow() ?? win; function createWindow() { - win = new BrowserWindow({ + const w = new BrowserWindow({ width: 1200, height: 760, minWidth: 900, minHeight: 580, center: true, frame: false, show: false, - transparent: true, backgroundColor: '#00000000', backgroundMaterial: 'acrylic', + transparent: true, backgroundColor: '#00000000', backgroundMaterial: 'none', + // 真透明:透明主题下窗口直接透出下层软件(不用 acrylic 模糊,保证可清晰看到背后内容) webPreferences: { preload: nodePath.join(__dirname, '../preload/index.mjs'), contextIsolation: true, nodeIntegration: false, sandbox: false } }); - win.on('ready-to-show', () => win?.show()); + if (!win) win = w; + w.on('ready-to-show', () => w.show()); // 转发渲染进程控制台消息到主进程终端(便于开发调试) - win.webContents.on('console-message', (e, level, message, line, sourceId) => { + w.webContents.on('console-message', (e, level, message, line, sourceId) => { const lv = ['log', 'info', 'warn', 'error'][level] || 'log'; console.log('[renderer:' + lv + '] ' + message + (sourceId ? ' (' + sourceId + ':' + line + ')' : '')); }); - win.on('closed', () => { win = null; }); - // 窗口控制(FR-W2) - ipcMain.on(IPC.winMinimize, () => win?.minimize()); - ipcMain.on(IPC.winMaximize, () => { if (win?.isMaximized()) win.unmaximize(); else win?.maximize(); }); - // FR-W2:返回窗口最大化状态供 TitleBar 切换图标 - ipcMain.handle(IPC.winIsMaximized, () => win?.isMaximized() ?? false); - ipcMain.on(IPC.winClose, () => win?.close()); - if (process.env.ELECTRON_RENDERER_URL) win.loadURL(process.env.ELECTRON_RENDERER_URL); - else win.loadFile(nodePath.join(__dirname, '../renderer/index.html')); - return win; + w.on('closed', () => { if (win === w) win = null; }); + if (process.env.ELECTRON_RENDERER_URL) w.loadURL(process.env.ELECTRON_RENDERER_URL); + else w.loadFile(nodePath.join(__dirname, '../renderer/index.html')); + return w; } +// 窗口控制(FR-W2 + 多窗口安全 + Typora 快捷键:全屏 F11 / 新窗口 Ctrl+Shift+N) +ipcMain.on(IPC.winMinimize, () => focusedWin()?.minimize()); +ipcMain.on(IPC.winMaximize, () => { const w = focusedWin(); if (w?.isMaximized()) w.unmaximize(); else w?.maximize(); }); +ipcMain.handle(IPC.winIsMaximized, () => focusedWin()?.isMaximized() ?? false); +ipcMain.on(IPC.winClose, () => focusedWin()?.close()); +ipcMain.on(IPC.winFullscreen, () => { const w = focusedWin(); if (w) w.setFullScreen(!w.isFullScreen()); }); +ipcMain.on(IPC.winNew, () => { createWindow(); }); +// Ctrl+O 打开文件(选 .md → 返回绝对路径,渲染层换算为库内相对路径) +ipcMain.handle(IPC.fileOpen, async () => { + const w = focusedWin(); + const r = await dialog.showOpenDialog(w ?? undefined as never, { properties: ['openFile'], filters: [{ name: 'Markdown', extensions: ['md'] }] }); + if (r.canceled || r.filePaths.length === 0) return null; + return r.filePaths[0]; +}); +// Ctrl+Shift+S 另存为:弹保存对话框写入正文副本 +ipcMain.handle(IPC.docSaveAs, async (_e, payload: { body: string; title: string }) => { + const w = focusedWin(); + const r = await dialog.showSaveDialog(w ?? undefined as never, { defaultPath: (payload.title || 'untitled') + '.md', filters: [{ name: 'Markdown', extensions: ['md'] }] }); + if (r.canceled || !r.filePath) return null; + await fs.writeFile(r.filePath, payload.body, 'utf8'); + return r.filePath; +}); + app.whenReady().then(async () => { const vault = new VaultService(); await vault.init(); diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx index aac4ad2..7fb86d7 100644 --- a/apps/desktop/src/renderer/App.tsx +++ b/apps/desktop/src/renderer/App.tsx @@ -22,6 +22,7 @@ import { useKeymap } from './features/keymap/keymap-service'; import { useLayout } from './features/shell/layout-store'; import { Resizer } from './features/shell/Resizer'; import { StatusBar } from './features/shell/StatusBar'; +import { useTypewriter } from './features/editor/typewriter-store'; import { EffectLayer } from './features/effects/EffectLayer'; import { useSettings } from './features/settings/settings-store'; import { SearchBox } from './features/search/SearchBox'; @@ -75,14 +76,63 @@ export function App() { }, onSave: () => { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); }, onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); }, - onToggleOutline: () => layout.toggleOutline(), + onToggleOutline: () => { setRightTab('outline'); layout.toggleOutline(); }, + onToggleInfo: () => { setRightTab('info'); if (!layout.outlineVisible) layout.toggleOutline(); }, + onToggleSidebar: () => layout.toggleSidebar(), + onFileTree: () => layout.toggleSidebar(), onCheat: () => setShowCheat(true), onSettings: () => setShowSettings(true), onFind: () => { window.dispatchEvent(new CustomEvent('dsh-toggle-find')); }, + onReplace: () => { window.dispatchEvent(new CustomEvent('dsh-toggle-find')); }, onClearFormat: () => { const ed = useEditorShared.getState().editor; ed?.chain().focus().clearNodes().unsetAllMarks().run(); }, + onCloseTab: () => { + const t = useTabs.getState(); + if (t.activeId) t.closeTab(t.activeId); + }, + onReopenTab: () => { + const t = useTabs.getState(); + const last = t.popClosed(); + if (last) t.openTab(last.path); + }, + onOpenFile: async () => { + const abs = await invoke(IPC.fileOpen, undefined as never); + if (!abs) return; + const root = useStore.getState().vault?.currentRoot ?? ''; + const norm = abs.replace(/\\/g, '/'); + let rel = norm; + if (root) { + const prefix = root.replace(/\\/g, '/') + '/'; + if (norm.startsWith(prefix)) rel = norm.slice(prefix.length); + } + if (rel && !rel.startsWith('..')) await useStore.getState().openDoc(rel); + }, + onQuickOpen: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); }, + onNewWindow: () => { void invoke(IPC.winNew, undefined as never); }, + onSaveAs: async () => { + const ed = useEditorShared.getState().editor; + if (!ed) return; + const body = (ed.storage as any)?.markdown?.getMarkdown?.() ?? ''; + const title = useStore.getState().currentPath?.split('/').pop()?.replace(/\.md$/, '') ?? 'untitled'; + await invoke(IPC.docSaveAs, { body, title }); + }, + onCopyMd: async () => { + const path = useStore.getState().currentPath; + if (!path) return; + try { const md = await invoke(IPC.copyMd, { path }); await navigator.clipboard.writeText(md); } catch { /* 剪贴板写入失败忽略 */ } + }, + onPastePlain: async () => { + const ed = useEditorShared.getState().editor; + if (!ed) return; + try { const text = await navigator.clipboard.readText(); ed.chain().focus().insertContent(text).run(); } catch { /* 剪贴板读取失败忽略 */ } + }, + onInsertLink: () => { window.dispatchEvent(new CustomEvent('dsh-insert-link')); }, + onInsertImage: () => { window.dispatchEvent(new CustomEvent('dsh-insert-image')); }, + onFullscreen: () => { void invoke(IPC.winFullscreen, undefined as never); }, + onFocusMode: () => layout.toggleMinimal(), + onTypewriter: () => useTypewriter.getState().toggle(), }); // 编辑器样式随设置生效(FR-编辑器设置真实生效) diff --git a/apps/desktop/src/renderer/features/editor/BlockOps.ts b/apps/desktop/src/renderer/features/editor/BlockOps.ts index 48389c0..73d93d4 100644 --- a/apps/desktop/src/renderer/features/editor/BlockOps.ts +++ b/apps/desktop/src/renderer/features/editor/BlockOps.ts @@ -3,12 +3,15 @@ import { Fragment, type Node } from '@tiptap/pm/model'; import { TextSelection } from '@tiptap/pm/state'; // 块级操作(FR-E7):Alt+↑/↓ 上移/下移当前顶层块,Alt+Backspace 删除当前块 +// Typora 标准:Ctrl+Shift+↑/↓ 段落上移/下移(同时保留 Alt+↑/↓ 兼容旧习惯) export const BlockOps = Extension.create({ name: 'blockOps', addKeyboardShortcuts() { return { 'Alt-ArrowUp': () => moveBlock(this.editor, 'up'), 'Alt-ArrowDown': () => moveBlock(this.editor, 'down'), + 'Mod-Shift-ArrowUp': () => moveBlock(this.editor, 'up'), + 'Mod-Shift-ArrowDown': () => moveBlock(this.editor, 'down'), 'Alt-Backspace': () => deleteBlock(this.editor), }; }, @@ -27,7 +30,7 @@ function topBlock(editor: { state: any }): { start: number; end: number; node: N return { start, end: start + node.nodeSize, node, idx }; } -function moveBlock(editor: any, dir: 'up' | 'down'): boolean { +export function moveBlock(editor: any, dir: 'up' | 'down'): boolean { const { doc, tr } = editor.state; const r = topBlock(editor); if (!r || r.idx < 0) return false; diff --git a/apps/desktop/src/renderer/features/editor/EditorToolbar.tsx b/apps/desktop/src/renderer/features/editor/EditorToolbar.tsx index e59f865..64bbeca 100644 --- a/apps/desktop/src/renderer/features/editor/EditorToolbar.tsx +++ b/apps/desktop/src/renderer/features/editor/EditorToolbar.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import type { Editor } from '@tiptap/react'; import { useStore } from '../../store'; import { useEditorRef } from './editor-context'; @@ -44,6 +45,14 @@ export function EditorToolbar() { if (!url) { editor.commands.focus(); return; } editor.chain().focus().setImage({ src: url }).run(); }; + // Typora:Ctrl+K 插入链接 / Ctrl+Shift+I 插入图片(由全局热键派发事件) + useEffect(() => { + const onLink = () => { void link(); }; + const onImg = () => { void pickImage(); }; + window.addEventListener('dsh-insert-link', onLink); + window.addEventListener('dsh-insert-image', onImg); + return () => { window.removeEventListener('dsh-insert-link', onLink); window.removeEventListener('dsh-insert-image', onImg); }; + }, [editor]); return (
await editor.click();
await a.page.keyboard.press('Control+A');
- await a.page.locator('button[title="行内代码 Ctrl+E"]').click();
+ await a.page.locator('button[title="行内代码 Ctrl+Shift+`"]').click();
expect(await html()).toContain('');
// 清除格式 -> 移除
diff --git a/apps/desktop/tests/e2e/keymap-all.spec.ts b/apps/desktop/tests/e2e/keymap-all.spec.ts
index 95899a6..ef05527 100644
--- a/apps/desktop/tests/e2e/keymap-all.spec.ts
+++ b/apps/desktop/tests/e2e/keymap-all.spec.ts
@@ -95,7 +95,7 @@ test('快捷键 Ctrl+Shift+L 大纲面板切换', async () => {
const btns = Array.from(document.querySelectorAll('button'));
return btns.some(b => b.textContent === '大纲');
});
- await a.page.keyboard.press('Control+Shift+l');
+ await a.page.keyboard.press('Control+Shift+1');
await a.page.waitForTimeout(500);
const afterPanel = await a.page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('button'));
@@ -125,7 +125,7 @@ test('快捷键 Ctrl+Shift+, 设置面板', async () => {
await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
const a = await launch(ud);
await a.invoke('vault:switch', { root: v });
- await a.page.keyboard.press('Control+Shift+Comma');
+ await a.page.keyboard.press('Control+Comma');
await expect(a.page.getByText('设置', { exact: true }).first()).toBeVisible({ timeout: 5000 });
await a.page.keyboard.press('Escape');
await a.app.close();
@@ -155,7 +155,7 @@ test('编辑器快捷键:加粗+斜体+行内代码+删除线+清除格式', a
expect(html).toContain('em');
// 行内代码
await a.page.keyboard.press('Control+a');
- await a.page.keyboard.press('Control+e');
+ await a.page.keyboard.press('Control+Shift+Backquote');
await a.page.waitForTimeout(100);
html = await a.page.locator('.ProseMirror').innerHTML();
expect(html).toContain('code');
diff --git a/apps/desktop/tests/e2e/transparency-check.spec.ts b/apps/desktop/tests/e2e/transparency-check.spec.ts
new file mode 100644
index 0000000..4d4b234
--- /dev/null
+++ b/apps/desktop/tests/e2e/transparency-check.spec.ts
@@ -0,0 +1,37 @@
+import { test, expect } from '@playwright/test';
+import { _electron as electron } from 'playwright';
+import { promises as fs } from 'node:fs';
+import nodePath from 'node:path';
+import os from 'node:os';
+
+const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js');
+
+// 透明主题真透明:bgPrimary=transparent 时根背景必须是 alpha=0(配合主进程 transparent:true + backgroundColor:#00000000 + backgroundMaterial:'none' 即可透出下层软件)
+test('透明主题:根背景 alpha=0(真透明,可见下层软件)', async () => {
+ const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-tr-'));
+ const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v');
+ await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
+ const app = await electron.launch({ args: ['--user-data-dir=' + ud, MAIN_JS] });
+ const page = await app.firstWindow();
+ await page.waitForLoadState('domcontentloaded');
+ await page.evaluate(({ root }) => (globalThis as any).dsh.invoke('vault:switch', { root }), { root: v });
+ // 应用透明主题的 CSS 变量(bgPrimary='transparent'),并移除背景图遮罩
+ await page.evaluate(() => {
+ const el = document.documentElement;
+ el.style.setProperty('--bg-primary', 'transparent');
+ el.style.setProperty('--bg-secondary', 'rgba(20,20,28,0.5)');
+ el.style.setProperty('--text-primary', '#ddd');
+ document.querySelectorAll('div[style*="background-image"]').forEach((n: any) => n.remove());
+ });
+ await page.waitForTimeout(400);
+ const rootBg = await page.evaluate(() => {
+ const root = document.querySelector('#root > div') as HTMLElement;
+ if (!root) return null;
+ return getComputedStyle(root).backgroundColor;
+ });
+ console.log('ROOT_BG', rootBg);
+ expect(rootBg).not.toBeNull();
+ // 透明背景应为 alpha=0(rgba(0,0,0,0) 或 transparent)
+ expect(rootBg).toMatch(/rgba\(0, 0, 0, 0\)|transparent/i);
+ await app.close();
+});
\ No newline at end of file
diff --git a/apps/desktop/tests/e2e/typora-shortcuts.spec.ts b/apps/desktop/tests/e2e/typora-shortcuts.spec.ts
new file mode 100644
index 0000000..5865390
--- /dev/null
+++ b/apps/desktop/tests/e2e/typora-shortcuts.spec.ts
@@ -0,0 +1,155 @@
+import { test, expect } from '@playwright/test';
+import { _electron as electron } from 'playwright';
+import { promises as fs } from 'node:fs';
+import nodePath from 'node:path';
+import os from 'node:os';
+
+const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js');
+
+async function launch(userData: string) {
+ const app = await electron.launch({ args: ['--user-data-dir=' + userData, MAIN_JS] });
+ const page = await app.firstWindow();
+ await page.waitForLoadState('domcontentloaded');
+ const invoke = (ch: string, args: any) => page.evaluate(({ ch, args }) => (globalThis as any).dsh.invoke(ch, args), { ch, args });
+ return { app, page, invoke };
+}
+
+async function newDoc(a: { page: any; invoke: any }, title: string) {
+ await a.page.getByRole('button', { name: /新建文档/ }).click();
+ await a.page.getByPlaceholder(/输入文档标题/).fill(title);
+ await a.page.getByPlaceholder(/输入文档标题/).press('Enter');
+ await expect(a.page.getByTestId('tabbar').getByText(title, { exact: true })).toBeVisible({ timeout: 10000 });
+}
+
+test('keymap store:完整 Typora 列表无冲突', async () => {
+ const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty-'));
+ const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v');
+ await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
+ const a = await launch(ud);
+ await a.invoke('vault:switch', { root: v });
+ const r = await a.page.evaluate(() => {
+ const store = (window as any).__KEYMAP_STORE__;
+ if (!store) return { error: 'no store' };
+ const bs = store.getState().bindings;
+ const ids = bs.map((b: any) => b.id);
+ const want = ['newDoc','save','closeTab','reopenTab','openFile','quickOpen','newWindow','saveAs','copyMd','pastePlain','selectLine','selectWord','selectSameFormat','deleteWord','docStart','docEnd','bold','italic','underline','strike','inlineCode','link','insertImage','clearFormat','heading1','heading2','heading3','heading4','heading5','heading6','paragraph','headingUp','headingDown','blockquote','bulletList','orderedList','codeBlock','mathBlock','table','indent','outdent','moveUp','moveDown','toggleSidebar','toggleOutline','toggleInfo','focusMode','typewriter','fullscreen','focusSearch','toggleCheat','toggleSettings','find','replace'];
+ return { count: bs.length, conflictCount: store.getState().conflictCount(), hasAll: want.every((id: string) => ids.includes(id)) };
+ });
+ console.log('TYPORA_STORE', JSON.stringify(r));
+ expect(r.error).toBeUndefined();
+ expect(r.conflictCount).toBe(0);
+ expect(r.count).toBeGreaterThanOrEqual(50);
+ expect(r.hasAll).toBe(true);
+ await a.app.close();
+});
+// 编辑器排版快捷键:行内代码 Ctrl+Shift+`、下划线 Ctrl+U、无序/有序列表、代码块、表格、引用
+test('Typora 编辑器快捷键:代码/下划线/列表/代码块/表格/引用', async () => {
+ const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty2-'));
+ const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v');
+ await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
+ const a = await launch(ud);
+ await a.invoke('vault:switch', { root: v });
+ await newDoc(a, 'TyporaHK');
+ const ed = a.page.locator('.ProseMirror');
+ await ed.click();
+ const html = async () => (await a.page.locator('.ProseMirror').innerHTML());
+ // 行内代码 Ctrl+Shift+`
+ await a.page.keyboard.type('code sample');
+ await a.page.keyboard.press('Control+a');
+ await a.page.keyboard.press('Control+Shift+Backquote');
+ await a.page.waitForTimeout(120);
+ expect(await html()).toContain('');
+ // 清除格式后重试下划线 Ctrl+U
+ await a.page.keyboard.press('Control+a');
+ await a.page.keyboard.press('Control+\\');
+ await a.page.keyboard.type('underline text');
+ await a.page.keyboard.press('Control+a');
+ await a.page.keyboard.press('Control+u');
+ await a.page.waitForTimeout(120);
+ expect(await html()).toContain('');
+ // 无序列表 Ctrl+Shift+]
+ await a.page.keyboard.press('Control+a');
+ await a.page.keyboard.press('Control+Shift+]');
+ await a.page.waitForTimeout(120);
+ expect(await html()).toContain(' {
+ const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty3-'));
+ const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v');
+ await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
+ const a = await launch(ud);
+ await a.invoke('vault:switch', { root: v });
+ await newDoc(a, 'BlockHK');
+ const ed = a.page.locator('.ProseMirror');
+ await ed.click();
+ await a.page.keyboard.type('AAAA');
+ await a.page.keyboard.press('Enter');
+ await a.page.keyboard.type('BBBB');
+ // 光标在第二段,Ctrl+Shift+↑ 上移(BBBB 块上移到顶部)
+ await a.page.keyboard.press('Control+Shift+ArrowUp');
+ await a.page.waitForTimeout(120);
+ const text = await a.page.locator('.ProseMirror').innerText();
+ expect(text.indexOf('BBBB')).toBeLessThan(text.indexOf('AAAA'));
+ // Ctrl+L 选中当前行
+ await a.page.keyboard.press('Control+l');
+ await a.page.waitForTimeout(120);
+ const sel = await a.page.evaluate(() => {
+ const s = window.getSelection();
+ return s ? s.toString() : '';
+ });
+ expect(sel.length).toBeGreaterThan(0);
+ await a.app.close();
+});
+
+// 视图快捷键:Ctrl+Shift+L 侧边栏 / Ctrl+Shift+1 大纲 / Ctrl+, 设置
+test('Typora 视图:Ctrl+Shift+L 侧边栏 / Ctrl+Shift+1 大纲 / Ctrl+, 设置', async () => {
+ const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty4-'));
+ const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v');
+ await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
+ const a = await launch(ud);
+ await a.invoke('vault:switch', { root: v });
+ await newDoc(a, 'ViewHK');
+ // Ctrl+Shift+L 隐藏侧边栏(搜索框消失)
+ await a.page.getByPlaceholder(/搜索/).waitFor({ timeout: 5000 });
+ await a.page.keyboard.press('Control+Shift+l');
+ await a.page.waitForTimeout(200);
+ await expect(a.page.getByPlaceholder(/搜索/)).not.toBeVisible({ timeout: 4000 });
+ // 再次 Ctrl+Shift+L 恢复
+ await a.page.keyboard.press('Control+Shift+l');
+ await a.page.waitForTimeout(200);
+ await expect(a.page.getByPlaceholder(/搜索/)).toBeVisible({ timeout: 4000 });
+ // Ctrl+Shift+1 大纲面板(右栏 大纲/信息 按钮可见性切换)
+ const infoBtn = a.page.getByRole('button', { name: '信息' });
+ const before = await infoBtn.isVisible().catch(() => false);
+ await a.page.keyboard.press('Control+Shift+1');
+ await a.page.waitForTimeout(200);
+ const after = await infoBtn.isVisible().catch(() => false);
+ expect(before).not.toBe(after);
+ // Ctrl+, 设置
+ await a.page.keyboard.press('Control+Comma');
+ await expect(a.page.getByText('设置', { exact: true }).first()).toBeVisible({ timeout: 5000 });
+ await a.page.keyboard.press('Escape');
+ await a.app.close();
+});
diff --git a/packages/shared/src/types/ipc.ts b/packages/shared/src/types/ipc.ts
index fa931f8..7534a18 100644
--- a/packages/shared/src/types/ipc.ts
+++ b/packages/shared/src/types/ipc.ts
@@ -31,6 +31,10 @@ export const IPC = {
winMaximize: 'win:maximize',
winIsMaximized: 'win:is-maximized',
winClose: 'win:close',
+ winFullscreen: 'win:fullscreen',
+ winNew: 'win:new',
+ fileOpen: 'file:open',
+ docSaveAs: 'doc:save-as',
gpuInfo: 'gpu:info',
copyMd: 'doc:copy-md',
printPdf: 'doc:print-pdf',
@@ -69,6 +73,10 @@ export interface IpcMap {
[IPC.winMaximize]: { args: void; result: void };
[IPC.winIsMaximized]: { args: void; result: boolean };
[IPC.winClose]: { args: void; result: void };
+ [IPC.winFullscreen]: { args: void; result: void };
+ [IPC.winNew]: { args: void; result: void };
+ [IPC.fileOpen]: { args: void; result: string | null };
+ [IPC.docSaveAs]: { args: { body: string; title: string }; result: string | null };
[IPC.gpuInfo]: { args: void; result: { softwareRendering: boolean; vendor: string; renderer: string } };
[IPC.copyMd]: { args: { path: string }; result: string };
[IPC.printPdf]: { args: void; result: void };