mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 19:05:03 +00:00
Polish Search view layout on tvOS
- Disable scroll clipping so focused source/channel/playlist cards show full halo - Remove rounded clip on source picker row that cut the Menu focus effect - Replace tappable recents header Button with a plain label on tvOS - Add vertical spacing between recent search items - Widen recent channel and playlist cards and reserve space for two-line titles - Increase horizontal spacing between cards so focus halos don't collide
This commit is contained in:
@@ -22,7 +22,13 @@ struct ChannelCardGridView: View {
|
|||||||
private var subscriberFont: Font { isCompact ? .caption2 : .caption }
|
private var subscriberFont: Font { isCompact ? .caption2 : .caption }
|
||||||
|
|
||||||
/// Minimum height for channel name to reserve space for 2 lines
|
/// Minimum height for channel name to reserve space for 2 lines
|
||||||
private var titleMinHeight: CGFloat { isCompact ? 32 : 40 }
|
private var titleMinHeight: CGFloat {
|
||||||
|
#if os(tvOS)
|
||||||
|
isCompact ? 56 : 80
|
||||||
|
#else
|
||||||
|
isCompact ? 32 : 40
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center, spacing: isCompact ? 8 : 12) {
|
VStack(alignment: .center, spacing: isCompact ? 8 : 12) {
|
||||||
@@ -46,7 +52,9 @@ struct ChannelCardGridView: View {
|
|||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.frame(minHeight: titleMinHeight, alignment: .top)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: titleMinHeight, alignment: .top)
|
||||||
|
|
||||||
// Subscriber count row - reserve space even when nil
|
// Subscriber count row - reserve space even when nil
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ struct PlaylistCardView: View {
|
|||||||
private var titleFont: Font { isCompact ? .caption : .subheadline }
|
private var titleFont: Font { isCompact ? .caption : .subheadline }
|
||||||
private var authorFont: Font { isCompact ? .caption2 : .caption }
|
private var authorFont: Font { isCompact ? .caption2 : .caption }
|
||||||
|
|
||||||
|
private var metadataHeight: CGFloat {
|
||||||
|
#if os(tvOS)
|
||||||
|
isCompact ? 90 : 110
|
||||||
|
#else
|
||||||
|
isCompact ? 50 : 58
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: isCompact ? 4 : 8) {
|
VStack(alignment: .leading, spacing: isCompact ? 4 : 8) {
|
||||||
// Thumbnail with video count badge - fixed 16:9 aspect ratio container
|
// Thumbnail with video count badge - fixed 16:9 aspect ratio container
|
||||||
@@ -61,6 +69,8 @@ struct PlaylistCardView: View {
|
|||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
Text(playlist.authorName)
|
Text(playlist.authorName)
|
||||||
.font(authorFont)
|
.font(authorFont)
|
||||||
@@ -69,7 +79,7 @@ struct PlaylistCardView: View {
|
|||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
.frame(height: isCompact ? 50 : 58)
|
.frame(height: metadataHeight)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,6 +362,22 @@ struct SearchView: View {
|
|||||||
|
|
||||||
// MARK: - Views
|
// MARK: - Views
|
||||||
|
|
||||||
|
private var recentItemsSpacing: CGFloat {
|
||||||
|
#if os(tvOS)
|
||||||
|
16
|
||||||
|
#else
|
||||||
|
0
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var horizontalCardSpacing: CGFloat {
|
||||||
|
#if os(tvOS)
|
||||||
|
48
|
||||||
|
#else
|
||||||
|
16
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/// Instance picker for selecting search source when multiple instances are available
|
/// Instance picker for selecting search source when multiple instances are available
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var instancePickerView: some View {
|
private var instancePickerView: some View {
|
||||||
@@ -393,7 +409,9 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
.padding(listStyle == .inset ? 4 : 0)
|
.padding(listStyle == .inset ? 4 : 0)
|
||||||
.background(listStyle == .inset ? ListBackgroundStyle.card.color : .clear)
|
.background(listStyle == .inset ? ListBackgroundStyle.card.color : .clear)
|
||||||
|
#if !os(tvOS)
|
||||||
.clipShape(.rect(cornerRadius: 10))
|
.clipShape(.rect(cornerRadius: 10))
|
||||||
|
#endif
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,6 +598,16 @@ struct SearchView: View {
|
|||||||
if !searchHistory.isEmpty {
|
if !searchHistory.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Header - tappable to expand/collapse when more than 5 items
|
// Header - tappable to expand/collapse when more than 5 items
|
||||||
|
#if os(tvOS)
|
||||||
|
HStack {
|
||||||
|
Text(String(localized: "search.recentSearches.title"))
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
#else
|
||||||
Button {
|
Button {
|
||||||
if hasMoreSearchHistory {
|
if hasMoreSearchHistory {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
@@ -604,9 +632,10 @@ struct SearchView: View {
|
|||||||
.disabled(!hasMoreSearchHistory)
|
.disabled(!hasMoreSearchHistory)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
|
#endif
|
||||||
|
|
||||||
// Items
|
// Items
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: recentItemsSpacing) {
|
||||||
ForEach(displayedSearchHistory) { history in
|
ForEach(displayedSearchHistory) { history in
|
||||||
Button {
|
Button {
|
||||||
executeSearch(history.query)
|
executeSearch(history.query)
|
||||||
@@ -674,7 +703,7 @@ struct SearchView: View {
|
|||||||
|
|
||||||
// Horizontal scroll
|
// Horizontal scroll
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 16) {
|
LazyHStack(spacing: horizontalCardSpacing) {
|
||||||
ForEach(recentChannels) { recentChannel in
|
ForEach(recentChannels) { recentChannel in
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
value: NavigationDestination.channel(
|
value: NavigationDestination.channel(
|
||||||
@@ -689,7 +718,11 @@ struct SearchView: View {
|
|||||||
channel: channelFromRecent(recentChannel),
|
channel: channelFromRecent(recentChannel),
|
||||||
isCompact: false
|
isCompact: false
|
||||||
)
|
)
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(width: 240)
|
||||||
|
#else
|
||||||
.frame(width: 160)
|
.frame(width: 160)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.zoomTransitionSource(id: recentChannel.channelID)
|
.zoomTransitionSource(id: recentChannel.channelID)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -704,6 +737,9 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.scrollClipDisabled()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,14 +756,18 @@ struct SearchView: View {
|
|||||||
|
|
||||||
// Horizontal scroll
|
// Horizontal scroll
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 16) {
|
LazyHStack(spacing: horizontalCardSpacing) {
|
||||||
ForEach(recentPlaylists) { recentPlaylist in
|
ForEach(recentPlaylists) { recentPlaylist in
|
||||||
NavigationLink(value: NavigationDestination.playlist(.remote(playlistIDFromRecent(recentPlaylist), instance: nil, title: recentPlaylist.title))) {
|
NavigationLink(value: NavigationDestination.playlist(.remote(playlistIDFromRecent(recentPlaylist), instance: nil, title: recentPlaylist.title))) {
|
||||||
PlaylistCardView(
|
PlaylistCardView(
|
||||||
playlist: playlistFromRecent(recentPlaylist),
|
playlist: playlistFromRecent(recentPlaylist),
|
||||||
isCompact: false
|
isCompact: false
|
||||||
)
|
)
|
||||||
|
#if os(tvOS)
|
||||||
|
.frame(width: 320)
|
||||||
|
#else
|
||||||
.frame(width: 200)
|
.frame(width: 200)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.zoomTransitionSource(id: playlistIDFromRecent(recentPlaylist))
|
.zoomTransitionSource(id: playlistIDFromRecent(recentPlaylist))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -742,6 +782,9 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.scrollClipDisabled()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,6 +806,9 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.scrollClipDisabled()
|
||||||
|
#endif
|
||||||
.background(listStyle == .inset ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color)
|
.background(listStyle == .inset ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color)
|
||||||
.accessibilityLabel("search.recents")
|
.accessibilityLabel("search.recents")
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
|
|||||||
Reference in New Issue
Block a user