diff --git a/Sources/PlumTV/PlayerView.swift b/Sources/PlumTV/PlayerView.swift index a91f09d..c9f4879 100644 --- a/Sources/PlumTV/PlayerView.swift +++ b/Sources/PlumTV/PlayerView.swift @@ -123,10 +123,15 @@ struct PlayerView: View { ), in: 0...scale, onEditingChanged: { editing in - // Commit once on release — never an ssh call per drag tick. + // Commit once on release — never an ssh call per drag tick. Hold + // the dragged value as the displayed value across the send + + // re-poll round-trip, then clear it (unless another drag started), + // so the thumb never snaps back to a stale polled value. if !editing, let v = dragVolume { - controller.command { await $0.setVolume(Int(v)) } - dragVolume = nil + Task { + await controller.commandAwait { await $0.setVolume(Int(v)) } + if dragVolume == v { dragVolume = nil } + } } } ) diff --git a/Sources/PlumTVCore/PlayerController.swift b/Sources/PlumTVCore/PlayerController.swift index 332d9b2..7b90468 100644 --- a/Sources/PlumTVCore/PlayerController.swift +++ b/Sources/PlumTVCore/PlayerController.swift @@ -122,6 +122,15 @@ public final class PlayerController { } } + /// Like `command`, but awaitable — the caller can hold an optimistic UI value + /// until the change is both sent and re-polled (the volume slider uses this to + /// avoid snapping the thumb back to a stale value during the round-trip). + public func commandAwait(_ op: @escaping (any PlayerTarget) async -> Void) async { + guard let active else { return } + await op(active) + await refreshActive() + } + public func refreshActive() async { guard let active, !polling else { return } polling = true