fix: match progress slider dragging to visible thumb
The 3-point progress thumb was drawn inside larger native knob bounds. Override those bounds for progress only so adjacent clicks seek; retain native volume geometry and tracking. Native click regression reproduced before the fix and passes afterward.
This commit is contained in:
@@ -201,8 +201,9 @@ struct CastPlaybackCheck {
|
|||||||
_ = NSApplication.shared
|
_ = NSApplication.shared
|
||||||
NSApp.setActivationPolicy(.prohibited)
|
NSApp.setActivationPolicy(.prohibited)
|
||||||
require(castVolumeSliderFeedbackCheck(), "Native slider keeps user position during tracking and applies receiver volume when idle")
|
require(castVolumeSliderFeedbackCheck(), "Native slider keeps user position during tracking and applies receiver volume when idle")
|
||||||
|
require(playbackSliderHitCheck(), "Progress slider seeks beside the visible thumb and grabs only the thumb")
|
||||||
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--slider-regression" {
|
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--slider-regression" {
|
||||||
print("PASS: native volume slider tracking preserves user input and idle receiver feedback")
|
print("PASS: progress thumb hit bounds, adjacent seeking, and volume tracking feedback")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let source = URL(fileURLWithPath: CommandLine.arguments[1])
|
let source = URL(fileURLWithPath: CommandLine.arguments[1])
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ To run without installing, use `make run`. The built `.build/terebi.app` is self
|
|||||||
|
|
||||||
- **Local files:** use **⌘O**, drag files onto the player, or choose Finder's **Open With**. Multiple files form a queue in natural filename order, with automatic advance. Use **View → Show Queue** to play, reorder or remove items; **+** or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it.
|
- **Local files:** use **⌘O**, drag files onto the player, or choose Finder's **Open With**. Multiple files form a queue in natural filename order, with automatic advance. Use **View → Show Queue** to play, reorder or remove items; **+** or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it.
|
||||||
- **Streams:** use **Open Stream…** (**⇧⌘O**) or drop a supported link. YouTube supports individual videos, not playlists or channels. Miruro links must use `https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>` with an explicit positive episode number; arbitrary streaming sites are not supported.
|
- **Streams:** use **Open Stream…** (**⇧⌘O**) or drop a supported link. YouTube supports individual videos, not playlists or channels. Miruro links must use `https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>` with an explicit positive episode number; arbitrary streaming sites are not supported.
|
||||||
|
- **Seeking:** click the progress track to seek, including immediately beside the visible thumb. Grab the thumb itself to drag without first jumping to the click position.
|
||||||
- **Miruro metadata:** titles and opening/ending intervals load independently of playback when available. Small rounded gaps mark interval boundaries in the slider, without visible labels; accessibility descriptions retain the interval names and times. Missing/invalid metadata leaves playback usable. Timings may differ between cuts. No automatic skipping is performed.
|
- **Miruro metadata:** titles and opening/ending intervals load independently of playback when available. Small rounded gaps mark interval boundaries in the slider, without visible labels; accessibility descriptions retain the interval names and times. Missing/invalid metadata leaves playback usable. Timings may differ between cuts. No automatic skipping is performed.
|
||||||
- **Resume:** videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress.
|
- **Resume:** videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress.
|
||||||
- **Compact mode:** press **P** for a floating player; double-click it to restore. Drag noninteractive areas to move the window. Use the titlebar pin to keep the normal window on top.
|
- **Compact mode:** press **P** for a floating player; double-click it to restore. Drag noninteractive areas to move the window. Use the titlebar pin to keep the normal window on top.
|
||||||
|
|||||||
@@ -2005,6 +2005,11 @@ private struct PlaybackSlider: NSViewRepresentable {
|
|||||||
var volume = false
|
var volume = false
|
||||||
var bufferedRange: ClosedRange<Double>?
|
var bufferedRange: ClosedRange<Double>?
|
||||||
var intervals: [MiruroInterval] = []
|
var intervals: [MiruroInterval] = []
|
||||||
|
override func knobRect(flipped: Bool) -> NSRect {
|
||||||
|
let rect = super.knobRect(flipped: flipped)
|
||||||
|
guard !volume else { return rect }
|
||||||
|
return NSRect(x: rect.midX - 1.5, y: rect.midY - 7.5, width: 3, height: 15)
|
||||||
|
}
|
||||||
override func drawBar(inside rect: NSRect, flipped: Bool) {
|
override func drawBar(inside rect: NSRect, flipped: Bool) {
|
||||||
let bar = NSRect(x: rect.minX + knobThickness / 2, y: rect.midY - 1.5,
|
let bar = NSRect(x: rect.minX + knobThickness / 2, y: rect.midY - 1.5,
|
||||||
width: max(0, rect.width - knobThickness), height: 3)
|
width: max(0, rect.width - knobThickness), height: 3)
|
||||||
@@ -2046,9 +2051,9 @@ private struct PlaybackSlider: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override func drawKnob(_ rect: NSRect) {
|
override func drawKnob(_ rect: NSRect) {
|
||||||
let width: CGFloat = volume ? 16 : 3
|
let knob = volume
|
||||||
let height: CGFloat = volume ? 12 : 15
|
? NSRect(x: rect.midX - 8, y: rect.midY - 6, width: 16, height: 12)
|
||||||
let knob = NSRect(x: rect.midX - width / 2, y: rect.midY - height / 2, width: width, height: height)
|
: rect
|
||||||
let color = isEnabled ? (volume ? SunsetPalette.text : SunsetPalette.secondaryText)
|
let color = isEnabled ? (volume ? SunsetPalette.text : SunsetPalette.secondaryText)
|
||||||
: SunsetPalette.disabledThumb
|
: SunsetPalette.disabledThumb
|
||||||
color.setFill()
|
color.setFill()
|
||||||
@@ -2060,6 +2065,38 @@ private struct PlaybackSlider: NSViewRepresentable {
|
|||||||
#if PLAYER_CHECK
|
#if PLAYER_CHECK
|
||||||
@MainActor
|
@MainActor
|
||||||
func castVolumeSliderFeedbackCheck() -> Bool { PlaybackSlider.Slider.feedbackCheck() }
|
func castVolumeSliderFeedbackCheck() -> Bool { PlaybackSlider.Slider.feedbackCheck() }
|
||||||
|
@MainActor
|
||||||
|
func playbackSliderHitCheck() -> Bool {
|
||||||
|
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 320, height: 80),
|
||||||
|
styleMask: [.borderless], backing: .buffered, defer: false)
|
||||||
|
let slider = PlaybackSlider.Slider(frame: NSRect(x: 20, y: 20, width: 280, height: 24))
|
||||||
|
slider.cell = PlaybackSlider.Cell()
|
||||||
|
slider.minValue = 0
|
||||||
|
slider.maxValue = 100
|
||||||
|
slider.isContinuous = true
|
||||||
|
window.contentView!.addSubview(slider)
|
||||||
|
window.display()
|
||||||
|
for offset: CGFloat in [-4, -2, -1, 0, 1, 2, 4] {
|
||||||
|
slider.doubleValue = 50
|
||||||
|
let cell = slider.cell as! NSSliderCell
|
||||||
|
let knob = cell.knobRect(flipped: slider.isFlipped)
|
||||||
|
let point = slider.convert(NSPoint(x: knob.midX + offset, y: knob.midY), to: nil)
|
||||||
|
let up = NSEvent.mouseEvent(with: .leftMouseUp, location: point, modifierFlags: [],
|
||||||
|
timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||||
|
eventNumber: 1, clickCount: 1, pressure: 0)!
|
||||||
|
let down = NSEvent.mouseEvent(with: .leftMouseDown, location: point, modifierFlags: [],
|
||||||
|
timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||||
|
eventNumber: 0, clickCount: 1, pressure: 1)!
|
||||||
|
NSApp.postEvent(up, atStart: true)
|
||||||
|
slider.mouseDown(with: down)
|
||||||
|
if abs(offset) <= 1 {
|
||||||
|
guard slider.doubleValue == 50 else { return false }
|
||||||
|
} else {
|
||||||
|
guard (slider.doubleValue - 50) * offset > 0 else { return false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
Reference in New Issue
Block a user