Files
yattee/Yattee/Views/Player/Panels/InfoPanelPage.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

135 lines
4.4 KiB
Swift

//
// InfoPanelPage.swift
// Yattee
//
// Details panel page with title, stats, channel row, and description.
// Consolidates the previously duplicated video info bodies.
//
import SwiftUI
#if os(iOS) || os(macOS)
struct InfoPanelPage: View {
let context: PanelPageContext
var settings: InfoPageSettings = InfoPageSettings()
var scrollConfig: PanelScrollConfig = PanelScrollConfig()
@State private var showFormattedDate = false
@State private var showOriginalTitle = false
private var appEnvironment: AppEnvironment? { context.appEnvironment }
private var video: Video { context.video }
private var playerService: PlayerService? { appEnvironment?.playerService }
private var playerState: PlayerState? { playerService?.state }
private var videoDetailsState: VideoDetailsLoadState { playerState?.videoDetailsState ?? .idle }
/// Returns the first enabled Yattee Server instance URL, if any.
private var yatteeServerURL: URL? {
appEnvironment?.instancesManager.yatteeServerInstances.first { $0.isEnabled }?.url
}
/// Returns the DeArrow title if available and enabled.
private var deArrowTitle: String? {
appEnvironment?.deArrowBrandingProvider.title(for: video)
}
/// Returns the display title based on toggle state.
private var displayTitle: String {
if let deArrowTitle {
return showOriginalTitle ? video.title : deArrowTitle
}
return video.title
}
var body: some View {
PanelPageScrollView(config: scrollConfig) {
videoInfo
if settings.effectiveShowDescription {
Divider()
.padding(.horizontal)
descriptionContent
.padding(.vertical)
}
}
.onChange(of: video.id) { _, _ in
showOriginalTitle = false
}
}
// MARK: - Video Info
private var videoInfo: some View {
VStack(alignment: .leading, spacing: 8) {
// Title - full width (shows DeArrow title if available, tap toggles)
Text(displayTitle)
.font(.title3)
.fontWeight(.semibold)
.lineLimit(3)
.onTapGesture {
if deArrowTitle != nil {
showOriginalTitle.toggle()
}
}
// Stats row - only show for non-media-source videos
if settings.effectiveShowStatsRow, !video.isFromMediaSource {
VideoStatsRow(
playerState: playerState,
showFormattedDate: $showFormattedDate,
returnYouTubeDislikeEnabled: appEnvironment?.settingsManager.returnYouTubeDislikeEnabled ?? false
)
}
// Channel row with context menu
if settings.effectiveShowChannelRow {
VideoChannelRow(
author: video.author,
source: video.id.source,
yatteeServerURL: yatteeServerURL,
onChannelTap: context.onChannelTap,
video: video,
accentColor: context.accentColor,
showSubscriberCount: !video.isFromMediaSource,
isLoadingDetails: videoDetailsState == .loading
)
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
// MARK: - Description
private var descriptionContent: some View {
let description = video.description ?? ""
return VStack(alignment: .leading, spacing: 8) {
if !description.isEmpty {
Text(DescriptionText.attributed(description, linkColor: context.accentColor))
.font(.subheadline)
.foregroundStyle(.secondary)
.tint(context.accentColor)
.padding(.horizontal)
.handleTimestampLinks(using: playerService)
} else if videoDetailsState == .loading {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding()
} else {
Text(String(localized: "player.noDescription"))
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.horizontal)
}
}
}
}
#endif