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
+28
-1
@@ -16,6 +16,7 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
|
||||
| Music playback (Navidrome) | ✅ | ✅ |
|
||||
| Seek bar / scrubbing | ✅ | ✅ |
|
||||
| Queue management | ✅ | ✅ |
|
||||
| Warteschlange bearbeiten (ziehen/löschen) | ✅ | ✅ |
|
||||
| Shuffle / Repeat | ✅ | ✅ |
|
||||
| Radio mode (similar songs) | ✅ | ✅ |
|
||||
| Now Playing overlay | ✅ | ✅ |
|
||||
@@ -79,7 +80,33 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
|
||||
|
||||
## [3.1] — 2026-09-22
|
||||
|
||||
**Android:** 1.15 (versionCode 18) → 1.18 (versionCode 21). 1.16 brachte den Rollbalken, 1.17 und 1.18 machten ihn tatsächlich bedienbar.
|
||||
**Android:** 1.15 (versionCode 18) → 1.19 (versionCode 22). 1.16 brachte den Rollbalken, 1.17 und 1.18 machten ihn bedienbar, 1.19 bringt das Bearbeiten der Warteschlange.
|
||||
|
||||
### Added
|
||||
- **Die Warteschlange lässt sich bearbeiten (Web + Android)** — bisher war sie eine reine Anzeige:
|
||||
reinlegen ging, umsortieren oder wieder herausnehmen nicht. Jetzt trägt jede Zeile einen Griff
|
||||
zum Ziehen und ein ✕ zum Entfernen, auf beiden Plattformen und in beiden Web-Listen (Seitenleiste
|
||||
und Now-Playing).
|
||||
- **Der Zeiger auf den laufenden Titel wandert mit.** Das ist der Teil, bei dem ein Fehler nicht
|
||||
auffällt, sondern später den falschen Song spielt: fällt ein Titel **vor** dem laufenden weg,
|
||||
rutscht dieser eine Stelle vor; trifft es den laufenden selbst, rückt der nächste nach (am
|
||||
Ende der Liste der davor); beim Verschieben wird der Zeiger über die **Position** des laufenden
|
||||
Titels neu bestimmt statt gerechnet — das stimmt auch, wenn er selbst gezogen wurde.
|
||||
- Diese Rechnung liegt im Web als reines Modul ohne Importe in `src/player/queueMath.js`, damit
|
||||
sie ohne DOM und ohne Wiedergabe prüfbar ist: **12 Fälle, alle grün.**
|
||||
- **Web: Zeiger-Ereignisse statt HTML5-Drag-and-Drop.** Dessen `dragstart` gibt es auf
|
||||
Touch-Geräten gar nicht — die Warteschlange wäre am Handy unbearbeitbar geblieben.
|
||||
`setPointerCapture` bedient Maus und Finger über denselben Weg. Die Zeilen zwischen Start und
|
||||
Ziel weichen aus, damit die Lücke dorthin wandert, wo man loslässt.
|
||||
- **Android: getauscht wird laufend**, sobald der Finger eine Zeilenhöhe zurückgelegt hat — so
|
||||
braucht es kein Geisterbild, die Liste zeigt die neue Reihenfolge sofort. Gezogen wird am
|
||||
Griff, nicht an der Zeile, sonst ließen sich Ziehen und Scrollen nicht auseinanderhalten; und
|
||||
der erste Fingerkontakt wird verbraucht, sonst beansprucht die `LazyColumn` die Geste (dieselbe
|
||||
Falle wie beim Rollbalken).
|
||||
- Im Player laufen die lokale Liste und Media3 im Gleichschritt (`moveMediaItem`,
|
||||
`removeMediaItem`). Liefen sie auseinander, zeigte die Oberfläche den falschen Song an, weil
|
||||
der laufende Index vom Controller kommt, der Titel dazu aber aus der lokalen Liste.
|
||||
|
||||
|
||||
### Changed
|
||||
- **Der Rollbalken ist greifbar (Web)** — er war 4 px breit. Sichtbar reichte das, mit der Maus war
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.novasonic.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 21
|
||||
versionName = "1.18"
|
||||
versionCode = 22
|
||||
versionName = "1.19"
|
||||
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
|
||||
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
|
||||
// Platzhalter stehen und der App-Link greift schlicht nicht.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { coverUrl } from '../api.js';
|
||||
import { playCurrent, _syncNpDirect, dlSongLocal, npToggleStar, _syncOverlayState, isPlaying, seekToMs, updatePlayerUI } from './player.js';
|
||||
import { navigate } from '../views/views.js';
|
||||
import { openPlModal, showSongInfo } from '../ui/ui.js';
|
||||
import { macheQueueBearbeitbar, QUEUE_WERKZEUGE } from './queueEdit.js';
|
||||
|
||||
// ─── NOW PLAYING OVERLAY ───
|
||||
export function openNowPlaying() {
|
||||
@@ -313,11 +314,13 @@ export function renderNpQueue() {
|
||||
S.queue.forEach((s, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'np-queue-item' + (i===S.qIdx ? ' nq-active' : '');
|
||||
item.dataset.qidx = i;
|
||||
const cv = coverUrl(s.coverArt, 128);
|
||||
item.innerHTML = `<div class="nqi-num">${i+1}</div><div class="nqi-art">${cv ? `<img src="${cv}">` : '♪'}</div><div class="nqi-info"><div class="nqi-title">${esc(s.title)}</div><div class="nqi-artist">${esc(s.artist||'')}</div></div>`;
|
||||
item.innerHTML = `<div class="nqi-num">${i+1}</div><div class="nqi-art">${cv ? `<img src="${cv}">` : '♪'}</div><div class="nqi-info"><div class="nqi-title">${esc(s.title)}</div><div class="nqi-artist">${esc(s.artist||'')}</div></div>` + QUEUE_WERKZEUGE;
|
||||
item.onclick = () => { S.qIdx=i; playCurrent(); renderNpQueue(); };
|
||||
el.appendChild(item);
|
||||
});
|
||||
macheQueueBearbeitbar('np-queue-list');
|
||||
el.querySelector('.nq-active')?.scrollIntoView({block:'nearest'});
|
||||
}
|
||||
|
||||
|
||||
+43
-2
@@ -3,8 +3,10 @@ import { esc, formatDur, toast } from '../utils.js';
|
||||
import { api, streamUrl, coverUrl, downloadUrl, dispTitle, dispArtist, dispArtistParts, dispAlbum } from '../api.js';
|
||||
// views.js/ui.js circular imports — safe (only used inside function bodies)
|
||||
import { loadRecommendations } from '../views/views.js';
|
||||
import { updateLyricsHighlight, onNpSongChange, syncNowPlaying } from './nowplaying.js';
|
||||
import { updateLyricsHighlight, onNpSongChange, syncNowPlaying, renderNpQueue } from './nowplaying.js';
|
||||
import { logPlay } from './scrobbleLog.js';
|
||||
import { macheQueueBearbeitbar, QUEUE_WERKZEUGE } from './queueEdit.js';
|
||||
import { queueNachEntfernen, queueNachVerschieben } from './queueMath.js';
|
||||
|
||||
// ─── NATIVE BRIDGE DETECTION ───────────────────────────────────────────────
|
||||
// True when running inside the NovaSonic Android wrapper (NSBridge injected before loadUrl).
|
||||
@@ -1187,14 +1189,53 @@ export function renderQueue() {
|
||||
S.queue.forEach((s, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'queue-item' + (i === S.qIdx ? ' active' : '');
|
||||
item.dataset.qidx = i;
|
||||
const cv = coverUrl(s.coverArt, 128);
|
||||
item.innerHTML = `<div class="qi-idx">${i+1}</div><div class="qi-art">${cv ? `<img src="${cv}">` : '♪'}</div><div class="qi-info"><div class="qi-title">${esc(dispTitle(s))}</div><div class="qi-artist">${esc(dispArtist(s))}</div></div>`;
|
||||
item.innerHTML = `<div class="qi-idx">${i+1}</div><div class="qi-art">${cv ? `<img src="${cv}">` : '♪'}</div><div class="qi-info"><div class="qi-title">${esc(dispTitle(s))}</div><div class="qi-artist">${esc(dispArtist(s))}</div></div>` + QUEUE_WERKZEUGE;
|
||||
item.onclick = () => { S.qIdx = i; playCurrent(); };
|
||||
el.appendChild(item);
|
||||
});
|
||||
macheQueueBearbeitbar('queue-list');
|
||||
el.querySelector('.active')?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
// ─── WARTESCHLANGE BEARBEITEN ─────────────────────────────────────────────
|
||||
// S.queue ist eine einfache Liste, S.qIdx zeigt auf den laufenden Titel. Beim Aendern muss der
|
||||
// Zeiger mitwandern — sonst spielt nach dem Entfernen eines Titels weiter oben ploetzlich der
|
||||
// falsche. Gemerkt wird deshalb die ID des laufenden Titels, nicht seine Position.
|
||||
|
||||
/** Entfernt den Titel an Position i. Traf es den laufenden, laeuft der naechste an. */
|
||||
export function queueEntfernen(i) {
|
||||
if (i < 0 || i >= S.queue.length) return;
|
||||
const { qIdx, warAktuell } = queueNachEntfernen(S.queue.length, S.qIdx, i);
|
||||
S.queue.splice(i, 1);
|
||||
S.qIdx = qIdx;
|
||||
_renderedQueueKey = '';
|
||||
|
||||
if (!S.queue.length) {
|
||||
S.qIdx = -1;
|
||||
try { S._audioEls?.forEach(a => { a.pause(); }); } catch {}
|
||||
setPlayIcon(false);
|
||||
renderQueue(); renderNpQueue();
|
||||
toast('Warteschlange geleert');
|
||||
return;
|
||||
}
|
||||
if (warAktuell) playCurrent();
|
||||
renderQueue(); renderNpQueue();
|
||||
}
|
||||
|
||||
/** Verschiebt den Titel von Position `von` auf Position `nach`. */
|
||||
export function queueVerschieben(von, nach) {
|
||||
const n = S.queue.length;
|
||||
if (von === nach || von < 0 || von >= n || nach < 0 || nach >= n) return;
|
||||
const { qIdx } = queueNachVerschieben(S.queue.map(x => x.id), S.qIdx, von, nach);
|
||||
const [titel] = S.queue.splice(von, 1);
|
||||
S.queue.splice(nach, 0, titel);
|
||||
S.qIdx = qIdx;
|
||||
_renderedQueueKey = '';
|
||||
renderQueue(); renderNpQueue();
|
||||
}
|
||||
|
||||
export function toggleQueue() {
|
||||
S.queueOpen = !S.queueOpen;
|
||||
document.getElementById('queue-panel').classList.toggle('open', S.queueOpen);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { queueEntfernen, queueVerschieben } from './player.js';
|
||||
|
||||
// ─── WARTESCHLANGE BEARBEITEN (Web) ─────────────────────────────────────────
|
||||
// Ziehen und Entfernen fuer beide Queue-Listen: die Seitenleiste und die im Now-Playing.
|
||||
//
|
||||
// Bewusst ueber Zeiger-Ereignisse statt HTML5-Drag-and-Drop: dessen `dragstart` gibt es auf
|
||||
// Touch-Geraeten gar nicht, die Warteschlange waere am Handy also unbearbeitbar geblieben.
|
||||
// `setPointerCapture` liefert dafuer Maus und Finger ueber denselben Weg.
|
||||
|
||||
const ZIEH_SCHWELLE = 4; // px, bevor aus einem Tippen ein Ziehen wird
|
||||
|
||||
export function macheQueueBearbeitbar(listeId) {
|
||||
const liste = document.getElementById(listeId);
|
||||
if (!liste || liste.dataset.bearbeitbar) return;
|
||||
liste.dataset.bearbeitbar = '1';
|
||||
|
||||
liste.addEventListener('click', (e) => {
|
||||
const weg = e.target.closest('.q-del');
|
||||
if (!weg) return;
|
||||
e.stopPropagation();
|
||||
const zeile = weg.closest('[data-qidx]');
|
||||
if (zeile) queueEntfernen(Number(zeile.dataset.qidx));
|
||||
});
|
||||
|
||||
liste.addEventListener('pointerdown', (e) => {
|
||||
const griff = e.target.closest('.q-grip');
|
||||
if (!griff || e.button > 0) return;
|
||||
const zeile = griff.closest('[data-qidx]');
|
||||
if (!zeile) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ziehen(liste, zeile, e);
|
||||
});
|
||||
}
|
||||
|
||||
function ziehen(liste, zeile, start) {
|
||||
const zeilen = [...liste.querySelectorAll('[data-qidx]')];
|
||||
const vonIdx = Number(zeile.dataset.qidx);
|
||||
const hoehe = zeile.offsetHeight || 1;
|
||||
const startY = start.clientY;
|
||||
let zielIdx = vonIdx;
|
||||
let aktiv = false;
|
||||
|
||||
const bewegen = (e) => {
|
||||
const dy = e.clientY - startY;
|
||||
if (!aktiv) {
|
||||
if (Math.abs(dy) < ZIEH_SCHWELLE) return;
|
||||
aktiv = true;
|
||||
zeile.classList.add('q-dragging');
|
||||
liste.classList.add('q-dragging-aktiv');
|
||||
}
|
||||
zeile.style.transform = `translateY(${dy}px)`;
|
||||
zielIdx = Math.max(0, Math.min(zeilen.length - 1, vonIdx + Math.round(dy / hoehe)));
|
||||
// Die Zeilen zwischen Start und Ziel weichen um eine Zeilenhoehe aus, damit die Luecke
|
||||
// dorthin wandert, wo losgelassen wird.
|
||||
zeilen.forEach((z, i) => {
|
||||
if (z === zeile) return;
|
||||
let v = 0;
|
||||
if (vonIdx < zielIdx && i > vonIdx && i <= zielIdx) v = -hoehe;
|
||||
else if (vonIdx > zielIdx && i >= zielIdx && i < vonIdx) v = hoehe;
|
||||
z.style.transform = v ? `translateY(${v}px)` : '';
|
||||
});
|
||||
};
|
||||
|
||||
const beenden = () => {
|
||||
liste.releasePointerCapture?.(start.pointerId);
|
||||
window.removeEventListener('pointermove', bewegen);
|
||||
window.removeEventListener('pointerup', beenden);
|
||||
window.removeEventListener('pointercancel', beenden);
|
||||
zeilen.forEach(z => { z.style.transform = ''; z.classList.remove('q-dragging'); });
|
||||
liste.classList.remove('q-dragging-aktiv');
|
||||
if (aktiv && zielIdx !== vonIdx) queueVerschieben(vonIdx, zielIdx);
|
||||
};
|
||||
|
||||
try { liste.setPointerCapture(start.pointerId); } catch {}
|
||||
window.addEventListener('pointermove', bewegen);
|
||||
window.addEventListener('pointerup', beenden);
|
||||
window.addEventListener('pointercancel', beenden);
|
||||
}
|
||||
|
||||
/** Griff und Entfernen-Knopf als HTML — in beiden Listen identisch. */
|
||||
export const QUEUE_WERKZEUGE =
|
||||
'<button class="q-del" title="Aus der Warteschlange entfernen" aria-label="Entfernen">' +
|
||||
'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
|
||||
'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' +
|
||||
'</button>' +
|
||||
'<span class="q-grip" title="Zum Verschieben ziehen" aria-label="Verschieben">' +
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
|
||||
'<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/></svg>' +
|
||||
'</span>';
|
||||
@@ -0,0 +1,37 @@
|
||||
// Reine Indexrechnung der Warteschlange — bewusst OHNE Importe, damit sie sich ohne DOM und
|
||||
// ohne Wiedergabe pruefen laesst. Hier steckt der Teil, bei dem ein Fehler nicht auffaellt,
|
||||
// sondern spaeter den falschen Titel spielt.
|
||||
|
||||
|
||||
/**
|
||||
* Entfernt Position `i`. Liefert den neuen Zeiger und ob der LAUFENDE Titel getroffen wurde.
|
||||
* Der Zeiger wandert mit: faellt ein Titel VOR dem laufenden weg, rutscht dieser eine Stelle vor.
|
||||
*/
|
||||
export function queueNachEntfernen(laenge, qIdx, i) {
|
||||
if (i < 0 || i >= laenge) return { qIdx, warAktuell: false, leer: laenge === 0 };
|
||||
const warAktuell = i === qIdx;
|
||||
const neueLaenge = laenge - 1;
|
||||
if (neueLaenge === 0) return { qIdx: -1, warAktuell, leer: true };
|
||||
let neu = qIdx;
|
||||
if (!warAktuell && i < qIdx) neu--;
|
||||
// Traf es den laufenden Titel, zeigt der Index jetzt auf den nachrueckenden — am Ende der
|
||||
// Liste gibt es keinen, dann auf den davor.
|
||||
if (warAktuell) neu = Math.min(neu, neueLaenge - 1);
|
||||
return { qIdx: neu, warAktuell, leer: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschiebt `von` nach `nach` und liefert die neue Reihenfolge samt nachgefuehrtem Zeiger.
|
||||
* Der Zeiger wird ueber die POSITION des laufenden Titels neu bestimmt, nicht gerechnet — das
|
||||
* stimmt auch, wenn er selbst gezogen wurde oder ein anderer ueber ihn hinweggezogen ist.
|
||||
*/
|
||||
export function queueNachVerschieben(ids, qIdx, von, nach) {
|
||||
const n = ids.length;
|
||||
if (von === nach || von < 0 || von >= n || nach < 0 || nach >= n) return { ids, qIdx };
|
||||
const laufende = ids[qIdx];
|
||||
const neu = ids.slice();
|
||||
const [x] = neu.splice(von, 1);
|
||||
neu.splice(nach, 0, x);
|
||||
const gefunden = neu.indexOf(laufende);
|
||||
return { ids: neu, qIdx: gefunden >= 0 ? gefunden : qIdx };
|
||||
}
|
||||
+19
@@ -1228,3 +1228,22 @@ button:focus{outline:none}
|
||||
.dublette-box .btn-danger{width:100%;padding:9px 14px;background:transparent;border:1px solid var(--danger);border-radius:var(--radius-sm);color:var(--danger);font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.dublette-box .btn-danger:hover:not(:disabled){background:rgba(224,85,85,.12)}
|
||||
.dublette-box .btn-danger:disabled{opacity:.55;cursor:default}
|
||||
|
||||
/* ── Warteschlange bearbeiten ── */
|
||||
/* Griff und Entfernen-Knopf sitzen am rechten Rand beider Queue-Listen. Am Zeigergeraet
|
||||
erscheinen sie erst beim Ueberfahren, damit die Liste ruhig bleibt; auf Touch-Geraeten gibt es
|
||||
kein Ueberfahren, dort stehen sie immer da. */
|
||||
.queue-item,.np-queue-item{position:relative}
|
||||
.q-del,.q-grip{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;background:none;color:var(--text3);border-radius:6px;cursor:pointer;opacity:0;transition:opacity .15s,color .15s,background .15s}
|
||||
.q-grip{cursor:grab;touch-action:none}
|
||||
.queue-item:hover .q-del,.queue-item:hover .q-grip,
|
||||
.np-queue-item:hover .q-del,.np-queue-item:hover .q-grip{opacity:1}
|
||||
.q-del:hover{color:var(--danger);background:rgba(224,85,85,.12)}
|
||||
.q-grip:hover{color:var(--text)}
|
||||
@media(hover:none){.q-del,.q-grip{opacity:.65}}
|
||||
/* Die gezogene Zeile schwebt ueber den anderen; die weichenden gleiten, sie nicht — sonst
|
||||
haengt sie dem Finger hinterher. */
|
||||
.q-dragging{z-index:5;background:var(--surface2)!important;box-shadow:0 8px 24px rgba(0,0,0,.45);cursor:grabbing}
|
||||
.q-dragging .q-grip{cursor:grabbing}
|
||||
.q-dragging-aktiv [data-qidx]:not(.q-dragging){transition:transform .16s ease}
|
||||
.q-dragging-aktiv{user-select:none}
|
||||
Reference in new issue
Block a user