♿ separate chapter numbers and titles as export options
This commit is contained in:
@@ -370,6 +370,7 @@ export function registerIpcHandlers(): void {
|
||||
|
||||
ipcMain.handle('export:projectPdf', async (event, opts: {
|
||||
romanNumerals: boolean
|
||||
showChapterTitle: boolean
|
||||
includeCover: boolean
|
||||
includeFrontMatter: boolean
|
||||
pageFrom: number | null
|
||||
@@ -507,21 +508,35 @@ export function registerIpcHandlers(): void {
|
||||
})
|
||||
}
|
||||
|
||||
let chapterTitle: string
|
||||
const fileName = doc.relativePath.split('/').pop()?.replace(/\.md$/, '') ?? doc.relativePath
|
||||
let headerTitle: string
|
||||
if (!isFrontMatter && opts.romanNumerals) {
|
||||
let headingHtml: string
|
||||
if (!isFrontMatter) {
|
||||
chapterIndex++
|
||||
chapterTitle = toRoman(chapterIndex)
|
||||
headerTitle = toRoman(chapterIndex)
|
||||
const numeral = opts.romanNumerals ? toRoman(chapterIndex) : null
|
||||
const title = opts.showChapterTitle ? fileName : null
|
||||
const safe = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<')
|
||||
if (numeral && title) {
|
||||
headingHtml = `<h2>${safe(numeral)}</h2><h3>${safe(title)}</h3>`
|
||||
headerTitle = `${numeral} — ${title}`
|
||||
} else if (numeral) {
|
||||
headingHtml = `<h2>${safe(numeral)}</h2>`
|
||||
headerTitle = numeral
|
||||
} else if (title) {
|
||||
headingHtml = `<h2>${safe(title)}</h2>`
|
||||
headerTitle = title
|
||||
} else {
|
||||
headingHtml = ''
|
||||
headerTitle = fileName
|
||||
}
|
||||
} else {
|
||||
chapterTitle = doc.relativePath.split('/').pop()?.replace(/\.md$/, '') ?? doc.relativePath
|
||||
headerTitle = chapterTitle
|
||||
headingHtml = `<h2>${fileName.replace(/&/g, '&').replace(/</g, '<')}</h2>`
|
||||
headerTitle = fileName
|
||||
}
|
||||
const safeChapterTitle = chapterTitle.replace(/&/g, '&').replace(/</g, '<')
|
||||
const contentHtml = await marked(normalize(doc.content))
|
||||
sections.push({
|
||||
title: headerTitle,
|
||||
bodyHtml: `<div class="chapter"><h2>${safeChapterTitle}</h2>${contentHtml}</div>`
|
||||
bodyHtml: `<div class="chapter">${headingHtml}${contentHtml}</div>`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,34 @@ import './Export.css'
|
||||
|
||||
export interface ExportOptions {
|
||||
romanNumerals: boolean
|
||||
showChapterTitle: boolean
|
||||
includeCover: boolean
|
||||
includeFrontMatter: boolean
|
||||
pageFrom: number | null
|
||||
pageTo: number | null
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'exportDialogPrefs'
|
||||
|
||||
function loadPrefs(): Pick<ExportOptions, 'romanNumerals' | 'showChapterTitle' | 'includeCover' | 'includeFrontMatter'> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw) return JSON.parse(raw)
|
||||
} catch { /* ignore */ }
|
||||
return { romanNumerals: true, showChapterTitle: false, includeCover: true, includeFrontMatter: true }
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClose: () => void
|
||||
onExport: (opts: ExportOptions) => void
|
||||
}
|
||||
|
||||
export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
||||
const [romanNumerals, setRomanNumerals] = useState(true)
|
||||
const [includeCover, setIncludeCover] = useState(true)
|
||||
const [includeFrontMatter, setIncludeFrontMatter] = useState(true)
|
||||
const prefs = loadPrefs()
|
||||
const [romanNumerals, setRomanNumerals] = useState(prefs.romanNumerals)
|
||||
const [showChapterTitle, setShowChapterTitle] = useState(prefs.showChapterTitle)
|
||||
const [includeCover, setIncludeCover] = useState(prefs.includeCover)
|
||||
const [includeFrontMatter, setIncludeFrontMatter] = useState(prefs.includeFrontMatter)
|
||||
const [pageFrom, setPageFrom] = useState('')
|
||||
const [pageTo, setPageTo] = useState('')
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
@@ -34,10 +47,15 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
||||
if (e.target === overlayRef.current) onClose()
|
||||
}
|
||||
|
||||
const savePrefs = (patch: Partial<typeof prefs>): void => {
|
||||
const next = { romanNumerals, showChapterTitle, includeCover, includeFrontMatter, ...patch }
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
|
||||
}
|
||||
|
||||
const handleExport = (): void => {
|
||||
const from = pageFrom.trim() ? parseInt(pageFrom, 10) : null
|
||||
const to = pageTo.trim() ? parseInt(pageTo, 10) : null
|
||||
onExport({ romanNumerals, includeCover, includeFrontMatter, pageFrom: from, pageTo: to })
|
||||
onExport({ romanNumerals, showChapterTitle, includeCover, includeFrontMatter, pageFrom: from, pageTo: to })
|
||||
}
|
||||
|
||||
const pageRangeValid = (): boolean => {
|
||||
@@ -63,17 +81,27 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={romanNumerals}
|
||||
onChange={e => setRomanNumerals(e.target.checked)}
|
||||
onChange={e => { setRomanNumerals(e.target.checked); savePrefs({ romanNumerals: e.target.checked }) }}
|
||||
/>
|
||||
<span className="export-toggle-label">Roman numeral chapter titles</span>
|
||||
<span className="export-toggle-hint">I, II, III… instead of file names</span>
|
||||
<span className="export-toggle-label">Roman numeral chapter numbers</span>
|
||||
<span className="export-toggle-hint">I, II, III… before the chapter heading</span>
|
||||
</label>
|
||||
|
||||
<label className="export-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showChapterTitle}
|
||||
onChange={e => { setShowChapterTitle(e.target.checked); savePrefs({ showChapterTitle: e.target.checked }) }}
|
||||
/>
|
||||
<span className="export-toggle-label">Show chapter title</span>
|
||||
<span className="export-toggle-hint">File name as the chapter heading</span>
|
||||
</label>
|
||||
|
||||
<label className="export-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeCover}
|
||||
onChange={e => setIncludeCover(e.target.checked)}
|
||||
onChange={e => { setIncludeCover(e.target.checked); savePrefs({ includeCover: e.target.checked }) }}
|
||||
/>
|
||||
<span className="export-toggle-label">Include cover page</span>
|
||||
<span className="export-toggle-hint">Author contact block and word count</span>
|
||||
@@ -83,7 +111,7 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeFrontMatter}
|
||||
onChange={e => setIncludeFrontMatter(e.target.checked)}
|
||||
onChange={e => { setIncludeFrontMatter(e.target.checked); savePrefs({ includeFrontMatter: e.target.checked }) }}
|
||||
/>
|
||||
<span className="export-toggle-label">Include front & back matter</span>
|
||||
<span className="export-toggle-hint">Prologue, Content Warning, Epilogue</span>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user