import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import type { TimelineConflict, TimelineEntry } from '@shared/timeline' import { useAppStore } from '@renderer/stores/useAppStore' import { useBookStore } from '@renderer/stores/useBookStore' import { useEditStore } from '@renderer/stores/useEditStore' import { ipcCall } from '@renderer/lib/ipc-client' export function TimelineModal(): React.JSX.Element | null { const { t } = useTranslation() const open = useAppStore((s) => s.timelineModalOpen) const setOpen = useAppStore((s) => s.setTimelineModalOpen) const bookId = useBookStore((s) => s.currentBookId) const settings = useBookStore((s) => s.settings) const switchTarget = useEditStore((s) => s.switchTarget) const [entries, setEntries] = useState([]) const [conflicts, setConflicts] = useState([]) const [loading, setLoading] = useState(false) const conflictChapterIds = useMemo( () => new Set(conflicts.map((c) => c.chapterId)), [conflicts] ) useEffect(() => { if (!open || !bookId) return setLoading(true) void Promise.all([ ipcCall(() => window.electronAPI.timeline.list(bookId)), ipcCall(() => window.electronAPI.timeline.conflicts(bookId)) ]) .then(([list, conf]) => { setEntries(list) setConflicts(conf) }) .finally(() => setLoading(false)) }, [open, bookId]) const handleJump = async (entry: TimelineEntry): Promise => { if (!entry.chapterId) return await switchTarget({ kind: 'chapter', id: entry.chapterId }) setOpen(false) } const handleAddEvent = async (): Promise => { if (!bookId) return const title = window.prompt(t('timeline.eventTitlePrompt')) if (!title?.trim()) return const storyTime = window.prompt(t('timeline.storyTimePrompt')) ?? '' await ipcCall(() => window.electronAPI.timeline.upsert(bookId, { title: title.trim(), storyTime: storyTime.trim() || null }) ) const list = await ipcCall(() => window.electronAPI.timeline.list(bookId)) setEntries(list) } if (!open) return null return (

{t('timeline.title')}

{loading ? (

{t('cockpit.loading')}

) : (
{entries.map((entry) => { const conflict = conflicts.find((c) => c.chapterId === entry.chapterId) const isConflict = entry.chapterId && conflictChapterIds.has(entry.chapterId) return (
{entry.title} {entry.storyTime ? entry.storyTime : t('timeline.noTime')} {entry.kind === 'manual' ? ` · ${t('timeline.manual')}` : ''}
{conflict && (
{conflict.message}
)} {entry.chapterId && ( )}
) })} {entries.length === 0 &&

{t('timeline.empty')}

}
)} {settings.filter((s) => s.type === 'character').length > 0 && (

{t('timeline.characterHint')}

)}
) }