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:
@@ -51,6 +51,7 @@ fun App() {
|
||||
var isCustomOrder by remember { mutableStateOf(false) }
|
||||
var statusMessages by remember { mutableStateOf<List<StatusMessage>>(emptyList()) }
|
||||
var isRunning by remember { mutableStateOf(false) }
|
||||
var isLoadingSongs by remember { mutableStateOf(false) }
|
||||
var lastBuildResult by remember { mutableStateOf<BuildResult?>(null) }
|
||||
val previewState = remember { PdfPreviewState() }
|
||||
|
||||
@@ -65,44 +66,58 @@ fun App() {
|
||||
isCustomOrder = false
|
||||
if (!projectDir.isDirectory) return
|
||||
|
||||
val configFile = File(projectDir, "songbook.yaml")
|
||||
var songsDir: File
|
||||
songsOrderConfig = "alphabetical"
|
||||
isLoadingSongs = true
|
||||
statusMessages = listOf(StatusMessage("Lieder werden geladen...", MessageType.INFO))
|
||||
|
||||
if (configFile.exists()) {
|
||||
try {
|
||||
val config = ConfigParser.parse(configFile)
|
||||
songsDir = File(projectDir, config.songs.directory)
|
||||
songsOrderConfig = config.songs.order
|
||||
} catch (_: Exception) {
|
||||
songsDir = File(projectDir, "songs")
|
||||
scope.launch {
|
||||
val (loadedSongs, order) = withContext(Dispatchers.IO) {
|
||||
val configFile = File(projectDir, "songbook.yaml")
|
||||
var songsDir: File
|
||||
var orderConfig = "alphabetical"
|
||||
|
||||
if (configFile.exists()) {
|
||||
try {
|
||||
val config = ConfigParser.parse(configFile)
|
||||
songsDir = File(projectDir, config.songs.directory)
|
||||
orderConfig = config.songs.order
|
||||
} catch (_: Exception) {
|
||||
songsDir = File(projectDir, "songs")
|
||||
}
|
||||
} else {
|
||||
songsDir = File(projectDir, "songs")
|
||||
}
|
||||
|
||||
if (!songsDir.isDirectory) return@withContext Pair(emptyList<SongEntry>(), orderConfig)
|
||||
|
||||
val songFiles = songsDir.listFiles { f: File -> f.extension in listOf("chopro", "cho", "crd") }
|
||||
?.sortedBy { it.name }
|
||||
?: emptyList()
|
||||
|
||||
val loaded = songFiles.mapNotNull { file ->
|
||||
try {
|
||||
val song = ChordProParser.parseFile(file)
|
||||
SongEntry(fileName = file.name, title = song.title.ifBlank { file.nameWithoutExtension })
|
||||
} catch (_: Exception) {
|
||||
SongEntry(fileName = file.name, title = "${file.nameWithoutExtension} (Fehler beim Lesen)")
|
||||
}
|
||||
}
|
||||
Pair(loaded, orderConfig)
|
||||
}
|
||||
} else {
|
||||
songsDir = File(projectDir, "songs")
|
||||
}
|
||||
|
||||
if (!songsDir.isDirectory) return
|
||||
|
||||
val songFiles = songsDir.listFiles { f: File -> f.extension in listOf("chopro", "cho", "crd") }
|
||||
?.sortedBy { it.name }
|
||||
?: emptyList()
|
||||
|
||||
val loadedSongs = songFiles.mapNotNull { file ->
|
||||
try {
|
||||
val song = ChordProParser.parseFile(file)
|
||||
SongEntry(fileName = file.name, title = song.title.ifBlank { file.nameWithoutExtension })
|
||||
} catch (_: Exception) {
|
||||
SongEntry(fileName = file.name, title = "${file.nameWithoutExtension} (Fehler beim Lesen)")
|
||||
songsOrderConfig = order
|
||||
songs = if (order == "alphabetical") {
|
||||
loadedSongs.sortedBy { it.title.lowercase() }
|
||||
} else {
|
||||
loadedSongs
|
||||
}
|
||||
originalSongs = songs.toList()
|
||||
isLoadingSongs = false
|
||||
statusMessages = if (loadedSongs.isNotEmpty()) {
|
||||
listOf(StatusMessage("${loadedSongs.size} Lieder geladen.", MessageType.SUCCESS))
|
||||
} else {
|
||||
listOf(StatusMessage("Keine Lieder gefunden.", MessageType.INFO))
|
||||
}
|
||||
}
|
||||
|
||||
// Apply config-based sort for display
|
||||
songs = if (songsOrderConfig == "alphabetical") {
|
||||
loadedSongs.sortedBy { it.title.lowercase() }
|
||||
} else {
|
||||
loadedSongs
|
||||
}
|
||||
originalSongs = songs.toList()
|
||||
}
|
||||
|
||||
MaterialTheme {
|
||||
@@ -185,7 +200,18 @@ fun App() {
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
if (songs.isEmpty() && projectPath.isNotBlank()) {
|
||||
if (isLoadingSongs) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(32.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Lieder werden geladen...", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
} else if (songs.isEmpty() && projectPath.isNotBlank()) {
|
||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
Text(
|
||||
"Keine Lieder gefunden. Bitte Projektverzeichnis pruefen.",
|
||||
@@ -233,7 +259,9 @@ fun App() {
|
||||
}
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
SongbookPipeline(File(projectPath)).build(customOrder)
|
||||
SongbookPipeline(File(projectPath)).build(customOrder) { msg ->
|
||||
statusMessages = listOf(StatusMessage(msg, MessageType.INFO))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
BuildResult(
|
||||
success = false,
|
||||
|
||||
Reference in New Issue
Block a user