mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 04:31:54 +00:00 
			
		
		
		
	Video details changes and channel sheet
This commit is contained in:
		| @@ -4,21 +4,146 @@ import SDWebImageSwiftUI | ||||
| import SwiftUI | ||||
|  | ||||
| struct VideoDetails: View { | ||||
|     enum DetailsPage: String, CaseIterable, Defaults.Serializable { | ||||
|         case info, comments, chapters, inspector | ||||
|     struct TitleView: View { | ||||
|         @ObservedObject private var model = PlayerModel.shared | ||||
|         @State private var titleSize = CGSize.zero | ||||
|  | ||||
|         var systemImageName: String { | ||||
|             switch self { | ||||
|             case .info: | ||||
|                 return "info.circle" | ||||
|             case .inspector: | ||||
|                 return "wand.and.stars" | ||||
|             case .comments: | ||||
|                 return "text.bubble" | ||||
|             case .chapters: | ||||
|                 return "bookmark" | ||||
|         var video: Video? { model.videoForDisplay } | ||||
|  | ||||
|         var body: some View { | ||||
|             HStack(spacing: 0) { | ||||
|                 Text(model.videoForDisplay?.displayTitle ?? "Not playing") | ||||
|                     .font(.title3.bold()) | ||||
|                     .lineLimit(4) | ||||
|             } | ||||
|             .padding(.vertical, 4) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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 { | ||||
|             rawValue.capitalized.localized() | ||||
| @@ -28,7 +153,7 @@ struct VideoDetails: View { | ||||
|     var video: Video? | ||||
|  | ||||
|     @Binding var fullScreen: Bool | ||||
|     var bottomPadding = false | ||||
|     @Binding var sidebarQueue: Bool | ||||
|  | ||||
|     @State private var detailsSize = CGSize.zero | ||||
|     @State private var detailsVisibility = Constants.detailsVisibility | ||||
| @@ -49,22 +174,40 @@ struct VideoDetails: View { | ||||
|  | ||||
|     @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike | ||||
|     @Default(.playerSidebar) private var playerSidebar | ||||
|     @Default(.showInspector) private var showInspector | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 0) { | ||||
|             ControlsBar( | ||||
|                 fullScreen: $fullScreen, | ||||
|                 expansionState: .constant(.full), | ||||
|                 presentingControls: false, | ||||
|                 backgroundEnabled: false, | ||||
|                 borderTop: false, | ||||
|                 detailsTogglePlayer: false, | ||||
|                 detailsToggleFullScreen: true | ||||
|             ) | ||||
|             .animation(nil, value: player.currentItem) | ||||
|             VStack(alignment: .leading, spacing: 0) { | ||||
|                 TitleView() | ||||
|                 if video != nil, !video!.isLocal { | ||||
|                     ChannelView() | ||||
|                         .layoutPriority(1) | ||||
|                         .padding(.bottom, 6) | ||||
|                 } | ||||
|             } | ||||
|             .frame(maxWidth: .infinity, alignment: .leading) | ||||
|             .contentShape(Rectangle()) | ||||
|             .padding(.horizontal, 16) | ||||
|             #if !os(tvOS) | ||||
|                 .tapRecognizer( | ||||
|                     tapSensitivity: 0.2, | ||||
|                     doubleTapAction: { | ||||
|                         withAnimation(.default) { | ||||
|                             fullScreen.toggle() | ||||
|                         } | ||||
|                     } | ||||
|                 ) | ||||
|             #endif | ||||
|  | ||||
|             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) | ||||
|                 .frame(minWidth: 0, maxWidth: .infinity) | ||||
|  | ||||
|             pageView | ||||
|             #if os(iOS) | ||||
| @@ -100,210 +243,112 @@ struct VideoDetails: 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) { | ||||
|             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 { | ||||
|         guard let video else { return false } | ||||
|  | ||||
|         switch page { | ||||
|         case .inspector: | ||||
|             return true | ||||
|         case .queue: | ||||
|             return !player.queue.isEmpty | ||||
|         default: | ||||
|             return !video.isLocal | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var pageView: some View { | ||||
|         ZStack(alignment: .topLeading) { | ||||
|             switch page { | ||||
|             case .info: | ||||
|                 ScrollView(.vertical, showsIndicators: false) { | ||||
|                     if let video { | ||||
|                         VStack(alignment: .leading, spacing: 10) { | ||||
|                             HStack { | ||||
|                                 videoProperties | ||||
|                                     .frame(maxWidth: .infinity, alignment: .trailing) | ||||
|                             } | ||||
|                             .padding(.bottom, 12) | ||||
|         ScrollViewReader { proxy in | ||||
|             ScrollView(.vertical, showsIndicators: false) { | ||||
|                 LazyVStack { | ||||
|                     pageMenu | ||||
|                         .id("top") | ||||
|                         .padding(5) | ||||
|  | ||||
|                             if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { | ||||
|                                 VStack { | ||||
|                                     ProgressView() | ||||
|                                         .progressViewStyle(.circular) | ||||
|                     switch page { | ||||
|                     case .info: | ||||
|                         Group { | ||||
|                             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) | ||||
|                             } 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(.bottom, 60) | ||||
|                             } | ||||
|                         } | ||||
|                         .padding(.top, 18) | ||||
|                         .padding(.bottom, 60) | ||||
|                     } | ||||
|                 } | ||||
|                 .onAppear { | ||||
|                     if video != nil, !pageAvailable(page) { | ||||
|                         page = .inspector | ||||
|                     } | ||||
|                 } | ||||
|                 #if os(iOS) | ||||
|                 .onAppear { | ||||
|                     if fullScreen { | ||||
|                         if let video, video.isLocal { | ||||
|                             page = .inspector | ||||
|                         .onChange(of: player.currentVideo?.cacheKey) { _ in | ||||
|                             proxy.scrollTo("top") | ||||
|                             page = .info | ||||
|                         } | ||||
|                         detailsVisibility = true | ||||
|                         return | ||||
|                         .onAppear { | ||||
|                             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 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var fadePlaceholderStartColor: Color { | ||||
|         #if os(macOS) | ||||
|             .secondaryBackground | ||||
|         #elseif os(iOS) | ||||
|             .background | ||||
|         #else | ||||
|             .clear | ||||
|         #if os(iOS) | ||||
|         .onAppear { | ||||
|             if fullScreen { | ||||
|                 if let video, video.isLocal { | ||||
|                     page = .info | ||||
|                 } | ||||
|                 detailsVisibility = true | ||||
|                 return | ||||
|             } | ||||
|             Delay.by(0.8) { withAnimation(.easeIn(duration: 0.25)) { self.detailsVisibility = true } } | ||||
|         } | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     @ViewBuilder var videoProperties: some View { | ||||
|         HStack(spacing: 4) { | ||||
|             Spacer() | ||||
|             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) | ||||
|                     } | ||||
|                 } | ||||
|         .onChange(of: player.queue) { _ in | ||||
|             if video != nil, !pageAvailable(page) { | ||||
|                 page = .info | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -311,6 +356,6 @@ struct VideoDetails: View { | ||||
|  | ||||
| struct VideoDetails_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         VideoDetails(video: .fixture, fullScreen: .constant(false)) | ||||
|         VideoDetails(video: .fixture, fullScreen: .constant(false), sidebarQueue: .constant(false)) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Arkadiusz Fal
					Arkadiusz Fal