mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
tvOS filters for all views
Vertical list for trending, popular, playlists, search Fix #413, #415
This commit is contained in:
parent
6596a440a5
commit
83dfdd6c0e
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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())]
|
||||||
|
@ -17,7 +17,7 @@ struct HideShortsButtons: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.font(.caption2)
|
.font(.caption)
|
||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user