Files
bilin/src/renderer/components/export/ExportModal.tsx
T
bing adf877861d feat: ship v1.1.0 writer toolkit with templates, import, export, and inspiration capture
Implement chapter templates, txt/md/docx import, submission export presets, and global inspiration shortcut. Split import handlers into a separate main bundle and fix EditorLayout setTarget loop that broke E2E.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 13:53:22 +08:00

135 lines
4.7 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { mergeSubmissionPresets } from '@shared/builtin-templates'
import { useBookStore } from '@renderer/stores/useBookStore'
import { useEditStore } from '@renderer/stores/useEditStore'
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
import { useAppStore } from '@renderer/stores/useAppStore'
import { useExportStore } from '@renderer/stores/useExportStore'
import { ipcCall } from '@renderer/lib/ipc-client'
export function ExportModal(): React.JSX.Element | null {
const { t } = useTranslation()
const open = useExportStore((s) => s.open)
const close = useExportStore((s) => s.close)
const showToast = useAppStore((s) => s.showToast)
const bookId = useBookStore((s) => s.currentBookId)
const chapters = useBookStore((s) => s.chapters)
const target = useEditStore((s) => s.target)
const customPresets = useSettingsStore((s) => s.submissionPresets)
const defaultPresetId = useSettingsStore((s) => s.defaultSubmissionPresetId ?? 'builtin-webnovel')
const presets = useMemo(() => mergeSubmissionPresets(customPresets ?? []), [customPresets])
const chapterId = target?.kind === 'chapter' ? target.id : null
const chapter = chapterId ? chapters.find((c) => c.id === chapterId) : null
const [presetId, setPresetId] = useState(defaultPresetId)
const [preview, setPreview] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!open) return
setPresetId(defaultPresetId)
}, [open, defaultPresetId])
useEffect(() => {
if (!open || !bookId || !chapterId) {
setPreview('')
return
}
setLoading(true)
void ipcCall(() => window.electronAPI.export.formatChapter(bookId, chapterId, presetId))
.then((text) => setPreview(text.slice(0, 500)))
.catch(() => setPreview(''))
.finally(() => setLoading(false))
}, [open, bookId, chapterId, presetId])
const handleCopy = async (): Promise<void> => {
if (!bookId || !chapterId) return
const text = await ipcCall(() =>
window.electronAPI.export.formatChapter(bookId, chapterId, presetId)
)
await ipcCall(() => window.electronAPI.export.copyClipboard(text))
showToast(t('export.copied'))
}
const handleSaveTxt = async (): Promise<void> => {
if (!bookId || !chapterId || !chapter) return
const text = await ipcCall(() =>
window.electronAPI.export.formatChapter(bookId, chapterId, presetId)
)
const defaultName = `${chapter.title}.txt`
const saved = await ipcCall(() => window.electronAPI.export.saveTxt(text, defaultName))
if (saved) {
showToast(t('export.saved', { path: saved }))
}
}
if (!open) return null
return (
<div
className="modal-overlay"
id="exportModal"
data-testid="export-modal"
role="dialog"
aria-modal="true"
aria-label={t('export.title')}
>
<div className="modal-content">
<div className="modal-header">
<h3>
{t('export.title')}
{chapter ? `${chapter.title}` : ''}
</h3>
<button type="button" className="modal-close" onClick={close}>
</button>
</div>
<div className="modal-body">
<label className="form-label">{t('export.submissionPresets')}</label>
<select
className="form-control form-control--wide"
data-testid="export-preset-select"
value={presetId}
onChange={(e) => setPresetId(e.target.value)}
>
{presets.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<label className="form-label">{t('export.preview')}</label>
<pre className="export-preview" data-testid="export-preview">
{loading ? t('export.loading') : preview || t('export.noPreview')}
</pre>
</div>
<div className="modal-footer">
<button type="button" className="btn" onClick={close}>
{t('dialog.cancel')}
</button>
<button
type="button"
className="btn"
data-testid="export-save-txt"
disabled={!chapterId}
onClick={() => void handleSaveTxt()}
>
{t('export.saveTxt')}
</button>
<button
type="button"
className="btn btn-primary"
data-testid="export-copy-clipboard"
disabled={!chapterId}
onClick={() => void handleCopy()}
>
{t('export.copyClipboard')}
</button>
</div>
</div>
</div>
)
}