feat: add drag-and-drop song reordering in the GUI (Closes #19)

Add a ReorderableSongList composable that supports long-press drag-and-drop
reordering with visual drop target highlighting. Drag-and-drop is only
enabled when songs.order is "manual"; alphabetical mode shows a hint.
The custom order is passed to SongbookPipeline via a new customSongOrder
parameter and a reset button restores the original order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-17 14:29:39 +01:00
parent 0f038a68d8
commit bed20d09ec
4 changed files with 292 additions and 57 deletions

View File

@@ -21,7 +21,15 @@ data class BuildResult(
class SongbookPipeline(private val projectDir: File) {
fun build(): BuildResult {
/**
* Build the songbook PDF.
*
* @param customSongOrder Optional list of song file names in the desired order.
* 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.
*/
fun build(customSongOrder: List<String>? = null): BuildResult {
// 1. Parse config
val configFile = File(projectDir, "songbook.yaml")
if (!configFile.exists()) {
@@ -55,7 +63,7 @@ class SongbookPipeline(private val projectDir: File) {
logger.info { "Found ${songFiles.size} song files" }
val songs = mutableListOf<Song>()
val songsByFileName = mutableMapOf<String, Song>()
val allErrors = mutableListOf<ValidationError>()
for (file in songFiles) {
@@ -65,7 +73,7 @@ class SongbookPipeline(private val projectDir: File) {
if (songErrors.isNotEmpty()) {
allErrors.addAll(songErrors)
} else {
songs.add(song)
songsByFileName[file.name] = song
}
} catch (e: Exception) {
allErrors.add(ValidationError(file.name, null, "Parse error: ${e.message}"))
@@ -76,10 +84,20 @@ class SongbookPipeline(private val projectDir: File) {
return BuildResult(false, errors = allErrors)
}
// Sort songs
val sortedSongs = when (config.songs.order) {
"alphabetical" -> songs.sortedBy { it.title.lowercase() }
else -> songs // manual order = file order
val songs = songsByFileName.values.toList()
// Sort songs: custom order takes priority, then config-based sort
val sortedSongs = if (customSongOrder != null) {
val orderMap = customSongOrder.withIndex().associate { (index, name) -> name to index }
songs.sortedBy { song ->
val fileName = songsByFileName.entries.find { it.value === song }?.key
orderMap[fileName] ?: Int.MAX_VALUE
}
} else {
when (config.songs.order) {
"alphabetical" -> songs.sortedBy { it.title.lowercase() }
else -> songs // manual order = file order
}
}
logger.info { "Parsed ${sortedSongs.size} songs" }