diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index 751e3baa..247c4e13 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -5,7 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { func body(content: Content) -> some View { content .environmentObject(AccountsModel()) - .environmentObject(CommentsModel()) + .environmentObject(comments) .environmentObject(InstancesModel()) .environmentObject(invidious) .environmentObject(NavigationModel()) @@ -21,6 +21,14 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { .environmentObject(ThumbnailsModel()) } + private var comments: CommentsModel { + let comments = CommentsModel() + comments.loaded = true + comments.all = [.fixture] + + return comments + } + private var invidious: InvidiousAPI { let api = InvidiousAPI() diff --git a/Model/NetworkStateModel.swift b/Model/NetworkStateModel.swift index 1cc1c46e..f4d2aa90 100644 --- a/Model/NetworkStateModel.swift +++ b/Model/NetworkStateModel.swift @@ -34,7 +34,7 @@ final class NetworkStateModel: ObservableObject { var needsUpdates: Bool { if let player = player { - return pausedForCache || player.isSeeking || player.isLoadingVideo + return pausedForCache || player.isSeeking || player.isLoadingVideo || player.controls.presentingControlsOverlay } return pausedForCache diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 46019b53..49de8107 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -589,5 +589,5 @@ final class AVPlayerBackend: PlayerBackend { func stopControlsUpdates() {} func setNeedsDrawing(_: Bool) {} func setSize(_: Double, _: Double) {} - func setNeedsNetworkStateUpdates() {} + func setNeedsNetworkStateUpdates(_: Bool) {} } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 9bcc52f8..f5057482 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -28,7 +28,7 @@ final class MPVBackend: PlayerBackend { } self.controls?.isLoadingVideo = self.isLoadingVideo - self.setNeedsNetworkStateUpdates() + self.setNeedsNetworkStateUpdates(true) if !self.isLoadingVideo { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in @@ -476,7 +476,11 @@ final class MPVBackend: PlayerBackend { } } - func setNeedsNetworkStateUpdates() { - networkStateTimer.resume() + func setNeedsNetworkStateUpdates(_ needsUpdates: Bool) { + if needsUpdates { + networkStateTimer.resume() + } else { + networkStateTimer.suspend() + } } } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index ac809517..faa454f0 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -52,7 +52,7 @@ protocol PlayerBackend { func startControlsUpdates() func stopControlsUpdates() - func setNeedsNetworkStateUpdates() + func setNeedsNetworkStateUpdates(_ needsUpdates: Bool) func setNeedsDrawing(_ needsDrawing: Bool) func setSize(_ width: Double, _ height: Double) diff --git a/Model/Player/PlayerControlsModel.swift b/Model/Player/PlayerControlsModel.swift index 1e108b1e..707ee2e4 100644 --- a/Model/Player/PlayerControlsModel.swift +++ b/Model/Player/PlayerControlsModel.swift @@ -6,7 +6,7 @@ final class PlayerControlsModel: ObservableObject { @Published var isLoadingVideo = false @Published var isPlaying = true @Published var presentingControls = false { didSet { handlePresentationChange() } } - @Published var presentingControlsOverlay = false + @Published var presentingControlsOverlay = false { didSet { handleOverlayPresentationChange() } } @Published var timer: Timer? var player: PlayerModel! @@ -40,6 +40,10 @@ final class PlayerControlsModel: ObservableObject { } } + func handleOverlayPresentationChange() { + player?.backend.setNeedsNetworkStateUpdates(presentingControlsOverlay) + } + func show() { guard !(player?.currentItem.isNil ?? true) else { return diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 4ef97d4e..bfc12767 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -67,7 +67,7 @@ final class PlayerModel: ObservableObject { @Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI() @Published var isSeeking = false { didSet { - backend.setNeedsNetworkStateUpdates() + backend.setNeedsNetworkStateUpdates(true) }} #if os(iOS) diff --git a/Shared/Player/ChaptersView.swift b/Shared/Player/ChaptersView.swift index 8d24402b..58534627 100644 --- a/Shared/Player/ChaptersView.swift +++ b/Shared/Player/ChaptersView.swift @@ -6,8 +6,8 @@ struct ChaptersView: View { @EnvironmentObject private var player var body: some View { - List { - if let chapters = player.currentVideo?.chapters, !chapters.isEmpty { + if let chapters = player.currentVideo?.chapters, !chapters.isEmpty { + List { Section(header: Text("Chapters")) { ForEach(chapters) { chapter in Button { @@ -18,18 +18,17 @@ struct ChaptersView: View { .buttonStyle(.plain) } } - } else { - Text(player.currentVideo?.title ?? "") } - } - .id(UUID()) - #if os(macOS) + #if os(macOS) .listStyle(.inset) - #elseif os(iOS) + #elseif os(iOS) .listStyle(.grouped) - #else + #else .listStyle(.plain) - #endif + #endif + } else { + NoCommentsView(text: "No chapters information available", systemImage: "xmark.circle.fill") + } } @ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View { diff --git a/Shared/Player/CommentView.swift b/Shared/Player/CommentView.swift index f0e942d6..281178d7 100644 --- a/Shared/Player/CommentView.swift +++ b/Shared/Player/CommentView.swift @@ -99,6 +99,7 @@ struct CommentView: View { #if os(tvOS) .padding(.horizontal, 20) #endif + .padding(.bottom, 10) } private var authorAvatar: some View { diff --git a/Shared/Player/CommentsView.swift b/Shared/Player/CommentsView.swift index 536c1daf..ae52a118 100644 --- a/Shared/Player/CommentsView.swift +++ b/Shared/Player/CommentsView.swift @@ -22,12 +22,7 @@ struct CommentsView: View { .onAppear { comments.loadNextPageIfNeeded(current: comment) } - .padding(.bottom, comment == last ? 5 : 0) - - if comment != last { - Divider() - .padding(.vertical, 5) - } + .borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor")) } } diff --git a/Shared/Player/Controls/ControlsOverlay.swift b/Shared/Player/Controls/ControlsOverlay.swift index 00f0e56f..b3ad1e8f 100644 --- a/Shared/Player/Controls/ControlsOverlay.swift +++ b/Shared/Player/Controls/ControlsOverlay.swift @@ -2,6 +2,7 @@ import Defaults import SwiftUI struct ControlsOverlay: View { + @EnvironmentObject private var networkState @EnvironmentObject private var player @EnvironmentObject private var model @@ -165,7 +166,7 @@ struct ControlsOverlay: View { Text("hw decoder: \(player.mpvBackend.hwDecoder)") Text("dropped: \(player.mpvBackend.frameDropCount)") Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))") - Text("buffering: \(String(format: "%.0f%%", player.mpvBackend.bufferingState))") + Text("buffering: \(String(format: "%.0f%%", networkState.bufferingState))") Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))") } .mask(RoundedRectangle(cornerRadius: 3)) diff --git a/Shared/Player/Controls/OSD/NetworkState.swift b/Shared/Player/Controls/OSD/NetworkState.swift index bbc01f93..4ece6978 100644 --- a/Shared/Player/Controls/OSD/NetworkState.swift +++ b/Shared/Player/Controls/OSD/NetworkState.swift @@ -6,7 +6,11 @@ struct NetworkState: View { var body: some View { Buffering(state: model.fullStateText) - .opacity(model.pausedForCache || player.isSeeking ? 1 : 0) + .opacity(visible ? 1 : 0) + } + + var visible: Bool { + player.isPlaying && (model.pausedForCache || player.isSeeking) } } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index e8c35b6c..5150d0fd 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -23,8 +23,6 @@ struct PlayerControls: View { @FocusState private var focusedField: Field? #endif - @Default(.controlsBarInPlayer) private var controlsBarInPlayer - init(player: PlayerModel, thumbnails: ThumbnailsModel) { self.player = player self.thumbnails = thumbnails @@ -191,12 +189,13 @@ struct PlayerControls: View { HStack(spacing: 20) { #if !os(tvOS) fullscreenButton - pipButton + + #if os(iOS) + pipButton + #endif Spacer() - button("overlay", systemImage: "info.circle") {} - button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) { withAnimation(Self.animation) { model.presentingControlsOverlay.toggle() diff --git a/Shared/Player/TimelineView.swift b/Shared/Player/TimelineView.swift index cded7dd7..d22c2735 100644 --- a/Shared/Player/TimelineView.swift +++ b/Shared/Player/TimelineView.swift @@ -114,12 +114,13 @@ struct TimelineView: View { ZStack(alignment: .leading) { ZStack(alignment: .leading) { Rectangle() - .fill(Color.gray.opacity(0.1)) + .fill(Color.white.opacity(0.2)) .frame(maxHeight: height) + .offset(x: current * oneUnitWidth) .zIndex(1) Rectangle() - .fill(Color.gray.opacity(0.5)) + .fill(Color.white.opacity(0.6)) .frame(maxHeight: height) .frame(width: current * oneUnitWidth) .zIndex(1) @@ -187,7 +188,7 @@ struct TimelineView: View { #endif } - .background(GeometryReader { proxy in + .overlay(GeometryReader { proxy in Color.clear .onAppear { self.size = proxy.size @@ -265,7 +266,6 @@ struct TimelineView: View { } var segments: [Segment] { - // [.init(category: "outro", segment: [25,30], uuid: UUID().uuidString, videoDuration: 100)] ?? player.sponsorBlock.segments } @@ -290,7 +290,7 @@ struct TimelineView: View { var chaptersLayers: some View { ForEach(chapters) { chapter in RoundedRectangle(cornerRadius: 4) - .fill(Color("AppBlueColor")) + .fill(Color.orange) .frame(maxWidth: 2, maxHeight: 12) .offset(x: (chapter.start * oneUnitWidth) - 1) } diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 57dc571b..b376eb4c 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -53,10 +53,81 @@ struct VideoDetails: View { player.currentVideo } + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ControlsBar( + presentingControls: false, + backgroundEnabled: false, + borderTop: false, + detailsTogglePlayer: false + ) + + HStack(spacing: 4) { + pageButton("Info", "info.circle", .info, !video.isNil) + pageButton("Chapters", "bookmark", .chapters, !(video?.chapters.isEmpty ?? true)) + pageButton("Comments", "text.bubble", .comments, !video.isNil) { comments.load() } + pageButton("Related", "rectangle.stack.fill", .related, !video.isNil) + pageButton("Queue", "list.number", .queue, !video.isNil) + } + .onChange(of: player.currentItem) { _ in + page.update(.moveToFirst) + } + .padding(.horizontal) + .padding(.vertical, 6) + + Pager(page: page, data: DetailsPage.allCases, id: \.self) { + detailsByPage($0) + } + .onPageWillChange { pageIndex in + if pageIndex == DetailsPage.comments.index { + comments.load() + } + } + } + .onAppear { + if video.isNil && !sidebarQueue { + page.update(.new(index: DetailsPage.queue.index)) + } + + 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) + } + } + .edgesIgnoringSafeArea(.horizontal) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) + } + + var publishedDateSection: some View { + Group { + if let video = player.currentVideo { + HStack(spacing: 4) { + if let published = video.publishedDate { + Text(published) + } + } + } + } + } + + 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: { @@ -69,25 +140,25 @@ struct VideoDetails: View { HStack(spacing: 4) { Image(systemName: symbolName) - if playerDetailsPageButtonLabelStyle.text { + if playerDetailsPageButtonLabelStyle.text && player.playerSize.width > 450 { Text(label) } } .frame(minHeight: 15) .lineLimit(1) .padding(.vertical, 4) - .foregroundColor(currentPage == destination ? .white : .accentColor) + .foregroundColor(currentPage == destination ? .white : (active ? Color.accentColor : .gray)) Spacer() } .contentShape(Rectangle()) } - .background(currentPage == destination ? Color.accentColor : .clear) + .background(currentPage == destination ? (active ? Color.accentColor : .gray) : .clear) .buttonStyle(.plain) .font(.system(size: 10).bold()) .overlay( RoundedRectangle(cornerRadius: 2) - .stroke(Color.accentColor, lineWidth: 2) + .stroke(active ? Color.accentColor : .gray, lineWidth: 2) .foregroundColor(.clear) ) .frame(maxWidth: .infinity) @@ -119,123 +190,6 @@ struct VideoDetails: View { .contentShape(Rectangle()) } - var body: some View { - VStack(alignment: .leading) { - Group { - HStack(spacing: 4) { - pageButton("Info", "info.circle", .info) - pageButton("Chapters", "bookmark", .chapters) - pageButton("Comments", "text.bubble", .comments) { comments.load() } - pageButton("Related", "rectangle.stack.fill", .related) - pageButton("Queue", "list.number", .queue) - } - .onChange(of: player.currentItem) { _ in - page.update(.moveToFirst) - } - .padding(.horizontal) - .padding(.top, 8) - } - .contentShape(Rectangle()) - - Pager(page: page, data: DetailsPage.allCases, id: \.self) { - detailsByPage($0) - } - .onPageWillChange { pageIndex in - if pageIndex == DetailsPage.comments.index { - comments.load() - } else { - print("comments not loading") - } - } - } - .onAppear { - if video.isNil && !sidebarQueue { - page.update(.new(index: DetailsPage.queue.index)) - } - - 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) - } - } - .edgesIgnoringSafeArea(.horizontal) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) - } - - var showAddToPlaylistButton: Bool { - accounts.app.supportsUserPlaylists && accounts.signedIn - } - - var publishedDateSection: some View { - Group { - if let video = player.currentVideo { - HStack(spacing: 4) { - if let published = video.publishedDate { - Text(published) - } - } - } - } - } - - private var contentItem: ContentItem { - ContentItem(video: player.currentVideo!) - } - - private var authorAvatar: some View { - Group { - if let video = video, let url = video.channel.thumbnailURL { - WebImage(url: url) - .resizable() - .placeholder { - Rectangle().fill(Color("PlaceholderColor")) - } - .retryOnAppear(true) - .indicator(.activity) - .clipShape(Circle()) - .frame(width: 35, height: 35, alignment: .leading) - } - } - } - - var videoProperties: some View { - HStack(spacing: 2) { - publishedDateSection - Spacer() - - HStack(spacing: 4) { - if let views = video?.viewsCount { - Image(systemName: "eye") - - Text(views) - } - - if let likes = video?.likesCount { - Image(systemName: "hand.thumbsup") - - Text(likes) - } - - if let likes = video?.dislikesCount { - Image(systemName: "hand.thumbsdown") - - Text(likes) - } - } - } - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - var detailsPage: some View { Group { VStack(alignment: .leading, spacing: 0) { @@ -316,6 +270,35 @@ struct VideoDetails: View { } } + var videoProperties: some View { + HStack(spacing: 2) { + publishedDateSection + Spacer() + + HStack(spacing: 4) { + if let views = video?.viewsCount { + Image(systemName: "eye") + + Text(views) + } + + if let likes = video?.likesCount { + Image(systemName: "hand.thumbsup") + + Text(likes) + } + + if let likes = video?.dislikesCount { + Image(systemName: "hand.thumbsdown") + + Text(likes) + } + } + } + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + func videoDetail(label: String, value: String, symbol: String) -> some View { VStack(spacing: 4) { HStack(spacing: 2) { diff --git a/Shared/Player/VideoDetailsPaddingModifier.swift b/Shared/Player/VideoDetailsPaddingModifier.swift index 08fc30e4..45c694d9 100644 --- a/Shared/Player/VideoDetailsPaddingModifier.swift +++ b/Shared/Player/VideoDetailsPaddingModifier.swift @@ -2,13 +2,7 @@ import Foundation import SwiftUI struct VideoDetailsPaddingModifier: ViewModifier { - static var defaultAdditionalDetailsPadding: Double { - #if os(macOS) - 5 - #else - 10 - #endif - } + static var defaultAdditionalDetailsPadding = 0.0 let geometry: GeometryProxy let aspectRatio: Double? diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index afbd3286..1cc0b651 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -61,10 +61,12 @@ struct VideoPlayerView: View { } var body: some View { - // TODO: remove - if #available(iOS 15.0, macOS 12.0, *) { - _ = Self._printChanges() - } + #if DEBUG + // TODO: remove + if #available(iOS 15.0, macOS 12.0, *) { + _ = Self._printChanges() + } + #endif #if os(macOS) return HSplitView { @@ -159,7 +161,7 @@ struct VideoPlayerView: View { GeometryReader { geometry in VStack(spacing: 0) { if player.playingInPictureInPicture { - pictureInPicturePlaceholder(geometry: geometry) + pictureInPicturePlaceholder } else { playerView #if !os(tvOS) @@ -170,7 +172,7 @@ struct VideoPlayerView: View { fullScreen: player.playingFullScreen ) ) -// .overlay(playerPlaceholder(geometry: geometry)) + .overlay(playerPlaceholder) #endif } } @@ -183,15 +185,11 @@ struct VideoPlayerView: View { .gesture( DragGesture(coordinateSpace: .global) .onChanged { value in - guard player.presentingPlayer else { - return // swiftlint:disable:this implicit_return - } + guard player.presentingPlayer else { return } let drag = value.translation.height - guard drag > 0 else { - return // swiftlint:disable:this implicit_return - } + guard drag > 0 else { return } guard drag < 100 else { player.hide() @@ -231,6 +229,7 @@ struct VideoPlayerView: View { #if os(iOS) if verticalSizeClass == .regular { VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails) + .edgesIgnoringSafeArea(.bottom) } #else @@ -248,12 +247,6 @@ struct VideoPlayerView: View { #endif } #endif - - #if !os(tvOS) - if !fullScreenLayout { - ControlsBar() - } - #endif } .background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all)) #if os(macOS) @@ -273,7 +266,6 @@ struct VideoPlayerView: View { #endif } } - .transition(.asymmetric(insertion: .slide, removal: .identity)) .ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set()) #if os(iOS) .statusBar(hidden: player.playingFullScreen) @@ -326,7 +318,7 @@ struct VideoPlayerView: View { #endif } - @ViewBuilder func playerPlaceholder(geometry: GeometryProxy) -> some View { + @ViewBuilder var playerPlaceholder: some View { if player.currentItem.isNil { ZStack(alignment: .topLeading) { HStack { @@ -359,11 +351,11 @@ struct VideoPlayerView: View { } .background(Color.black) .contentShape(Rectangle()) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio) + .frame(width: player.playerSize.width, height: player.playerSize.height) } } - func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View { + var pictureInPicturePlaceholder: some View { HStack { Spacer() VStack { @@ -389,7 +381,7 @@ struct VideoPlayerView: View { } } .contentShape(Rectangle()) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio) + .frame(width: player.playerSize.width, height: player.playerSize.height) } #if os(iOS) diff --git a/Shared/Views/BrowserPlayerControls.swift b/Shared/Views/BrowserPlayerControls.swift index 8ad43157..d0f2855e 100644 --- a/Shared/Views/BrowserPlayerControls.swift +++ b/Shared/Views/BrowserPlayerControls.swift @@ -8,6 +8,7 @@ struct BrowserPlayerControls: View { } let content: Content + let toolbar: Toolbar? init( context _: Context? = nil, @@ -15,7 +16,7 @@ struct BrowserPlayerControls: View { @ViewBuilder content: @escaping () -> Content ) { self.content = content() - toolbar() + self.toolbar = toolbar() } init( @@ -26,195 +27,29 @@ struct BrowserPlayerControls: View { } var body: some View { - if #available(iOS 15.0, macOS 12.0, *) { - _ = Self._printChanges() - } + // TODO: remove + #if DEBUG + if #available(iOS 15.0, macOS 12.0, *) { + Self._printChanges() + } + #endif - return VStack(spacing: 0) { + return ZStack(alignment: .bottomLeading) { content #if !os(tvOS) - ControlsBar() - .edgesIgnoringSafeArea(.bottom) + VStack(spacing: 0) { + toolbar + .borderTop(height: 0.4, color: Color("ControlsBorderColor")) + .modifier(ControlBackgroundModifier()) + ControlsBar() + .edgesIgnoringSafeArea(.bottom) + } #endif } } } -// struct BrowserPlayerControls: View { -// enum Context { -// case browser, player -// } -// -// let context: Context -// let content: Content -// let toolbar: Toolbar? -// -// @Environment(\.navigationStyle) private var navigationStyle -// @EnvironmentObject private var playerControls -// @EnvironmentObject private var model -// -// var barHeight: Double { -// 75 -// } -// -// init( -// context: Context? = nil, -// @ViewBuilder toolbar: @escaping () -> Toolbar? = { nil }, -// @ViewBuilder content: @escaping () -> Content -// ) { -// self.context = context ?? .browser -// self.content = content() -// self.toolbar = toolbar() -// } -// -// init( -// context: Context? = nil, -// @ViewBuilder content: @escaping () -> Content -// ) where Toolbar == EmptyView { -// self.init(context: context, toolbar: { EmptyView() }, content: content) -// } -// -// var body: some View { -// ZStack(alignment: .bottomLeading) { -// VStack(spacing: 0) { -// content -// -// Color.clear.frame(height: barHeight) -// } -// #if !os(tvOS) -// .frame(minHeight: 0, maxHeight: .infinity) -// #endif -// -// -// VStack { -// #if !os(tvOS) -// #if !os(macOS) -// toolbar -// .frame(height: 100) -// .offset(x: 0, y: -28) -// #endif -// -// if context != .player || !playerControls.playingFullscreen { -// controls -// } -// #endif -// } -// .borderTop(height: 0.4, color: Color("ControlsBorderColor")) -// #if os(macOS) -// .background(VisualEffectBlur(material: .sidebar)) -// #elseif os(iOS) -// .background(VisualEffectBlur(blurStyle: .systemThinMaterial).edgesIgnoringSafeArea(.all)) -// #endif -// } -// .background(Color.debug) -// } -// -// private var controls: some View { -// VStack(spacing: 0) { -// TimelineView(duration: playerControls.durationBinding, current: playerControls.currentTimeBinding) -// .foregroundColor(.secondary) -// -// Button(action: { -// model.togglePlayer() -// }) { -// HStack(spacing: 8) { -// authorAvatar -// -// VStack(alignment: .leading, spacing: 5) { -// Text(model.currentVideo?.title ?? "Not playing") -// .font(.headline) -// .foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor) -// .lineLimit(1) -// -// Text(model.currentVideo?.author ?? "") -// .font(.subheadline) -// .foregroundColor(.secondary) -// .lineLimit(1) -// } -// -// Spacer() -// -// HStack { -// Group { -// if !model.currentItem.isNil { -// Button { -// model.closeCurrentItem() -// model.closePiP() -// } label: { -// Label("Close Video", systemImage: "xmark") -// .padding(.horizontal, 4) -// .contentShape(Rectangle()) -// } -// } -// -// if playerControls.isPlaying { -// Button(action: { -// model.pause() -// }) { -// Label("Pause", systemImage: "pause.fill") -// .padding(.horizontal, 4) -// .contentShape(Rectangle()) -// } -// } else { -// Button(action: { -// model.play() -// }) { -// Label("Play", systemImage: "play.fill") -// .padding(.horizontal, 4) -// .contentShape(Rectangle()) -// } -// } -// } -// .disabled(playerControls.isLoadingVideo || model.currentItem.isNil) -// .font(.system(size: 30)) -// .frame(minWidth: 30) -// -// Button(action: { model.advanceToNextItem() }) { -// Label("Next", systemImage: "forward.fill") -// .padding(.vertical) -// .contentShape(Rectangle()) -// } -// .disabled(model.queue.isEmpty) -// } -// } -// .buttonStyle(.plain) -// .contentShape(Rectangle()) -// } -// } -// .buttonStyle(.plain) -// .labelStyle(.iconOnly) -// .padding(.horizontal) -// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight) -// .borderTop(height: 0.4, color: Color("ControlsBorderColor")) -// .borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor")) -// } -// -// private var authorAvatar: some View { -// Group { -// if let video = model.currentItem?.video, let url = video.channel.thumbnailURL { -// WebImage(url: url) -// .resizable() -// .placeholder { -// Rectangle().fill(Color("PlaceholderColor")) -// } -// .retryOnAppear(true) -// .indicator(.activity) -// .clipShape(Circle()) -// .frame(width: 44, height: 44, alignment: .leading) -// } -// } -// } -// -// private var progressViewValue: Double { -// [model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0 -// } -// -// private var progressViewTotal: Double { -// model.videoDuration ?? 100 -// } -// } -// struct PlayerControlsView_Previews: PreviewProvider { static var previews: some View { BrowserPlayerControls(context: .player) { diff --git a/Shared/Views/ControlsBar.swift b/Shared/Views/ControlsBar.swift index 5c042112..607a850b 100644 --- a/Shared/Views/ControlsBar.swift +++ b/Shared/Views/ControlsBar.swift @@ -1,13 +1,8 @@ import Defaults import SDWebImageSwiftUI import SwiftUI -import SwiftUIPager struct ControlsBar: View { - enum Pages: CaseIterable { - case details, controls - } - @EnvironmentObject private var accounts @EnvironmentObject private var navigation @EnvironmentObject private var playerControls @@ -19,27 +14,28 @@ struct ControlsBar: View { @State private var presentingShareSheet = false @State private var shareURL: URL? - @StateObject private var controlsPage = Page.first() + var presentingControls = true + var backgroundEnabled = true + var borderTop = true + var borderBottom = true + var detailsTogglePlayer = true var body: some View { - VStack(spacing: 0) { - Pager(page: controlsPage, data: Pages.allCases, id: \.self) { index in - switch index { - case .details: - details - default: - controls - } + HStack(spacing: 0) { + detailsButton + + if presentingControls { + controls + .frame(maxWidth: 120) } - .pagingPriority(.simultaneous) } .buttonStyle(.plain) .labelStyle(.iconOnly) .padding(.horizontal) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight) - .borderTop(height: 0.4, color: Color("ControlsBorderColor")) - .borderBottom(height: 0.4, color: Color("ControlsBorderColor")) - .modifier(ControlBackgroundModifier(edgesIgnoringSafeArea: .bottom)) + .borderTop(height: borderTop ? 0.4 : 0, color: Color("ControlsBorderColor")) + .borderBottom(height: borderBottom ? 0.4 : 0, color: Color("ControlsBorderColor")) + .modifier(ControlBackgroundModifier(enabled: backgroundEnabled, edgesIgnoringSafeArea: .bottom)) #if os(iOS) .background( EmptyView().sheet(isPresented: $presentingShareSheet) { @@ -51,6 +47,19 @@ struct ControlsBar: View { #endif } + @ViewBuilder var detailsButton: some View { + if detailsTogglePlayer { + Button { + model.togglePlayer() + } label: { + details + .contentShape(Rectangle()) + } + } else { + details + } + } + var controls: some View { HStack(spacing: 4) { Group { @@ -59,32 +68,18 @@ struct ControlsBar: View { model.closePiP() } label: { Label("Close Video", systemImage: "xmark") - .padding(.horizontal, 4) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) .contentShape(Rectangle()) } - Spacer() - - Button(action: { model.backend.seek(to: 0) }) { - Label("Restart", systemImage: "backward.end.fill") - .contentShape(Rectangle()) - } - - Spacer() - - Button { - model.backend.seek(relative: .secondsInDefaultTimescale(-10)) - } label: { - Label("Backward", systemImage: "gobackward.10") - } - Spacer() - if playerControls.isPlaying { Button(action: { model.pause() }) { Label("Pause", systemImage: "pause.fill") - .padding(.horizontal, 4) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) .contentShape(Rectangle()) } } else { @@ -92,38 +87,27 @@ struct ControlsBar: View { model.play() }) { Label("Play", systemImage: "play.fill") - .padding(.horizontal, 4) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) .contentShape(Rectangle()) } } - Spacer() - - Button { - model.backend.seek(relative: .secondsInDefaultTimescale(10)) - } label: { - Label("Forward", systemImage: "goforward.10") - } - - Spacer() } .disabled(playerControls.isLoadingVideo || model.currentItem.isNil) Button(action: { model.advanceToNextItem() }) { Label("Next", systemImage: "forward.fill") + .padding(.vertical, 10) + .frame(maxWidth: .infinity) .contentShape(Rectangle()) } .disabled(model.queue.isEmpty) - - Spacer() } - .padding(.vertical) - .font(.system(size: 24)) - .frame(maxWidth: .infinity) } var barHeight: Double { - 75 + 55 } var details: some View { @@ -147,8 +131,6 @@ struct ControlsBar: View { if let video = model.currentVideo { Group { Section { - Text(video.title) - if accounts.app.supportsUserPlaylists && accounts.signedIn { Section { Button { @@ -165,14 +147,14 @@ struct ControlsBar: View { } } } - - ShareButton( - contentItem: .init(video: model.currentVideo), - presentingShareSheet: $presentingShareSheet, - shareURL: $shareURL - ) } + ShareButton( + contentItem: .init(video: model.currentVideo), + presentingShareSheet: $presentingShareSheet, + shareURL: $shareURL + ) + Section { Button { NavigationModel.openChannel( @@ -208,6 +190,12 @@ struct ControlsBar: View { } } } + + Button { + model.closeCurrentItem() + } label: { + Label("Close Video", systemImage: "xmark") + } } .labelStyle(.automatic) } @@ -234,9 +222,7 @@ struct ControlsBar: View { } private var authorAvatar: some View { - Button { - model.togglePlayer() - } label: { + Group { if let video = model.currentItem?.video, let url = video.channel.thumbnailURL { WebImage(url: url) .resizable() @@ -246,9 +232,15 @@ struct ControlsBar: View { .retryOnAppear(true) .indicator(.activity) } else { - Image(systemName: "play.rectangle") - .foregroundColor(.accentColor) - .font(.system(size: 30)) + ZStack { + Color(white: 0.8) + .opacity(0.5) + + Image(systemName: "play.rectangle") + .foregroundColor(.accentColor) + .font(.system(size: 20)) + .contentShape(Rectangle()) + } } } .frame(width: 44, height: 44, alignment: .leading) diff --git a/macOS/AppleAVPlayerViewController.swift b/macOS/AppleAVPlayerViewController.swift index 02b31b8b..a6c5073a 100644 --- a/macOS/AppleAVPlayerViewController.swift +++ b/macOS/AppleAVPlayerViewController.swift @@ -24,8 +24,8 @@ final class AppleAVPlayerViewController: NSViewController { playerView.player = playerModel.avPlayerBackend.avPlayer pictureInPictureDelegate.playerModel = playerModel + playerView.controlsStyle = .none playerView.allowsPictureInPicturePlayback = true - playerView.showsFullScreenToggleButton = true playerView.pictureInPictureDelegate = pictureInPictureDelegate