Minor UI changes

This commit is contained in:
Arkadiusz Fal 2022-06-25 01:39:29 +02:00
parent 7b09805b81
commit c940fb3198
20 changed files with 247 additions and 436 deletions

View File

@ -5,7 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.environmentObject(AccountsModel()) .environmentObject(AccountsModel())
.environmentObject(CommentsModel()) .environmentObject(comments)
.environmentObject(InstancesModel()) .environmentObject(InstancesModel())
.environmentObject(invidious) .environmentObject(invidious)
.environmentObject(NavigationModel()) .environmentObject(NavigationModel())
@ -21,6 +21,14 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
.environmentObject(ThumbnailsModel()) .environmentObject(ThumbnailsModel())
} }
private var comments: CommentsModel {
let comments = CommentsModel()
comments.loaded = true
comments.all = [.fixture]
return comments
}
private var invidious: InvidiousAPI { private var invidious: InvidiousAPI {
let api = InvidiousAPI() let api = InvidiousAPI()

View File

@ -34,7 +34,7 @@ final class NetworkStateModel: ObservableObject {
var needsUpdates: Bool { var needsUpdates: Bool {
if let player = player { if let player = player {
return pausedForCache || player.isSeeking || player.isLoadingVideo return pausedForCache || player.isSeeking || player.isLoadingVideo || player.controls.presentingControlsOverlay
} }
return pausedForCache return pausedForCache

View File

@ -589,5 +589,5 @@ final class AVPlayerBackend: PlayerBackend {
func stopControlsUpdates() {} func stopControlsUpdates() {}
func setNeedsDrawing(_: Bool) {} func setNeedsDrawing(_: Bool) {}
func setSize(_: Double, _: Double) {} func setSize(_: Double, _: Double) {}
func setNeedsNetworkStateUpdates() {} func setNeedsNetworkStateUpdates(_: Bool) {}
} }

View File

@ -28,7 +28,7 @@ final class MPVBackend: PlayerBackend {
} }
self.controls?.isLoadingVideo = self.isLoadingVideo self.controls?.isLoadingVideo = self.isLoadingVideo
self.setNeedsNetworkStateUpdates() self.setNeedsNetworkStateUpdates(true)
if !self.isLoadingVideo { if !self.isLoadingVideo {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
@ -476,7 +476,11 @@ final class MPVBackend: PlayerBackend {
} }
} }
func setNeedsNetworkStateUpdates() { func setNeedsNetworkStateUpdates(_ needsUpdates: Bool) {
networkStateTimer.resume() if needsUpdates {
networkStateTimer.resume()
} else {
networkStateTimer.suspend()
}
} }
} }

View File

