Improve macOS channel toolbar header

This commit is contained in:
Arkadiusz Fal
2026-04-23 23:10:37 +02:00
parent 6df80c0e79
commit 85223894ff

View File

@@ -284,6 +284,11 @@ struct ChannelView: View {
isPlayerExpanded: appEnvironment?.navigationCoordinator.isPlayerExpanded ?? false isPlayerExpanded: appEnvironment?.navigationCoordinator.isPlayerExpanded ?? false
)) ))
.toolbar { .toolbar {
#if os(macOS)
ToolbarItem(placement: .principal) {
collapsedToolbarTitle(name: channel.name, thumbnailURL: channel.thumbnailURL)
}
#else
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
// Collapsed title in toolbar // Collapsed title in toolbar
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -308,6 +313,7 @@ struct ChannelView: View {
} }
.opacity(collapsedTitleOpacity) .opacity(collapsedTitleOpacity)
} }
#endif
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button { Button {
@@ -437,6 +443,11 @@ struct ChannelView: View {
.background(viewBackgroundColor) .background(viewBackgroundColor)
.ignoresSafeArea(edges: .top) .ignoresSafeArea(edges: .top)
.toolbar { .toolbar {
#if os(macOS)
ToolbarItem(placement: .principal) {
collapsedToolbarTitle(name: cached.name, thumbnailURL: cached.thumbnailURL)
}
#else
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
// Collapsed title in toolbar (using cached data) // Collapsed title in toolbar (using cached data)
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -461,6 +472,7 @@ struct ChannelView: View {
} }
.opacity(collapsedTitleOpacity) .opacity(collapsedTitleOpacity)
} }
#endif
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button { Button {
@@ -806,6 +818,30 @@ struct ChannelView: View {
// MARK: - Header // MARK: - Header
#if os(macOS)
private func collapsedToolbarTitle(name: String, thumbnailURL: URL?) -> some View {
HStack(spacing: 8) {
LazyImage(url: thumbnailURL) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Circle()
.fill(.quaternary)
}
}
.frame(width: 28, height: 28)
.clipShape(Circle())
Text(name)
.font(.headline)
.lineLimit(1)
}
.padding(.horizontal, 10)
}
#endif
private func header(_ channel: Channel) -> some View { private func header(_ channel: Channel) -> some View {
header(name: channel.name, thumbnailURL: channel.thumbnailURL, bannerURL: channel.bannerURL) header(name: channel.name, thumbnailURL: channel.thumbnailURL, bannerURL: channel.bannerURL)
} }
@@ -814,11 +850,17 @@ struct ChannelView: View {
GeometryReader { geometry in GeometryReader { geometry in
// Calculate avatar vertical position // Calculate avatar vertical position
let avatarBottomPadding: CGFloat = 20 let avatarBottomPadding: CGFloat = 20
#if os(macOS)
let headerAvatarSize = avatarSize
let nameSpace: CGFloat = 0
#else
// Smoothly interpolate the space for channel name (40pt when visible, 0 when collapsed) // Smoothly interpolate the space for channel name (40pt when visible, 0 when collapsed)
// Name starts fading at progress 0, fully gone at 0.3 // Name starts fading at progress 0, fully gone at 0.3
let nameSpaceProgress = min(collapseProgress / 0.3, 1.0) let nameSpaceProgress = min(collapseProgress / 0.3, 1.0)
let nameSpace: CGFloat = 40 * (1 - nameSpaceProgress) let nameSpace: CGFloat = 40 * (1 - nameSpaceProgress)
let avatarContentHeight: CGFloat = currentAvatarSize + nameSpace + avatarBottomPadding let headerAvatarSize = currentAvatarSize
#endif
let avatarContentHeight: CGFloat = headerAvatarSize + nameSpace + avatarBottomPadding
let idealAvatarY = headerHeight - avatarContentHeight let idealAvatarY = headerHeight - avatarContentHeight
// Minimum Y position to prevent going into nav bar // Minimum Y position to prevent going into nav bar
// On iOS 18+ iPhone, the search bar is present in the navigation area (~56pt + padding) // On iOS 18+ iPhone, the search bar is present in the navigation area (~56pt + padding)
@@ -860,10 +902,14 @@ struct ChannelView: View {
.offset(y: pinOffset) .offset(y: pinOffset)
// Avatar and channel name - pinned to bottom of banner // Avatar and channel name - pinned to bottom of banner
#if os(macOS)
let avatarOpacity: CGFloat = 1
#else
// Smooth opacity fade based on collapse progress // Smooth opacity fade based on collapse progress
let avatarOpacity = max(0, 1.0 - collapseProgress * 1.4) let avatarOpacity = max(0, 1.0 - collapseProgress * 1.4)
// Channel name fades out faster than avatar (starts fading at 0, gone by 0.3) // Channel name fades out faster than avatar (starts fading at 0, gone by 0.3)
let nameOpacity = max(0, 1.0 - collapseProgress * 3.3) let nameOpacity = max(0, 1.0 - collapseProgress * 3.3)
#endif
VStack(spacing: 8) { VStack(spacing: 8) {
LazyImage(url: thumbnailURL) { state in LazyImage(url: thumbnailURL) { state in
@@ -876,12 +922,12 @@ struct ChannelView: View {
.fill(.ultraThinMaterial) .fill(.ultraThinMaterial)
.overlay { .overlay {
Text(String(name.prefix(1))) Text(String(name.prefix(1)))
.font(.system(size: currentAvatarSize * 0.4, weight: .semibold)) .font(.system(size: headerAvatarSize * 0.4, weight: .semibold))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
} }
.frame(width: currentAvatarSize, height: currentAvatarSize) .frame(width: headerAvatarSize, height: headerAvatarSize)
.clipShape(Circle()) .clipShape(Circle())
.overlay( .overlay(
Circle() Circle()
@@ -889,6 +935,7 @@ struct ChannelView: View {
) )
.shadow(color: .black.opacity(0.4), radius: 10, y: 5) .shadow(color: .black.opacity(0.4), radius: 10, y: 5)
#if !os(macOS)
// Channel name with smooth fade // Channel name with smooth fade
Text(name) Text(name)
.font(.title2) .font(.title2)
@@ -896,6 +943,7 @@ struct ChannelView: View {
.foregroundStyle(.white) .foregroundStyle(.white)
.shadow(color: .black.opacity(0.6), radius: 4, y: 2) .shadow(color: .black.opacity(0.6), radius: 4, y: 2)
.opacity(nameOpacity) .opacity(nameOpacity)
#endif
} }
.offset(y: clampedAvatarY + pinOffset) .offset(y: clampedAvatarY + pinOffset)
.opacity(avatarOpacity) .opacity(avatarOpacity)
@@ -2382,6 +2430,9 @@ private struct ChannelScrollOffsetModifier: ViewModifier {
var isPlayerExpanded: Bool var isPlayerExpanded: Bool
func body(content: Content) -> some View { func body(content: Content) -> some View {
#if os(macOS)
content
#else
content content
.onScrollGeometryChange(for: CGFloat.self) { geometry in .onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y geometry.contentOffset.y
@@ -2393,6 +2444,7 @@ private struct ChannelScrollOffsetModifier: ViewModifier {
scrollOffset = newValue scrollOffset = newValue
} }
} }
#endif
} }
} }