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:
+62
-1
@@ -14,7 +14,12 @@ import { useAppStore } from '@renderer/stores/useAppStore'
|
||||
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
|
||||
import { useTabStore } from '@renderer/stores/useTabStore'
|
||||
import { useBookStore } from '@renderer/stores/useBookStore'
|
||||
import { useEditStore } from '@renderer/stores/useEditStore'
|
||||
import { SearchModal } from '@renderer/components/search/SearchModal'
|
||||
import { useSearchStore } from '@renderer/stores/useSearchStore'
|
||||
import { useReferenceStore } from '@renderer/stores/useReferenceStore'
|
||||
import { flushEditorSave } from '@renderer/components/editor/TipTapEditor'
|
||||
import { insertLandmarkInEditor } from '@renderer/lib/editor-commands'
|
||||
import { ipcCall } from '@renderer/lib/ipc-client'
|
||||
|
||||
function AppInner(): React.JSX.Element {
|
||||
@@ -40,11 +45,35 @@ function AppInner(): React.JSX.Element {
|
||||
if (loaded && !onboardingCompleted) setShowOnboarding(true)
|
||||
}, [loaded, onboardingCompleted])
|
||||
|
||||
useEffect(() => {
|
||||
const openSearch = (): void => useSearchStore.getState().setOpen(true)
|
||||
const openLandmarks = (): void => useAppStore.getState().setLandmarksModalOpen(true)
|
||||
const onInsertLandmark = (e: Event): void => {
|
||||
const label = (e as CustomEvent<{ label: string }>).detail?.label
|
||||
if (!label) return
|
||||
const { currentBookId } = useBookStore.getState()
|
||||
const target = useEditStore.getState().target
|
||||
if (!currentBookId || target?.kind !== 'chapter') return
|
||||
insertLandmarkInEditor({ label, landmarkType: 'todo' })
|
||||
void ipcCall(() =>
|
||||
window.electronAPI.bookmark.create(currentBookId, target.id, 0, label, 'todo')
|
||||
)
|
||||
}
|
||||
window.addEventListener('bilin:e2e-open-search', openSearch)
|
||||
window.addEventListener('bilin:e2e-open-landmarks', openLandmarks)
|
||||
window.addEventListener('bilin:e2e-insert-landmark', onInsertLandmark)
|
||||
return () => {
|
||||
window.removeEventListener('bilin:e2e-open-search', openSearch)
|
||||
window.removeEventListener('bilin:e2e-open-landmarks', openLandmarks)
|
||||
window.removeEventListener('bilin:e2e-insert-landmark', onInsertLandmark)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = window.electronAPI.onShortcutTriggered(async (action) => {
|
||||
if (action === 'saveChapter') {
|
||||
try {
|
||||
await flushEditorSave()
|
||||
await flushEditorSave(true)
|
||||
} catch {
|
||||
showToast(t('toast.saveFailed'))
|
||||
}
|
||||
@@ -75,6 +104,37 @@ function AppInner(): React.JSX.Element {
|
||||
} else setView('home')
|
||||
return
|
||||
}
|
||||
if (action === 'globalSearch') {
|
||||
useSearchStore.getState().setOpen(true)
|
||||
return
|
||||
}
|
||||
if (action === 'openReference') {
|
||||
useReferenceStore.getState().setOpen(true)
|
||||
return
|
||||
}
|
||||
if (action === 'openLandmarks') {
|
||||
useAppStore.getState().setLandmarksModalOpen(true)
|
||||
return
|
||||
}
|
||||
if (action === 'insertLandmark') {
|
||||
if (view !== 'editor') return
|
||||
document.querySelector<HTMLButtonElement>('[data-testid="insert-landmark"]')?.click()
|
||||
return
|
||||
}
|
||||
if (action === 'captureInspiration') {
|
||||
if (view !== 'editor') return
|
||||
const { currentBookId, addInspirationLocal } = useBookStore.getState()
|
||||
if (!currentBookId) return
|
||||
void (async () => {
|
||||
const item = await ipcCall(() =>
|
||||
window.electronAPI.inspiration.create(currentBookId, t('inspiration.newItem'))
|
||||
)
|
||||
addInspirationLocal(item)
|
||||
useAppStore.getState().setSidebarPanel('inspiration')
|
||||
await useEditStore.getState().switchTarget({ kind: 'inspiration', id: item.id })
|
||||
})()
|
||||
return
|
||||
}
|
||||
showToast(t('feature.comingSoon'))
|
||||
})
|
||||
return unsub
|
||||
@@ -93,6 +153,7 @@ function AppInner(): React.JSX.Element {
|
||||
{view === 'settings' && <SettingsPage />}
|
||||
</div>
|
||||
{showOnboarding && <OnboardingWizard onComplete={handleOnboardingComplete} />}
|
||||
<SearchModal />
|
||||
{toast && <div className="toast">{toast}</div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user