feat(web+android): Warteschlange bearbeiten — ziehen und entfernen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
77a3bb33f0
commit
d5b5c8cf84
9 files changed
+330
-7
No files matched your search
@@ -204,6 +204,40 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
|
||||
ctrl.addMediaItem(song.toMediaItem(repo))
|
||||
}
|
||||
|
||||
// ── Warteschlange bearbeiten ────────────────────────────────────────────
|
||||
// queue (lokal) und die MediaItems des Controllers muessen im Gleichschritt bleiben:
|
||||
// syncState() liest den laufenden Index vom Controller, den Titel dazu aber aus queue.
|
||||
// Liefen die beiden auseinander, zeigte die Oberflaeche den falschen Song an.
|
||||
|
||||
/** Entfernt Position [index]. Traf es den laufenden Titel, rueckt Media3 selbst weiter. */
|
||||
fun entferneAusQueue(index: Int) {
|
||||
val ctrl = controller ?: return
|
||||
if (index !in queue.indices) return
|
||||
val raus = queue[index]
|
||||
queue = queue.toMutableList().apply { removeAt(index) }
|
||||
// originalQueue haelt die Reihenfolge fuer das Zuruecknehmen des Mischens — dort denselben
|
||||
// Titel EINMAL entfernen, sonst taucht er beim Entmischen wieder auf.
|
||||
originalQueue = originalQueue.toMutableList().apply {
|
||||
val i = indexOfFirst { it.id == raus.id }
|
||||
if (i >= 0) removeAt(i)
|
||||
}
|
||||
ctrl.removeMediaItem(index)
|
||||
if (queue.isEmpty()) {
|
||||
ctrl.stop()
|
||||
ctrl.clearMediaItems()
|
||||
}
|
||||
syncState()
|
||||
}
|
||||
|
||||
/** Verschiebt Position [von] auf [nach]. */
|
||||
fun verschiebeInQueue(von: Int, nach: Int) {
|
||||
val ctrl = controller ?: return
|
||||
if (von == nach || von !in queue.indices || nach !in queue.indices) return
|
||||
queue = queue.toMutableList().apply { add(nach, removeAt(von)) }
|
||||
ctrl.moveMediaItem(von, nach)
|
||||
syncState()
|
||||
}
|
||||
|
||||
fun toggleRepeat() {
|
||||
val ctrl = controller ?: return
|
||||
ctrl.repeatMode = when (ctrl.repeatMode) {
|
||||
|
||||
@@ -60,6 +60,11 @@ import com.novasonic.app.player.PlayerState
|
||||
import com.novasonic.app.ui.theme.*
|
||||
import com.novasonic.app.viewmodel.LyricsViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
|
||||
private enum class NpTab { PLAYER, LYRICS, QUEUE }
|
||||
|
||||
@@ -573,18 +578,38 @@ fun NowPlayingScreen(
|
||||
color = Color.White.copy(0.4f))
|
||||
}
|
||||
} else {
|
||||
// ── Warteschlange bearbeiten ────────────────────────────
|
||||
// Gezogen wird am Griff, nicht an der Zeile: die Liste selbst scrollt, und
|
||||
// beides an derselben Stelle liesse sich nicht auseinanderhalten.
|
||||
// Getauscht wird LAUFEND, sobald der Finger eine Zeilenhoehe zurueckgelegt
|
||||
// hat — dadurch braucht es kein Geisterbild, die Liste zeigt die neue
|
||||
// Reihenfolge sofort.
|
||||
val zeilePx = with(LocalDensity.current) { 56.dp.toPx() }
|
||||
var ziehIdx by remember { mutableIntStateOf(-1) }
|
||||
var ziehOffset by remember { mutableFloatStateOf(0f) }
|
||||
val queueLaenge = rememberUpdatedState(state.queue.size)
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
contentPadding = PaddingValues(vertical = 4.dp),
|
||||
) {
|
||||
itemsIndexed(state.queue, key = { idx, _ -> idx }) { idx, qSong ->
|
||||
val active = state.currentIndex == idx
|
||||
val wirdGezogen = ziehIdx == idx
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer {
|
||||
if (wirdGezogen) { translationY = ziehOffset; shadowElevation = 12f }
|
||||
}
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(if (active) Color.White.copy(0.08f) else Color.Transparent)
|
||||
.background(
|
||||
when {
|
||||
wirdGezogen -> Color.White.copy(0.16f)
|
||||
active -> Color.White.copy(0.08f)
|
||||
else -> Color.Transparent
|
||||
}
|
||||
)
|
||||
.clickable { controller.seekToItem(idx) }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
@@ -612,6 +637,53 @@ fun NowPlayingScreen(
|
||||
Icon(Icons.AutoMirrored.Filled.VolumeUp, null,
|
||||
tint = NsAccent, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
IconButton(
|
||||
onClick = { controller.entferneAusQueue(idx) },
|
||||
modifier = Modifier.size(32.dp),
|
||||
) {
|
||||
Icon(Icons.Default.Close, "Aus der Warteschlange entfernen",
|
||||
tint = Color.White.copy(0.45f), modifier = Modifier.size(16.dp))
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.DragHandle, "Zum Verschieben ziehen",
|
||||
tint = Color.White.copy(if (wirdGezogen) 0.9f else 0.45f),
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.padding(6.dp)
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
// Der erste Kontakt wird sofort verbraucht, sonst
|
||||
// beansprucht die LazyColumn die Geste, sobald die
|
||||
// Wischschwelle faellt.
|
||||
val start = awaitFirstDown(requireUnconsumed = false)
|
||||
start.consume()
|
||||
var aktuell = idx
|
||||
var rest = 0f
|
||||
ziehIdx = aktuell
|
||||
try {
|
||||
while (true) {
|
||||
val ev = awaitPointerEvent()
|
||||
val p = ev.changes.firstOrNull { it.id == start.id } ?: break
|
||||
if (!p.pressed) break
|
||||
rest += p.positionChange().y
|
||||
p.consume()
|
||||
while (rest >= zeilePx && aktuell < queueLaenge.value - 1) {
|
||||
controller.verschiebeInQueue(aktuell, aktuell + 1)
|
||||
aktuell++; rest -= zeilePx
|
||||
}
|
||||
while (rest <= -zeilePx && aktuell > 0) {
|
||||
controller.verschiebeInQueue(aktuell, aktuell - 1)
|
||||
aktuell--; rest += zeilePx
|
||||
}
|
||||
ziehIdx = aktuell
|
||||
ziehOffset = rest
|
||||
}
|
||||
} finally {
|
||||
ziehIdx = -1; ziehOffset = 0f
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user