feat: deliver P2 v0.2.0 with unified editor, search, and snapshots
Implement schema v2 migration, outline/setting/inspiration CRUD, unified TipTap editing, reference panel with mentions, FTS global search, chapter version history, writing landmarks, word frequency analysis, light theme contrast fixes, and full unit/E2E test coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+163
-1
@@ -2,14 +2,25 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'
|
||||
import { IPC } from '../shared/ipc-channels'
|
||||
import type {
|
||||
ActionId,
|
||||
Bookmark,
|
||||
BookMeta,
|
||||
BookOpenResult,
|
||||
Chapter,
|
||||
CreateBookParams,
|
||||
GlobalSettings,
|
||||
InspirationEntry,
|
||||
IpcResult,
|
||||
LandmarkType,
|
||||
OutlineItem,
|
||||
OutlineStatus,
|
||||
SearchResult,
|
||||
SettingEntry,
|
||||
SettingType,
|
||||
Snapshot,
|
||||
SnapshotType,
|
||||
UpdateChapterParams,
|
||||
Volume
|
||||
Volume,
|
||||
WordFreqResult
|
||||
} from '../shared/types'
|
||||
|
||||
const electronAPI = {
|
||||
@@ -58,6 +69,157 @@ const electronAPI = {
|
||||
delete: (bookId: string, chapterId: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.CHAPTER_DELETE, { bookId, chapterId })
|
||||
},
|
||||
outline: {
|
||||
list: (bookId: string): Promise<IpcResult<OutlineItem[]>> =>
|
||||
ipcRenderer.invoke(IPC.OUTLINE_LIST, { bookId }),
|
||||
create: (
|
||||
bookId: string,
|
||||
title: string,
|
||||
parentId?: string | null,
|
||||
sortOrder?: number
|
||||
): Promise<IpcResult<OutlineItem>> =>
|
||||
ipcRenderer.invoke(IPC.OUTLINE_CREATE, { bookId, parentId, title, sortOrder }),
|
||||
update: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
patch: Partial<{
|
||||
title: string
|
||||
description: string
|
||||
status: OutlineStatus
|
||||
expectedWordCount: number | null
|
||||
chapterId: string | null
|
||||
sortOrder: number
|
||||
}>
|
||||
): Promise<IpcResult<OutlineItem>> =>
|
||||
ipcRenderer.invoke(IPC.OUTLINE_UPDATE, { bookId, id, patch }),
|
||||
delete: (bookId: string, id: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.OUTLINE_DELETE, { bookId, id }),
|
||||
move: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
parentId: string | null,
|
||||
sortOrder: number
|
||||
): Promise<IpcResult<OutlineItem>> =>
|
||||
ipcRenderer.invoke(IPC.OUTLINE_MOVE, { bookId, id, parentId, sortOrder })
|
||||
},
|
||||
setting: {
|
||||
list: (bookId: string, type?: SettingType): Promise<IpcResult<SettingEntry[]>> =>
|
||||
ipcRenderer.invoke(IPC.SETTING_LIST, { bookId, type }),
|
||||
create: (bookId: string, type: SettingType, name: string): Promise<IpcResult<SettingEntry>> =>
|
||||
ipcRenderer.invoke(IPC.SETTING_CREATE, { bookId, type, name }),
|
||||
update: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
patch: Partial<{ name: string; description: string; type: SettingType; properties: Record<string, unknown> }>
|
||||
): Promise<IpcResult<SettingEntry>> =>
|
||||
ipcRenderer.invoke(IPC.SETTING_UPDATE, { bookId, id, patch }),
|
||||
delete: (bookId: string, id: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.SETTING_DELETE, { bookId, id }),
|
||||
syncRefs: (bookId: string, id: string, chapterIds: string[]): Promise<IpcResult<SettingEntry>> =>
|
||||
ipcRenderer.invoke(IPC.SETTING_SYNC_REFS, { bookId, id, chapterIds })
|
||||
},
|
||||
inspiration: {
|
||||
list: (bookId: string): Promise<IpcResult<InspirationEntry[]>> =>
|
||||
ipcRenderer.invoke(IPC.INSPIRATION_LIST, { bookId }),
|
||||
create: (
|
||||
bookId: string,
|
||||
title?: string,
|
||||
content?: string,
|
||||
tags?: string[]
|
||||
): Promise<IpcResult<InspirationEntry>> =>
|
||||
ipcRenderer.invoke(IPC.INSPIRATION_CREATE, { bookId, title, content, tags }),
|
||||
update: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
patch: Partial<{ title: string; content: string; tags: string[] }>
|
||||
): Promise<IpcResult<InspirationEntry>> =>
|
||||
ipcRenderer.invoke(IPC.INSPIRATION_UPDATE, { bookId, id, patch }),
|
||||
delete: (bookId: string, id: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.INSPIRATION_DELETE, { bookId, id }),
|
||||
convert: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
targetKind: 'outline' | 'setting',
|
||||
settingType?: SettingType
|
||||
): Promise<IpcResult<OutlineItem | SettingEntry>> =>
|
||||
ipcRenderer.invoke(IPC.INSPIRATION_CONVERT, { bookId, id, targetKind, settingType })
|
||||
},
|
||||
snapshot: {
|
||||
list: (bookId: string, chapterId: string): Promise<IpcResult<Snapshot[]>> =>
|
||||
ipcRenderer.invoke(IPC.SNAPSHOT_LIST, { bookId, chapterId }),
|
||||
create: (
|
||||
bookId: string,
|
||||
chapterId: string,
|
||||
type: SnapshotType,
|
||||
name?: string,
|
||||
content?: string
|
||||
): Promise<IpcResult<Snapshot>> =>
|
||||
ipcRenderer.invoke(IPC.SNAPSHOT_CREATE, { bookId, chapterId, type, name, content }),
|
||||
restore: (
|
||||
bookId: string,
|
||||
chapterId: string,
|
||||
snapshotId: string
|
||||
): Promise<IpcResult<Chapter>> =>
|
||||
ipcRenderer.invoke(IPC.SNAPSHOT_RESTORE, { bookId, chapterId, snapshotId }),
|
||||
delete: (bookId: string, snapshotId: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.SNAPSHOT_DELETE, { bookId, snapshotId }),
|
||||
startAutoSave: (bookId: string, chapterId: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.SNAPSHOT_START_AUTO, { bookId, chapterId }),
|
||||
stopAutoSave: (bookId: string, chapterId: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.SNAPSHOT_STOP_AUTO, { bookId, chapterId })
|
||||
},
|
||||
bookmark: {
|
||||
list: (
|
||||
bookId: string,
|
||||
chapterId?: string,
|
||||
resolved?: boolean
|
||||
): Promise<IpcResult<Bookmark[]>> =>
|
||||
ipcRenderer.invoke(IPC.BOOKMARK_LIST, { bookId, chapterId, resolved }),
|
||||
create: (
|
||||
bookId: string,
|
||||
chapterId: string,
|
||||
position: number,
|
||||
label: string,
|
||||
landmarkType?: LandmarkType
|
||||
): Promise<IpcResult<Bookmark>> =>
|
||||
ipcRenderer.invoke(IPC.BOOKMARK_CREATE, { bookId, chapterId, position, label, landmarkType }),
|
||||
update: (
|
||||
bookId: string,
|
||||
id: string,
|
||||
patch: Partial<{ label: string; position: number; landmarkType: LandmarkType }>
|
||||
): Promise<IpcResult<Bookmark>> =>
|
||||
ipcRenderer.invoke(IPC.BOOKMARK_UPDATE, { bookId, id, patch }),
|
||||
resolve: (bookId: string, id: string, resolved: boolean): Promise<IpcResult<Bookmark>> =>
|
||||
ipcRenderer.invoke(IPC.BOOKMARK_RESOLVE, { bookId, id, resolved }),
|
||||
delete: (bookId: string, id: string): Promise<IpcResult<void>> =>
|
||||
ipcRenderer.invoke(IPC.BOOKMARK_DELETE, { bookId, id })
|
||||
},
|
||||
search: {
|
||||
query: (
|
||||
bookId: string,
|
||||
query: string,
|
||||
options?: { regex?: boolean; caseSensitive?: boolean; matchWholeWord?: boolean }
|
||||
): Promise<IpcResult<SearchResult[]>> =>
|
||||
ipcRenderer.invoke(IPC.SEARCH_QUERY, { bookId, query, options }),
|
||||
replace: (
|
||||
bookId: string,
|
||||
query: string,
|
||||
replacement: string,
|
||||
dryRun: boolean,
|
||||
options?: { regex?: boolean; caseSensitive?: boolean }
|
||||
): Promise<IpcResult<{ count: number; previews: SearchResult[] }>> =>
|
||||
ipcRenderer.invoke(IPC.SEARCH_REPLACE, { bookId, query, replacement, dryRun, options }),
|
||||
getHistory: (): Promise<IpcResult<string[]>> => ipcRenderer.invoke(IPC.SEARCH_GET_HISTORY),
|
||||
saveHistory: (query: string): Promise<IpcResult<string[]>> =>
|
||||
ipcRenderer.invoke(IPC.SEARCH_SAVE_HISTORY, { query })
|
||||
},
|
||||
wordfreq: {
|
||||
analyze: (
|
||||
bookId: string,
|
||||
scope: { kind: 'book' | 'volume' | 'chapter'; chapterId?: string; volumeId?: string }
|
||||
): Promise<IpcResult<WordFreqResult>> =>
|
||||
ipcRenderer.invoke(IPC.WORDFREQ_ANALYZE, { bookId, scope })
|
||||
},
|
||||
shortcut: {
|
||||
getAll: (): Promise<IpcResult<Record<ActionId, string>>> =>
|
||||
ipcRenderer.invoke(IPC.SHORTCUT_GET_ALL),
|
||||
|
||||
Reference in New Issue
Block a user