diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index bc6b807c..def85ef9 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -72,7 +72,9 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { #if os(iOS) player.playerSize = .init(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) #endif - player.queue = Video.allFixtures.map { PlayerQueueItem($0) } + let local = (1 ... 10).map { Video.local(URL(string: "https://\($0)")!) } + let videos = Video.allFixtures + local + player.queue = videos.map { PlayerQueueItem($0) } return player } diff --git a/Model/CommentsModel.swift b/Model/CommentsModel.swift index 8dbc3fc1..6f544eb7 100644 --- a/Model/CommentsModel.swift +++ b/Model/CommentsModel.swift @@ -25,6 +25,11 @@ final class CommentsModel: ObservableObject { !(nextPage?.isEmpty ?? true) } + func loadIfNeeded() { + guard !loaded else { return } + load() + } + func load(page: String? = nil) { guard let video = player.currentVideo else { return } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 71ef8710..c3c74149 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -212,7 +212,11 @@ struct PlayerControls: View { var detailsHeight: Double { guard let player, player.playerSize.height.isFinite else { return 200 } - return [player.playerSize.height, 500].min()! + var inset = 0.0 + #if os(iOS) + inset = SafeArea.insets.bottom + #endif + return [player.playerSize.height - inset, 500].min()! } @ViewBuilder var controlsBackground: some View { diff --git a/Shared/Player/Controls/VideoDetailsOverlay.swift b/Shared/Player/Controls/VideoDetailsOverlay.swift index a1088a10..33430b7e 100644 --- a/Shared/Player/Controls/VideoDetailsOverlay.swift +++ b/Shared/Player/Controls/VideoDetailsOverlay.swift @@ -5,8 +5,7 @@ struct VideoDetailsOverlay: View { @EnvironmentObject private var controls var body: some View { - VideoDetails(sidebarQueue: false, fullScreen: fullScreenBinding) - .modifier(ControlBackgroundModifier()) + VideoDetails(sidebarQueue: .constant(false), fullScreen: fullScreenBinding) .clipShape(RoundedRectangle(cornerRadius: 4)) } diff --git a/Shared/Player/PlayerQueueRow.swift b/Shared/Player/PlayerQueueRow.swift index e17e563e..d277c425 100644 --- a/Shared/Player/PlayerQueueRow.swift +++ b/Shared/Player/PlayerQueueRow.swift @@ -29,9 +29,12 @@ struct PlayerQueueRow: View { var body: some View { Button { + guard let video = item.video else { + return + } #if os(iOS) - guard !item.video.localStreamIsDirectory else { - if let url = item.video?.localStream?.localURL { + guard !video.localStreamIsDirectory else { + if let url = video.localStream?.localURL { withAnimation { DocumentsModel.shared.goToURL(url) } @@ -40,7 +43,7 @@ struct PlayerQueueRow: View { } #endif - if item.video.localStreamIsFile, let url = item.video.localStream?.localURL { + if video.localStreamIsFile, let url = video.localStream?.localURL { URLBookmarkModel.shared.saveBookmark(url) } @@ -48,7 +51,7 @@ struct PlayerQueueRow: View { player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture - player.videoBeingOpened = item.video + player.videoBeingOpened = video let playItem = { if history { diff --git a/Shared/Player/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift similarity index 100% rename from Shared/Player/ChapterView.swift rename to Shared/Player/Video Details/ChapterView.swift diff --git a/Shared/Player/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift similarity index 100% rename from Shared/Player/ChaptersView.swift rename to Shared/Player/Video Details/ChaptersView.swift diff --git a/Shared/Player/CommentView.swift b/Shared/Player/Video Details/CommentView.swift similarity index 100% rename from Shared/Player/CommentView.swift rename to Shared/Player/Video Details/CommentView.swift diff --git a/Shared/Player/CommentsView.swift b/Shared/Player/Video Details/CommentsView.swift similarity index 100% rename from Shared/Player/CommentsView.swift rename to Shared/Player/Video Details/CommentsView.swift diff --git a/Shared/Player/Video Details/InspectorView.swift b/Shared/Player/Video Details/InspectorView.swift new file mode 100644 index 00000000..12788354 --- /dev/null +++ b/Shared/Player/Video Details/InspectorView.swift @@ -0,0 +1,85 @@ +import SwiftUI + +struct InspectorView: View { + var video: Video? + + @EnvironmentObject private var player + + var body: some View { + ScrollView { + VStack(spacing: 4) { + if let video { + Group { + if player.activeBackend == .mpv, player.mpvBackend.videoFormat != "unknown" { + videoDetailGroupHeading("Video") + + videoDetailRow("Format", value: player.mpvBackend.videoFormat) + videoDetailRow("Codec", value: player.mpvBackend.videoCodec) + videoDetailRow("Hardware Decoder", value: player.mpvBackend.hwDecoder) + videoDetailRow("Driver", value: player.mpvBackend.currentVo) + videoDetailRow("Size", value: player.formattedSize) + videoDetailRow("FPS", value: player.mpvBackend.formattedOutputFps) + } else if player.activeBackend == .appleAVPlayer, let width = player.backend.videoWidth, width > 0 { + videoDetailGroupHeading("Video") + videoDetailRow("Size", value: player.formattedSize) + } + } + + if player.activeBackend == .mpv, player.mpvBackend.audioFormat != "unknown" { + Group { + videoDetailGroupHeading("Audio") + videoDetailRow("Format", value: player.mpvBackend.audioFormat) + videoDetailRow("Codec", value: player.mpvBackend.audioCodec) + videoDetailRow("Driver", value: player.mpvBackend.currentAo) + videoDetailRow("Channels", value: player.mpvBackend.audioChannels) + videoDetailRow("Sample Rate", value: player.mpvBackend.audioSampleRate) + } + } + + if video.localStream != nil || video.localStreamFileExtension != nil { + videoDetailGroupHeading("File") + } + + if let fileExtension = video.localStreamFileExtension { + videoDetailRow("File Extension", value: fileExtension) + } + + if let url = video.localStream?.localURL, video.localStreamIsRemoteURL { + videoDetailRow("URL", value: url.absoluteString) + } + } + } + } + .padding(.horizontal) + } + + @ViewBuilder func videoDetailGroupHeading(_ heading: String) -> some View { + Text(heading.uppercased()) + .font(.footnote) + .foregroundColor(.secondary) + } + + @ViewBuilder func videoDetailRow(_ detail: String, value: String) -> some View { + HStack { + Text(detail) + .foregroundColor(.secondary) + Spacer() + let value = Text(value) + if #available(iOS 15.0, macOS 12.0, *) { + value + #if !os(tvOS) + .textSelection(.enabled) + #endif + } else { + value + } + } + .font(.caption) + } +} + +struct InspectorView_Previews: PreviewProvider { + static var previews: some View { + InspectorView(video: .fixture) + } +} diff --git a/Shared/Player/NoCommentsView.swift b/Shared/Player/Video Details/NoCommentsView.swift similarity index 100% rename from Shared/Player/NoCommentsView.swift rename to Shared/Player/Video Details/NoCommentsView.swift diff --git a/Shared/Player/PlayerQueueView.swift b/Shared/Player/Video Details/PlayerQueueView.swift similarity index 100% rename from Shared/Player/PlayerQueueView.swift rename to Shared/Player/Video Details/PlayerQueueView.swift diff --git a/Shared/Player/Video Details/VideoActions.swift b/Shared/Player/Video Details/VideoActions.swift new file mode 100644 index 00000000..ec86faae --- /dev/null +++ b/Shared/Player/Video Details/VideoActions.swift @@ -0,0 +1,142 @@ +import Defaults +import SwiftUI + +struct VideoActions: View { + @EnvironmentObject private var accounts + @EnvironmentObject private var navigation + @EnvironmentObject private var subscriptions + @EnvironmentObject private var player + + var video: Video? + + var body: some View { + HStack { + if let video { + ShareButton(contentItem: .init(video: video)) { + actionButton("Share", systemImage: "square.and.arrow.up") + } + + Spacer() + + actionButton("Add", systemImage: "text.badge.plus") { + navigation.presentAddToPlaylist(video) + } + if accounts.app.supportsSubscriptions, accounts.signedIn { + Spacer() + if subscriptions.isSubscribing(video.channel.id) { + actionButton("Unsubscribe", systemImage: "xmark.circle") { + #if os(tvOS) + subscriptions.unsubscribe(video.channel.id) + #else + navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions) + #endif + } + } else { + actionButton("Subscribe", systemImage: "star.circle") { + subscriptions.subscribe(video.channel.id) { + navigation.sidebarSectionChanged.toggle() + } + } + } + } + } + Spacer() + + actionButton("Hide", systemImage: "chevron.down") { + player.hide(animate: true) + } + if player.currentItem != nil { + Spacer() + actionButton("Close", systemImage: "xmark") { + player.closeCurrentItem() + } + } + } + .padding(.horizontal) + .borderBottom(height: 0.4, color: Color("ControlsBorderColor")) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .frame(height: 50) + .foregroundColor(.accentColor) + } + + func actionButton( + _ name: String, + systemImage: String, + action: @escaping () -> Void = {} + ) -> some View { + Button(action: action) { + VStack(spacing: 3) { + Image(systemName: systemImage) + .frame(width: 20, height: 20) + Text(name) + .foregroundColor(.secondary) + .font(.caption2) + } + .padding(.horizontal, 10) + .padding(.vertical, 5) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(Text(name)) + } + + @ViewBuilder var videoProperties: some View { + HStack(spacing: 2) { + publishedDateSection + + Spacer() + + HStack(spacing: 4) { + Image(systemName: "eye") + + if let views = video?.viewsCount, player.videoBeingOpened.isNil { + Text(views) + } else { + Text("1,234M").redacted(reason: .placeholder) + } + + Image(systemName: "hand.thumbsup") + + if let likes = video?.likesCount, player.videoBeingOpened.isNil { + Text(likes) + } else { + Text("1,234M").redacted(reason: .placeholder) + } + + if Defaults[.enableReturnYouTubeDislike] { + Image(systemName: "hand.thumbsdown") + + if let dislikes = video?.dislikesCount, player.videoBeingOpened.isNil { + Text(dislikes) + } else { + Text("1,234M").redacted(reason: .placeholder) + } + } + } + } + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + + var publishedDateSection: some View { + Group { + if let video { + HStack(spacing: 4) { + if let published = video.publishedDate { + Text(published) + } else { + Text("1 century ago").redacted(reason: .placeholder) + } + } + } + } + } +} + +struct VideoActions_Previews: PreviewProvider { + static var previews: some View { + VideoActions() + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Player/VideoDescription.swift b/Shared/Player/Video Details/VideoDescription.swift similarity index 100% rename from Shared/Player/VideoDescription.swift rename to Shared/Player/Video Details/VideoDescription.swift diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift new file mode 100644 index 00000000..206f7614 --- /dev/null +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -0,0 +1,177 @@ +import Defaults +import Foundation +import SDWebImageSwiftUI +import SwiftUI + +struct VideoDetails: View { + enum DetailsPage: String, CaseIterable, Defaults.Serializable { + case info, inspector, chapters, comments, related, queue + } + + @Binding var sidebarQueue: Bool + @Binding var fullScreen: Bool + var bottomPadding = false + + @State private var subscribed = false + @State private var subscriptionToggleButtonDisabled = false + + @State private var page = DetailsPage.queue + + @Environment(\.navigationStyle) private var navigationStyle + #if os(iOS) + @Environment(\.verticalSizeClass) private var verticalSizeClass + #endif + + @Environment(\.colorScheme) private var colorScheme + + @EnvironmentObject private var accounts + @EnvironmentObject private var comments + @EnvironmentObject private var navigation + @EnvironmentObject private var player + @EnvironmentObject private var recents + @EnvironmentObject private var subscriptions + + @Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle + @Default(.playerSidebar) private var playerSidebar + + var video: Video? { + player.currentVideo + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ControlsBar( + fullScreen: $fullScreen, + presentingControls: false, + backgroundEnabled: false, + borderTop: false, + detailsTogglePlayer: false, + detailsToggleFullScreen: true + ) + + VideoActions(video: video) + + ZStack(alignment: .bottom) { + currentPage + .transition(.fade) + + HStack(alignment: .center) { + Spacer() + VideoDetailsToolbar(video: video, page: $page, sidebarQueue: sidebarQueue) + Spacer() + } + #if os(iOS) + .offset(y: bottomPadding ? -SafeArea.insets.bottom : 0) + #endif + } + .onChange(of: player.currentItem) { newItem in + guard let newItem else { + page = sidebarQueue ? .inspector : .queue + return + } + + if let video = newItem.video { + page = video.isLocal ? .inspector : .info + } else { + page = sidebarQueue ? .inspector : .queue + } + } + } + .onAppear { + page = sidebarQueue ? .inspector : .queue + + guard video != nil, accounts.app.supportsSubscriptions else { + subscribed = false + return + } + } + .onChange(of: sidebarQueue) { queue in + if queue { + if page == .related || page == .queue { + page = video.isNil || video!.isLocal ? .inspector : .info + } + } else if video.isNil { + page = .inspector + } + } + .overlay(GeometryReader { proxy in + Color.clear + .onAppear { + detailsSize = proxy.size + } + .onChange(of: proxy.size) { newSize in + detailsSize = newSize + } + }) + .background(colorScheme == .dark ? Color.black : .white) + } + + private var contentItem: ContentItem { + ContentItem(video: player.currentVideo) + } + + var currentPage: some View { + VStack { + switch page { + case .info: + detailsPage + + case .inspector: + InspectorView(video: video) + + case .chapters: + ChaptersView() + + case .comments: + CommentsView(embedInScrollView: true) + .onAppear { + comments.loadIfNeeded() + } + + case .related: + RelatedView() + + case .queue: + PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen) + } + } + .contentShape(Rectangle()) + } + + @State private var detailsSize = CGSize.zero + + var detailsPage: some View { + ScrollView(.vertical, showsIndicators: false) { + if let video { + VStack(alignment: .leading, spacing: 10) { + if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { + VStack(alignment: .leading, spacing: 0) { + ForEach(1 ... Int.random(in: 2 ... 5), id: \.self) { _ in + Text(String(repeating: Video.fixture.description ?? "", count: Int.random(in: 1 ... 4))) + } + } + .redacted(reason: .placeholder) + } else if video.description != nil, !video.description!.isEmpty { + VideoDescription(video: video, detailsSize: detailsSize) + #if os(iOS) + .padding(.bottom, player.playingFullScreen ? 10 : SafeArea.insets.bottom) + #endif + } else if !video.isLocal { + Text("No description") + .foregroundColor(.secondary) + } + } + .padding(.top, 10) + .padding(.bottom, 60) + } + } + .padding(.horizontal) + } +} + +struct VideoDetails_Previews: PreviewProvider { + static var previews: some View { + VideoDetails(sidebarQueue: .constant(true), fullScreen: .constant(false)) + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Player/VideoDetailsPaddingModifier.swift b/Shared/Player/Video Details/VideoDetailsPaddingModifier.swift similarity index 100% rename from Shared/Player/VideoDetailsPaddingModifier.swift rename to Shared/Player/Video Details/VideoDetailsPaddingModifier.swift diff --git a/Shared/Player/Video Details/VideoDetailsTool.swift b/Shared/Player/Video Details/VideoDetailsTool.swift new file mode 100644 index 00000000..87254ec5 --- /dev/null +++ b/Shared/Player/Video Details/VideoDetailsTool.swift @@ -0,0 +1,32 @@ +import Foundation + +struct VideoDetailsTool: Identifiable { + var id: String { + page.rawValue + } + + var icon: String + var name: String + var toolPostion: CGRect = .zero + var page = VideoDetails.DetailsPage.info + + func isAvailable(for video: Video?, sidebarQueue: Bool) -> Bool { + guard !YatteeApp.isForPreviews else { + return true + } + switch page { + case .info: + return video != nil && !video!.isLocal + case .inspector: + return true + case .chapters: + return video != nil && !video!.chapters.isEmpty + case .comments: + return video != nil && !video!.isLocal + case .related: + return !sidebarQueue && video != nil && !video!.isLocal + case .queue: + return !sidebarQueue + } + } +} diff --git a/Shared/Player/Video Details/VideoDetailsToolbar.swift b/Shared/Player/Video Details/VideoDetailsToolbar.swift new file mode 100644 index 00000000..1a0401f9 --- /dev/null +++ b/Shared/Player/Video Details/VideoDetailsToolbar.swift @@ -0,0 +1,133 @@ +import SwiftUI + +struct VideoDetailsToolbar: View { + var video: Video? + @Binding var page: VideoDetails.DetailsPage + var sidebarQueue: Bool + + @State private var tools: [VideoDetailsTool] = [ + .init(icon: "info.circle", name: "Info", page: .info), + .init(icon: "wand.and.stars", name: "Inspector", page: .inspector), + .init(icon: "bookmark", name: "Chapters", page: .chapters), + .init(icon: "text.bubble", name: "Comments", page: .comments), + .init(icon: "rectangle.stack.fill", name: "Related", page: .related), + .init(icon: "list.number", name: "Queue", page: .queue) + ] + + @State private var activeTool: VideoDetailsTool? + @State private var startedToolPosition: CGRect = .zero + @State private var opacity = 1.0 + + var body: some View { + Group { + VStack { + HStack(spacing: 12) { + ForEach($tools) { $tool in + if $tool.wrappedValue.isAvailable(for: video, sidebarQueue: sidebarQueue) { + ToolView(tool: $tool) + .padding(.vertical, 10) + } + } + } + .onChange(of: page) { newValue in + activeTool = tools.first { $0.id == newValue.rawValue } + } + .coordinateSpace(name: "toolbarArea") + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + withAnimation(.linear(duration: 0.2)) { + opacity = 1 + } + + guard let firstTool = tools.first else { return } + if startedToolPosition == .zero { + startedToolPosition = firstTool.toolPostion + } + let location = CGPoint(x: value.location.x, y: value.location.y) + + if let index = tools.firstIndex(where: { $0.toolPostion.contains(location) }), + activeTool?.id != tools[index].id, + tools[index].isAvailable(for: video, sidebarQueue: sidebarQueue) + { + withAnimation(.interpolatingSpring(stiffness: 230, damping: 22)) { + activeTool = tools[index] + } + withAnimation(.linear(duration: 0.25)) { + page = activeTool?.page ?? .info + } + } + } + .onEnded { _ in + withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 1, blendDuration: 1)) { + startedToolPosition = .zero + } + Delay.by(2) { + withAnimation(.easeOut(duration: 1)) { + opacity = 0.1 + } + } + } + ) + } + .onAppear { + Delay.by(2) { + withAnimation(.linear(duration: 1)) { + opacity = 0.1 + } + } + } + .opacity(opacity) + } + .background( + Rectangle() + .contentShape(Rectangle()) + .foregroundColor(.clear) + ) + .onHover { hovering in + withAnimation(.linear(duration: 0.1)) { + opacity = hovering ? 1 : 0.1 + } + } + } + + @ViewBuilder func ToolView(tool: Binding) -> some View { + HStack(spacing: 0) { + Image(systemName: tool.wrappedValue.icon) + .font(.title2) + .foregroundColor(.white) + .frame(width: 30, height: 30) + .layoutPriority(1) + + .background( + GeometryReader { proxy in + let frame = proxy.frame(in: .named("toolbarArea")) + Color.clear + .preference(key: RectKey.self, value: frame) + .onPreferenceChange(RectKey.self) { rect in + tool.wrappedValue.toolPostion = rect + } + } + ) + + if activeToolID == tool.wrappedValue.id, false { + Text(tool.wrappedValue.name) + .font(.system(size: 14).bold()) + .foregroundColor(.white) + .allowsTightening(true) + .lineLimit(1) + .layoutPriority(2) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(activeToolID == tool.wrappedValue.id ? Color.accentColor : Color.secondary) + ) + } + + var activeToolID: VideoDetailsTool.ID { + activeTool?.id ?? "queue" + } +} diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift deleted file mode 100644 index d4c1d969..00000000 --- a/Shared/Player/VideoDetails.swift +++ /dev/null @@ -1,396 +0,0 @@ -import Defaults -import Foundation -import SDWebImageSwiftUI -import SwiftUI -import SwiftUIPager - -struct VideoDetails: View { - enum DetailsPage: String, CaseIterable, Defaults.Serializable { - case info, chapters, comments, related, queue - - var index: Int { - switch self { - case .info: - return 0 - case .chapters: - return 1 - case .comments: - return 2 - case .related: - return 3 - case .queue: - return 4 - } - } - } - - var sidebarQueue: Bool - @Binding var fullScreen: Bool - - @State private var subscribed = false - @State private var subscriptionToggleButtonDisabled = false - - @StateObject private var page: Page = .first() - - @Environment(\.navigationStyle) private var navigationStyle - #if os(iOS) - @Environment(\.verticalSizeClass) private var verticalSizeClass - #endif - - @EnvironmentObject private var accounts - @EnvironmentObject private var comments - @EnvironmentObject private var navigation - @EnvironmentObject private var player - @EnvironmentObject private var recents - @EnvironmentObject private var subscriptions - - @Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle - - var currentPage: DetailsPage { - DetailsPage.allCases.first { $0.index == page.index } ?? .info - } - - var video: Video? { - player.currentVideo - } - - var body: some View { - if #available(iOS 15, macOS 12, *) { - Self._printChanges() - } - - return VStack(alignment: .leading, spacing: 0) { - ControlsBar( - fullScreen: $fullScreen, - presentingControls: false, - backgroundEnabled: false, - borderTop: false, - detailsTogglePlayer: false, - detailsToggleFullScreen: true - ) - - HStack(spacing: 4) { - pageButton( - "Info".localized(), - "info.circle", .info, !video.isNil - ) - if let video, !video.isLocal { - pageButton( - "Chapters".localized(), - "bookmark", .chapters, !video.chapters.isEmpty && !video.isLocal - ) - pageButton( - "Comments".localized(), - "text.bubble", .comments, !video.isLocal - ) { comments.load() } - pageButton( - "Related".localized(), - "rectangle.stack.fill", .related, !video.isLocal - ) - } - pageButton( - "Queue".localized(), - "list.number", .queue, !player.queue.isEmpty - ) - } - .onChange(of: player.currentItem) { _ in - page.update(.moveToFirst) - } - .padding(.horizontal) - .padding(.vertical, 6) - - Pager(page: page, data: DetailsPage.allCases, id: \.self) { - if !player.currentItem.isNil || page.index == DetailsPage.queue.index { - detailsByPage($0) - #if os(iOS) - .padding(.bottom, SafeArea.insets.bottom) - #else - .padding(.bottom, 6) - #endif - } else { - VStack {} - } - } - - .onPageWillChange { pageIndex in - if pageIndex == DetailsPage.comments.index { - comments.load() - } - } - .frame(maxWidth: detailsSize.width) - } - .onAppear { - page.update(.moveToFirst) - - guard video != nil, accounts.app.supportsSubscriptions else { - subscribed = false - return - } - } - .onChange(of: sidebarQueue) { queue in - if queue { - if currentPage == .related || currentPage == .queue { - page.update(.moveToFirst) - } - } else if video.isNil { - page.update(.moveToLast) - } - } - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) - .overlay(GeometryReader { proxy in - Color.clear - .onAppear { - detailsSize = proxy.size - } - .onChange(of: proxy.size) { newSize in - detailsSize = newSize - } - }) - } - - var publishedDateSection: some View { - Group { - if let video { - HStack(spacing: 4) { - if let published = video.publishedDate { - Text(published) - } else { - Text("1 century ago").redacted(reason: .placeholder) - } - } - } - } - } - - private var contentItem: ContentItem { - ContentItem(video: player.currentVideo) - } - - func pageButton( - _ label: String, - _ symbolName: String, - _ destination: DetailsPage, - _ active: Bool = true, - pageChangeAction: (() -> Void)? = nil - ) -> some View { - Button(action: { - page.update(.new(index: destination.index)) - pageChangeAction?() - }) { - HStack { - Spacer() - - HStack(spacing: 4) { - Image(systemName: symbolName) - - if playerDetailsPageButtonLabelStyle.text && player.playerSize.width > 450 { - Text(label) - } - } - .frame(minHeight: 15) - .lineLimit(1) - .padding(.vertical, 4) - .foregroundColor(currentPage == destination ? .white : (active ? Color.accentColor : .gray)) - - Spacer() - } - .contentShape(Rectangle()) - } - .background(currentPage == destination ? (active ? Color.accentColor : .gray) : .clear) - .buttonStyle(.plain) - .font(.system(size: 10).bold()) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(active ? Color.accentColor : .gray, lineWidth: 1.2) - .foregroundColor(.clear) - ) - .frame(maxWidth: .infinity) - } - - @ViewBuilder func detailsByPage(_ page: DetailsPage) -> some View { - Group { - switch page { - case .info: - ScrollView(.vertical, showsIndicators: false) { - detailsPage - } - case .chapters: - ChaptersView() - - case .comments: - CommentsView(embedInScrollView: true) - - case .related: - RelatedView() - - case .queue: - PlayerQueueView(sidebarQueue: sidebarQueue, fullScreen: $fullScreen) - } - } - .contentShape(Rectangle()) - } - - @State private var detailsSize = CGSize.zero - - var detailsPage: some View { - VStack(alignment: .leading, spacing: 0) { - if let video { - if !video.isLocal { - VStack(spacing: 6) { - videoProperties - - Divider() - } - .padding(.bottom, 6) - } - - VStack(alignment: .leading, spacing: 10) { - if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { - VStack(alignment: .leading, spacing: 0) { - ForEach(1 ... Int.random(in: 2 ... 5), id: \.self) { _ in - Text(String(repeating: Video.fixture.description ?? "", count: Int.random(in: 1 ... 4))) - } - } - .redacted(reason: .placeholder) - } else if video.description != nil, !video.description!.isEmpty { - VideoDescription(video: video, detailsSize: detailsSize) - #if os(iOS) - .padding(.bottom, player.playingFullScreen ? 10 : SafeArea.insets.bottom) - #endif - } else if !video.isLocal { - Text("No description") - .foregroundColor(.secondary) - } - } - - VStack(spacing: 4) { - Group { - if player.activeBackend == .mpv, player.mpvBackend.videoFormat != "unknown" { - videoDetailGroupHeading("Video") - - videoDetailRow("Format", value: player.mpvBackend.videoFormat) - videoDetailRow("Codec", value: player.mpvBackend.videoCodec) - videoDetailRow("Hardware Decoder", value: player.mpvBackend.hwDecoder) - videoDetailRow("Driver", value: player.mpvBackend.currentVo) - videoDetailRow("Size", value: player.formattedSize) - videoDetailRow("FPS", value: player.mpvBackend.formattedOutputFps) - } else if player.activeBackend == .appleAVPlayer, let width = player.backend.videoWidth, width > 0 { - videoDetailGroupHeading("Video") - videoDetailRow("Size", value: player.formattedSize) - } - } - - if player.activeBackend == .mpv, player.mpvBackend.audioFormat != "unknown" { - Group { - videoDetailGroupHeading("Audio") - videoDetailRow("Format", value: player.mpvBackend.audioFormat) - videoDetailRow("Codec", value: player.mpvBackend.audioCodec) - videoDetailRow("Driver", value: player.mpvBackend.currentAo) - videoDetailRow("Channels", value: player.mpvBackend.audioChannels) - videoDetailRow("Sample Rate", value: player.mpvBackend.audioSampleRate) - } - } - - if video.localStream != nil || video.localStreamFileExtension != nil { - videoDetailGroupHeading("File") - } - - if let fileExtension = video.localStreamFileExtension { - videoDetailRow("File Extension", value: fileExtension) - } - - if let url = video.localStream?.localURL, video.localStreamIsRemoteURL { - videoDetailRow("URL", value: url.absoluteString) - } - } - .padding(.bottom, 6) - } - } - .padding(.horizontal) - } - - @ViewBuilder func videoDetailGroupHeading(_ heading: String) -> some View { - Text(heading.uppercased()) - .font(.footnote) - .foregroundColor(.secondary) - } - - @ViewBuilder func videoDetailRow(_ detail: String, value: String) -> some View { - HStack { - Text(detail) - .foregroundColor(.secondary) - Spacer() - let value = Text(value) - if #available(iOS 15.0, macOS 12.0, *) { - value - #if !os(tvOS) - .textSelection(.enabled) - #endif - } else { - value - } - } - .font(.caption) - } - - @ViewBuilder var videoProperties: some View { - HStack(spacing: 2) { - publishedDateSection - - Spacer() - - HStack(spacing: 4) { - Image(systemName: "eye") - - if let views = video?.viewsCount, player.videoBeingOpened.isNil { - Text(views) - } else { - Text("1,234M").redacted(reason: .placeholder) - } - - Image(systemName: "hand.thumbsup") - - if let likes = video?.likesCount, player.videoBeingOpened.isNil { - Text(likes) - } else { - Text("1,234M").redacted(reason: .placeholder) - } - - if Defaults[.enableReturnYouTubeDislike] { - Image(systemName: "hand.thumbsdown") - - if let dislikes = video?.dislikesCount, player.videoBeingOpened.isNil { - Text(dislikes) - } else { - Text("1,234M").redacted(reason: .placeholder) - } - } - } - } - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - - func videoDetail(label: String, value: String, symbol: String) -> some View { - VStack(spacing: 4) { - HStack(spacing: 2) { - Image(systemName: symbol) - - Text(label.uppercased()) - } - .font(.system(size: 9)) - .opacity(0.6) - - Text(value) - } - - .frame(maxWidth: 100) - } -} - -struct VideoDetails_Previews: PreviewProvider { - static var previews: some View { - VideoDetails(sidebarQueue: true, fullScreen: .constant(false)) - .injectFixtureEnvironmentObjects() - } -} diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 86fc951d..eda29c0e 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -28,9 +28,7 @@ struct VideoPlayerView: View { #endif } - @State private var playerSize: CGSize = .zero { didSet { - sidebarQueue = playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits - }} + @State private var playerSize: CGSize = .zero { didSet { updateSidebarQueue() } } @State private var hoveringPlayer = false @State private var fullScreenDetails = false @State private var sidebarQueue = defaultSidebarQueueValue @@ -70,6 +68,7 @@ struct VideoPlayerView: View { @Default(.horizontalPlayerGestureEnabled) var horizontalPlayerGestureEnabled @Default(.seekGestureSpeed) var seekGestureSpeed @Default(.seekGestureSensitivity) var seekGestureSensitivity + @Default(.playerSidebar) var playerSidebar @ObservedObject internal var controlsOverlayModel = ControlOverlaysModel.shared @@ -87,6 +86,10 @@ struct VideoPlayerView: View { if player.musicMode { player.backend.startControlsUpdates() } + updateSidebarQueue() + } + .onChange(of: playerSidebar) { _ in + updateSidebarQueue() } } @@ -190,6 +193,14 @@ struct VideoPlayerView: View { #endif } + func updateSidebarQueue() { + #if os(iOS) + sidebarQueue = playerSize.width > 900 && playerSidebar == .whenFits + #elseif os(macOS) + sidebarQueue = playerSidebar != .never + #endif + } + var overlay: some View { VStack { if controlsOverlayModel.presenting { @@ -328,11 +339,10 @@ struct VideoPlayerView: View { #if !os(tvOS) if !fullScreenPlayer { - VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) + VideoDetails(sidebarQueue: $sidebarQueue, fullScreen: $fullScreenDetails, bottomPadding: detailsNeedBottomPadding) #if os(iOS) .ignoresSafeArea(.all, edges: .bottom) #endif - .background(colorScheme == .dark ? Color.black : Color.white) .modifier(VideoDetailsPaddingModifier( playerSize: player.playerSize, fullScreen: fullScreenDetails @@ -411,6 +421,14 @@ struct VideoPlayerView: View { #endif } + var detailsNeedBottomPadding: Bool { + #if os(iOS) + return true + #else + return false + #endif + } + var fullScreenPlayer: Bool { #if os(iOS) player.playingFullScreen || verticalSizeClass == .compact @@ -449,6 +467,7 @@ struct VideoPlayerView: View { Image(systemName: "xmark") .font(.system(size: 40)) } + .opacity(fullScreenPlayer ? 1 : 0) .buttonStyle(.plain) .padding(10) .foregroundColor(.gray) diff --git a/Shared/PreferenceKeys.swift b/Shared/PreferenceKeys.swift new file mode 100644 index 00000000..f0363249 --- /dev/null +++ b/Shared/PreferenceKeys.swift @@ -0,0 +1,9 @@ +import Foundation +import SwiftUI + +struct RectKey: PreferenceKey { + static var defaultValue: CGRect = .zero + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } +} diff --git a/Shared/Views/ControlsBar.swift b/Shared/Views/ControlsBar.swift index 4abd0bdb..2d883c99 100644 --- a/Shared/Views/ControlsBar.swift +++ b/Shared/Views/ControlsBar.swift @@ -137,20 +137,32 @@ struct ControlsBar: View { var details: some View { HStack { HStack(spacing: 8) { - ZStack(alignment: .bottomTrailing) { - authorAvatar + Button { + if let video = model.currentVideo, !video.isLocal { + NavigationModel.openChannel( + video.channel, + player: model, + recents: recents, + navigation: navigation, + navigationStyle: navigationStyle + ) + } + } label: { + ZStack(alignment: .bottomTrailing) { + authorAvatar - if accounts.app.supportsSubscriptions, - accounts.signedIn, - let video = model.currentVideo, - subscriptions.isSubscribing(video.channel.id) - { - Image(systemName: "star.circle.fill") - #if !os(tvOS) - .background(Color.background) - #endif - .clipShape(Circle()) - .foregroundColor(.secondary) + if accounts.app.supportsSubscriptions, + accounts.signedIn, + let video = model.currentVideo, + subscriptions.isSubscribing(video.channel.id) + { + Image(systemName: "star.circle.fill") + #if !os(tvOS) + .background(Color.background) + #endif + .clipShape(Circle()) + .foregroundColor(.secondary) + } } } .contextMenu { diff --git a/Shared/Views/ShareButton.swift b/Shared/Views/ShareButton.swift index 65ee6ab4..62d4304b 100644 --- a/Shared/Views/ShareButton.swift +++ b/Shared/Views/ShareButton.swift @@ -1,14 +1,22 @@ import SwiftUI -struct ShareButton: View { +struct ShareButton: View { let contentItem: ContentItem @EnvironmentObject private var accounts @EnvironmentObject private var navigation @EnvironmentObject private var player - init(contentItem: ContentItem) { + let label: LabelView? + + init( + contentItem: ContentItem, + @ViewBuilder label: @escaping () -> LabelView? = { + Label("Share...", systemImage: "square.and.arrow.up") + } + ) { self.contentItem = contentItem + self.label = label() } @ViewBuilder var body: some View { @@ -24,11 +32,11 @@ struct ShareButton: View { } } } label: { - Label("Share...", systemImage: "square.and.arrow.up") + label } .menuStyle(.borderlessButton) #if os(macOS) - .frame(maxWidth: 35) + .frame(maxWidth: 60) #endif } } diff --git a/Shared/Views/VideoContextMenuView.swift b/Shared/Views/VideoContextMenuView.swift index 1f4adda3..f06e8585 100644 --- a/Shared/Views/VideoContextMenuView.swift +++ b/Shared/Views/VideoContextMenuView.swift @@ -90,7 +90,10 @@ struct VideoContextMenuView: View { } #if os(iOS) - if video.isLocal, let url = video.localStream?.localURL, DocumentsModel.shared.isDocument(url) { + if video.isLocal, + let url = video.localStream?.localURL, + DocumentsModel.shared.isDocument(url) + { Section { removeDocumentButton } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index d30f4c07..fd00c4dd 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -307,6 +307,18 @@ 374924DA2921050B0017D862 /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924D92921050B0017D862 /* LocationsSettings.swift */; }; 374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924D92921050B0017D862 /* LocationsSettings.swift */; }; 374924DC2921050B0017D862 /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924D92921050B0017D862 /* LocationsSettings.swift */; }; + 374924E0292126A00017D862 /* VideoDetailsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */; }; + 374924E1292126A00017D862 /* VideoDetailsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */; }; + 374924E3292141320017D862 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E2292141320017D862 /* InspectorView.swift */; }; + 374924E4292141320017D862 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E2292141320017D862 /* InspectorView.swift */; }; + 374924E729215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */; }; + 374924E829215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */; }; + 374924EA2921666E0017D862 /* VideoDetailsTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E92921666E0017D862 /* VideoDetailsTool.swift */; }; + 374924EB2921666E0017D862 /* VideoDetailsTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E92921666E0017D862 /* VideoDetailsTool.swift */; }; + 374924ED2921669B0017D862 /* PreferenceKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EC2921669B0017D862 /* PreferenceKeys.swift */; }; + 374924EE2921669B0017D862 /* PreferenceKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EC2921669B0017D862 /* PreferenceKeys.swift */; }; + 374924F029216C630017D862 /* VideoActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EF29216C630017D862 /* VideoActions.swift */; }; + 374924F129216C630017D862 /* VideoActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EF29216C630017D862 /* VideoActions.swift */; }; 37494EA529200B14000DF176 /* DocumentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37494EA429200B14000DF176 /* DocumentsView.swift */; }; 37494EA729200E0B000DF176 /* DocumentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37494EA629200E0B000DF176 /* DocumentsModel.swift */; }; 374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; }; @@ -870,8 +882,6 @@ 37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; }; 37F7D82D289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; }; 37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */; }; - 37F9619B27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */; }; - 37F9619C27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */; }; 37F9619F27BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; }; 37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; }; 37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; }; @@ -1100,6 +1110,12 @@ 37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = ""; }; 374924D92921050B0017D862 /* LocationsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationsSettings.swift; sourceTree = ""; }; 374924DE29211F5F0017D862 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; + 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoDetailsToolbar.swift; sourceTree = ""; }; + 374924E2292141320017D862 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; + 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = ""; }; + 374924E92921666E0017D862 /* VideoDetailsTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsTool.swift; sourceTree = ""; }; + 374924EC2921669B0017D862 /* PreferenceKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceKeys.swift; sourceTree = ""; }; + 374924EF29216C630017D862 /* VideoActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActions.swift; sourceTree = ""; }; 37494EA429200B14000DF176 /* DocumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsView.swift; sourceTree = ""; }; 37494EA629200E0B000DF176 /* DocumentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsModel.swift; sourceTree = ""; }; 3749BF6F27ADA135000480FF /* client.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = client.h; sourceTree = ""; }; @@ -1331,7 +1347,6 @@ 37F7AB4E28A94E0600FB46B5 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 37F7AB5428A951B200FB46B5 /* Power.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Power.swift; sourceTree = ""; }; 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPickerModifier.swift; sourceTree = ""; }; - 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = ""; }; 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendType.swift; sourceTree = ""; }; 37FADFFF272ED58000330459 /* EditFavorites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFavorites.swift; sourceTree = ""; }; 37FB28402721B22200A57617 /* ContentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItem.swift; sourceTree = ""; }; @@ -1620,28 +1635,20 @@ 371AAE2426CEBA4100901972 /* Player */ = { isa = PBXGroup; children = ( + 374924E529215F560017D862 /* Video Details */, 371114F227B9552400C2EF7B /* Controls */, 375E45F327B1973400BA7902 /* MPV */, 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */, 37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */, - 3752069C285E910600CA655F /* ChapterView.swift */, - 37192D5628B179D60012EEDD /* ChaptersView.swift */, - 371B7E602759706A00D21217 /* CommentsView.swift */, - 37EF9A75275BEB8E0043B585 /* CommentView.swift */, - 37DD9DA22785BBC900539416 /* NoCommentsView.swift */, 375F740F289DC35A00747050 /* PlayerBackendView.swift */, 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */, 3703100127B0713600ECDDAA /* PlayerGestures.swift */, 373031F22838388A000CFD59 /* PlayerLayerView.swift */, 374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, - 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */, 373197D82732015300EF734F /* RelatedView.swift */, 3795593527B08538007FF8F4 /* StreamControl.swift */, - 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */, - 37B81AFE26D2CA3700675966 /* VideoDetails.swift */, - 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */, - 37CFB48428AFE2510070024C /* VideoDescription.swift */, + 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */, 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, ); @@ -1831,6 +1838,26 @@ path = Settings; sourceTree = ""; }; + 374924E529215F560017D862 /* Video Details */ = { + isa = PBXGroup; + children = ( + 3752069C285E910600CA655F /* ChapterView.swift */, + 37192D5628B179D60012EEDD /* ChaptersView.swift */, + 371B7E602759706A00D21217 /* CommentsView.swift */, + 37EF9A75275BEB8E0043B585 /* CommentView.swift */, + 374924E2292141320017D862 /* InspectorView.swift */, + 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */, + 37DD9DA22785BBC900539416 /* NoCommentsView.swift */, + 374924EF29216C630017D862 /* VideoActions.swift */, + 37CFB48428AFE2510070024C /* VideoDescription.swift */, + 37B81AFE26D2CA3700675966 /* VideoDetails.swift */, + 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */, + 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */, + 374924E92921666E0017D862 /* VideoDetailsTool.swift */, + ); + path = "Video Details"; + sourceTree = ""; + }; 37494EA329200AD4000DF176 /* Documents */ = { isa = PBXGroup; children = ( @@ -2059,6 +2086,7 @@ 375B537828DF6CBB004C1D19 /* Localizable.strings */, 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */, + 374924EC2921669B0017D862 /* PreferenceKeys.swift */, 37ECED55289FE166002BC2C9 /* SafeArea.swift */, 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, 37FFC43F272734C3009FFD26 /* Throttle.swift */, @@ -2792,6 +2820,7 @@ 374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */, 37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, + 374924E0292126A00017D862 /* VideoDetailsToolbar.swift in Sources */, 37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */, 371B7E612759706A00D21217 /* CommentsView.swift in Sources */, 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */, @@ -2816,6 +2845,7 @@ 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, 37D2E0D428B67EFC00F64D52 /* Delay.swift in Sources */, 3759234628C26C7B00C052EC /* Refreshable+Backport.swift in Sources */, + 374924ED2921669B0017D862 /* PreferenceKeys.swift in Sources */, 37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */, 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */, 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, @@ -2824,6 +2854,7 @@ 3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */, 378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */, 374DE88328BB8A280062BBF2 /* PlayerOrientation.swift in Sources */, + 374924F029216C630017D862 /* VideoActions.swift in Sources */, 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */, 37E80F40287B472300561799 /* ScrollContentBackground+Backport.swift in Sources */, @@ -2926,6 +2957,7 @@ 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, + 374924EA2921666E0017D862 /* VideoDetailsTool.swift in Sources */, 379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */, 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */, @@ -2978,10 +3010,11 @@ 3763C989290C7A50004D3B5F /* OpenVideosView.swift in Sources */, 37F0F4EE286F734400C06C2E /* AdvancedSettings.swift in Sources */, 37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */, + 374924E3292141320017D862 /* InspectorView.swift in Sources */, 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */, 37599F30272B42810087F250 /* FavoriteItem.swift in Sources */, + 374924E729215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */, 373197D92732015300EF734F /* RelatedView.swift in Sources */, - 37F9619B27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */, 37F4AD1B28612B23004D0F66 /* OpeningStream.swift in Sources */, 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 372915E62687E3B900F5A35B /* Defaults.swift in Sources */, @@ -3035,6 +3068,7 @@ 37BE0BDC26A2367F0092E2DB /* AppleAVPlayerView.swift in Sources */, 3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */, 37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */, + 374924F129216C630017D862 /* VideoActions.swift in Sources */, 37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 3703100327B0713600ECDDAA /* PlayerGestures.swift in Sources */, @@ -3048,6 +3082,7 @@ 37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 37DD9DA42785BBC900539416 /* NoCommentsView.swift in Sources */, 37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, + 374924EE2921669B0017D862 /* PreferenceKeys.swift in Sources */, 37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 37F7D82D289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */, @@ -3115,6 +3150,7 @@ 37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */, 37F4AD2028612DFD004D0F66 /* Buffering.swift in Sources */, 375EC973289F2ABF00751258 /* MultiselectRow.swift in Sources */, + 374924E829215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */, 377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */, 37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */, 37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */, @@ -3124,6 +3160,7 @@ 378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */, 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, + 374924E1292126A00017D862 /* VideoDetailsToolbar.swift in Sources */, 37F5E8BB291BEF69006C15F5 /* CacheModel.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 37030FFC27B0398000ECDDAA /* MPVClient.swift in Sources */, @@ -3142,6 +3179,7 @@ 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37579D5E27864F5F00FD0B98 /* Help.swift in Sources */, 37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */, + 374924EB2921666E0017D862 /* VideoDetailsTool.swift in Sources */, 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 3744A96128B99ADD005DE0A7 /* PlayerControlsLayout.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */, @@ -3150,7 +3188,6 @@ 37EFAC0928C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */, 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 376578922685490700D4EA09 /* PlaylistsView.swift in Sources */, - 37F9619C27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */, 37484C2626FC83E000287258 /* InstanceForm.swift in Sources */, 3751BA7E27E63F1D007B1A60 /* MPVOGLView.swift in Sources */, 377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */, @@ -3205,6 +3242,7 @@ 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */, 37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */, + 374924E4292141320017D862 /* InspectorView.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */, diff --git a/tvOS/NowPlayingView.swift b/tvOS/NowPlayingView.swift index ad744a2b..325bd021 100644 --- a/tvOS/NowPlayingView.swift +++ b/tvOS/NowPlayingView.swift @@ -107,7 +107,7 @@ struct NowPlayingView: View { VStack(alignment: .center) { PlaceholderProgressView() .onAppear { - comments.load() + comments.loadIfNeeded() } } } else {