Files
yattee/Yattee/Views/Player/Panels/PanelPageContext.swift
Arkadiusz Fal d5285b2b83 Add unified page-based details panel on existing layouts
Phase 1 of the player panels redesign:

- New PanelSettings model (pages, regions, layout mode, default
  visibility) stored as an optional field on PlayerControlsLayout with
  an effectivePanelSettings accessor; lenient Codable so future page
  types and settings payloads never corrupt saved presets.
- New Views/Player/Panels content engine: PlayerPanelContentView
  renders a page bar plus the selected page (info, comments, related,
  chapters, queue, tracks), with runtime hiding of empty pages.
- PortraitDetailsPanel and FloatingDetailsPanel now host the shared
  content instead of their duplicated info bodies; drag machinery,
  pills, and fades are untouched.
- Comments list extracted into CommentsListContent shared by the
  overlay and the comments page; comments pill routes to the comments
  page (expanding the portrait panel) when the page is enabled, and
  falls back to the overlay when it is not.
- Tracks page embeds QualitySelectorView via a new isEmbedded mode.
- Deleted the unused video info bodies in ExpandedPlayerSheet+VideoInfo
  and the dead PlayerInfoTab model.
2026-09-21 23:20:45 +02:00

110 lines
3.7 KiB
Swift

//
// PanelPageContext.swift
// Yattee
//
// Shared context and scroll plumbing for the details panel pages.
//
import SwiftUI
#if os(iOS) || os(macOS)
// MARK: - Page Context
/// Lightweight context handed to every panel page. Pages reach services via
/// `appEnvironment`; only host-specific callbacks travel explicitly.
struct PanelPageContext {
let appEnvironment: AppEnvironment?
let video: Video
let accentColor: Color
var onChannelTap: (() -> Void)?
}
// MARK: - Scroll Config
/// Scroll behavior the host wants applied to scrollable pages: content
/// insets, scroll-offset reporting (pills collapse / scroll-to-top button),
/// and the portrait overscroll-to-collapse gesture.
struct PanelScrollConfig {
/// Extra space above the content (e.g. below the drag handle).
var topInset: CGFloat = 10
/// Extra space below the content so it can scroll above pills/fades.
var bottomInset: CGFloat = 80
/// Reports the current scroll offset (0 at top, positive when scrolled).
var scrollOffsetChanged: ((CGFloat) -> Void)?
/// Toggling this scrolls the page back to the top.
var scrollToTopTrigger: Bool = false
/// Disables scrolling (while the portrait drag handle is active).
var isScrollDisabled: Bool = false
/// Portrait overscroll-to-collapse drag callbacks (iOS only).
var onOverscrollDragChanged: ((CGFloat) -> Void)?
var onOverscrollDragEnded: ((CGFloat, CGFloat) -> Void)?
}
// MARK: - Page Scroll View
/// Shared scroll container used by the scrollable panel pages. Applies the
/// host's insets, reports scroll offset, handles scroll-to-top, and attaches
/// the portrait overscroll gesture when the host provides callbacks.
struct PanelPageScrollView<Content: View>: View {
let config: PanelScrollConfig
@ViewBuilder let content: Content
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Color.clear
.frame(height: 0)
.id("panelPageTop")
.background(
GeometryReader { geo in
Color.clear
.onChange(of: geo.frame(in: .named("panelPageScroll")).origin.y) { _, newValue in
config.scrollOffsetChanged?(max(0, -newValue))
}
}
)
Color.clear
.frame(height: config.topInset)
content
Spacer()
.frame(height: config.bottomInset)
}
}
.coordinateSpace(name: "panelPageScroll")
#if os(iOS)
.background {
if config.onOverscrollDragChanged != nil || config.onOverscrollDragEnded != nil {
OverscrollGestureView(
isEnabled: !config.isScrollDisabled,
onDragChanged: { offset in
config.onOverscrollDragChanged?(offset)
},
onDragEnded: { offset, predicted in
config.onOverscrollDragEnded?(offset, predicted)
}
)
}
}
#endif
.scrollDisabled(config.isScrollDisabled)
.onChange(of: config.scrollToTopTrigger) { _, _ in
withAnimation(.easeInOut(duration: 0.3)) {
proxy.scrollTo("panelPageTop", anchor: .top)
}
}
}
}
}
#endif