fix: keep playlist reordering active while dragging

This commit is contained in:
2026-10-18 22:21:42 +02:00
parent 2e5255e194
commit ef3aae842d
5 changed files with 314 additions and 17 deletions
+96 -14
View File
@@ -2038,6 +2038,13 @@ private struct PlayerContent: View {
@State private var youTubeURL = ""
@FocusState private var youTubeInputFocused: Bool
@State private var controlSize = CGSize(width: 440, height: 68)
@State private var queueFrames: [UUID: CGRect] = [:]
@State private var queueDrag: PlaybackQueueDrag?
@GestureState private var queueDragging = false
@State private var queueScroll = ScrollPosition(y: 0)
@State private var queueScrollOffset: CGFloat = 0
@State private var queueViewportHeight: CGFloat = 0
@State private var queueContentHeight: CGFloat = 0
private func compactTitleLabel(progress: String?, constrained: Bool) -> some View {
HStack(spacing: 0) {
@@ -2364,17 +2371,46 @@ private struct PlayerContent: View {
.allowsHitTesting(false)
.accessibilityHidden(true)
}
private var queueReorderAnimation: Animation? {
reduceMotion ? nil : .easeOut(duration: 0.16)
}
private func updateQueueDrag(_ value: DragGesture.Value, item: PlaybackQueue.Item) {
NSLog("[DEBUG-queue-drag] changed %@ translation %@ frames %d", item.url.lastPathComponent, String(describing: value.translation), queueFrames.count)
if queueDrag == nil {
let frames = model.queue.items.compactMap { queueFrames[$0.id] }
guard frames.count == model.queue.items.count,
let source = model.queue.items.firstIndex(where: { $0.id == item.id }) else { return }
queueDrag = PlaybackQueueDrag(source: source, frames: frames, scrollOffset: queueScrollOffset)
}
queueDrag?.pointerY = value.location.y
queueDrag?.update(translation: value.translation.height, scrollOffset: queueScrollOffset)
}
private func queueRowDrag(_ item: PlaybackQueue.Item) -> some Gesture {
DragGesture(minimumDistance: 4, coordinateSpace: .named("queueViewport"))
.updating($queueDragging) { _, active, _ in active = true }
.onChanged { updateQueueDrag($0, item: item) }
.onEnded { value in
NSLog("[DEBUG-queue-drag] ended %@", String(describing: queueDrag))
updateQueueDrag(value, item: item)
guard let drag = queueDrag else { return }
withAnimation(queueReorderAnimation) {
model.moveQueueItems(fromOffsets: IndexSet(integer: drag.source), toOffset: drag.insertionOffset)
queueDrag = nil
}
}
}
private var queueSidebar: some View {
VStack(spacing: 0) {
List {
Color.clear
.frame(height: 52)
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
.allowsHitTesting(false)
.accessibilityHidden(true)
ForEach(model.queue.items) { item in
ScrollView {
VStack(spacing: 0) {
Color.clear
.frame(height: 52)
.allowsHitTesting(false)
.accessibilityHidden(true)
ForEach(Array(model.queue.items.enumerated()), id: \.element.id) { index, item in
let isCurrent = item.id == model.queue.currentID
HStack(spacing: 8) {
VStack(spacing: 4) {
@@ -2433,14 +2469,60 @@ private struct PlayerContent: View {
}
}
.accessibilityAction(named: Text("Play")) { model.selectQueueItem(item.id) }
.listRowInsets(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
.listRowSeparator(.hidden)
.accessibilityAction(named: Text("Move Up")) {
if index > 0 {
model.moveQueueItems(fromOffsets: IndexSet(integer: index), toOffset: index - 1)
}
}
.accessibilityAction(named: Text("Move Down")) {
if index + 1 < model.queue.items.count {
model.moveQueueItems(fromOffsets: IndexSet(integer: index), toOffset: index + 2)
}
}
.padding(.vertical, 2)
.onGeometryChange(for: CGRect.self) { $0.frame(in: .named("queueContent")) } action: {
NSLog("[DEBUG-queue-drag] frame %@ %@", item.url.lastPathComponent, String(describing: $0))
queueFrames[item.id] = $0
}
.offset(y: queueDrag?.offset(at: index) ?? 0)
.zIndex(queueDrag?.source == index ? 1 : 0)
.animation(queueDrag?.source == index ? nil : queueReorderAnimation, value: queueDrag?.destination)
.highPriorityGesture(queueRowDrag(item))
}
}
.onMove { model.moveQueueItems(fromOffsets: $0, toOffset: $1) }
.coordinateSpace(name: "queueContent")
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { queueContentHeight = $0 }
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.contentMargins(.horizontal, 0, for: .scrollContent)
.coordinateSpace(name: "queueViewport")
.scrollPosition($queueScroll)
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { queueViewportHeight = $0 }
.onScrollGeometryChange(for: CGFloat.self) { $0.contentOffset.y } action: { _, offset in
queueScrollOffset = offset
if let drag = queueDrag {
queueDrag?.update(translation: drag.translation, scrollOffset: offset)
}
}
.task(id: queueDrag?.source) {
guard queueDrag != nil else { return }
while !Task.isCancelled {
do { try await Task.sleep(for: .milliseconds(16)) } catch { return }
guard let drag = queueDrag else { return }
let step: CGFloat = drag.pointerY < 64 ? -6 : (drag.pointerY > queueViewportHeight - 32 ? 6 : 0)
if step != 0 {
let offset = min(max(0, queueScrollOffset + step), max(0, queueContentHeight - queueViewportHeight))
if offset != queueScrollOffset { queueScroll.scrollTo(y: offset) }
}
}
}
.onChange(of: queueDragging) { _, active in
NSLog("[DEBUG-queue-drag] active %d", active)
if !active { withAnimation(queueReorderAnimation) { queueDrag = nil } }
}
.onChange(of: model.queue.items.map(\.id)) { _, ids in
queueDrag = nil
queueFrames = queueFrames.filter { ids.contains($0.key) }
}
.onDisappear { queueDrag = nil }
.overlay {
if model.queue.items.isEmpty {
Text("Drop local files here to add them to the queue.")