Video details changes and channel sheet

This commit is contained in:
Arkadiusz Fal 2023-04-22 10:56:42 +02:00
parent 5db74a3997
commit 67690bc435
15 changed files with 389 additions and 256 deletions

View File

@ -84,6 +84,9 @@ final class NavigationModel: ObservableObject {
@Published var presentingAccounts = false @Published var presentingAccounts = false
@Published var presentingWelcomeScreen = false @Published var presentingWelcomeScreen = false
@Published var presentingChannelSheet = false
@Published var channelPresentedInSheet: Channel!
@Published var presentingShareSheet = false @Published var presentingShareSheet = false
@Published var shareURL: URL? @Published var shareURL: URL?
@ -103,7 +106,6 @@ final class NavigationModel: ObservableObject {
hideKeyboard() hideKeyboard()
let presentingPlayer = player.presentingPlayer let presentingPlayer = player.presentingPlayer
player.hide()
presentingChannel = false presentingChannel = false
#if os(macOS) #if os(macOS)
@ -113,20 +115,30 @@ final class NavigationModel: ObservableObject {
let recent = RecentItem(from: channel) let recent = RecentItem(from: channel)
recents.add(RecentItem(from: channel)) recents.add(RecentItem(from: channel))
if navigationStyle == .sidebar { let navigateToChannel = {
sidebarSectionChanged.toggle()
tabSelection = .recentlyOpened(recent.tag)
} else {
var delay = 0.0
#if os(iOS) #if os(iOS)
if presentingPlayer { delay = 1.0 } self.player.hide()
#endif #endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if navigationStyle == .sidebar {
self.sidebarSectionChanged.toggle()
self.tabSelection = .recentlyOpened(recent.tag)
} else {
withAnimation(Constants.overlayAnimation) { withAnimation(Constants.overlayAnimation) {
self.presentingChannel = true self.presentingChannel = true
} }
} }
} }
#if os(iOS)
if presentingPlayer {
presentChannelInSheet(channel)
} else {
navigateToChannel()
}
#else
navigateToChannel()
#endif
} }
func openChannelPlaylist(_ playlist: ChannelPlaylist, navigationStyle: NavigationStyle) { func openChannelPlaylist(_ playlist: ChannelPlaylist, navigationStyle: NavigationStyle) {
@ -273,6 +285,11 @@ final class NavigationModel: ObservableObject {
shareURL = url shareURL = url
presentingShareSheet = true presentingShareSheet = true
} }
func presentChannelInSheet(_ channel: Channel) {
channelPresentedInSheet = channel
presentingChannelSheet = true
}
} }
typealias TabSelection = NavigationModel.TabSelection typealias TabSelection = NavigationModel.TabSelection

View File

