feat: add intro page, rotated TOC headers, progress indicator, fix TOC padding (Closes #33)
- Add intro/title page before TOC (configurable via intro.enabled in songbook.yaml) - Rotate reference book column headers 90° in TOC to prevent text truncation - Add progress callback to SongbookPipeline.build() with CLI and GUI integration - Make GUI song loading async with spinner indicator - Fix TOC page padding: use actual rendered page count instead of blindly adding tocPages-1 blank pages - Pre-render TOC to measure actual pages needed (TocRenderer.measurePages) - Account for intro pages in pagination offset and page numbering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import de.pfadfinder.songbook.model.*
|
||||
import de.pfadfinder.songbook.parser.*
|
||||
import de.pfadfinder.songbook.parser.ForewordParser
|
||||
import de.pfadfinder.songbook.layout.*
|
||||
import de.pfadfinder.songbook.renderer.pdf.*
|
||||
import de.pfadfinder.songbook.renderer.pdf.PdfBookRenderer
|
||||
import de.pfadfinder.songbook.renderer.pdf.PdfFontMetrics
|
||||
import de.pfadfinder.songbook.renderer.pdf.TocRenderer
|
||||
import mu.KotlinLogging
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -28,9 +30,11 @@ class SongbookPipeline(private val projectDir: File) {
|
||||
* When provided, songs are sorted to match this order instead of using the
|
||||
* config-based sort (alphabetical or manual). Files not in this list are
|
||||
* appended at the end.
|
||||
* @param onProgress Optional callback invoked with status messages during the build.
|
||||
*/
|
||||
fun build(customSongOrder: List<String>? = null): BuildResult {
|
||||
fun build(customSongOrder: List<String>? = null, onProgress: ((String) -> Unit)? = null): BuildResult {
|
||||
// 1. Parse config
|
||||
onProgress?.invoke("Konfiguration wird geladen...")
|
||||
val configFile = File(projectDir, "songbook.yaml")
|
||||
if (!configFile.exists()) {
|
||||
return BuildResult(false, errors = listOf(ValidationError(configFile.name, null, "songbook.yaml not found")))
|
||||
@@ -61,12 +65,16 @@ class SongbookPipeline(private val projectDir: File) {
|
||||
return BuildResult(false, errors = listOf(ValidationError(config.songs.directory, null, "No song files found")))
|
||||
}
|
||||
|
||||
onProgress?.invoke("Lieder werden importiert (${songFiles.size} Dateien)...")
|
||||
logger.info { "Found ${songFiles.size} song files" }
|
||||
|
||||
val songsByFileName = mutableMapOf<String, Song>()
|
||||
val allErrors = mutableListOf<ValidationError>()
|
||||
|
||||
for (file in songFiles) {
|
||||
for ((index, file) in songFiles.withIndex()) {
|
||||
if (index > 0 && index % 50 == 0) {
|
||||
onProgress?.invoke("Lieder werden importiert... ($index/${songFiles.size})")
|
||||
}
|
||||
try {
|
||||
val song = ChordProParser.parseFile(file)
|
||||
val songErrors = Validator.validateSong(song, file.name)
|
||||
@@ -116,21 +124,39 @@ class SongbookPipeline(private val projectDir: File) {
|
||||
}
|
||||
|
||||
// 3. Measure songs
|
||||
onProgress?.invoke("Layout wird berechnet...")
|
||||
val fontMetrics = PdfFontMetrics()
|
||||
val measurementEngine = MeasurementEngine(fontMetrics, config)
|
||||
val measuredSongs = sortedSongs.map { measurementEngine.measure(it) }
|
||||
|
||||
// 4. Generate TOC and paginate
|
||||
val tocGenerator = TocGenerator(config)
|
||||
val tocPages = tocGenerator.estimateTocPages(sortedSongs)
|
||||
val estimatedTocPages = tocGenerator.estimateTocPages(sortedSongs)
|
||||
|
||||
// Intro page takes 2 pages (title + blank back) for double-sided printing
|
||||
val introPages = if (config.intro?.enabled == true) 2 else 0
|
||||
// Foreword always takes 2 pages (for double-sided printing)
|
||||
val forewordPages = if (foreword != null) 2 else 0
|
||||
|
||||
val headerPages = introPages + estimatedTocPages + forewordPages
|
||||
val paginationEngine = PaginationEngine(config)
|
||||
val pages = paginationEngine.paginate(measuredSongs, tocPages + forewordPages)
|
||||
val pages = paginationEngine.paginate(measuredSongs, headerPages)
|
||||
|
||||
val tocEntries = tocGenerator.generate(pages, tocPages + forewordPages)
|
||||
// Generate initial TOC entries, then measure actual pages needed
|
||||
val initialTocEntries = tocGenerator.generate(pages, headerPages)
|
||||
val tocRenderer = TocRenderer(fontMetrics, config)
|
||||
val tocPages = tocRenderer.measurePages(initialTocEntries)
|
||||
|
||||
// Re-generate TOC entries with corrected page offset if count changed.
|
||||
// Since tocPages is always even, the pagination layout (left/right parity)
|
||||
// stays the same — only page numbers in the TOC entries need updating.
|
||||
val actualHeaderPages = introPages + tocPages + forewordPages
|
||||
val tocEntries = if (tocPages != estimatedTocPages) {
|
||||
logger.info { "TOC pages: estimated $estimatedTocPages, actual $tocPages" }
|
||||
tocGenerator.generate(pages, actualHeaderPages)
|
||||
} else {
|
||||
initialTocEntries
|
||||
}
|
||||
|
||||
// Build final page list with foreword pages inserted before song content
|
||||
val allPages = mutableListOf<PageContent>()
|
||||
@@ -141,14 +167,17 @@ class SongbookPipeline(private val projectDir: File) {
|
||||
allPages.addAll(pages)
|
||||
|
||||
val layoutResult = LayoutResult(
|
||||
introPages = introPages,
|
||||
tocPages = tocPages,
|
||||
pages = allPages,
|
||||
tocEntries = tocEntries
|
||||
)
|
||||
|
||||
logger.info { "Layout: ${tocPages} TOC pages, ${pages.size} content pages" }
|
||||
val totalPages = introPages + tocPages + pages.size
|
||||
logger.info { "Layout: ${introPages} intro, ${tocPages} TOC, ${pages.size} content pages" }
|
||||
|
||||
// 5. Render PDF
|
||||
onProgress?.invoke("PDF wird erzeugt (${sortedSongs.size} Lieder, $totalPages Seiten)...")
|
||||
val outputDir = File(projectDir, config.output.directory)
|
||||
outputDir.mkdirs()
|
||||
val outputFile = File(outputDir, config.output.filename)
|
||||
@@ -160,13 +189,13 @@ class SongbookPipeline(private val projectDir: File) {
|
||||
renderer.render(layoutResult, config, fos)
|
||||
}
|
||||
|
||||
logger.info { "Build complete: ${sortedSongs.size} songs, ${pages.size + tocPages} pages" }
|
||||
logger.info { "Build complete: ${sortedSongs.size} songs, $totalPages pages" }
|
||||
|
||||
return BuildResult(
|
||||
success = true,
|
||||
outputFile = outputFile,
|
||||
songCount = sortedSongs.size,
|
||||
pageCount = pages.size + tocPages
|
||||
pageCount = totalPages
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user