@ -52,7 +52,7 @@ protocol PlayerBackend {
func startControlsUpdates() func startControlsUpdates()
func stopControlsUpdates() func stopControlsUpdates()
func setNeedsNetworkStateUpdates() func setNeedsNetworkStateUpdates(_ needsUpdates: Bool)
func setNeedsDrawing(_ needsDrawing: Bool) func setNeedsDrawing(_ needsDrawing: Bool)
func setSize(_ width: Double, _ height: Double) func setSize(_ width: Double, _ height: Double)

View File

@ -6,7 +6,7 @@ final class PlayerControlsModel: ObservableObject {
@Published var isLoadingVideo = false @Published var isLoadingVideo = false
@Published var isPlaying = true @Published var isPlaying = true
@Published var presentingControls = false { didSet { handlePresentationChange() } } @Published var presentingControls = false { didSet { handlePresentationChange() } }
@Published var presentingControlsOverlay = false @Published var presentingControlsOverlay = false { didSet { handleOverlayPresentationChange() } }
@Published var timer: Timer? @Published var timer: Timer?
var player: PlayerModel! var player: PlayerModel!
@ -40,6 +40,10 @@ final class PlayerControlsModel: ObservableObject {
} }
} }
func handleOverlayPresentationChange() {
player?.backend.setNeedsNetworkStateUpdates(presentingControlsOverlay)
}
func show() { func show() {
guard !(player?.currentItem.isNil ?? true) else { guard !(player?.currentItem.isNil ?? true) else {
return return

View File

@ -67,7 +67,7 @@ final class PlayerModel: ObservableObject {
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI() @Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
@Published var isSeeking = false { didSet { @Published var isSeeking = false { didSet {
backend.setNeedsNetworkStateUpdates() backend.setNeedsNetworkStateUpdates(true)
}} }}
#if os(iOS) #if os(iOS)

View File

@ -6,8 +6,8 @@ struct ChaptersView: View {
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
var body: some View { 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")) { Section(header: Text("Chapters")) {
ForEach(chapters) { chapter in ForEach(chapters) { chapter in
Button { Button {
@ -18,18 +18,17 @@ struct ChaptersView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} }
} else {
Text(player.currentVideo?.title ?? "")
} }
} #if os(macOS)
.id(UUID())
#if os(macOS)
.listStyle(.inset) .listStyle(.inset)
#elseif os(iOS) #elseif os(iOS)
.listStyle(.grouped) .listStyle(.grouped)
#else #else
.listStyle(.plain) .listStyle(.plain)
#endif #endif
} else {
NoCommentsView(text: "No chapters information available", systemImage: "xmark.circle.fill")
}
} }
@ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View { @ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View {

View File

@ -99,6 +99,7 @@ struct CommentView: View {
#if os(tvOS) #if os(tvOS)
.padding(.horizontal, 20) .padding(.horizontal, 20)
#endif #endif
.padding(.bottom, 10)
} }
private var authorAvatar: some View { private var authorAvatar: some View {

View File

@ -22,12 +22,7 @@ struct CommentsView: View {
.onAppear { .onAppear {
comments.loadNextPageIfNeeded(current: comment) comments.loadNextPageIfNeeded(current: comment)
} }
.padding(.bottom, comment == last ? 5 : 0) .borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor"))
if comment != last {
Divider()
.padding(.vertical, 5)
}
} }
} }

View File

@ -2,6 +2,7 @@ import Defaults
import SwiftUI import SwiftUI
struct ControlsOverlay: View { struct ControlsOverlay: View {
@EnvironmentObject<NetworkStateModel> private var networkState
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlayerControlsModel> private var model @EnvironmentObject<PlayerControlsModel> private var model
@ -165,7 +166,7 @@ struct ControlsOverlay: View {
Text("hw decoder: \(player.mpvBackend.hwDecoder)") Text("hw decoder: \(player.mpvBackend.hwDecoder)")
Text("dropped: \(player.mpvBackend.frameDropCount)") Text("dropped: \(player.mpvBackend.frameDropCount)")
Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))") 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))") Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))")
} }
.mask(RoundedRectangle(cornerRadius: 3)) .mask(RoundedRectangle(cornerRadius: 3))

View File

@ -6,7 +6,11 @@ struct NetworkState: View {
var body: some View { var body: some View {
Buffering(state: model.fullStateText) Buffering(state: model.fullStateText)
.opacity(model.pausedForCache || player.isSeeking ? 1 : 0) .opacity(visible ? 1 : 0)
}
var visible: Bool {
player.isPlaying && (model.pausedForCache || player.isSeeking)
} }
} }

View File

