tvOS filters for all views

Vertical list for trending, popular, playlists, search

Fix #413, #415
This commit is contained in:
Arkadiusz Fal 2023-04-22 21:07:30 +02:00
parent 6596a440a5
commit 83dfdd6c0e
11 changed files with 175 additions and 142 deletions

View File

@ -242,7 +242,7 @@ extension Defaults.Keys {
static let subscriptionsListingStyle = Key<ListingStyle>("subscriptionsListingStyle", default: .cells) static let subscriptionsListingStyle = Key<ListingStyle>("subscriptionsListingStyle", default: .cells)
static let popularListingStyle = Key<ListingStyle>("popularListingStyle", default: .cells) static let popularListingStyle = Key<ListingStyle>("popularListingStyle", default: .cells)
static let trendingListingStyle = Key<ListingStyle>("trendingListingStyle", default: .cells) static let trendingListingStyle = Key<ListingStyle>("trendingListingStyle", default: .cells)
static let playlistListingStyle = Key<ListingStyle>("playlistListingStyle", default: .cells) static let playlistListingStyle = Key<ListingStyle>("playlistListingStyle", default: .list)
static let channelPlaylistListingStyle = Key<ListingStyle>("channelPlaylistListingStyle", default: .cells) static let channelPlaylistListingStyle = Key<ListingStyle>("channelPlaylistListingStyle", default: .cells)
static let searchListingStyle = Key<ListingStyle>("searchListingStyle", default: .cells) static let searchListingStyle = Key<ListingStyle>("searchListingStyle", default: .cells)
static let hideShorts = Key<Bool>("hideShorts", default: false) static let hideShorts = Key<Bool>("hideShorts", default: false)

View File

@ -63,41 +63,17 @@ struct PlaylistsView: View {
var body: some View { var body: some View {
SignInRequiredView(title: "Playlists".localized()) { SignInRequiredView(title: "Playlists".localized()) {
Section { VStack {
VStack { VerticalCells(items: items, allowEmpty: true) { if shouldDisplayHeader { header } }
#if os(tvOS) .environment(\.scrollViewBottomPadding, 70)
toolbar .environment(\.currentPlaylistID, currentPlaylist?.id)
#endif .environment(\.listingStyle, playlistListingStyle)
if currentPlaylist != nil, items.isEmpty { .environment(\.hideShorts, hideShorts)
hintText("Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"".localized())
} else if model.all.isEmpty {
hintText("You have no playlists\n\nTap on \"New Playlist\" to create one".localized())
} else {
Group {
#if os(tvOS)
HorizontalCells(items: items)
.padding(.top, 40)
Spacer()
#else
VerticalCells(items: items) {
if showCacheStatus {
HStack {
Spacer()
CacheStatusHeader( if currentPlaylist != nil, items.isEmpty {
refreshTime: cache.getFormattedPlaylistTime(account: accounts.current), hintText("Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"".localized())
isLoading: model.isLoading } else if model.all.isEmpty {
) hintText("You have no playlists\n\nTap on \"New Playlist\" to create one".localized())
}
}
}
.environment(\.scrollViewBottomPadding, 70)
#endif
}
.environment(\.currentPlaylistID, currentPlaylist?.id)
.environment(\.listingStyle, playlistListingStyle)
.environment(\.hideShorts, hideShorts)
}
} }
} }
} }
@ -268,37 +244,6 @@ struct PlaylistsView: View {
} }
#endif #endif
#if os(tvOS)
var toolbar: some View {
HStack {
if model.isEmpty {
Text("No Playlists")
.foregroundColor(.secondary)
} else {
Text("Current Playlist")
.foregroundColor(.secondary)
selectPlaylistButton
}
if let playlist = currentPlaylist {
editPlaylistButton
FavoriteButton(item: FavoriteItem(section: .playlist(accounts.current.id, playlist.id)))
.labelStyle(.iconOnly)
playButtons
}
Spacer()
newPlaylistButton
.padding(.leading, 40)
}
.labelStyle(.iconOnly)
}
#endif
func hintText(_ text: String) -> some View { func hintText(_ text: String) -> some View {
VStack { VStack {
Spacer() Spacer()
@ -341,12 +286,15 @@ struct PlaylistsView: View {
var selectPlaylistButton: some View { var selectPlaylistButton: some View {
#if os(tvOS) #if os(tvOS)
Button(currentPlaylist?.title ?? "Select playlist") { Button {
guard currentPlaylist != nil else { guard currentPlaylist != nil else {
return return
} }
selectedPlaylistID = model.all.next(after: currentPlaylist!)?.id ?? "" selectedPlaylistID = model.all.next(after: currentPlaylist!)?.id ?? ""
} label: {
Text(currentPlaylist?.title ?? "Select playlist")
.frame(maxWidth: .infinity)
} }
.lineLimit(1) .lineLimit(1)
.contextMenu { .contextMenu {
@ -405,6 +353,64 @@ struct PlaylistsView: View {
} }
return model.find(id: selectedPlaylistID) ?? model.all.first return model.find(id: selectedPlaylistID) ?? model.all.first
} }
var shouldDisplayHeader: Bool {
#if os(tvOS)
true
#else
showCacheStatus
#endif
}
var header: some View {
HStack {
if model.isEmpty {
Text("No Playlists")
.foregroundColor(.secondary)
} else {
selectPlaylistButton
}
if let playlist = currentPlaylist {
editPlaylistButton
FavoriteButton(item: FavoriteItem(section: .playlist(accounts.current.id, playlist.id)))
.labelStyle(.iconOnly)
playButtons
}
newPlaylistButton
Spacer()
ListingStyleButtons(listingStyle: $playlistListingStyle)
HideShortsButtons(hide: $hideShorts)
if let account = accounts.current, showCacheStatus {
CacheStatusHeader(
refreshTime: cache.getFormattedPlaylistTime(account: account),
isLoading: model.isLoading
)
}
Button {
model.load(force: true)
loadResource()
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
}
}
.labelStyle(.iconOnly)
.font(.caption)
.imageScale(.small)
.padding(.leading, 30)
#if os(tvOS)
.padding(.bottom, 15)
.padding(.trailing, 30)
#endif
}
} }
struct PlaylistsView_Provider: PreviewProvider { struct PlaylistsView_Provider: PreviewProvider {

View File

@ -236,27 +236,12 @@ struct SearchView: View {
if showRecentQueries { if showRecentQueries {
recentQueries recentQueries
} else { } else {
#if os(tvOS) VerticalCells(items: state.store.collection, allowEmpty: state.query.isEmpty) {
ScrollView(.vertical, showsIndicators: false) { if shouldDisplayHeader {
HStack(spacing: 0) { header
if accounts.app.supportsSearchFilters {
filtersHorizontalStack
}
FavoriteButton(item: favoriteItem)
.id(favoriteItem?.id)
.labelStyle(.iconOnly)
.font(.system(size: 25))
}
HorizontalCells(items: state.store.collection)
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
} }
.edgesIgnoringSafeArea(.horizontal) }
#else .environment(\.loadMoreContentHandler) { state.loadNextPage() }
VerticalCells(items: state.store.collection, allowEmpty: state.query.isEmpty)
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
#endif
if noResults { if noResults {
Text("No results") Text("No results")

View File

@ -111,7 +111,7 @@ struct ChannelsView: View {
Label("Refresh", systemImage: "arrow.clockwise") Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.imageScale(.small) .imageScale(.small)
.font(.caption2) .font(.caption)
} }
#endif #endif

View File

@ -68,16 +68,13 @@ struct FeedView: View {
} }
#if os(tvOS) #if os(tvOS)
if !showCacheStatus {
Spacer()
}
Button { Button {
feed.loadResources(force: true) feed.loadResources(force: true)
} label: { } label: {
Label("Refresh", systemImage: "arrow.clockwise") Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.imageScale(.small) .imageScale(.small)
.font(.caption2) .font(.caption)
} }
#endif #endif
} }

View File

@ -10,7 +10,7 @@ struct SubscriptionsPageButton: View {
} label: { } label: {
Text(subscriptionsViewPage.rawValue.capitalized) Text(subscriptionsViewPage.rawValue.capitalized)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.font(.caption2) .font(.caption)
} }
} }
} }

View File

@ -39,20 +39,9 @@ struct TrendingView: View {
var body: some View { var body: some View {
Section { Section {
VStack(spacing: 0) { VerticalCells(items: trending) { if shouldDisplayHeader { header } }
#if os(tvOS) .environment(\.listingStyle, trendingListingStyle)
toolbar .environment(\.hideShorts, hideShorts)
HorizontalCells(items: trending)
.padding(.top, 40)
Spacer()
#else
VerticalCells(items: trending)
.environment(\.scrollViewBottomPadding, 70)
#endif
}
.environment(\.listingStyle, trendingListingStyle)
.environment(\.hideShorts, hideShorts)
} }
.toolbar { .toolbar {
@ -66,9 +55,7 @@ struct TrendingView: View {
.id(favoriteItem.id) .id(favoriteItem.id)
} }
if accounts.app.supportsTrendingCategories { categoryButton
categoryButton
}
countryButton countryButton
} }
#endif #endif
@ -182,9 +169,7 @@ struct TrendingView: View {
Menu { Menu {
countryButton countryButton
if accounts.app.supportsTrendingCategories { categoryButton
categoryButton
}
ListingStyleButtons(listingStyle: $trendingListingStyle) ListingStyleButtons(listingStyle: $trendingListingStyle)
@ -210,26 +195,28 @@ struct TrendingView: View {
} }
#endif #endif
private var categoryButton: some View { @ViewBuilder private var categoryButton: some View {
#if os(tvOS) if accounts.app.supportsTrendingCategories {
Button(category.name) { #if os(tvOS)
self.category = category.next() Button(category.name) {
} self.category = category.next()
.contextMenu { }
ForEach(TrendingCategory.allCases) { category in .contextMenu {
Button(category.controlLabel) { self.category = category } ForEach(TrendingCategory.allCases) { category in
Button(category.controlLabel) { self.category = category }
}
Button("Cancel", role: .cancel) {}
} }
Button("Cancel", role: .cancel) {} #else
} Picker(category.controlLabel, selection: $category) {
ForEach(TrendingCategory.allCases) { category in
#else Label(category.controlLabel, systemImage: category.systemImage).tag(category)
Picker(category.controlLabel, selection: $category) { }
ForEach(TrendingCategory.allCases) { category in
Label(category.controlLabel, systemImage: category.systemImage).tag(category)
} }
} #endif
#endif }
} }
private var countryButton: some View { private var countryButton: some View {
@ -249,6 +236,42 @@ struct TrendingView: View {
private func updateFavoriteItem() { private func updateFavoriteItem() {
favoriteItem = FavoriteItem(section: .trending(country.rawValue, category.rawValue)) favoriteItem = FavoriteItem(section: .trending(country.rawValue, category.rawValue))
} }
var header: some View {
HStack {
Group {
categoryButton
countryButton
}
.font(.caption)
Spacer()
ListingStyleButtons(listingStyle: $trendingListingStyle)
HideShortsButtons(hide: $hideShorts)
Button {
resource.load()
.onFailure { self.error = $0 }
.onSuccess { _ in self.error = nil }
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
}
}
.padding(.leading, 30)
.padding(.bottom, 15)
.padding(.trailing, 30)
}
var shouldDisplayHeader: Bool {
#if os(tvOS)
true
#else
false
#endif
}
} }
struct TrendingView_Previews: PreviewProvider { struct TrendingView_Previews: PreviewProvider {

View File

@ -26,7 +26,7 @@ struct VerticalCells<Header: View>: View {
var body: some View { var body: some View {
ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) { ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) {
LazyVGrid(columns: columns, alignment: .center) { LazyVGrid(columns: adaptiveItem, alignment: .center) {
Section(header: header) { Section(header: header) {
ForEach(contentItems) { item in ForEach(contentItems) { item in
ContentItemView(item: item) ContentItemView(item: item)
@ -58,14 +58,6 @@ struct VerticalCells<Header: View>: View {
} }
} }
var columns: [GridItem] {
#if os(tvOS)
contentItems.count < 3 ? Array(repeating: GridItem(.fixed(500)), count: [contentItems.count, 1].max()!) : adaptiveItem
#else
adaptiveItem
#endif
}
var adaptiveItem: [GridItem] { var adaptiveItem: [GridItem] {
if listingStyle == .list { if listingStyle == .list {
return [.init(.flexible())] return [.init(.flexible())]

View File

@ -17,7 +17,7 @@ struct HideShortsButtons: View {
} }
} }
#if os(tvOS) #if os(tvOS)
.font(.caption2) .font(.caption)
.imageScale(.small) .imageScale(.small)
#endif #endif
} }

View File

@ -12,7 +12,7 @@ struct ListingStyleButtons: View {
} label: { } label: {
Label(listingStyle.rawValue.capitalized, systemImage: listingStyle.systemImage) Label(listingStyle.rawValue.capitalized, systemImage: listingStyle.systemImage)
#if os(tvOS) #if os(tvOS)
.font(.caption2) .font(.caption)
.imageScale(.small) .imageScale(.small)
#endif #endif
} }

View File

@ -21,7 +21,7 @@ struct PopularView: View {
} }
var body: some View { var body: some View {
VerticalCells(items: videos) VerticalCells(items: videos) { if shouldDisplayHeader { header } }
.onAppear { .onAppear {
resource?.addObserver(store) resource?.addObserver(store)
resource?.loadIfNeeded()? resource?.loadIfNeeded()?
@ -116,6 +116,36 @@ struct PopularView: View {
} }
} }
#endif #endif
var shouldDisplayHeader: Bool {
#if os(tvOS)
true
#else
false
#endif
}
var header: some View {
HStack {
Spacer()
ListingStyleButtons(listingStyle: $popularListingStyle)
HideShortsButtons(hide: $hideShorts)
Button {
resource?.load()
.onFailure { self.error = $0 }
.onSuccess { _ in self.error = nil }
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
}
}
.padding(.leading, 30)
.padding(.bottom, 15)
.padding(.trailing, 30)
}
} }
struct PopularView_Previews: PreviewProvider { struct PopularView_Previews: PreviewProvider {