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 popularListingStyle = Key<ListingStyle>("popularListingStyle", 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 searchListingStyle = Key<ListingStyle>("searchListingStyle", default: .cells)
static let hideShorts = Key<Bool>("hideShorts", default: false)

View File

@ -63,41 +63,17 @@ struct PlaylistsView: View {
var body: some View {
SignInRequiredView(title: "Playlists".localized()) {
Section {
VStack {
#if os(tvOS)
toolbar
#endif
if currentPlaylist != nil, items.isEmpty {
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()
VStack {
VerticalCells(items: items, allowEmpty: true) { if shouldDisplayHeader { header } }
.environment(\.scrollViewBottomPadding, 70)
.environment(\.currentPlaylistID, currentPlaylist?.id)
.environment(\.listingStyle, playlistListingStyle)
.environment(\.hideShorts, hideShorts)
CacheStatusHeader(
refreshTime: cache.getFormattedPlaylistTime(account: accounts.current),
isLoading: model.isLoading
)
}
}
}
.environment(\.scrollViewBottomPadding, 70)
#endif
}
.environment(\.currentPlaylistID, currentPlaylist?.id)
.environment(\.listingStyle, playlistListingStyle)
.environment(\.hideShorts, hideShorts)
}
if currentPlaylist != nil, items.isEmpty {
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())
}
}
}
@ -268,37 +244,6 @@ struct PlaylistsView: View {
}
#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 {
VStack {
Spacer()
@ -341,12 +286,15 @@ struct PlaylistsView: View {
var selectPlaylistButton: some View {
#if os(tvOS)
Button(currentPlaylist?.title ?? "Select playlist") {
Button {
guard currentPlaylist != nil else {
return
}
selectedPlaylistID = model.all.next(after: currentPlaylist!)?.id ?? ""
} label: {
Text(currentPlaylist?.title ?? "Select playlist")
.frame(maxWidth: .infinity)
}
.lineLimit(1)
.contextMenu {
@ -405,6 +353,64 @@ struct PlaylistsView: View {
}
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 {

View File

@ -236,27 +236,12 @@ struct SearchView: View {
if showRecentQueries {
recentQueries
} else {
#if os(tvOS)
ScrollView(.vertical, showsIndicators: false) {
HStack(spacing: 0) {
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() }
VerticalCells(items: state.store.collection, allowEmpty: state.query.isEmpty) {
if shouldDisplayHeader {
header
}
.edgesIgnoringSafeArea(.horizontal)
#else
VerticalCells(items: state.store.collection, allowEmpty: state.query.isEmpty)
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
#endif
}
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
if noResults {
Text("No results")

View File

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

View File

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

View File

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

View File

@ -39,20 +39,9 @@ struct TrendingView: View {
var body: some View {
Section {
VStack(spacing: 0) {
#if os(tvOS)
toolbar
HorizontalCells(items: trending)
.padding(.top, 40)
Spacer()
#else
VerticalCells(items: trending)
.environment(\.scrollViewBottomPadding, 70)
#endif
}
.environment(\.listingStyle, trendingListingStyle)
.environment(\.hideShorts, hideShorts)
VerticalCells(items: trending) { if shouldDisplayHeader { header } }
.environment(\.listingStyle, trendingListingStyle)
.environment(\.hideShorts, hideShorts)
}
.toolbar {
@ -66,9 +55,7 @@ struct TrendingView: View {
.id(favoriteItem.id)
}
if accounts.app.supportsTrendingCategories {
categoryButton
}
categoryButton
countryButton
}
#endif
@ -182,9 +169,7 @@ struct TrendingView: View {
Menu {
countryButton
if accounts.app.supportsTrendingCategories {
categoryButton
}
categoryButton
ListingStyleButtons(listingStyle: $trendingListingStyle)
@ -210,26 +195,28 @@ struct TrendingView: View {
}
#endif
private var categoryButton: some View {
#if os(tvOS)
Button(category.name) {
self.category = category.next()
}
.contextMenu {
ForEach(TrendingCategory.allCases) { category in
Button(category.controlLabel) { self.category = category }
@ViewBuilder private var categoryButton: some View {
if accounts.app.supportsTrendingCategories {
#if os(tvOS)
Button(category.name) {
self.category = category.next()
}
.contextMenu {
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
Label(category.controlLabel, systemImage: category.systemImage).tag(category)
#else
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 {
@ -249,6 +236,42 @@ struct TrendingView: View {
private func updateFavoriteItem() {
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 {

View File

@ -26,7 +26,7 @@ struct VerticalCells<Header: View>: View {
var body: some View {
ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) {
LazyVGrid(columns: columns, alignment: .center) {
LazyVGrid(columns: adaptiveItem, alignment: .center) {
Section(header: header) {
ForEach(contentItems) { item in
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] {
if listingStyle == .list {
return [.init(.flexible())]

View File

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

View File

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

View File

@ -21,7 +21,7 @@ struct PopularView: View {
}
var body: some View {
VerticalCells(items: videos)
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
.onAppear {
resource?.addObserver(store)
resource?.loadIfNeeded()?
@ -116,6 +116,36 @@ struct PopularView: View {
}
}
#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 {