@ -23,8 +23,6 @@ struct PlayerControls: View {
@FocusState private var focusedField: Field? @FocusState private var focusedField: Field?
#endif #endif
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
init(player: PlayerModel, thumbnails: ThumbnailsModel) { init(player: PlayerModel, thumbnails: ThumbnailsModel) {
self.player = player self.player = player
self.thumbnails = thumbnails self.thumbnails = thumbnails
@ -191,12 +189,13 @@ struct PlayerControls: View {
HStack(spacing: 20) { HStack(spacing: 20) {
#if !os(tvOS) #if !os(tvOS)
fullscreenButton fullscreenButton
pipButton
#if os(iOS)
pipButton
#endif
Spacer() Spacer()
button("overlay", systemImage: "info.circle") {}
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) { button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
withAnimation(Self.animation) { withAnimation(Self.animation) {
model.presentingControlsOverlay.toggle() model.presentingControlsOverlay.toggle()

View File

@ -114,12 +114,13 @@ struct TimelineView: View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Rectangle() Rectangle()
.fill(Color.gray.opacity(0.1)) .fill(Color.white.opacity(0.2))
.frame(maxHeight: height) .frame(maxHeight: height)
.offset(x: current * oneUnitWidth)
.zIndex(1) .zIndex(1)
Rectangle() Rectangle()
.fill(Color.gray.opacity(0.5)) .fill(Color.white.opacity(0.6))
.frame(maxHeight: height) .frame(maxHeight: height)
.frame(width: current * oneUnitWidth) .frame(width: current * oneUnitWidth)
.zIndex(1) .zIndex(1)
@ -187,7 +188,7 @@ struct TimelineView: View {
#endif #endif
} }
.background(GeometryReader { proxy in .overlay(GeometryReader { proxy in
Color.clear Color.clear
.onAppear { .onAppear {
self.size = proxy.size self.size = proxy.size
@ -265,7 +266,6 @@ struct TimelineView: View {
} }
var segments: [Segment] { var segments: [Segment] {
// [.init(category: "outro", segment: [25,30], uuid: UUID().uuidString, videoDuration: 100)] ??
player.sponsorBlock.segments player.sponsorBlock.segments
} }
@ -290,7 +290,7 @@ struct TimelineView: View {
var chaptersLayers: some View { var chaptersLayers: some View {
ForEach(chapters) { chapter in ForEach(chapters) { chapter in
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill(Color("AppBlueColor")) .fill(Color.orange)
.frame(maxWidth: 2, maxHeight: 12) .frame(maxWidth: 2, maxHeight: 12)
.offset(x: (chapter.start * oneUnitWidth) - 1) .offset(x: (chapter.start * oneUnitWidth) - 1)
} }

View File

@ -53,10 +53,81 @@ struct VideoDetails: View {
player.currentVideo 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( func pageButton(
_ label: String, _ label: String,
_ symbolName: String, _ symbolName: String,
_ destination: DetailsPage, _ destination: DetailsPage,
_ active: Bool = true,
pageChangeAction: (() -> Void)? = nil pageChangeAction: (() -> Void)? = nil
) -> some View { ) -> some View {
Button(action: { Button(action: {
@ -69,25 +140,25 @@ struct VideoDetails: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: symbolName) Image(systemName: symbolName)
if playerDetailsPageButtonLabelStyle.text { if playerDetailsPageButtonLabelStyle.text && player.playerSize.width > 450 {
Text(label) Text(label)
} }
} }
.frame(minHeight: 15) .frame(minHeight: 15)
.lineLimit(1) .lineLimit(1)
.padding(.vertical, 4) .padding(.vertical, 4)
.foregroundColor(currentPage == destination ? .white : .accentColor) .foregroundColor(currentPage == destination ? .white : (active ? Color.accentColor : .gray))
Spacer() Spacer()
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.background(currentPage == destination ? Color.accentColor : .clear) .background(currentPage == destination ? (active ? Color.accentColor : .gray) : .clear)
.buttonStyle(.plain) .buttonStyle(.plain)
.font(.system(size: 10).bold()) .font(.system(size: 10).bold())
.overlay( .overlay(
RoundedRectangle(cornerRadius: 2) RoundedRectangle(cornerRadius: 2)
.stroke(Color.accentColor, lineWidth: 2) .stroke(active ? Color.accentColor : .gray, lineWidth: 2)
.foregroundColor(.clear) .foregroundColor(.clear)
) )
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -119,123 +190,6 @@ struct VideoDetails: View {
.contentShape(Rectangle()) .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 { var detailsPage: some View {
Group { Group {
VStack(alignment: .leading, spacing: 0) { 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 { func videoDetail(label: String, value: String, symbol: String) -> some View {
VStack(spacing: 4) { VStack(spacing: 4) {
HStack(spacing: 2) { HStack(spacing: 2) {

View File

@ -2,13 +2,7 @@ import Foundation
import SwiftUI import SwiftUI
struct VideoDetailsPaddingModifier: ViewModifier { struct VideoDetailsPaddingModifier: ViewModifier {
static var defaultAdditionalDetailsPadding: Double { static var defaultAdditionalDetailsPadding = 0.0
#if os(macOS)
5
#else
10
#endif
}
let geometry: GeometryProxy let geometry: GeometryProxy
let aspectRatio: Double? let aspectRatio: Double?

View File

@ -61,10 +61,12 @@ struct VideoPlayerView: View {
} }
var body: some View { var body: some View {
// TODO: remove #if DEBUG
if #available(iOS 15.0, macOS 12.0, *) { // TODO: remove
_ = Self._printChanges() if #available(iOS 15.0, macOS 12.0, *) {
} _ = Self._printChanges()
}
#endif
#if os(macOS) #if os(macOS)
return HSplitView { return HSplitView {
@ -159,7 +161,7 @@ struct VideoPlayerView: View {
GeometryReader { geometry in GeometryReader { geometry in
VStack(spacing: 0) { VStack(spacing: 0) {
if player.playingInPictureInPicture { if player.playingInPictureInPicture {
pictureInPicturePlaceholder(geometry: geometry) pictureInPicturePlaceholder
} else { } else {
playerView playerView
#if !os(tvOS) #if !os(tvOS)
@ -170,7 +172,7 @@ struct VideoPlayerView: View {
fullScreen: player.playingFullScreen fullScreen: player.playingFullScreen
) )
) )
// .overlay(playerPlaceholder(geometry: geometry)) .overlay(playerPlaceholder)
#endif #endif
} }
} }
@ -183,15 +185,11 @@ struct VideoPlayerView: View {
.gesture( .gesture(
DragGesture(coordinateSpace: .global) DragGesture(coordinateSpace: .global)
.onChanged { value in .onChanged { value in
guard player.presentingPlayer else { guard player.presentingPlayer else { return }
return // swiftlint:disable:this implicit_return
}
let drag = value.translation.height let drag = value.translation.height
guard drag > 0 else { guard drag > 0 else { return }
return // swiftlint:disable:this implicit_return
}
guard drag < 100 else { guard drag < 100 else {
player.hide() player.hide()
@ -231,6 +229,7 @@ struct VideoPlayerView: View {
#if os(iOS) #if os(iOS)
if verticalSizeClass == .regular { if verticalSizeClass == .regular {
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails) VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
.edgesIgnoringSafeArea(.bottom)
} }
#else #else
@ -248,12 +247,6 @@ struct VideoPlayerView: View {
#endif #endif
} }
#endif #endif
#if !os(tvOS)
if !fullScreenLayout {
ControlsBar()
}
#endif
} }
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all)) .background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
#if os(macOS) #if os(macOS)
@ -273,7 +266,6 @@ struct VideoPlayerView: View {
#endif #endif
} }
} }
.transition(.asymmetric(insertion: .slide, removal: .identity))
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set()) .ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
#if os(iOS) #if os(iOS)
.statusBar(hidden: player.playingFullScreen) .statusBar(hidden: player.playingFullScreen)
@ -326,7 +318,7 @@ struct VideoPlayerView: View {
#endif #endif
} }
@ViewBuilder func playerPlaceholder(geometry: GeometryProxy) -> some View { @ViewBuilder var playerPlaceholder: some View {
if player.currentItem.isNil { if player.currentItem.isNil {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
HStack { HStack {
@ -359,11 +351,11 @@ struct VideoPlayerView: View {
} }
.background(Color.black) .background(Color.black)
.contentShape(Rectangle()) .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 { HStack {
Spacer() Spacer()
VStack { VStack {
@ -389,7 +381,7 @@ struct VideoPlayerView: View {
} }
} }
.contentShape(Rectangle()) .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) #if os(iOS)

View File

@ -8,6 +8,7 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
} }
let content: Content let content: Content
let toolbar: Toolbar?
init( init(
context _: Context? = nil, context _: Context? = nil,
@ -15,7 +16,7 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
@ViewBuilder content: @escaping () -> Content @ViewBuilder content: @escaping () -> Content
) { ) {
self.content = content() self.content = content()
toolbar() self.toolbar = toolbar()
} }
init( init(
@ -26,195 +27,29 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
} }
var body: some View { var body: some View {
if #available(iOS 15.0, macOS 12.0, *) { // TODO: remove
_ = Self._printChanges() #if DEBUG
} if #available(iOS 15.0, macOS 12.0, *) {
Self._printChanges()
}
#endif
return VStack(spacing: 0) { return ZStack(alignment: .bottomLeading) {
content content
#if !os(tvOS) #if !os(tvOS)
ControlsBar() VStack(spacing: 0) {
.edgesIgnoringSafeArea(.bottom) toolbar
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
.modifier(ControlBackgroundModifier())
ControlsBar()
.edgesIgnoringSafeArea(.bottom)
}
#endif #endif
} }
} }
} }
// struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
// enum Context {
// case browser, player
// }
//
// let context: Context
// let content: Content
// let toolbar: Toolbar?
//
// @Environment(\.navigationStyle) private var navigationStyle
// @EnvironmentObject<PlayerControlsModel> private var playerControls
// @EnvironmentObject<PlayerModel> 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 { struct PlayerControlsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
BrowserPlayerControls(context: .player) { BrowserPlayerControls(context: .player) {

View File

@ -1,13 +1,8 @@
import Defaults import Defaults
import SDWebImageSwiftUI import SDWebImageSwiftUI
import SwiftUI import SwiftUI
import SwiftUIPager
struct ControlsBar: View { struct ControlsBar: View {
enum Pages: CaseIterable {
case details, controls
}
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerControlsModel> private var playerControls @EnvironmentObject<PlayerControlsModel> private var playerControls
@ -19,27 +14,28 @@ struct ControlsBar: View {
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @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 { var body: some View {
VStack(spacing: 0) { HStack(spacing: 0) {
Pager(page: controlsPage, data: Pages.allCases, id: \.self) { index in detailsButton
switch index {
case .details: if presentingControls {
details controls
default: .frame(maxWidth: 120)
controls
}
} }
.pagingPriority(.simultaneous)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.padding(.horizontal) .padding(.horizontal)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight)
.borderTop(height: 0.4, color: Color("ControlsBorderColor")) .borderTop(height: borderTop ? 0.4 : 0, color: Color("ControlsBorderColor"))
.borderBottom(height: 0.4, color: Color("ControlsBorderColor")) .borderBottom(height: borderBottom ? 0.4 : 0, color: Color("ControlsBorderColor"))
.modifier(ControlBackgroundModifier(edgesIgnoringSafeArea: .bottom)) .modifier(ControlBackgroundModifier(enabled: backgroundEnabled, edgesIgnoringSafeArea: .bottom))
#if os(iOS) #if os(iOS)
.background( .background(
EmptyView().sheet(isPresented: $presentingShareSheet) { EmptyView().sheet(isPresented: $presentingShareSheet) {
@ -51,6 +47,19 @@ struct ControlsBar: View {
#endif #endif
} }
@ViewBuilder var detailsButton: some View {
if detailsTogglePlayer {
Button {
model.togglePlayer()
} label: {
details
.contentShape(Rectangle())
}
} else {
details
}
}
var controls: some View { var controls: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
Group { Group {
@ -59,32 +68,18 @@ struct ControlsBar: View {
model.closePiP() model.closePiP()
} label: { } label: {
Label("Close Video", systemImage: "xmark") Label("Close Video", systemImage: "xmark")
.padding(.horizontal, 4) .padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle()) .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 { if playerControls.isPlaying {
Button(action: { Button(action: {
model.pause() model.pause()
}) { }) {
Label("Pause", systemImage: "pause.fill") Label("Pause", systemImage: "pause.fill")
.padding(.horizontal, 4) .padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
} else { } else {
@ -92,38 +87,27 @@ struct ControlsBar: View {
model.play() model.play()
}) { }) {
Label("Play", systemImage: "play.fill") Label("Play", systemImage: "play.fill")
.padding(.horizontal, 4) .padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
} }
Spacer()
Button {
model.backend.seek(relative: .secondsInDefaultTimescale(10))
} label: {
Label("Forward", systemImage: "goforward.10")
}
Spacer()
} }
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil) .disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
Button(action: { model.advanceToNextItem() }) { Button(action: { model.advanceToNextItem() }) {
Label("Next", systemImage: "forward.fill") Label("Next", systemImage: "forward.fill")
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.disabled(model.queue.isEmpty) .disabled(model.queue.isEmpty)
Spacer()
} }
.padding(.vertical)
.font(.system(size: 24)) .font(.system(size: 24))
.frame(maxWidth: .infinity)
} }
var barHeight: Double { var barHeight: Double {
75 55
} }
var details: some View { var details: some View {
@ -147,8 +131,6 @@ struct ControlsBar: View {
if let video = model.currentVideo { if let video = model.currentVideo {
Group { Group {
Section { Section {
Text(video.title)
if accounts.app.supportsUserPlaylists && accounts.signedIn { if accounts.app.supportsUserPlaylists && accounts.signedIn {
Section { Section {
Button { 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 { Section {
Button { Button {
NavigationModel.openChannel( NavigationModel.openChannel(
@ -208,6 +190,12 @@ struct ControlsBar: View {
} }
} }
} }
Button {
model.closeCurrentItem()
} label: {
Label("Close Video", systemImage: "xmark")
}
} }
.labelStyle(.automatic) .labelStyle(.automatic)
} }
@ -234,9 +222,7 @@ struct ControlsBar: View {
} }
private var authorAvatar: some View { private var authorAvatar: some View {
Button { Group {
model.togglePlayer()
} label: {
if let video = model.currentItem?.video, let url = video.channel.thumbnailURL { if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
WebImage(url: url) WebImage(url: url)
.resizable() .resizable()
@ -246,9 +232,15 @@ struct ControlsBar: View {
.retryOnAppear(true) .retryOnAppear(true)
.indicator(.activity) .indicator(.activity)
} else { } else {
Image(systemName: "play.rectangle") ZStack {
.foregroundColor(.accentColor) Color(white: 0.8)
.font(.system(size: 30)) .opacity(0.5)
Image(systemName: "play.rectangle")
.foregroundColor(.accentColor)
.font(.system(size: 20))
.contentShape(Rectangle())
}
} }
} }
.frame(width: 44, height: 44, alignment: .leading) .frame(width: 44, height: 44, alignment: .leading)

View File

@ -24,8 +24,8 @@ final class AppleAVPlayerViewController: NSViewController {
playerView.player = playerModel.avPlayerBackend.avPlayer playerView.player = playerModel.avPlayerBackend.avPlayer
pictureInPictureDelegate.playerModel = playerModel pictureInPictureDelegate.playerModel = playerModel
playerView.controlsStyle = .none
playerView.allowsPictureInPicturePlayback = true playerView.allowsPictureInPicturePlayback = true
playerView.showsFullScreenToggleButton = true
playerView.pictureInPictureDelegate = pictureInPictureDelegate playerView.pictureInPictureDelegate = pictureInPictureDelegate