import { useEffect, useState } from 'react' import { Provider as JotaiProvider } from 'jotai' import { useTranslation } from 'react-i18next' import './i18n' import './styles/themes.css' import './styles/globals.css' import './styles/layout.css' import { TopBar } from '@renderer/components/layout/TopBar' import { HomePage } from '@renderer/components/home/HomePage' import { EditorLayout } from '@renderer/components/layout/EditorLayout' import { SettingsPage } from '@renderer/components/settings/SettingsPage' import { OnboardingWizard } from '@renderer/components/onboarding/OnboardingWizard' 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 { InspirationModal } from '@renderer/components/inspiration/InspirationModal' 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 { useAiStore } from '@renderer/stores/useAiStore' import { ipcCall } from '@renderer/lib/ipc-client' function AppInner(): React.JSX.Element { const { t } = useTranslation() const view = useAppStore((s) => s.view) const toast = useAppStore((s) => s.toast) const showToast = useAppStore((s) => s.showToast) const setView = useAppStore((s) => s.setView) const { loaded, onboardingCompleted, load: loadSettings } = useSettingsStore() const loadBooks = useBookStore((s) => s.loadBooks) const { activeTabId, openTabs } = useTabStore() const openBook = useBookStore((s) => s.openBook) const [showOnboarding, setShowOnboarding] = useState(false) useEffect(() => { void (async () => { await loadSettings() await loadBooks() })() }, [loadSettings, loadBooks]) useEffect(() => { if (loaded && !onboardingCompleted) setShowOnboarding(true) }, [loaded, onboardingCompleted]) useEffect(() => { const openSearch = (): void => useSearchStore.getState().setOpen(true) const openLandmarks = (): void => useAppStore.getState().setLandmarksModalOpen(true) const openInspiration = (): void => { if (useAppStore.getState().view !== 'editor') return if (!useBookStore.getState().currentBookId) return useAppStore.getState().setInspirationModalOpen(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') ) } const onSetAiOnline = (e: Event): void => { const online = (e as CustomEvent<{ online: boolean }>).detail?.online if (typeof online === 'boolean') useAiStore.setState({ online }) } const onFlushEditor = (): void => { void flushEditorSave() } window.addEventListener('bilin:e2e-open-search', openSearch) window.addEventListener('bilin:e2e-open-landmarks', openLandmarks) window.addEventListener('bilin:e2e-capture-inspiration', openInspiration) window.addEventListener('bilin:e2e-insert-landmark', onInsertLandmark) window.addEventListener('bilin:e2e-set-ai-online', onSetAiOnline) window.addEventListener('bilin:e2e-flush-editor', onFlushEditor) return () => { window.removeEventListener('bilin:e2e-open-search', openSearch) window.removeEventListener('bilin:e2e-open-landmarks', openLandmarks) window.removeEventListener('bilin:e2e-capture-inspiration', openInspiration) window.removeEventListener('bilin:e2e-insert-landmark', onInsertLandmark) window.removeEventListener('bilin:e2e-set-ai-online', onSetAiOnline) window.removeEventListener('bilin:e2e-flush-editor', onFlushEditor) } }, []) useEffect(() => { const unsub = window.electronAPI.onShortcutTriggered(async (action) => { if (action === 'saveChapter') { try { await flushEditorSave(true) } catch { showToast(t('toast.saveFailed')) } return } if (action === 'newChapter') { if (view !== 'editor') return const { currentBookId, activeVolumeId, refreshChapters, setSelectedChapter } = useBookStore.getState() if (!currentBookId || !activeVolumeId) return await flushEditorSave() const ch = await ipcCall(() => window.electronAPI.chapter.create(currentBookId, activeVolumeId, t('editor.newChapter')) ) await refreshChapters() setSelectedChapter(ch.id) return } if (action === 'closeTab') { const tab = openTabs.find((x) => x.id === activeTabId) if (!tab) return const next = useTabStore.getState().closeTab(tab.id) if (next) { const nt = useTabStore.getState().openTabs.find((x) => x.id === next) if (nt?.type === 'book' && nt.bookId) { await openBook(nt.bookId) setView('editor') } else setView('settings') } 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('[data-testid="insert-landmark"]')?.click() return } if (action === 'captureInspiration') { if (view !== 'editor') return if (!useBookStore.getState().currentBookId) return useAppStore.getState().setInspirationModalOpen(true) return } showToast(t('feature.comingSoon')) }) return unsub }, [view, activeTabId, openTabs, openBook, setView, showToast, t]) const handleOnboardingComplete = (): void => { setShowOnboarding(false) } return (
{view === 'home' && } {view === 'editor' && } {view === 'settings' && }
{showOnboarding && } {toast &&
{toast}
}
) } export default function App(): React.JSX.Element { return ( ) }