mirror of
https://github.com/yattee/yattee.git
synced 2025-01-22 04:37:04 +00:00
Video details changes and channel sheet
This commit is contained in:
parent
5db74a3997
commit
67690bc435
@ -84,6 +84,9 @@ final class NavigationModel: ObservableObject {
|
||||
@Published var presentingAccounts = false
|
||||
@Published var presentingWelcomeScreen = false
|
||||
|
||||
@Published var presentingChannelSheet = false
|
||||
@Published var channelPresentedInSheet: Channel!
|
||||
|
||||
@Published var presentingShareSheet = false
|
||||
@Published var shareURL: URL?
|
||||
|
||||
@ -103,7 +106,6 @@ final class NavigationModel: ObservableObject {
|
||||
|
||||
hideKeyboard()
|
||||
let presentingPlayer = player.presentingPlayer
|
||||
player.hide()
|
||||
presentingChannel = false
|
||||
|
||||
#if os(macOS)
|
||||
@ -113,20 +115,30 @@ final class NavigationModel: ObservableObject {
|
||||
let recent = RecentItem(from: channel)
|
||||
recents.add(RecentItem(from: channel))
|
||||
|
||||
if navigationStyle == .sidebar {
|
||||
sidebarSectionChanged.toggle()
|
||||
tabSelection = .recentlyOpened(recent.tag)
|
||||
} else {
|
||||
var delay = 0.0
|
||||
let navigateToChannel = {
|
||||
#if os(iOS)
|
||||
if presentingPlayer { delay = 1.0 }
|
||||
self.player.hide()
|
||||
#endif
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
|
||||
if navigationStyle == .sidebar {
|
||||
self.sidebarSectionChanged.toggle()
|
||||
self.tabSelection = .recentlyOpened(recent.tag)
|
||||
} else {
|
||||
withAnimation(Constants.overlayAnimation) {
|
||||
self.presentingChannel = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if presentingPlayer {
|
||||
presentChannelInSheet(channel)
|
||||
} else {
|
||||
navigateToChannel()
|
||||
}
|
||||
#else
|
||||
navigateToChannel()
|
||||
#endif
|
||||
}
|
||||
|
||||
func openChannelPlaylist(_ playlist: ChannelPlaylist, navigationStyle: NavigationStyle) {
|
||||
@ -273,6 +285,11 @@ final class NavigationModel: ObservableObject {
|
||||
shareURL = url
|
||||
presentingShareSheet = true
|
||||
}
|
||||
|
||||
func presentChannelInSheet(_ channel: Channel) {
|
||||
channelPresentedInSheet = channel
|
||||
presentingChannelSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
typealias TabSelection = NavigationModel.TabSelection
|
||||
|
@ -108,6 +108,7 @@ struct OpenVideosModel {
|
||||
)
|
||||
|
||||
WatchNextViewModel.shared.hide()
|
||||
NavigationModel.shared.presentingChannelSheet = false
|
||||
|
||||
if playbackMode == .playNow || playbackMode == .shuffleAll {
|
||||
#if os(iOS)
|
||||
|
@ -335,6 +335,7 @@ final class PlayerModel: ObservableObject {
|
||||
videoBeingOpened = video
|
||||
|
||||
WatchNextViewModel.shared.hide()
|
||||
navigation.presentingChannelSheet = false
|
||||
|
||||
var changeBackendHandler: (() -> Void)?
|
||||
|
||||
|
@ -15,6 +15,8 @@ extension PlayerModel {
|
||||
|
||||
func play(_ videos: [Video], shuffling: Bool = false) {
|
||||
WatchNextViewModel.shared.hide()
|
||||
navigation.presentingChannelSheet = false
|
||||
|
||||
playbackMode = shuffling ? .shuffle : .queue
|
||||
|
||||
videos.forEach { enqueueVideo($0, loadDetails: false) }
|
||||
@ -33,6 +35,8 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func playNow(_ video: Video, at time: CMTime? = nil) {
|
||||
navigation.presentingChannelSheet = false
|
||||
|
||||
if playingInPictureInPicture, closePiPOnNavigation {
|
||||
closePiP()
|
||||
}
|
||||
@ -56,6 +60,7 @@ extension PlayerModel {
|
||||
comments.reset()
|
||||
stream = nil
|
||||
WatchNextViewModel.shared.hide()
|
||||
navigation.presentingChannelSheet = false
|
||||
|
||||
withAnimation {
|
||||
aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||
@ -176,6 +181,7 @@ extension PlayerModel {
|
||||
remove(newItem)
|
||||
|
||||
WatchNextViewModel.shared.hide()
|
||||
navigation.presentingChannelSheet = false
|
||||
currentItem = newItem
|
||||
currentItem.playbackTime = time
|
||||
|
||||
@ -219,9 +225,12 @@ extension PlayerModel {
|
||||
let item = PlayerQueueItem(video, playbackTime: atTime)
|
||||
|
||||
if play {
|
||||
navigation.presentingChannelSheet = false
|
||||
|
||||
withAnimation {
|
||||
aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||
WatchNextViewModel.shared.hide()
|
||||
navigation.presentingChannelSheet = false
|
||||
currentItem = item
|
||||
}
|
||||
videoBeingOpened = video
|
||||
|
@ -6,6 +6,7 @@ import SwiftUI
|
||||
struct ChannelVideosView: View {
|
||||
var channel: Channel?
|
||||
var showCloseButton = false
|
||||
var inNavigationView = true
|
||||
|
||||
@State private var presentingShareSheet = false
|
||||
@State private var shareURL: URL?
|
||||
@ -119,24 +120,28 @@ struct ChannelVideosView: View {
|
||||
Button {
|
||||
withAnimation(Constants.overlayAnimation) {
|
||||
navigation.presentingChannel = false
|
||||
navigation.presentingChannelSheet = false
|
||||
}
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
#if !os(macOS)
|
||||
.buttonStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if !os(iOS)
|
||||
#if os(macOS)
|
||||
ToolbarItem(placement: .navigation) {
|
||||
thumbnail
|
||||
}
|
||||
ToolbarItem {
|
||||
ToolbarItemGroup {
|
||||
if !inNavigationView {
|
||||
Text(navigationTitle)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
|
||||
}
|
||||
ToolbarItem {
|
||||
HideShortsButtons(hide: $hideShorts)
|
||||
}
|
||||
ToolbarItem {
|
||||
contentTypePicker
|
||||
}
|
||||
|
||||
@ -160,10 +165,12 @@ struct ChannelVideosView: View {
|
||||
|
||||
ToolbarItem {
|
||||
favoriteButton
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
|
||||
ToolbarItem {
|
||||
toggleWatchedButton
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -234,14 +241,14 @@ struct ChannelVideosView: View {
|
||||
Group {
|
||||
if let subscribers = store.item?.channel?.subscriptionsString {
|
||||
HStack(spacing: 0) {
|
||||
Text(subscribers)
|
||||
Image(systemName: "person.2.fill")
|
||||
Text(subscribers)
|
||||
}
|
||||
} else if store.item.isNil {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "person.2.fill")
|
||||
Text("1234")
|
||||
.redacted(reason: .placeholder)
|
||||
Image(systemName: "person.2.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -252,10 +259,10 @@ struct ChannelVideosView: View {
|
||||
var viewsLabel: some View {
|
||||
HStack(spacing: 0) {
|
||||
if let views = store.item?.channel?.totalViewsString {
|
||||
Text(views)
|
||||
|
||||
Image(systemName: "eye.fill")
|
||||
.imageScale(.small)
|
||||
|
||||
Text(views)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
@ -328,6 +335,7 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
|
||||
private func typeAvailable(_ type: Channel.ContentType) -> Bool {
|
||||
@ -463,7 +471,7 @@ struct ChannelVideosView: View {
|
||||
struct ChannelVideosView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
#if os(macOS)
|
||||
ChannelVideosView(channel: Video.fixture.channel)
|
||||
ChannelVideosView(channel: Video.fixture.channel, showCloseButton: true, inNavigationView: false)
|
||||
.environment(\.navigationStyle, .sidebar)
|
||||
#else
|
||||
NavigationView {
|
||||
|
@ -241,6 +241,7 @@ extension Defaults.Keys {
|
||||
static let openWatchNextOnFinishedWatchingDelay = Key<String>("openWatchNextOnFinishedWatchingDelay", default: "5")
|
||||
|
||||
static let hideShorts = Key<Bool>("hideShorts", default: false)
|
||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||
}
|
||||
|
||||
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||
|
@ -120,6 +120,15 @@ struct ContentView: View {
|
||||
OpenVideosView()
|
||||
}
|
||||
)
|
||||
#if !os(macOS)
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingChannelSheet) {
|
||||
NavigationView {
|
||||
ChannelVideosView(channel: navigation.channelPresentedInSheet, showCloseButton: true)
|
||||
}
|
||||
}
|
||||
)
|
||||
#endif
|
||||
.alert(isPresented: $navigation.presentingAlert) { navigation.alert }
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ struct VideoDetailsOverlay: View {
|
||||
@ObservedObject private var controls = PlayerControlsModel.shared
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@ struct RelatedView: View {
|
||||
@ObservedObject private var player = PlayerModel.shared
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if let related = player.currentVideo?.related {
|
||||
Section(header: Text("Related")) {
|
||||
LazyVStack {
|
||||
if let related = player.videoForDisplay?.related {
|
||||
Section(header: header) {
|
||||
ForEach(related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video))
|
||||
.listRowBackground(Color.clear)
|
||||
@ -34,6 +34,15 @@ struct RelatedView: View {
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
Text("Related")
|
||||
#if !os(macOS)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct RelatedView_Previews: PreviewProvider {
|
||||
|
@ -25,7 +25,6 @@ struct CommentsView: View {
|
||||
.borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor"))
|
||||
}
|
||||
}
|
||||
.padding(.top, 55)
|
||||
|
||||
if embedInScrollView {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
|
@ -6,7 +6,7 @@ struct InspectorView: View {
|
||||
@ObservedObject private var player = PlayerModel.shared
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
Section(header: header) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let video {
|
||||
VStack(spacing: 4) {
|
||||
@ -53,10 +53,14 @@ struct InspectorView: View {
|
||||
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 {
|
||||
|
@ -13,7 +13,7 @@ struct PlayerQueueView: View {
|
||||
@Default(.saveHistory) private var saveHistory
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Group {
|
||||
Group {
|
||||
if player.playbackMode == .related {
|
||||
autoplaying
|
||||
@ -34,15 +34,6 @@ struct PlayerQueueView: View {
|
||||
.listRowSeparator(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 {
|
||||
@ -65,6 +56,8 @@ struct PlayerQueueView: View {
|
||||
var autoplayingHeader: some View {
|
||||
HStack {
|
||||
Text("Autoplaying Next")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
Spacer()
|
||||
Button {
|
||||
player.setRelatedAutoplayItem()
|
||||
@ -78,7 +71,7 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
|
||||
var playingNext: some View {
|
||||
Section(header: Text("Queue")) {
|
||||
Section(header: queueHeader) {
|
||||
if player.queue.isEmpty {
|
||||
Text("Queue is empty")
|
||||
.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] {
|
||||
watches.filter { $0.videoID != player.currentVideo?.videoID }
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +92,14 @@ struct VideoPlayerView: View {
|
||||
.onChange(of: playerSidebar) { _ in
|
||||
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 {
|
||||
@ -323,7 +331,7 @@ struct VideoPlayerView: View {
|
||||
VideoDetails(
|
||||
video: player.videoForDisplay,
|
||||
fullScreen: $fullScreenDetails,
|
||||
bottomPadding: detailsNeedBottomPadding
|
||||
sidebarQueue: $sidebarQueue
|
||||
)
|
||||
#if os(iOS)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
@ -386,16 +394,29 @@ struct VideoPlayerView: View {
|
||||
if !fullScreenPlayer {
|
||||
#if os(iOS)
|
||||
if sidebarQueue {
|
||||
PlayerQueueView(sidebarQueue: true)
|
||||
.frame(maxWidth: 350)
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
.transition(.move(edge: .bottom))
|
||||
List {
|
||||
PlayerQueueView(sidebarQueue: true)
|
||||
}
|
||||
#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)
|
||||
if Defaults[.playerSidebar] != .never {
|
||||
PlayerQueueView(sidebarQueue: true)
|
||||
.frame(width: 350)
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
List {
|
||||
PlayerQueueView(sidebarQueue: true)
|
||||
}
|
||||
.frame(maxWidth: 350)
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -415,14 +436,6 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var detailsNeedBottomPadding: Bool {
|
||||
#if os(iOS)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
var fullScreenPlayer: Bool {
|
||||
#if os(iOS)
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
|
@ -27,6 +27,7 @@ struct PlayerSettings: View {
|
||||
@Default(.openWatchNextOnClose) private var openWatchNextOnClose
|
||||
@Default(.openWatchNextOnFinishedWatching) private var openWatchNextOnFinishedWatching
|
||||
@Default(.openWatchNextOnFinishedWatchingDelay) private var openWatchNextOnFinishedWatchingDelay
|
||||
@Default(.showInspector) private var showInspector
|
||||
|
||||
@ObservedObject private var accounts = AccountsModel.shared
|
||||
|
||||
@ -68,6 +69,12 @@ struct PlayerSettings: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
Section(header: SettingsHeader(text: "Inspector".localized())) {
|
||||
inspectorVisibilityPicker
|
||||
}
|
||||
#endif
|
||||
|
||||
Section(header: SettingsHeader(text: "Watch Next")) {
|
||||
openWatchNextOnFinishedWatchingToggle
|
||||
openWatchNextOnFinishedWatchingDelayTextField
|
||||
@ -235,6 +242,14 @@ struct PlayerSettings: View {
|
||||
Toggle("Close PiP and open player when application enters foreground", isOn: $closePiPAndOpenPlayerOnEnteringForeground)
|
||||
}
|
||||
#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 {
|
||||
|
Loading…
Reference in New Issue
Block a user