feat: 实现笔临 P0/P1 Electron 桌面应用 v0.1.0
交付写作核心(TipTap、自动保存、分卷分章)、Onboarding、7 主题与双语 i18n、快捷键设置;主进程使用 node:sqlite;补全 Vitest 与 Playwright E2E;统一 design/ 目录并移除 desigin 拼写错误路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import type { BookMeta, CreateBookParams } from '../../../shared/types'
|
||||
import { BookRegistryService } from '../../services/book-registry'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerBookHandlers(registry: BookRegistryService): void {
|
||||
ipcMain.handle(IPC.BOOK_LIST, () => wrap(() => registry.list()))
|
||||
|
||||
ipcMain.handle(IPC.BOOK_CREATE, (_event, params: CreateBookParams) =>
|
||||
wrap(() => registry.create(params))
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC.BOOK_DELETE, (_event, { bookId }: { bookId: string }) =>
|
||||
wrap(() => {
|
||||
registry.delete(bookId)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC.BOOK_OPEN, (_event, { bookId }: { bookId: string }) =>
|
||||
wrap(() => registry.open(bookId))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.BOOK_UPDATE_META,
|
||||
(
|
||||
_event,
|
||||
{
|
||||
bookId,
|
||||
...patch
|
||||
}: {
|
||||
bookId: string
|
||||
lastChapterId?: string | null
|
||||
lastOpenedAt?: string
|
||||
status?: BookMeta['status']
|
||||
}
|
||||
) => wrap(() => registry.updateMeta(bookId, patch))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import type { ChapterStatus } from '../../../shared/types'
|
||||
import { BookRegistryService } from '../../services/book-registry'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerChapterHandlers(registry: BookRegistryService): void {
|
||||
ipcMain.handle(
|
||||
IPC.VOLUME_CREATE,
|
||||
(_event, { bookId, name }: { bookId: string; name: string }) =>
|
||||
wrap(() => {
|
||||
const repo = registry.getVolumeRepo(bookId)
|
||||
const volumes = repo.list()
|
||||
return repo.create(name, volumes.length)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.VOLUME_UPDATE,
|
||||
(
|
||||
_event,
|
||||
{ bookId, volumeId, name, description }: { bookId: string; volumeId: string; name?: string; description?: string }
|
||||
) => wrap(() => registry.getVolumeRepo(bookId).update(volumeId, { name, description }))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.VOLUME_DELETE,
|
||||
(_event, { bookId, volumeId }: { bookId: string; volumeId: string }) =>
|
||||
wrap(() => {
|
||||
registry.getVolumeRepo(bookId).delete(volumeId)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.CHAPTER_CREATE,
|
||||
(
|
||||
_event,
|
||||
{ bookId, volumeId, title }: { bookId: string; volumeId: string; title: string }
|
||||
) =>
|
||||
wrap(() => {
|
||||
const repo = registry.getChapterRepo(bookId)
|
||||
const sortOrder = repo.nextSortOrder(volumeId)
|
||||
return repo.create(volumeId, title, sortOrder)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.CHAPTER_GET,
|
||||
(_event, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
|
||||
wrap(() => {
|
||||
const ch = registry.getChapterRepo(bookId).get(chapterId)
|
||||
if (!ch) throw new Error('Chapter not found')
|
||||
return ch
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.CHAPTER_UPDATE,
|
||||
(
|
||||
_event,
|
||||
{
|
||||
bookId,
|
||||
chapterId,
|
||||
title,
|
||||
content,
|
||||
status,
|
||||
cursorOffset
|
||||
}: {
|
||||
bookId: string
|
||||
chapterId: string
|
||||
title?: string
|
||||
content?: string
|
||||
status?: ChapterStatus
|
||||
cursorOffset?: number
|
||||
}
|
||||
) =>
|
||||
wrap(() => {
|
||||
const chapter = registry.getChapterRepo(bookId).update(chapterId, {
|
||||
title,
|
||||
content,
|
||||
status,
|
||||
cursorOffset
|
||||
})
|
||||
registry.updateMeta(bookId, { lastChapterId: chapterId })
|
||||
return chapter
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.CHAPTER_DELETE,
|
||||
(_event, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
|
||||
wrap(() => {
|
||||
registry.getChapterRepo(bookId).delete(chapterId)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import { GlobalSettingsService } from '../services/global-settings'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerSettingsHandlers(settings: GlobalSettingsService): void {
|
||||
ipcMain.handle(IPC.SETTINGS_GET, () => wrap(() => settings.get()))
|
||||
ipcMain.handle(IPC.SETTINGS_UPDATE, (_event, partial) =>
|
||||
wrap(() => settings.update(partial))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import type { ActionId } from '../../../shared/types'
|
||||
import { ShortcutManager } from '../shortcuts/manager'
|
||||
import { wrap } from '../result'
|
||||
|
||||
export function registerShortcutHandlers(shortcuts: ShortcutManager): void {
|
||||
ipcMain.handle(IPC.SHORTCUT_GET_ALL, () => wrap(() => shortcuts.getAll()))
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.SHORTCUT_REGISTER,
|
||||
(_event, { action, accelerator }: { action: ActionId; accelerator: string }) =>
|
||||
wrap(() => {
|
||||
const ok = shortcuts.save(action, accelerator)
|
||||
if (!ok) throw new Error('快捷键注册失败,可能与其他应用冲突')
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
import { GlobalSettingsService } from '../services/global-settings'
|
||||
import { BookRegistryService } from '../services/book-registry'
|
||||
import { ShortcutManager } from '../shortcuts/manager'
|
||||
import { registerSettingsHandlers } from './handlers/settings.handler'
|
||||
import { registerBookHandlers } from './handlers/book.handler'
|
||||
import { registerChapterHandlers } from './handlers/chapter.handler'
|
||||
import { registerShortcutHandlers } from './handlers/shortcut.handler'
|
||||
|
||||
let shortcutManager: ShortcutManager | null = null
|
||||
|
||||
export function registerIpc(): { settings: GlobalSettingsService; books: BookRegistryService; shortcuts: ShortcutManager } {
|
||||
const userData = app.getPath('userData')
|
||||
|
||||
const settings = new GlobalSettingsService(userData)
|
||||
const books = new BookRegistryService(userData)
|
||||
shortcutManager = new ShortcutManager(settings)
|
||||
|
||||
registerSettingsHandlers(settings)
|
||||
registerBookHandlers(books)
|
||||
registerChapterHandlers(books)
|
||||
registerShortcutHandlers(shortcutManager)
|
||||
|
||||
return { settings, books, shortcuts: shortcutManager }
|
||||
}
|
||||
|
||||
export function getShortcutManager(): ShortcutManager | null {
|
||||
return shortcutManager
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ErrorCode, type IpcError, type IpcResult } from '../../shared/types'
|
||||
|
||||
export function ok<T>(data: T): IpcResult<T> {
|
||||
return { ok: true, data }
|
||||
}
|
||||
|
||||
export function fail(code: ErrorCode, message: string, recoverable: boolean): IpcResult<never> {
|
||||
return { ok: false, error: { code, message, recoverable } }
|
||||
}
|
||||
|
||||
export function wrap<T>(fn: () => T, recoverable = true): IpcResult<T> {
|
||||
try {
|
||||
return ok(fn())
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
return fail(ErrorCode.UNKNOWN, message, recoverable)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user