♿ 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: {
|
ipcMain.handle('export:projectPdf', async (event, opts: {
|
||||||
romanNumerals: boolean
|
romanNumerals: boolean
|
||||||
|
showChapterTitle: boolean
|
||||||
includeCover: boolean
|
includeCover: boolean
|
||||||
includeFrontMatter: boolean
|
includeFrontMatter: boolean
|
||||||
pageFrom: number | null
|
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
|
let headerTitle: string
|
||||||
if (!isFrontMatter && opts.romanNumerals) {
|
let headingHtml: string
|
||||||
|
if (!isFrontMatter) {
|
||||||
chapterIndex++
|
chapterIndex++
|
||||||
chapterTitle = toRoman(chapterIndex)
|
const numeral = opts.romanNumerals ? toRoman(chapterIndex) : null
|
||||||
headerTitle = toRoman(chapterIndex)
|
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 {
|
} else {
|
||||||
chapterTitle = doc.relativePath.split('/').pop()?.replace(/\.md$/, '') ?? doc.relativePath
|
headingHtml = `<h2>${fileName.replace(/&/g, '&').replace(/</g, '<')}</h2>`
|
||||||
headerTitle = chapterTitle
|
headerTitle = fileName
|
||||||
}
|
}
|
||||||
const safeChapterTitle = chapterTitle.replace(/&/g, '&').replace(/</g, '<')
|
|
||||||
const contentHtml = await marked(normalize(doc.content))
|
const contentHtml = await marked(normalize(doc.content))
|
||||||
sections.push({
|
sections.push({
|
||||||
title: headerTitle,
|
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 {
|
export interface ExportOptions {
|
||||||
romanNumerals: boolean
|
romanNumerals: boolean
|
||||||
|
showChapterTitle: boolean
|
||||||
includeCover: boolean
|
includeCover: boolean
|
||||||
includeFrontMatter: boolean
|
includeFrontMatter: boolean
|
||||||
pageFrom: number | null
|
pageFrom: number | null
|
||||||
pageTo: 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 {
|
interface Props {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onExport: (opts: ExportOptions) => void
|
onExport: (opts: ExportOptions) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
||||||
const [romanNumerals, setRomanNumerals] = useState(true)
|
const prefs = loadPrefs()
|
||||||
const [includeCover, setIncludeCover] = useState(true)
|
const [romanNumerals, setRomanNumerals] = useState(prefs.romanNumerals)
|
||||||
const [includeFrontMatter, setIncludeFrontMatter] = useState(true)
|
const [showChapterTitle, setShowChapterTitle] = useState(prefs.showChapterTitle)
|
||||||
|
const [includeCover, setIncludeCover] = useState(prefs.includeCover)
|
||||||
|
const [includeFrontMatter, setIncludeFrontMatter] = useState(prefs.includeFrontMatter)
|
||||||
const [pageFrom, setPageFrom] = useState('')
|
const [pageFrom, setPageFrom] = useState('')
|
||||||
const [pageTo, setPageTo] = useState('')
|
const [pageTo, setPageTo] = useState('')
|
||||||
const overlayRef = useRef<HTMLDivElement>(null)
|
const overlayRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -34,10 +47,15 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
|||||||
if (e.target === overlayRef.current) onClose()
|
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 handleExport = (): void => {
|
||||||
const from = pageFrom.trim() ? parseInt(pageFrom, 10) : null
|
const from = pageFrom.trim() ? parseInt(pageFrom, 10) : null
|
||||||
const to = pageTo.trim() ? parseInt(pageTo, 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 => {
|
const pageRangeValid = (): boolean => {
|
||||||
@@ -63,17 +81,27 @@ export function ExportDialog({ onClose, onExport }: Props): JSX.Element {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={romanNumerals}
|
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-label">Roman numeral chapter numbers</span>
|
||||||
<span className="export-toggle-hint">I, II, III… instead of file names</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>
|
||||||
|
|
||||||
<label className="export-toggle">
|
<label className="export-toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={includeCover}
|
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-label">Include cover page</span>
|
||||||
<span className="export-toggle-hint">Author contact block and word count</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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={includeFrontMatter}
|
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-label">Include front & back matter</span>
|
||||||
<span className="export-toggle-hint">Prologue, Content Warning, Epilogue</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