Fix video position jump when dismissing the portrait details panel

Two causes: the drag/dismiss animation targeted the content-area center
(screenHeight/2) while the resting position once the panel flips hidden
is fullScreenHeight/2 - top inset - the player window adds extra bottom
safe area, so the two centers differ and the flip snapped the video by
half that difference. And the hidden-state flip ran on a fixed 0.25s
timer that cut the 0.3-response spring short, snapping the video the
rest of the way. A shared hiddenVideoY is now the target everywhere
(drag interpolation, dismiss animation, swipe-up reveal), and the flip
runs in the spring's completion callback.

Also mark DEBUG os_log output public: without it the console shows
<private> for processes not launched by Xcode (e.g. USB syslog capture).
This commit is contained in:
Arkadiusz Fal
2026-09-22 23:55:54 +02:00
parent 3e5b387532
commit ad01e113fc
2 changed files with 27 additions and 17 deletions

View File

@@ -199,15 +199,17 @@ final class LoggingService: Sendable {
#if DEBUG #if DEBUG
if UserDefaults.standard.bool(forKey: "loggingEnabled") { if UserDefaults.standard.bool(forKey: "loggingEnabled") {
let fullMessage = details.map { "\(message) - \($0)" } ?? message let fullMessage = details.map { "\(message) - \($0)" } ?? message
// .public: without it os_log masks the text as <private> when the
// process isn't attached to Xcode (e.g. syslog capture over USB).
switch level { switch level {
case .debug: case .debug:
osLogger.debug("[\(category.rawValue)] \(fullMessage)") osLogger.debug("[\(category.rawValue, privacy: .public)] \(fullMessage, privacy: .public)")
case .info: case .info:
osLogger.info("[\(category.rawValue)] \(fullMessage)") osLogger.info("[\(category.rawValue, privacy: .public)] \(fullMessage, privacy: .public)")
case .warning: case .warning:
osLogger.warning("[\(category.rawValue)] \(fullMessage)") osLogger.warning("[\(category.rawValue, privacy: .public)] \(fullMessage, privacy: .public)")
case .error: case .error:
osLogger.error("[\(category.rawValue)] \(fullMessage)") osLogger.error("[\(category.rawValue, privacy: .public)] \(fullMessage, privacy: .public)")
} }
} }
#endif #endif

View File

@@ -237,7 +237,12 @@ extension ExpandedPlayerSheet {
// videoYOffset is a @State that we animate explicitly for smooth drag release animation. // videoYOffset is a @State that we animate explicitly for smooth drag release animation.
// Base positions are computed, then videoYOffset is added. // Base positions are computed, then videoYOffset is added.
let topY = videoAreaHeight / 2 // Video centered in video area (above panel) let topY = videoAreaHeight / 2 // Video centered in video area (above panel)
let centerY = screenHeight / 2 // Video centered when panel hidden // Video center when the panel is hidden, in content-area coordinates:
// full-screen center shifted up by the top safe area. NOT screenHeight/2 —
// top and bottom insets differ (the player window adds extra bottom
// inset), so animating to screenHeight/2 and then flipping to the hidden
// base position causes a visible jump of (bottom - top) / 2 points.
let hiddenVideoY = fullScreenHeight / 2 - safeAreaInsets.top
let videoY: CGFloat = { let videoY: CGFloat = {
if isPortraitPanelVisible { if isPortraitPanelVisible {
@@ -248,10 +253,10 @@ extension ExpandedPlayerSheet {
if panelRevealOffset < 0 { if panelRevealOffset < 0 {
// Revealing panel - move video towards top // Revealing panel - move video towards top
let revealProgress = min(1.0, -panelRevealOffset / pinnedPanelHeight) let revealProgress = min(1.0, -panelRevealOffset / pinnedPanelHeight)
return centerY - (centerY - topY) * revealProgress return hiddenVideoY - (hiddenVideoY - topY) * revealProgress
} }
// Fullscreen: fill entire screen (need to offset for safe area) // Fullscreen: fill entire screen (need to offset for safe area)
return fullScreenHeight / 2 + fullscreenPlayerOffset return hiddenVideoY
} }
}() }()
@@ -388,7 +393,7 @@ extension ExpandedPlayerSheet {
appEnvironment?.settingsManager.detailsPanelLastVisible = true appEnvironment?.settingsManager.detailsPanelLastVisible = true
// Match video position from reveal formula // Match video position from reveal formula
let revealProgress = min(1.0, -currentRevealOffset / pinnedPanelHeight) let revealProgress = min(1.0, -currentRevealOffset / pinnedPanelHeight)
let maxVideoOffset = centerY - topY let maxVideoOffset = hiddenVideoY - topY
videoYOffset = maxVideoOffset * (1.0 - revealProgress) videoYOffset = maxVideoOffset * (1.0 - revealProgress)
} }
@@ -464,8 +469,8 @@ extension ExpandedPlayerSheet {
// Dragging DOWN - existing dismiss logic // Dragging DOWN - existing dismiss logic
panelDragOffset = offset panelDragOffset = offset
panelExpandOffset = 0 panelExpandOffset = 0
// Calculate video offset: interpolate from 0 to (centerY - topY) based on drag // Calculate video offset: interpolate from 0 to (hiddenVideoY - topY) based on drag
let maxOffset = centerY - topY let maxOffset = hiddenVideoY - topY
let dragProgress = min(1.0, offset / pinnedPanelHeight) let dragProgress = min(1.0, offset / pinnedPanelHeight)
videoYOffset = maxOffset * dragProgress videoYOffset = maxOffset * dragProgress
} }
@@ -475,7 +480,7 @@ extension ExpandedPlayerSheet {
let dismissThreshold: CGFloat = 80 let dismissThreshold: CGFloat = 80
let expandThreshold: CGFloat = -80 // Upward threshold let expandThreshold: CGFloat = -80 // Upward threshold
let targetOffset = pinnedPanelHeight + safeAreaInsets.bottom let targetOffset = pinnedPanelHeight + safeAreaInsets.bottom
let maxVideoOffset = centerY - topY let maxVideoOffset = hiddenVideoY - topY
// Setting isPanelDragging = false activates the .animation modifiers // Setting isPanelDragging = false activates the .animation modifiers
isPanelDragging = false isPanelDragging = false
@@ -499,11 +504,14 @@ extension ExpandedPlayerSheet {
panelDragOffset = 0 panelDragOffset = 0
videoYOffset = 0 videoYOffset = 0
} else if offset > dismissThreshold || predictedOffset > dismissThreshold * 2 { } else if offset > dismissThreshold || predictedOffset > dismissThreshold * 2 {
// Dismissing - animate panel sliding off and video to center // Dismissing - animate panel sliding off and video to center.
panelDragOffset = targetOffset // The hidden-state flip must wait for the spring to finish:
videoYOffset = maxVideoOffset // a fixed-delay timer fired mid-animation and snapped the
// Hide after animation completes - disable animations for cleanup // video the rest of the way (visible jump on release).
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { withAnimation(.spring(response: 0.3, dampingFraction: 1.0), completionCriteria: .logicallyComplete) {
panelDragOffset = targetOffset
videoYOffset = maxVideoOffset
} completion: {
var transaction = Transaction() var transaction = Transaction()
transaction.disablesAnimations = true transaction.disablesAnimations = true
withTransaction(transaction) { withTransaction(transaction) {
@@ -514,7 +522,7 @@ extension ExpandedPlayerSheet {
navigationCoordinator?.isCommentsExpanded = false navigationCoordinator?.isCommentsExpanded = false
navigationCoordinator?.commentsFrame = .zero navigationCoordinator?.commentsFrame = .zero
panelDragOffset = 0 panelDragOffset = 0
videoYOffset = 0 // Reset offset (centerY is now the base) videoYOffset = 0 // Reset offset (hiddenVideoY is now the base)
} }
} }
} else { } else {