fix: keep seek target visible until acknowledged and make volume thumb opaque

This commit is contained in:
2026-10-10 16:09:29 +02:00
parent c9b9264109
commit 9356ed8b4d
5 changed files with 48 additions and 15 deletions
+23 -8
View File
@@ -28,6 +28,7 @@ final class PlayerModel: ObservableObject {
private var windowActive = true
private var lastAudibleVolume = 1.0
private var needsVideoFit = false
private var seekPending = false
private init() {
let player = player_create()
@@ -100,6 +101,7 @@ final class PlayerModel: ObservableObject {
needsVideoFit = true
commandError = nil
isSeeking = false
seekPending = false
seekPosition = 0
window?.representedURL = url
window?.title = url.lastPathComponent
@@ -117,7 +119,9 @@ final class PlayerModel: ObservableObject {
isSeeking = false
guard let handle, canSeek else { return }
let position = min(max(0, seekPosition), duration)
commandError = player_seek(handle, position) ? nil : "Could not queue the seek. Please try again."
let accepted = player_seek(handle, position)
if accepted { seekPending = true }
commandError = accepted ? nil : "Could not queue the seek. Please try again."
revealControls()
}
@@ -133,6 +137,7 @@ final class PlayerModel: ObservableObject {
window?.aspectRatio = .zero
commandError = nil
isSeeking = false
seekPending = false
window?.representedURL = nil
window?.title = "Player"
revealControls()
@@ -140,9 +145,13 @@ final class PlayerModel: ObservableObject {
func seekRelative(_ seconds: Double) {
guard canSeek, let handle else { return }
let target = min(max(0, state.position + seconds), duration)
commandError = player_seek(handle, target) ? nil : "Could not queue the seek. Please try again."
seekPosition = target
let target = min(max(0, (seekPending ? seekPosition : state.position) + seconds), duration)
let accepted = player_seek(handle, target)
if accepted {
seekPosition = target
seekPending = true
}
commandError = accepted ? nil : "Could not queue the seek. Please try again."
revealControls()
}
@@ -234,7 +243,7 @@ final class PlayerModel: ObservableObject {
private var shouldHideControls: Bool {
canTogglePlayback && state.has_video && fileName != nil &&
!isSeeking && !controlsHovered && !titleHovered && !controlsDragging &&
!isSeeking && !seekPending && !controlsHovered && !titleHovered && !controlsDragging &&
windowActive && errorText == nil &&
!NSWorkspace.shared.isVoiceOverEnabled
}
@@ -265,14 +274,18 @@ final class PlayerModel: ObservableObject {
private func poll() {
guard let handle else { return }
let next = player_get_state(handle)
applyState(player_get_state(handle))
}
func applyState(_ next: PlayerState) {
if let revision = pendingRevision {
guard next.revision != revision, next.status != PLAYER_OPENING else { return }
pendingRevision = nil
}
let statusChanged = state.status != next.status
state = next
if !isSeeking {
if !next.seek_pending || next.status == PLAYER_ERROR { seekPending = false }
if !isSeeking && !seekPending {
seekPosition = min(max(0, next.position.isFinite ? next.position : 0), duration)
}
fitVideoWindow()
@@ -379,7 +392,9 @@ private struct PlaybackSlider: NSViewRepresentable {
let width: CGFloat = volume ? 16 : 3
let height: CGFloat = volume ? 12 : 15
let knob = NSRect(x: rect.midX - width / 2, y: rect.midY - height / 2, width: width, height: height)
NSColor.white.withAlphaComponent(isEnabled ? 0.65 : 0.25).setFill()
let color = volume ? NSColor(white: isEnabled ? 0.85 : 0.45, alpha: 1)
: NSColor.white.withAlphaComponent(isEnabled ? 0.65 : 0.25)
color.setFill()
NSBezierPath(roundedRect: knob, xRadius: volume ? 6 : 1, yRadius: volume ? 6 : 1).fill()
}
}
+5
View File
@@ -137,6 +137,7 @@ struct Player {
state.status = PLAYER_OPENING;
state.revision = revision;
}
if (commands.back().action == Action::Seek) state.seek_pending = true;
wake.notify_one();
return true;
}
@@ -179,6 +180,10 @@ struct Player {
state.duration = duration;
state.has_audio = tracks[0].codec != nullptr;
state.has_video = tracks[1].codec != nullptr;
// The worker publishes only outside seek execution; retain pending for later queued seeks.
state.seek_pending = std::any_of(commands.begin(), commands.end(), [this](const Command &command) {
return command.action == Action::Seek && command.epoch == activeEpoch;
});
state.video_aspect_ratio = 0;
if (const auto *video = tracks[1].codec; video && video->width > 0 && video->height > 0) {
const auto aspect = av_guess_sample_aspect_ratio(format, format->streams[tracks[1].index], nullptr);
+2
View File
@@ -17,6 +17,8 @@ typedef struct PlayerState {
double duration;
bool has_audio;
bool has_video;
// True from accepted seek until all queued seeks on this timeline finish.
bool seek_pending;
// Display width / height, including sample aspect ratio; zero without video.
double video_aspect_ratio;
uint64_t revision;