diff --git a/Shared/Channels/ChannelLinkView.swift b/Shared/Channels/ChannelLinkView.swift new file mode 100644 index 00000000..fe171ab1 --- /dev/null +++ b/Shared/Channels/ChannelLinkView.swift @@ -0,0 +1,76 @@ +import Foundation +import SwiftUI + +struct ChannelLinkView: View { + let channel: Channel + let channelLabel: ChannelLabel + + @Environment(\.inChannelView) private var inChannelView + @Environment(\.navigationStyle) private var navigationStyle + + init( + channel: Channel, + @ViewBuilder channelLabel: () -> ChannelLabel + ) { + self.channel = channel + self.channelLabel = channelLabel() + } + + var body: some View { + channelControl + } + + @ViewBuilder private var channelControl: some View { + if !channel.name.isEmpty { + #if os(tvOS) + channelLabel + #else + if navigationStyle == .tab { + channelNavigationLink + } else { + channelButton + #if os(macOS) + .onHover(perform: onHover(_:)) + #endif + } + #endif + } + } + + @ViewBuilder private var channelNavigationLink: some View { + NavigationLink(destination: ChannelVideosView(channel: channel)) { + channelLabel + } + } + + @ViewBuilder private var channelButton: some View { + Button { + guard !inChannelView else { + return + } + + NavigationModel.shared.openChannel( + channel, + navigationStyle: navigationStyle + ) + } label: { + channelLabel + } + #if os(tvOS) + .buttonStyle(.card) + #else + .buttonStyle(.plain) + #endif + .help("\(channel.name) Channel") + } + + #if os(macOS) + private func onHover(_ inside: Bool) { + if inside { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + #endif +} diff --git a/Shared/Constants.swift b/Shared/Constants.swift index 5a3620ab..cd3fb309 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -11,4 +11,20 @@ struct Constants { 0.6 #endif } + + static var channelThumbnailSize: Double { + #if os(tvOS) + 50 + #else + 30 + #endif + } + + static var channelDetailsStackSpacing: Double { + #if os(tvOS) + 12 + #else + 6 + #endif + } } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 5ad9f3a1..00b41cea 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -44,7 +44,7 @@ extension Defaults.Keys { #if os(iOS) static let lockPortraitWhenBrowsing = Key("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone) #endif - static let channelOnThumbnail = Key("channelOnThumbnail", default: true) + static let channelOnThumbnail = Key("channelOnThumbnail", default: false) static let timeOnThumbnail = Key("timeOnThumbnail", default: true) static let roundedThumbnails = Key("roundedThumbnails", default: true) static let thumbnailsQuality = Key("thumbnailsQuality", default: .highest) diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 8c772735..6943d195 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -114,11 +114,11 @@ struct PlayerControls: View { Text(player.currentVideo?.displayAuthor ?? "") .fontWeight(.semibold) .shadow(radius: 10) - .foregroundColor(.secondary) + .foregroundColor(.init(white: 0.8)) .font(.system(size: playerControlsLayout.authorLineFontSize)) .lineLimit(1) } - + .foregroundColor(.white) .frame(maxWidth: .infinity, alignment: .leading) .offset(y: -40) } diff --git a/Shared/Videos/VideoBanner.swift b/Shared/Videos/VideoBanner.swift index 2b4c455f..f320b78d 100644 --- a/Shared/Videos/VideoBanner.swift +++ b/Shared/Videos/VideoBanner.swift @@ -74,21 +74,22 @@ struct VideoBanner: View { HStack { HStack { - if !inChannelView, - let video, - let url = video.channel.thumbnailURLOrCached - { - ThumbnailView(url: url) - .frame(width: 30, height: 30) - .clipShape(Circle()) - } - VStack(alignment: .leading) { Group { if let video { if !inChannelView, !video.isLocal || video.localStreamIsRemoteURL { - channelControl - .font(.subheadline) + ChannelLinkView(channel: video.channel) { + HStack(spacing: Constants.channelDetailsStackSpacing) { + if let url = video.channel.thumbnailURLOrCached { + ThumbnailView(url: url) + .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) + .clipShape(Circle()) + } + + channelLabel + .font(.subheadline) + } + } } else { #if os(iOS) if DocumentsModel.shared.isDocument(video) { @@ -220,7 +221,7 @@ struct VideoBanner: View { private var thumbnailWidth: Double { #if os(tvOS) - 250 + 356 #else 120 #endif @@ -228,7 +229,7 @@ struct VideoBanner: View { private var thumbnailHeight: Double { #if os(tvOS) - 140 + 200 #else 72 #endif @@ -308,50 +309,7 @@ struct VideoBanner: View { (progressViewValue / progressViewTotal) * 100 > Double(Defaults[.watchedThreshold]) } - @ViewBuilder private var channelControl: some View { - if let video, !video.displayAuthor.isEmpty { - #if os(tvOS) - displayAuthor - #else - if navigationStyle == .tab, inNavigationView { - channelNavigationLink - } else { - channelButton - } - #endif - } - } - - @ViewBuilder private var channelNavigationLink: some View { - if let channel = video?.channel { - NavigationLink(destination: ChannelVideosView(channel: channel)) { - displayAuthor - } - } - } - - @ViewBuilder private var channelButton: some View { - if let video { - Button { - guard !inChannelView else { return } - - NavigationModel.shared.openChannel( - video.channel, - navigationStyle: navigationStyle - ) - } label: { - displayAuthor - } - #if os(tvOS) - .buttonStyle(.card) - #else - .buttonStyle(.plain) - #endif - .help("\(video.channel.name) Channel") - } - } - - @ViewBuilder private var displayAuthor: some View { + @ViewBuilder private var channelLabel: some View { if let video, !video.displayAuthor.isEmpty { Text(video.displayAuthor) .fontWeight(.semibold) diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index 08a059c8..aa025b6b 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -41,7 +41,7 @@ struct VideoCell: View { Button(action: playAction) { content #if os(tvOS) - .frame(width: 580, height: 470) + .frame(width: 580, height: channelOnThumbnail ? 470 : 500) #endif } .opacity(contentOpacity) @@ -166,17 +166,22 @@ struct VideoCell: View { videoDetail(video.displayTitle, lineLimit: 5) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - HStack(spacing: 12) { + HStack(spacing: Constants.channelDetailsStackSpacing) { if !inChannelView, let video, let url = video.channel.thumbnailURLOrCached { - ThumbnailView(url: url) - .frame(width: 30, height: 30) - .clipShape(Circle()) + ChannelLinkView(channel: video.channel) { + ThumbnailView(url: url) + .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) + .clipShape(Circle()) + } } + if !channelOnThumbnail, !inChannelView { - channelControl(badge: false) + ChannelLinkView(channel: video.channel) { + channelLabel(badge: false) + } } } @@ -259,14 +264,22 @@ struct VideoCell: View { #if os(tvOS) .frame(minHeight: 60, alignment: .top) #elseif os(macOS) - .frame(minHeight: 32, alignment: .top) + .frame(minHeight: 35, alignment: .top) #else - .frame(minHeight: 40, alignment: .top) + .frame(minHeight: 43, alignment: .top) #endif if !channelOnThumbnail, !inChannelView { - channelControl(badge: false) - .padding(.top, 4) - .padding(.bottom, 6) + ChannelLinkView(channel: video.channel) { + HStack(spacing: Constants.channelDetailsStackSpacing) { + if let url = video.channel.thumbnailURLOrCached { + ThumbnailView(url: url) + .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) + .clipShape(Circle()) + } + + channelLabel(badge: false) + } + } } } .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) @@ -274,20 +287,23 @@ struct VideoCell: View { #if os(tvOS) .frame(minHeight: channelOnThumbnail ? 80 : 120, alignment: .top) #elseif os(macOS) - .frame(minHeight: 35, alignment: .top) + .frame(minHeight: channelOnThumbnail ? 52 : 75, alignment: .top) #else - .frame(minHeight: 50, alignment: .top) + .frame(minHeight: channelOnThumbnail ? 50 : 70, alignment: .top) #endif .padding(.bottom, 4) HStack(spacing: 8) { - if !inChannelView, + if channelOnThumbnail, + !inChannelView, let video, let url = video.channel.thumbnailURLOrCached { - ThumbnailView(url: url) - .frame(width: 30, height: 30) - .clipShape(Circle()) + ChannelLinkView(channel: video.channel) { + ThumbnailView(url: url) + .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) + .clipShape(Circle()) + } } if let date = video.publishedDate { @@ -314,7 +330,7 @@ struct VideoCell: View { } .lineLimit(1) .foregroundColor(.secondary) - .frame(maxWidth: .infinity, minHeight: 30, alignment: .topLeading) + .frame(maxWidth: .infinity, minHeight: 35, alignment: .topLeading) #if os(tvOS) .padding(.bottom, 10) #endif @@ -327,47 +343,6 @@ struct VideoCell: View { } } - @ViewBuilder private func channelControl(badge: Bool = true) -> some View { - if !video.channel.name.isEmpty { - #if os(tvOS) - channelButton(badge: badge) - #else - if navigationStyle == .tab { - channelNavigationLink(badge: badge) - } else { - channelButton(badge: badge) - } - #endif - } - } - - @ViewBuilder private func channelNavigationLink(badge: Bool = true) -> some View { - NavigationLink(destination: ChannelVideosView(channel: video.channel)) { - channelLabel(badge: badge) - } - } - - @ViewBuilder private func channelButton(badge: Bool = true) -> some View { - Button { - guard !inChannelView else { - return - } - - NavigationModel.shared.openChannel( - video.channel, - navigationStyle: navigationStyle - ) - } label: { - channelLabel(badge: badge) - } - #if os(tvOS) - .buttonStyle(.card) - #else - .buttonStyle(.plain) - #endif - .help("\(video.channel.name) Channel") - } - @ViewBuilder private func channelLabel(badge: Bool = true) -> some View { if badge { DetailBadge(text: video.author, style: .prominent) @@ -415,7 +390,9 @@ struct VideoCell: View { Spacer() if channelOnThumbnail, !inChannelView { - channelControl() + ChannelLinkView(channel: video.channel) { + channelLabel() + } } } #if os(tvOS) diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index a3c06233..4e173943 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -171,6 +171,9 @@ 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; + 3717407D2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717407C2949D40800FDDBC7 /* ChannelLinkView.swift */; }; + 3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717407C2949D40800FDDBC7 /* ChannelLinkView.swift */; }; + 3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3717407C2949D40800FDDBC7 /* ChannelLinkView.swift */; }; 3718B9A02921A9620003DB2E /* VideoDetailsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */; }; 3718B9A12921A9640003DB2E /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; }; 3718B9A22921A9670003DB2E /* VideoActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EF29216C630017D862 /* VideoActions.swift */; }; @@ -1144,6 +1147,7 @@ 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = ""; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = ""; }; + 3717407C2949D40800FDDBC7 /* ChannelLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelLinkView.swift; sourceTree = ""; }; 37192D5628B179D60012EEDD /* ChaptersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChaptersView.swift; sourceTree = ""; }; 371B7E5B27596B8400D21217 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 371B7E602759706A00D21217 /* CommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsView.swift; sourceTree = ""; }; @@ -2143,6 +2147,7 @@ children = ( 3776924D294630110055EC18 /* ChannelAvatarView.swift */, 3743B86727216D3600261544 /* ChannelCell.swift */, + 3717407C2949D40800FDDBC7 /* ChannelLinkView.swift */, 37BDFF1A29487C5A000C6404 /* ChannelListItem.swift */, 37C3A24827235FAA0087A57A /* ChannelPlaylistCell.swift */, 37BDFF1E29488117000C6404 /* ChannelPlaylistListItem.swift */, @@ -3079,6 +3084,7 @@ 3722AEBC274DA396005EA4D6 /* Badge+Backport.swift in Sources */, 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, 37599F34272B44000087F250 /* FavoritesModel.swift in Sources */, + 3717407D2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 37030FF727B0347C00ECDDAA /* MPVPlayerView.swift in Sources */, 37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, @@ -3484,6 +3490,7 @@ 378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */, 370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */, 37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */, + 3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 37FB28422721B22200A57617 /* ContentItem.swift in Sources */, 376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, 37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, @@ -3694,6 +3701,7 @@ 374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 37130A61277657300033018A /* PersistenceController.swift in Sources */, 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, + 3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */, 376A33E62720CB35000C1D6B /* Account.swift in Sources */, 3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */,