This commit was merged in pull request #22.
This commit is contained in:
41
.plans/issue-19-drag-and-drop.md
Normal file
41
.plans/issue-19-drag-and-drop.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Issue #19: Add drag-and-drop song reordering in the GUI
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add drag-and-drop reordering of songs in the GUI song list. When `songs.order` is "manual", users can drag songs to rearrange them. The custom order is passed to the pipeline at build time. When order is "alphabetical", drag-and-drop is disabled with a hint.
|
||||||
|
|
||||||
|
## AC Verification Checklist
|
||||||
|
|
||||||
|
1. Songs can be reordered via drag-and-drop
|
||||||
|
2. Reordered list is used when building (overrides config order)
|
||||||
|
3. Visual indicator shows drop target (highlight)
|
||||||
|
4. Order can be reset via a button
|
||||||
|
5. Reordering only enabled when songs.order is "manual"
|
||||||
|
6. When alphabetical, list shows alphabetical order and drag is disabled (with hint)
|
||||||
|
7. GUI remains responsive during drag operations
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Step 1: Add customSongOrder parameter to SongbookPipeline.build()
|
||||||
|
|
||||||
|
Add an optional `customSongOrder: List<String>? = null` parameter. When provided, use this ordered list of file names to sort the parsed songs instead of the config-based sort.
|
||||||
|
|
||||||
|
### Step 2: Create ReorderableSongList composable
|
||||||
|
|
||||||
|
Build a song list that supports drag-and-drop reordering:
|
||||||
|
- Use `detectDragGesturesAfterLongPress` on each item to detect drag start
|
||||||
|
- Track the dragged item index and current hover position
|
||||||
|
- Show a visual indicator (highlighted background) at the drop target
|
||||||
|
- On drop, reorder the list
|
||||||
|
|
||||||
|
### Step 3: Integrate into App.kt
|
||||||
|
|
||||||
|
- Track `songsOrder` config value ("alphabetical" or "manual")
|
||||||
|
- Track `originalSongs` list (from file loading) to support reset
|
||||||
|
- When manual: enable drag-and-drop, show reset button
|
||||||
|
- When alphabetical: disable drag-and-drop, show hint
|
||||||
|
- Pass custom order (file names) to pipeline on build
|
||||||
|
|
||||||
|
### Step 4: Add reset button
|
||||||
|
|
||||||
|
"Reihenfolge zurücksetzen" button restores `songs` to `originalSongs`.
|
||||||
@@ -21,7 +21,15 @@ data class BuildResult(
|
|||||||
|
|
||||||
class SongbookPipeline(private val projectDir: File) {
|
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
|
// 1. Parse config
|
||||||
val configFile = File(projectDir, "songbook.yaml")
|
val configFile = File(projectDir, "songbook.yaml")
|
||||||
if (!configFile.exists()) {
|
if (!configFile.exists()) {
|
||||||
@@ -55,7 +63,7 @@ class SongbookPipeline(private val projectDir: File) {
|
|||||||
|
|
||||||
logger.info { "Found ${songFiles.size} song files" }
|
logger.info { "Found ${songFiles.size} song files" }
|
||||||
|
|
||||||
val songs = mutableListOf<Song>()
|
val songsByFileName = mutableMapOf<String, Song>()
|
||||||
val allErrors = mutableListOf<ValidationError>()
|
val allErrors = mutableListOf<ValidationError>()
|
||||||
|
|
||||||
for (file in songFiles) {
|
for (file in songFiles) {
|
||||||
@@ -65,7 +73,7 @@ class SongbookPipeline(private val projectDir: File) {
|
|||||||
if (songErrors.isNotEmpty()) {
|
if (songErrors.isNotEmpty()) {
|
||||||
allErrors.addAll(songErrors)
|
allErrors.addAll(songErrors)
|
||||||
} else {
|
} else {
|
||||||
songs.add(song)
|
songsByFileName[file.name] = song
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
allErrors.add(ValidationError(file.name, null, "Parse error: ${e.message}"))
|
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)
|
return BuildResult(false, errors = allErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort songs
|
val songs = songsByFileName.values.toList()
|
||||||
val sortedSongs = when (config.songs.order) {
|
|
||||||
"alphabetical" -> songs.sortedBy { it.title.lowercase() }
|
// Sort songs: custom order takes priority, then config-based sort
|
||||||
else -> songs // manual order = file order
|
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" }
|
logger.info { "Parsed ${sortedSongs.size} songs" }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package de.pfadfinder.songbook.gui
|
package de.pfadfinder.songbook.gui
|
||||||
|
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.VerticalScrollbar
|
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.foundation.VerticalScrollbar
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.window.application
|
|||||||
import de.pfadfinder.songbook.app.BuildResult
|
import de.pfadfinder.songbook.app.BuildResult
|
||||||
import de.pfadfinder.songbook.app.SongbookPipeline
|
import de.pfadfinder.songbook.app.SongbookPipeline
|
||||||
import de.pfadfinder.songbook.parser.ChordProParser
|
import de.pfadfinder.songbook.parser.ChordProParser
|
||||||
|
import de.pfadfinder.songbook.parser.ConfigParser
|
||||||
import de.pfadfinder.songbook.parser.ValidationError
|
import de.pfadfinder.songbook.parser.ValidationError
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -45,6 +46,9 @@ data class SongEntry(val fileName: String, val title: String)
|
|||||||
fun App() {
|
fun App() {
|
||||||
var projectPath by remember { mutableStateOf("") }
|
var projectPath by remember { mutableStateOf("") }
|
||||||
var songs by remember { mutableStateOf<List<SongEntry>>(emptyList()) }
|
var songs by remember { mutableStateOf<List<SongEntry>>(emptyList()) }
|
||||||
|
var originalSongs by remember { mutableStateOf<List<SongEntry>>(emptyList()) }
|
||||||
|
var songsOrderConfig by remember { mutableStateOf("alphabetical") }
|
||||||
|
var isCustomOrder by remember { mutableStateOf(false) }
|
||||||
var statusMessages by remember { mutableStateOf<List<StatusMessage>>(emptyList()) }
|
var statusMessages by remember { mutableStateOf<List<StatusMessage>>(emptyList()) }
|
||||||
var isRunning by remember { mutableStateOf(false) }
|
var isRunning by remember { mutableStateOf(false) }
|
||||||
var lastBuildResult by remember { mutableStateOf<BuildResult?>(null) }
|
var lastBuildResult by remember { mutableStateOf<BuildResult?>(null) }
|
||||||
@@ -52,21 +56,29 @@ fun App() {
|
|||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val reorderEnabled = songsOrderConfig != "alphabetical"
|
||||||
|
|
||||||
fun loadSongs(path: String) {
|
fun loadSongs(path: String) {
|
||||||
val projectDir = File(path)
|
val projectDir = File(path)
|
||||||
songs = emptyList()
|
songs = emptyList()
|
||||||
|
originalSongs = emptyList()
|
||||||
|
isCustomOrder = false
|
||||||
if (!projectDir.isDirectory) return
|
if (!projectDir.isDirectory) return
|
||||||
|
|
||||||
val configFile = File(projectDir, "songbook.yaml")
|
val configFile = File(projectDir, "songbook.yaml")
|
||||||
val songsDir = if (configFile.exists()) {
|
var songsDir: File
|
||||||
|
songsOrderConfig = "alphabetical"
|
||||||
|
|
||||||
|
if (configFile.exists()) {
|
||||||
try {
|
try {
|
||||||
val config = de.pfadfinder.songbook.parser.ConfigParser.parse(configFile)
|
val config = ConfigParser.parse(configFile)
|
||||||
File(projectDir, config.songs.directory)
|
songsDir = File(projectDir, config.songs.directory)
|
||||||
|
songsOrderConfig = config.songs.order
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
File(projectDir, "songs")
|
songsDir = File(projectDir, "songs")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
File(projectDir, "songs")
|
songsDir = File(projectDir, "songs")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!songsDir.isDirectory) return
|
if (!songsDir.isDirectory) return
|
||||||
@@ -75,7 +87,7 @@ fun App() {
|
|||||||
?.sortedBy { it.name }
|
?.sortedBy { it.name }
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
|
||||||
songs = songFiles.mapNotNull { file ->
|
val loadedSongs = songFiles.mapNotNull { file ->
|
||||||
try {
|
try {
|
||||||
val song = ChordProParser.parseFile(file)
|
val song = ChordProParser.parseFile(file)
|
||||||
SongEntry(fileName = file.name, title = song.title.ifBlank { file.nameWithoutExtension })
|
SongEntry(fileName = file.name, title = song.title.ifBlank { file.nameWithoutExtension })
|
||||||
@@ -83,6 +95,14 @@ fun App() {
|
|||||||
SongEntry(fileName = file.name, title = "${file.nameWithoutExtension} (Fehler beim Lesen)")
|
SongEntry(fileName = file.name, title = "${file.nameWithoutExtension} (Fehler beim Lesen)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply config-based sort for display
|
||||||
|
songs = if (songsOrderConfig == "alphabetical") {
|
||||||
|
loadedSongs.sortedBy { it.title.lowercase() }
|
||||||
|
} else {
|
||||||
|
loadedSongs
|
||||||
|
}
|
||||||
|
originalSongs = songs.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
@@ -141,47 +161,55 @@ fun App() {
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Song list
|
// Song list header with optional reset button
|
||||||
Text(
|
Row(
|
||||||
text = "Lieder (${songs.size}):",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontWeight = FontWeight.Medium
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Text(
|
||||||
|
text = "Lieder (${songs.size}):",
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
fontWeight = FontWeight.Medium,
|
||||||
val listState = rememberLazyListState()
|
modifier = Modifier.weight(1f)
|
||||||
LazyColumn(
|
)
|
||||||
state = listState,
|
if (reorderEnabled && isCustomOrder) {
|
||||||
modifier = Modifier.fillMaxSize().padding(end = 12.dp)
|
Button(
|
||||||
) {
|
onClick = {
|
||||||
if (songs.isEmpty() && projectPath.isNotBlank()) {
|
songs = originalSongs.toList()
|
||||||
item {
|
isCustomOrder = false
|
||||||
Text(
|
},
|
||||||
"Keine Lieder gefunden. Bitte Projektverzeichnis prüfen.",
|
enabled = !isRunning
|
||||||
color = Color.Gray,
|
) {
|
||||||
modifier = Modifier.padding(8.dp)
|
Text("Reihenfolge zuruecksetzen")
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (projectPath.isBlank()) {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
"Bitte ein Projektverzeichnis auswählen.",
|
|
||||||
color = Color.Gray,
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(songs) { song ->
|
|
||||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp, horizontal = 8.dp)) {
|
|
||||||
Text(song.title, modifier = Modifier.weight(1f))
|
|
||||||
Text(song.fileName, color = Color.Gray, fontSize = 12.sp)
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VerticalScrollbar(
|
}
|
||||||
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
adapter = rememberScrollbarAdapter(listState)
|
|
||||||
|
if (songs.isEmpty() && projectPath.isNotBlank()) {
|
||||||
|
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
"Keine Lieder gefunden. Bitte Projektverzeichnis pruefen.",
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (projectPath.isBlank()) {
|
||||||
|
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
"Bitte ein Projektverzeichnis auswaehlen.",
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReorderableSongList(
|
||||||
|
songs = songs,
|
||||||
|
reorderEnabled = reorderEnabled,
|
||||||
|
onReorder = { newList ->
|
||||||
|
songs = newList
|
||||||
|
isCustomOrder = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,9 +225,15 @@ fun App() {
|
|||||||
lastBuildResult = null
|
lastBuildResult = null
|
||||||
statusMessages = listOf(StatusMessage("Buch wird erstellt...", MessageType.INFO))
|
statusMessages = listOf(StatusMessage("Buch wird erstellt...", MessageType.INFO))
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
// Build custom song order from the current GUI list
|
||||||
|
val customOrder = if (isCustomOrder) {
|
||||||
|
songs.map { it.fileName }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
SongbookPipeline(File(projectPath)).build()
|
SongbookPipeline(File(projectPath)).build(customOrder)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
BuildResult(
|
BuildResult(
|
||||||
success = false,
|
success = false,
|
||||||
@@ -249,7 +283,7 @@ fun App() {
|
|||||||
if (projectPath.isBlank()) return@Button
|
if (projectPath.isBlank()) return@Button
|
||||||
isRunning = true
|
isRunning = true
|
||||||
lastBuildResult = null
|
lastBuildResult = null
|
||||||
statusMessages = listOf(StatusMessage("Validierung läuft...", MessageType.INFO))
|
statusMessages = listOf(StatusMessage("Validierung laeuft...", MessageType.INFO))
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val errors = withContext(Dispatchers.IO) {
|
val errors = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -288,7 +322,7 @@ fun App() {
|
|||||||
Desktop.getDesktop().open(file)
|
Desktop.getDesktop().open(file)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
statusMessages = statusMessages + StatusMessage(
|
statusMessages = statusMessages + StatusMessage(
|
||||||
"PDF konnte nicht geöffnet werden: ${e.message}",
|
"PDF konnte nicht geoeffnet werden: ${e.message}",
|
||||||
MessageType.ERROR
|
MessageType.ERROR
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -296,7 +330,7 @@ fun App() {
|
|||||||
},
|
},
|
||||||
enabled = !isRunning
|
enabled = !isRunning
|
||||||
) {
|
) {
|
||||||
Text("PDF öffnen")
|
Text("PDF oeffnen")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide preview button
|
// Show/hide preview button
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package de.pfadfinder.songbook.gui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.VerticalScrollbar
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.rememberScrollbarAdapter
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A song list that supports drag-and-drop reordering when enabled.
|
||||||
|
*
|
||||||
|
* @param songs The current song list
|
||||||
|
* @param reorderEnabled Whether drag-and-drop is enabled
|
||||||
|
* @param onReorder Callback when songs are reordered, provides the new list
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ReorderableSongList(
|
||||||
|
songs: List<SongEntry>,
|
||||||
|
reorderEnabled: Boolean,
|
||||||
|
onReorder: (List<SongEntry>) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
// Track drag state
|
||||||
|
var draggedIndex by remember { mutableStateOf(-1) }
|
||||||
|
var hoverIndex by remember { mutableStateOf(-1) }
|
||||||
|
var dragOffset by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
// Approximate item height for calculating target index from drag offset
|
||||||
|
val itemHeightPx = 36f // approximate height of each row in pixels
|
||||||
|
|
||||||
|
Box(modifier = modifier.fillMaxWidth()) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(end = 12.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(songs, key = { _, song -> song.fileName }) { index, song ->
|
||||||
|
val isDragTarget = hoverIndex == index && draggedIndex != -1 && draggedIndex != index
|
||||||
|
val isBeingDragged = draggedIndex == index
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (isDragTarget) {
|
||||||
|
Modifier.background(Color(0xFFBBDEFB)) // light blue drop indicator
|
||||||
|
} else if (isBeingDragged) {
|
||||||
|
Modifier.background(Color(0xFFE0E0E0)) // grey for dragged item
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(vertical = 2.dp, horizontal = 8.dp)
|
||||||
|
.then(
|
||||||
|
if (reorderEnabled) {
|
||||||
|
Modifier.pointerInput(songs) {
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDragStart = {
|
||||||
|
draggedIndex = index
|
||||||
|
hoverIndex = index
|
||||||
|
dragOffset = 0f
|
||||||
|
},
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
dragOffset += dragAmount.y
|
||||||
|
// Calculate target index based on cumulative drag offset
|
||||||
|
val indexDelta = (dragOffset / itemHeightPx).toInt()
|
||||||
|
val newHover = (draggedIndex + indexDelta).coerceIn(0, songs.size - 1)
|
||||||
|
hoverIndex = newHover
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
if (draggedIndex != -1 && hoverIndex != -1 && draggedIndex != hoverIndex) {
|
||||||
|
val mutable = songs.toMutableList()
|
||||||
|
val item = mutable.removeAt(draggedIndex)
|
||||||
|
mutable.add(hoverIndex, item)
|
||||||
|
onReorder(mutable)
|
||||||
|
}
|
||||||
|
draggedIndex = -1
|
||||||
|
hoverIndex = -1
|
||||||
|
dragOffset = 0f
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
draggedIndex = -1
|
||||||
|
hoverIndex = -1
|
||||||
|
dragOffset = 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (reorderEnabled) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Menu,
|
||||||
|
contentDescription = "Ziehen zum Verschieben",
|
||||||
|
modifier = Modifier.size(16.dp).padding(end = 4.dp),
|
||||||
|
tint = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(song.title, modifier = Modifier.weight(1f))
|
||||||
|
Text(song.fileName, color = Color.Gray, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reorderEnabled && songs.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Reihenfolge ist alphabetisch. Wechsle in songbook.yaml zu songs.order: \"manual\" um die Reihenfolge zu aendern.",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VerticalScrollbar(
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||||
|
adapter = rememberScrollbarAdapter(listState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user