mirror of
https://github.com/yattee/yattee.git
synced 2026-10-27 23:17:18 +00:00
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.
135 lines
4.4 KiB
Swift
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
|