@ -108,6 +108,7 @@ struct OpenVideosModel {
) )
WatchNextViewModel.shared.hide() WatchNextViewModel.shared.hide()
NavigationModel.shared.presentingChannelSheet = false
if playbackMode == .playNow || playbackMode == .shuffleAll { if playbackMode == .playNow || playbackMode == .shuffleAll {
#if os(iOS) #if os(iOS)

View File

@ -335,6 +335,7 @@ final class PlayerModel: ObservableObject {
videoBeingOpened = video videoBeingOpened = video
WatchNextViewModel.shared.hide() WatchNextViewModel.shared.hide()
navigation.presentingChannelSheet = false
var changeBackendHandler: (() -> Void)? var changeBackendHandler: (() -> Void)?

View File

@ -15,6 +15,8 @@ extension PlayerModel {
func play(_ videos: [Video], shuffling: Bool = false) { func play(_ videos: [Video], shuffling: Bool = false) {
WatchNextViewModel.shared.hide() WatchNextViewModel.shared.hide()
navigation.presentingChannelSheet = false
playbackMode = shuffling ? .shuffle : .queue playbackMode = shuffling ? .shuffle : .queue
videos.forEach { enqueueVideo($0, loadDetails: false) } videos.forEach { enqueueVideo($0, loadDetails: false) }
@ -33,6 +35,8 @@ extension PlayerModel {
} }
func playNow(_ video: Video, at time: CMTime? = nil) { func playNow(_ video: Video, at time: CMTime? = nil) {
navigation.presentingChannelSheet = false
if playingInPictureInPicture, closePiPOnNavigation { if playingInPictureInPicture, closePiPOnNavigation {
closePiP() closePiP()
} }
@ -56,6 +60,7 @@ extension PlayerModel {
comments.reset() comments.reset()
stream = nil stream = nil
WatchNextViewModel.shared.hide() WatchNextViewModel.shared.hide()
navigation.presentingChannelSheet = false
withAnimation { withAnimation {
aspectRatio = VideoPlayerView.defaultAspectRatio aspectRatio = VideoPlayerView.defaultAspectRatio
@ -176,6 +181,7 @@ extension PlayerModel {
remove(newItem) remove(newItem)
WatchNextViewModel.shared.hide() WatchNextViewModel.shared.hide()
navigation.presentingChannelSheet = false
currentItem = newItem currentItem = newItem
currentItem.playbackTime = time currentItem.playbackTime = time
@ -219,9 +225,12 @@ extension PlayerModel {
let item = PlayerQueueItem(video, playbackTime: atTime) let item = PlayerQueueItem(video, playbackTime: atTime)
if play { if play {
navigation.presentingChannelSheet = false
withAnimation { withAnimation {
aspectRatio = VideoPlayerView.defaultAspectRatio aspectRatio = VideoPlayerView.defaultAspectRatio
WatchNextViewModel.shared.hide() WatchNextViewModel.shared.hide()
navigation.presentingChannelSheet = false
currentItem = item currentItem = item
} }
videoBeingOpened = video videoBeingOpened = video

View File

@ -6,6 +6,7 @@ import SwiftUI
struct ChannelVideosView: View { struct ChannelVideosView: View {
var channel: Channel? var channel: Channel?
var showCloseButton = false var showCloseButton = false
var inNavigationView = true
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @State private var shareURL: URL?
@ -119,24 +120,28 @@ struct ChannelVideosView: View {
Button { Button {
withAnimation(Constants.overlayAnimation) { withAnimation(Constants.overlayAnimation) {
navigation.presentingChannel = false navigation.presentingChannel = false
navigation.presentingChannelSheet = false
} }
} label: { } label: {
Label("Close", systemImage: "xmark") Label("Close", systemImage: "xmark")
} }
#if !os(macOS)
.buttonStyle(.plain) .buttonStyle(.plain)
#endif
} }
} }
#if !os(iOS) #if os(macOS)
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
thumbnail thumbnail
} }
ToolbarItem { ToolbarItemGroup {
if !inNavigationView {
Text(navigationTitle)
.fontWeight(.bold)
}
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle) ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
}
ToolbarItem {
HideShortsButtons(hide: $hideShorts) HideShortsButtons(hide: $hideShorts)
}
ToolbarItem {
contentTypePicker contentTypePicker
} }
@ -160,10 +165,12 @@ struct ChannelVideosView: View {
ToolbarItem { ToolbarItem {
favoriteButton favoriteButton
.labelStyle(.iconOnly)
} }
ToolbarItem { ToolbarItem {
toggleWatchedButton toggleWatchedButton
.labelStyle(.iconOnly)
} }
#endif #endif
} }
@ -234,14 +241,14 @@ struct ChannelVideosView: View {
Group { Group {
if let subscribers = store.item?.channel?.subscriptionsString { if let subscribers = store.item?.channel?.subscriptionsString {
HStack(spacing: 0) { HStack(spacing: 0) {
Text(subscribers)
Image(systemName: "person.2.fill") Image(systemName: "person.2.fill")
Text(subscribers)
} }
} else if store.item.isNil { } else if store.item.isNil {
HStack(spacing: 0) { HStack(spacing: 0) {
Image(systemName: "person.2.fill")
Text("1234") Text("1234")
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
Image(systemName: "person.2.fill")
} }
} }
} }
@ -252,10 +259,10 @@ struct ChannelVideosView: View {
var viewsLabel: some View { var viewsLabel: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
if let views = store.item?.channel?.totalViewsString { if let views = store.item?.channel?.totalViewsString {
Text(views)
Image(systemName: "eye.fill") Image(systemName: "eye.fill")
.imageScale(.small) .imageScale(.small)
Text(views)
} }
} }
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -328,6 +335,7 @@ struct ChannelVideosView: View {
} }
} }
} }
.labelsHidden()
} }
private func typeAvailable(_ type: Channel.ContentType) -> Bool { private func typeAvailable(_ type: Channel.ContentType) -> Bool {
@ -463,7 +471,7 @@ struct ChannelVideosView: View {
struct ChannelVideosView_Previews: PreviewProvider { struct ChannelVideosView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
#if os(macOS) #if os(macOS)
ChannelVideosView(channel: Video.fixture.channel) ChannelVideosView(channel: Video.fixture.channel, showCloseButton: true, inNavigationView: false)
.environment(\.navigationStyle, .sidebar) .environment(\.navigationStyle, .sidebar)
#else #else
NavigationView { NavigationView {

View File

@ -241,6 +241,7 @@ extension Defaults.Keys {
static let openWatchNextOnFinishedWatchingDelay = Key<String>("openWatchNextOnFinishedWatchingDelay", default: "5") static let openWatchNextOnFinishedWatchingDelay = Key<String>("openWatchNextOnFinishedWatchingDelay", default: "5")
static let hideShorts = Key<Bool>("hideShorts", default: false) static let hideShorts = Key<Bool>("hideShorts", default: false)
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
} }
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable { enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {

View File

@ -120,6 +120,15 @@ struct ContentView: View {
OpenVideosView() OpenVideosView()
} }
) )
#if !os(macOS)
.background(
EmptyView().sheet(isPresented: $navigation.presentingChannelSheet) {
NavigationView {
ChannelVideosView(channel: navigation.channelPresentedInSheet, showCloseButton: true)
}
}
)
#endif
.alert(isPresented: $navigation.presentingAlert) { navigation.alert } .alert(isPresented: $navigation.presentingAlert) { navigation.alert }
} }

View File

@ -5,7 +5,7 @@ struct VideoDetailsOverlay: View {
@ObservedObject private var controls = PlayerControlsModel.shared @ObservedObject private var controls = PlayerControlsModel.shared
var body: some View { var body: some View {
VideoDetails(video: controls.player.videoForDisplay, fullScreen: fullScreenBinding) VideoDetails(video: controls.player.videoForDisplay, fullScreen: fullScreenBinding, sidebarQueue: .constant(false))
.clipShape(RoundedRectangle(cornerRadius: 4)) .clipShape(RoundedRectangle(cornerRadius: 4))
} }

View File

@ -5,9 +5,9 @@ struct RelatedView: View {
@ObservedObject private var player = PlayerModel.shared @ObservedObject private var player = PlayerModel.shared
var body: some View { var body: some View {
List { LazyVStack {
if let related = player.currentVideo?.related { if let related = player.videoForDisplay?.related {
Section(header: Text("Related")) { Section(header: header) {
ForEach(related) { video in ForEach(related) { video in
PlayerQueueRow(item: PlayerQueueItem(video)) PlayerQueueRow(item: PlayerQueueItem(video))
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
@ -34,6 +34,15 @@ struct RelatedView: View {
.listStyle(.plain) .listStyle(.plain)
#endif #endif
} }
var header: some View {
Text("Related")
#if !os(macOS)
.font(.caption)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
#endif
}
} }
struct RelatedView_Previews: PreviewProvider { struct RelatedView_Previews: PreviewProvider {

View File

@ -25,7 +25,6 @@ struct CommentsView: View {
.borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor")) .borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor"))
} }
} }
.padding(.top, 55)
if embedInScrollView { if embedInScrollView {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {

View File

@ -6,7 +6,7 @@ struct InspectorView: View {
@ObservedObject private var player = PlayerModel.shared @ObservedObject private var player = PlayerModel.shared
var body: some View { var body: some View {
ScrollView { Section(header: header) {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
if let video { if let video {
VStack(spacing: 4) { VStack(spacing: 4) {
@ -53,10 +53,14 @@ struct InspectorView: View {
NoCommentsView(text: "Not playing", systemImage: "stop.circle.fill") NoCommentsView(text: "Not playing", systemImage: "stop.circle.fill")
} }
} }
.padding(.top, 60)
.padding(.bottom, 50)
} }
.padding(.horizontal) }
var header: some View {
Text("Inspector")
.font(.caption)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
} }
@ViewBuilder func videoDetailGroupHeading(_ heading: String, image systemName: String? = nil) -> some View { @ViewBuilder func videoDetailGroupHeading(_ heading: String, image systemName: String? = nil) -> some View {

View File

@ -13,7 +13,7 @@ struct PlayerQueueView: View {
@Default(.saveHistory) private var saveHistory @Default(.saveHistory) private var saveHistory
var body: some View { var body: some View {
List { Group {
Group { Group {
if player.playbackMode == .related { if player.playbackMode == .related {
autoplaying autoplaying
@ -34,15 +34,6 @@ struct PlayerQueueView: View {
.listRowSeparator(false) .listRowSeparator(false)
} }
.environment(\.inNavigationView, false) .environment(\.inNavigationView, false)
#if os(macOS)
.listStyle(.inset)
#elseif os(iOS)
.listStyle(.grouped)
.backport
.scrollContentBackground(false)
#else
.listStyle(.plain)
#endif
} }
@ViewBuilder var autoplaying: some View { @ViewBuilder var autoplaying: some View {
@ -65,6 +56,8 @@ struct PlayerQueueView: View {
var autoplayingHeader: some View { var autoplayingHeader: some View {
HStack { HStack {
Text("Autoplaying Next") Text("Autoplaying Next")
.foregroundColor(.secondary)
.font(.caption)
Spacer() Spacer()
Button { Button {
player.setRelatedAutoplayItem() player.setRelatedAutoplayItem()
@ -78,7 +71,7 @@ struct PlayerQueueView: View {
} }
var playingNext: some View { var playingNext: some View {
Section(header: Text("Queue")) { Section(header: queueHeader) {
if player.queue.isEmpty { if player.queue.isEmpty {
Text("Queue is empty") Text("Queue is empty")
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -96,6 +89,15 @@ struct PlayerQueueView: View {
} }
} }
var queueHeader: some View {
Text("Queue".localized())
#if !os(macOS)
.foregroundColor(.secondary)
.font(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
#endif
}
private var visibleWatches: [Watch] { private var visibleWatches: [Watch] {
watches.filter { $0.videoID != player.currentVideo?.videoID } watches.filter { $0.videoID != player.currentVideo?.videoID }
} }

View File

@ -4,21 +4,146 @@ import SDWebImageSwiftUI
import SwiftUI import SwiftUI
struct VideoDetails: View { struct VideoDetails: View {
enum DetailsPage: String, CaseIterable, Defaults.Serializable { struct TitleView: View {
case info, comments, chapters, inspector @ObservedObject private var model = PlayerModel.shared
@State private var titleSize = CGSize.zero
var systemImageName: String { var video: Video? { model.videoForDisplay }
switch self {
case .info: var body: some View {
return "info.circle" HStack(spacing: 0) {
case .inspector: Text(model.videoForDisplay?.displayTitle ?? "Not playing")
return "wand.and.stars" .font(.title3.bold())
case .comments: .lineLimit(4)
return "text.bubble" }
case .chapters: .padding(.vertical, 4)
return "bookmark" }
}
struct ChannelView: View {
@ObservedObject private var model = PlayerModel.shared
var video: Video? { model.videoForDisplay }
var body: some View {
HStack {
Button {
guard let channel = video?.channel else { return }
NavigationModel.shared.openChannel(channel, navigationStyle: .sidebar)
} label: {
ChannelAvatarView(
channel: video?.channel,
video: video
)
.frame(maxWidth: 40, maxHeight: 40)
.padding(.trailing, 5)
}
.buttonStyle(.plain)
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(model.videoForDisplay?.channel.name ?? "Yattee")
.font(.subheadline)
.fontWeight(.semibold)
.lineLimit(1)
if let video, !video.isLocal {
Group {
Text("")
HStack(spacing: 2) {
Image(systemName: "person.2.fill")
if let channel = model.videoForDisplay?.channel {
if let subscriptions = channel.subscriptionsString {
Text(subscriptions)
} else {
Text("1234").redacted(reason: .placeholder)
}
}
}
}
.font(.caption2)
}
}
.foregroundColor(.secondary)
if video != nil {
VideoMetadataView()
}
}
} }
} }
}
struct VideoMetadataView: View {
@ObservedObject private var model = PlayerModel.shared
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
var video: Video? { model.videoForDisplay }
var body: some View {
HStack(spacing: 4) {
publishedDateSection
Text("")
HStack(spacing: 4) {
if model.videoBeingOpened != nil || video?.viewsCount != nil {
Image(systemName: "eye")
}
if let views = video?.viewsCount {
Text(views)
} else if model.videoBeingOpened != nil {
Text("1,234M").redacted(reason: .placeholder)
}
if model.videoBeingOpened != nil || video?.likesCount != nil {
Image(systemName: "hand.thumbsup")
}
if let likes = video?.likesCount {
Text(likes)
} else if model.videoBeingOpened == nil {
Text("1,234M").redacted(reason: .placeholder)
}
if enableReturnYouTubeDislike {
if model.videoBeingOpened != nil || video?.dislikesCount != nil {
Image(systemName: "hand.thumbsdown")
}
if let dislikes = video?.dislikesCount {
Text(dislikes)
} else if model.videoBeingOpened == nil {
Text("1,234M").redacted(reason: .placeholder)
}
}
}
}
.font(.caption2)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
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)
}
}
}
}
}
}
enum DetailsPage: String, CaseIterable, Defaults.Serializable {
case info, comments, queue
var title: String { var title: String {
rawValue.capitalized.localized() rawValue.capitalized.localized()
@ -28,7 +153,7 @@ struct VideoDetails: View {
var video: Video? var video: Video?
@Binding var fullScreen: Bool @Binding var fullScreen: Bool
var bottomPadding = false @Binding var sidebarQueue: Bool
@State private var detailsSize = CGSize.zero @State private var detailsSize = CGSize.zero
@State private var detailsVisibility = Constants.detailsVisibility @State private var detailsVisibility = Constants.detailsVisibility
@ -49,22 +174,40 @@ struct VideoDetails: View {
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
@Default(.playerSidebar) private var playerSidebar @Default(.playerSidebar) private var playerSidebar
@Default(.showInspector) private var showInspector
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ControlsBar( VStack(alignment: .leading, spacing: 0) {
fullScreen: $fullScreen, TitleView()
expansionState: .constant(.full), if video != nil, !video!.isLocal {
presentingControls: false, ChannelView()
backgroundEnabled: false, .layoutPriority(1)
borderTop: false, .padding(.bottom, 6)
detailsTogglePlayer: false, }
detailsToggleFullScreen: true }
) .frame(maxWidth: .infinity, alignment: .leading)
.animation(nil, value: player.currentItem) .contentShape(Rectangle())
.padding(.horizontal, 16)
#if !os(tvOS)
.tapRecognizer(
tapSensitivity: 0.2,
doubleTapAction: {
withAnimation(.default) {
fullScreen.toggle()
}
}
)
#endif
VideoActions(video: player.videoForDisplay) VideoActions(video: player.videoForDisplay)
.padding(.vertical, 5)
.frame(maxHeight: 50)
.frame(maxWidth: .infinity)
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
.animation(nil, value: player.currentItem) .animation(nil, value: player.currentItem)
.frame(minWidth: 0, maxWidth: .infinity)
pageView pageView
#if os(iOS) #if os(iOS)
@ -100,210 +243,112 @@ struct VideoDetails: View {
} }
@ViewBuilder var pageMenu: some View { @ViewBuilder var pageMenu: some View {
#if os(macOS)
pagePicker
.labelsHidden()
.offset(x: 15, y: 15)
.frame(maxWidth: 200)
#elseif os(iOS)
Menu {
pagePicker
} label: {
HStack {
Label(page.title, systemImage: page.systemImageName)
Image(systemName: "chevron.up.chevron.down")
.imageScale(.small)
}
.padding(10)
.fixedSize(horizontal: true, vertical: false)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 6))
.frame(width: 200, alignment: .leading)
.transaction { t in t.animation = nil }
}
.animation(nil, value: detailsVisibility)
.modifier(SettingsPickerModifier())
.offset(x: 15, y: 5)
#endif
}
var pagePicker: some View {
Picker("Page", selection: $page) { Picker("Page", selection: $page) {
ForEach(DetailsPage.allCases.filter { pageAvailable($0) }, id: \.rawValue) { page in ForEach(DetailsPage.allCases.filter { pageAvailable($0) }, id: \.rawValue) { page in
Label(page.title, systemImage: page.systemImageName).tag(page) Text(page.title).tag(page)
} }
} }
.pickerStyle(.segmented)
.labelsHidden()
} }
func pageAvailable(_ page: DetailsPage) -> Bool { func pageAvailable(_ page: DetailsPage) -> Bool {
guard let video else { return false } guard let video else { return false }
switch page { switch page {
case .inspector: case .queue:
return true return !player.queue.isEmpty
default: default:
return !video.isLocal return !video.isLocal
} }
} }
var pageView: some View { var pageView: some View {
ZStack(alignment: .topLeading) { ScrollViewReader { proxy in
switch page { ScrollView(.vertical, showsIndicators: false) {
case .info: LazyVStack {
ScrollView(.vertical, showsIndicators: false) { pageMenu
if let video { .id("top")
VStack(alignment: .leading, spacing: 10) { .padding(5)
HStack {
videoProperties
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.bottom, 12)
if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { switch page {
VStack { case .info:
ProgressView() Group {
.progressViewStyle(.circular) if let video {
VStack(alignment: .leading, spacing: 10) {
if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) {
VStack {
ProgressView()
.progressViewStyle(.circular)
}
.frame(maxWidth: .infinity)
} else if let description = video.description, !description.isEmpty {
VideoDescription(video: video, detailsSize: detailsSize)
} else if !video.isLocal {
Text("No description")
.font(.caption)
.foregroundColor(.secondary)
}
if video.isLocal || showInspector == .always {
InspectorView(video: player.videoForDisplay)
}
if !sidebarQueue,
!(player.videoForDisplay?.related.isEmpty ?? true)
{
RelatedView()
.padding(.top, 20)
}
} }
.frame(maxWidth: .infinity) .padding(.bottom, 60)
} 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")
.font(.caption)
.foregroundColor(.secondary)
} }
} }
.padding(.top, 18) .onChange(of: player.currentVideo?.cacheKey) { _ in
.padding(.bottom, 60) proxy.scrollTo("top")
} page = .info
}
.onAppear {
if video != nil, !pageAvailable(page) {
page = .inspector
}
}
#if os(iOS)
.onAppear {
if fullScreen {
if let video, video.isLocal {
page = .inspector
} }
detailsVisibility = true .onAppear {
return if video != nil, !pageAvailable(page) {
page = .info
}
}
.transition(.opacity)
.animation(nil, value: player.currentItem)
.padding(.horizontal)
#if os(iOS)
.frame(maxWidth: YatteeApp.isForPreviews ? .infinity : maxWidth)
#endif
case .queue:
PlayerQueueView(sidebarQueue: false)
.padding(.horizontal)
case .comments:
CommentsView(embedInScrollView: false)
.onAppear {
comments.loadIfNeeded()
}
} }
Delay.by(0.4) { withAnimation(.easeIn(duration: 0.25)) { self.detailsVisibility = true } }
} }
#endif
.transition(.opacity)
.animation(nil, value: player.currentItem)
.padding(.horizontal)
#if os(iOS)
.frame(maxWidth: YatteeApp.isForPreviews ? .infinity : maxWidth)
#endif
case .inspector:
InspectorView(video: video)
case .chapters:
ChaptersView()
case .comments:
CommentsView(embedInScrollView: true)
.onAppear {
comments.loadIfNeeded()
}
} }
pageMenu
.font(.headline)
.foregroundColor(.accentColor)
.zIndex(1)
#if !os(tvOS)
if #available(iOS 16, macOS 13, *) {
Rectangle()
.fill(
LinearGradient(
gradient: .init(colors: [fadePlaceholderStartColor, .clear]),
startPoint: .top,
endPoint: .bottom
)
)
.zIndex(0)
.frame(maxHeight: 22)
}
#endif
} }
} #if os(iOS)
.onAppear {
var fadePlaceholderStartColor: Color { if fullScreen {
#if os(macOS) if let video, video.isLocal {
.secondaryBackground page = .info
#elseif os(iOS) }
.background detailsVisibility = true
#else return
.clear }
Delay.by(0.8) { withAnimation(.easeIn(duration: 0.25)) { self.detailsVisibility = true } }
}
#endif #endif
}
@ViewBuilder var videoProperties: some View { .onChange(of: player.queue) { _ in
HStack(spacing: 4) { if video != nil, !pageAvailable(page) {
Spacer() page = .info
publishedDateSection
Text("")
HStack(spacing: 4) {
if player.videoBeingOpened != nil || video?.viewsCount != nil {
Image(systemName: "eye")
}
if let views = video?.viewsCount {
Text(views)
} else if player.videoBeingOpened != nil {
Text("1,234M").redacted(reason: .placeholder)
}
if player.videoBeingOpened != nil || video?.likesCount != nil {
Image(systemName: "hand.thumbsup")
}
if let likes = video?.likesCount {
Text(likes)
} else if player.videoBeingOpened == nil {
Text("1,234M").redacted(reason: .placeholder)
}
if enableReturnYouTubeDislike {
if player.videoBeingOpened != nil || video?.dislikesCount != nil {
Image(systemName: "hand.thumbsdown")
}
if let dislikes = video?.dislikesCount {
Text(dislikes)
} else if player.videoBeingOpened == nil {
Text("1,234M").redacted(reason: .placeholder)
}
}
}
}
.font(.caption)
.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)
}
}
} }
} }
} }
@ -311,6 +356,6 @@ struct VideoDetails: View {
struct VideoDetails_Previews: PreviewProvider { struct VideoDetails_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
VideoDetails(video: .fixture, fullScreen: .constant(false)) VideoDetails(video: .fixture, fullScreen: .constant(false), sidebarQueue: .constant(false))
} }
} }

View File

@ -92,6 +92,14 @@ struct VideoPlayerView: View {
.onChange(of: playerSidebar) { _ in .onChange(of: playerSidebar) { _ in
updateSidebarQueue() updateSidebarQueue()
} }
#if os(macOS)
.background(
EmptyView().sheet(isPresented: $navigation.presentingChannelSheet) {
ChannelVideosView(channel: navigation.channelPresentedInSheet, showCloseButton: true, inNavigationView: false)
.frame(minWidth: 1000, minHeight: 700)
}
)
#endif
} }
var videoPlayer: some View { var videoPlayer: some View {
@ -323,7 +331,7 @@ struct VideoPlayerView: View {
VideoDetails( VideoDetails(
video: player.videoForDisplay, video: player.videoForDisplay,
fullScreen: $fullScreenDetails, fullScreen: $fullScreenDetails,
bottomPadding: detailsNeedBottomPadding sidebarQueue: $sidebarQueue
) )
#if os(iOS) #if os(iOS)
.ignoresSafeArea(.all, edges: .bottom) .ignoresSafeArea(.all, edges: .bottom)
@ -386,16 +394,29 @@ struct VideoPlayerView: View {
if !fullScreenPlayer { if !fullScreenPlayer {
#if os(iOS) #if os(iOS)
if sidebarQueue { if sidebarQueue {
PlayerQueueView(sidebarQueue: true) List {
.frame(maxWidth: 350) PlayerQueueView(sidebarQueue: true)
.background(colorScheme == .dark ? Color.black : Color.white) }
.transition(.move(edge: .bottom)) #if os(macOS)
.listStyle(.inset)
#elseif os(iOS)
.listStyle(.grouped)
.backport
.scrollContentBackground(false)
#else
.listStyle(.plain)
#endif
.frame(maxWidth: 350)
.background(colorScheme == .dark ? Color.black : Color.white)
.transition(.move(edge: .bottom))
} }
#elseif os(macOS) #elseif os(macOS)
if Defaults[.playerSidebar] != .never { if Defaults[.playerSidebar] != .never {
PlayerQueueView(sidebarQueue: true) List {
.frame(width: 350) PlayerQueueView(sidebarQueue: true)
.background(colorScheme == .dark ? Color.black : Color.white) }
.frame(maxWidth: 350)
.background(colorScheme == .dark ? Color.black : Color.white)
} }
#endif #endif
} }
@ -415,14 +436,6 @@ struct VideoPlayerView: View {
#endif #endif
} }
var detailsNeedBottomPadding: Bool {
#if os(iOS)
return true
#else
return false
#endif
}
var fullScreenPlayer: Bool { var fullScreenPlayer: Bool {
#if os(iOS) #if os(iOS)
player.playingFullScreen || verticalSizeClass == .compact player.playingFullScreen || verticalSizeClass == .compact

View File

@ -27,6 +27,7 @@ struct PlayerSettings: View {
@Default(.openWatchNextOnClose) private var openWatchNextOnClose @Default(.openWatchNextOnClose) private var openWatchNextOnClose
@Default(.openWatchNextOnFinishedWatching) private var openWatchNextOnFinishedWatching @Default(.openWatchNextOnFinishedWatching) private var openWatchNextOnFinishedWatching
@Default(.openWatchNextOnFinishedWatchingDelay) private var openWatchNextOnFinishedWatchingDelay @Default(.openWatchNextOnFinishedWatchingDelay) private var openWatchNextOnFinishedWatchingDelay
@Default(.showInspector) private var showInspector
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ -68,6 +69,12 @@ struct PlayerSettings: View {
#endif #endif
} }
#if !os(tvOS)
Section(header: SettingsHeader(text: "Inspector".localized())) {
inspectorVisibilityPicker
}
#endif
Section(header: SettingsHeader(text: "Watch Next")) { Section(header: SettingsHeader(text: "Watch Next")) {
openWatchNextOnFinishedWatchingToggle openWatchNextOnFinishedWatchingToggle
openWatchNextOnFinishedWatchingDelayTextField openWatchNextOnFinishedWatchingDelayTextField
@ -235,6 +242,14 @@ struct PlayerSettings: View {
Toggle("Close PiP and open player when application enters foreground", isOn: $closePiPAndOpenPlayerOnEnteringForeground) Toggle("Close PiP and open player when application enters foreground", isOn: $closePiPAndOpenPlayerOnEnteringForeground)
} }
#endif #endif
private var inspectorVisibilityPicker: some View {
Picker("Visibility", selection: $showInspector) {
Text("Always").tag(ShowInspectorSetting.always)
Text("Only for local files and URLs").tag(ShowInspectorSetting.onlyLocal)
}
.labelsHidden()
}
} }
struct PlayerSettings_Previews: PreviewProvider { struct PlayerSettings_Previews: PreviewProvider {