diff --git a/Shared/Channels/ChannelPlaylistView.swift b/Shared/Channels/ChannelPlaylistView.swift index 4a76616e..1ca2d48e 100644 --- a/Shared/Channels/ChannelPlaylistView.swift +++ b/Shared/Channels/ChannelPlaylistView.swift @@ -15,14 +15,15 @@ struct ChannelPlaylistView: View { var player = PlayerModel.shared @ObservedObject private var recents = RecentsModel.shared - @State private var isLoading = false - private var items: [ContentItem] { ContentItem.array(of: store.item?.videos ?? []) } private var resource: Resource? { - accounts.api.channelPlaylist(playlist.id) + let resource = accounts.api.channelPlaylist(playlist.id) + resource?.addObserver(store) + + return resource } var body: some View { @@ -47,7 +48,7 @@ struct ChannelPlaylistView: View { .labelStyle(.iconOnly) } #endif - VerticalCells(items: items, isLoading: isLoading) + VerticalCells(items: items) .environment(\.inChannelPlaylistView, true) } .environment(\.listingStyle, channelPlaylistListingStyle) @@ -55,16 +56,11 @@ struct ChannelPlaylistView: View { if let cache = ChannelPlaylistsCacheModel.shared.retrievePlaylist(playlist) { store.replace(cache) } - isLoading = true - resource? - .load() - .onSuccess { response in - if let playlist: ChannelPlaylist = response.typedContent() { - ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist) - store.replace(playlist) - } + resource?.loadIfNeeded()?.onSuccess { response in + if let playlist: ChannelPlaylist = response.typedContent() { + ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist) } - .onCompletion { _ in isLoading = false } + } } #if os(tvOS) .background(Color.background(scheme: colorScheme)) diff --git a/Shared/Channels/ChannelVideosView.swift b/Shared/Channels/ChannelVideosView.swift index 8d86b1da..107084f9 100644 --- a/Shared/Channels/ChannelVideosView.swift +++ b/Shared/Channels/ChannelVideosView.swift @@ -65,7 +65,7 @@ struct ChannelVideosView: View { .frame(maxWidth: .infinity) #endif - VerticalCells(items: contentItems, isLoading: resource?.isLoading ?? false, edgesIgnoringSafeArea: verticalCellsEdgesIgnoringSafeArea) { + VerticalCells(items: contentItems, edgesIgnoringSafeArea: verticalCellsEdgesIgnoringSafeArea) { if let description = presentedChannel?.description, !description.isEmpty { Button { withAnimation(.spring()) { diff --git a/Shared/Home/FavoriteItemView.swift b/Shared/Home/FavoriteItemView.swift index ba34af0f..9d11363e 100644 --- a/Shared/Home/FavoriteItemView.swift +++ b/Shared/Home/FavoriteItemView.swift @@ -42,9 +42,21 @@ struct FavoriteItemView: View { .padding(.leading, 15) #endif - if limitedItems.isEmpty { - EmptyItems(isLoading: resource?.isLoading ?? false) { reloadVisibleWatches() } - .padding(.vertical, 10) + if limitedItems.isEmpty, !(resource?.isLoading ?? false) { + VStack(alignment: .leading) { + Text(emptyItemsText) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(.secondary) + + if hideShorts || hideWatched { + AccentButton(text: "Disable filters", maxWidth: nil, verticalPadding: 0, minHeight: 30) { + hideShorts = false + hideWatched = false + reloadVisibleWatches() + } + } + } + .padding(.vertical, 10) #if os(tvOS) .padding(.horizontal, 40) #else @@ -101,6 +113,19 @@ struct FavoriteItemView: View { } } + var emptyItemsText: String { + var filterText = "" + if hideShorts && hideWatched { + filterText = "(watched and shorts hidden)" + } else if hideShorts { + filterText = "(shorts hidden)" + } else if hideWatched { + filterText = "(watched hidden)" + } + + return "No videos to show".localized() + " " + filterText.localized() + } + var contextMenu: some View { Group { if item.section == .history { diff --git a/Shared/Playlists/PlaylistVideosView.swift b/Shared/Playlists/PlaylistVideosView.swift index b9bb786c..dcf4c037 100644 --- a/Shared/Playlists/PlaylistVideosView.swift +++ b/Shared/Playlists/PlaylistVideosView.swift @@ -70,7 +70,7 @@ struct PlaylistVideosView: View { } var body: some View { - VerticalCells(items: contentItems, isLoading: resource?.isLoading ?? false) + VerticalCells(items: contentItems) .onAppear { guard contentItems.isEmpty else { return } loadResource() diff --git a/Shared/Playlists/PlaylistsView.swift b/Shared/Playlists/PlaylistsView.swift index f56241b7..9f927ee1 100644 --- a/Shared/Playlists/PlaylistsView.swift +++ b/Shared/Playlists/PlaylistsView.swift @@ -63,11 +63,13 @@ struct PlaylistsView: View { var body: some View { SignInRequiredView(title: "Playlists".localized()) { VStack { - VerticalCells(items: items, isLoading: resource?.isLoading ?? false) { if shouldDisplayHeader { header } } + VerticalCells(items: items, allowEmpty: true) { if shouldDisplayHeader { header } } .environment(\.currentPlaylistID, currentPlaylist?.id) .environment(\.listingStyle, playlistListingStyle) - if model.all.isEmpty { + 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()) } } diff --git a/Shared/Search/SearchView.swift b/Shared/Search/SearchView.swift index 68d047a8..c5d47a3b 100644 --- a/Shared/Search/SearchView.swift +++ b/Shared/Search/SearchView.swift @@ -244,12 +244,22 @@ struct SearchView: View { if showRecentQueries { recentQueries } else { - VerticalCells(items: state.store.collection, isLoading: state.isLoading) { + VerticalCells(items: state.store.collection, allowEmpty: state.query.isEmpty) { if shouldDisplayHeader { header } } .environment(\.loadMoreContentHandler) { state.loadNextPage() } + + if noResults { + Text("No results") + + if searchFiltersActive { + Button("Reset search filters", action: resetFilters) + } + + Spacer() + } } } } @@ -270,6 +280,12 @@ struct SearchView: View { searchDuration != .any || searchDate != .any } + private func resetFilters() { + searchSortOrder = .relevance + searchDate = .any + searchDuration = .any + } + private var noResults: Bool { state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty } diff --git a/Shared/Subscriptions/FeedView.swift b/Shared/Subscriptions/FeedView.swift index 34c6ca06..cb21a124 100644 --- a/Shared/Subscriptions/FeedView.swift +++ b/Shared/Subscriptions/FeedView.swift @@ -16,7 +16,7 @@ struct FeedView: View { } var body: some View { - VerticalCells(items: videos, isLoading: feed.isLoading) { if shouldDisplayHeader { header } } + VerticalCells(items: videos) { if shouldDisplayHeader { header } } .environment(\.loadMoreContentHandler) { feed.loadNextPage() } .onAppear { feed.loadResources() diff --git a/Shared/Trending/TrendingView.swift b/Shared/Trending/TrendingView.swift index 06d81620..73d15135 100644 --- a/Shared/Trending/TrendingView.swift +++ b/Shared/Trending/TrendingView.swift @@ -22,51 +22,62 @@ struct TrendingView: View { } @State private var error: RequestError? - @State private var resource: Resource? - @State private var isLoading = false init(_ videos: [Video] = [Video]()) { self.videos = videos } - var body: some View { - VerticalCells(items: trending, isLoading: isLoading) { if shouldDisplayHeader { header } } - .environment(\.listingStyle, trendingListingStyle) - .toolbar { - ToolbarItem { - RequestErrorButton(error: error) - } - #if os(macOS) - ToolbarItemGroup { - if let favoriteItem { - FavoriteButton(item: favoriteItem) - .id(favoriteItem.id) - } + var resource: Resource { + let newResource: Resource - categoryButton - countryButton + newResource = accounts.api.trending(country: country, category: category) + newResource.addObserver(store) + + return newResource + } + + var body: some View { + Section { + VerticalCells(items: trending) { if shouldDisplayHeader { header } } + .environment(\.listingStyle, trendingListingStyle) + } + + .toolbar { + ToolbarItem { + RequestErrorButton(error: error) + } + #if os(macOS) + ToolbarItemGroup { + if let favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) } - #endif - } - .onChange(of: category) { _ in updateResource() } - .onChange(of: country) { _ in updateResource() } - .onChange(of: accounts.current) { _ in updateResource() } - .onChange(of: resource) { _ in - isLoading = true - resource?.load() - .onFailure { self.error = $0 } - .onSuccess { _ in self.error = nil } - .onCompletion { _ in self.isLoading = false } - } - .onAppear { updateResource() - } + + categoryButton + countryButton + } + #endif + } + .onChange(of: resource) { _ in + resource.load() + .onFailure { self.error = $0 } + .onSuccess { _ in self.error = nil } + updateFavoriteItem() + } + .onAppear { + resource.loadIfNeeded()? + .onFailure { self.error = $0 } + .onSuccess { _ in self.error = nil } + + updateFavoriteItem() + } #if os(tvOS) - .fullScreenCover(isPresented: $presentingCountrySelection) { - TrendingCountry(selectedCountry: $country) - } + .fullScreenCover(isPresented: $presentingCountrySelection) { + TrendingCountry(selectedCountry: $country) + } #else - .sheet(isPresented: $presentingCountrySelection) { + .sheet(isPresented: $presentingCountrySelection) { TrendingCountry(selectedCountry: $country) #if os(macOS) .frame(minWidth: 400, minHeight: 400) @@ -74,11 +85,9 @@ struct TrendingView: View { } .background( Button("Refresh") { - isLoading = true - resource?.load() + resource.load() .onFailure { self.error = $0 } .onSuccess { _ in self.error = nil } - .onCompletion { _ in self.isLoading = false } } .keyboardShortcut("r") .opacity(0) @@ -87,18 +96,16 @@ struct TrendingView: View { #endif #if os(iOS) .refreshControl { refreshControl in - resource?.load().onCompletion { _ in + resource.load().onCompletion { _ in refreshControl.endRefreshing() } } .backport .refreshable { DispatchQueue.main.async { - isLoading = true - resource?.load() + resource.load() .onFailure { self.error = $0 } .onSuccess { _ in self.error = nil } - .onCompletion { _ in self.isLoading = false } } } .navigationBarTitleDisplayMode(.inline) @@ -124,13 +131,9 @@ struct TrendingView: View { } #else .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in - let request = resource?.loadIfNeeded() - if request != nil { - isLoading = true - } - request?.onFailure { self.error = $0 } + resource.loadIfNeeded()? + .onFailure { self.error = $0 } .onSuccess { _ in self.error = nil } - .onCompletion { _ in self.isLoading = false } } #endif } @@ -222,7 +225,7 @@ struct TrendingView: View { private var countryButton: some View { Button(action: { presentingCountrySelection.toggle() - resource?.removeObservers(ownedBy: store) + resource.removeObservers(ownedBy: store) }) { #if os(iOS) Label("Country", systemImage: "flag") @@ -233,13 +236,6 @@ struct TrendingView: View { } } - private func updateResource() { - let resource = accounts.api.trending(country: country, category: category) - resource.addObserver(store) - self.resource = resource - updateFavoriteItem() - } - private func updateFavoriteItem() { favoriteItem = FavoriteItem(section: .trending(country.rawValue, category.rawValue)) } @@ -258,7 +254,7 @@ struct TrendingView: View { HideShortsButtons() Button { - resource?.load() + resource.load() .onFailure { self.error = $0 } .onSuccess { _ in self.error = nil } } label: { diff --git a/Shared/Videos/VerticalCells.swift b/Shared/Videos/VerticalCells.swift index 1579ded4..9bf6f207 100644 --- a/Shared/Videos/VerticalCells.swift +++ b/Shared/Videos/VerticalCells.swift @@ -10,7 +10,7 @@ struct VerticalCells: View { @Environment(\.listingStyle) private var listingStyle var items = [ContentItem]() - var isLoading: Bool + var allowEmpty = false var edgesIgnoringSafeArea = Edge.Set.horizontal let header: Header? @@ -19,48 +19,32 @@ struct VerticalCells: View { init( items: [ContentItem], - isLoading: Bool, + allowEmpty: Bool = false, edgesIgnoringSafeArea: Edge.Set = .horizontal, @ViewBuilder header: @escaping () -> Header? = { nil } ) { self.items = items - self.isLoading = isLoading + self.allowEmpty = allowEmpty self.edgesIgnoringSafeArea = edgesIgnoringSafeArea self.header = header() } init( items: [ContentItem], - isLoading: Bool + allowEmpty: Bool = false ) where Header == EmptyView { - self.init(items: items, isLoading: isLoading) { EmptyView() } + self.init(items: items, allowEmpty: allowEmpty) { EmptyView() } } var body: some View { ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) { - Group { - LazyVGrid(columns: adaptiveItem, alignment: .center) { - Section(header: header) { - ForEach(contentItems) { item in - ContentItemView(item: item) - .onAppear { loadMoreContentItemsIfNeeded(current: item) } - } + LazyVGrid(columns: adaptiveItem, alignment: .center) { + Section(header: header) { + ForEach(contentItems) { item in + ContentItemView(item: item) + .onAppear { loadMoreContentItemsIfNeeded(current: item) } } } - .overlay( - GeometryReader { proxy in - Color.clear - .onAppear { - gridSize = proxy.size - } - .onChange(of: proxy.size) { newValue in - gridSize = newValue - } - } - ) - if !isLoading && gridSize.height < 50 { - EmptyItems() - } } .padding() } @@ -73,7 +57,7 @@ struct VerticalCells: View { } var contentItems: [ContentItem] { - items.isEmpty && isLoading ? (ContentItem.placeholders) : items.sorted { $0 < $1 } + items.isEmpty ? (allowEmpty ? items : ContentItem.placeholders) : items.sorted { $0 < $1 } } func loadMoreContentItemsIfNeeded(current item: ContentItem) { @@ -120,7 +104,7 @@ struct VerticalCells: View { struct VeticalCells_Previews: PreviewProvider { static var previews: some View { - VerticalCells(items: ContentItem.array(of: Array(repeating: Video.fixture, count: 30)), isLoading: false) + VerticalCells(items: ContentItem.array(of: Array(repeating: Video.fixture, count: 30))) .injectFixtureEnvironmentObjects() } } diff --git a/Shared/Views/EmptyItems.swift b/Shared/Views/EmptyItems.swift deleted file mode 100644 index 52b0df69..00000000 --- a/Shared/Views/EmptyItems.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Defaults -import SwiftUI - -struct EmptyItems: View { - @Default(.hideShorts) private var hideShorts - @Default(.hideWatched) private var hideWatched - - var isLoading = false - var onDisableFilters: () -> Void = {} - - var body: some View { - VStack(alignment: .leading) { - Group { - if isLoading { - HStack(spacing: 10) { - ProgressView() - .progressViewStyle(.circular) - Text("Loading...") - } - } else { - Text(emptyItemsText) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundColor(.secondary) - - if hideShorts || hideWatched { - AccentButton(text: "Disable filters", maxWidth: nil, verticalPadding: 0, minHeight: 30) { - hideShorts = false - hideWatched = false - onDisableFilters() - } - } - } - } - - var emptyItemsText: String { - var filterText = "" - if hideShorts && hideWatched { - filterText = "(watched and shorts hidden)" - } else if hideShorts { - filterText = "(shorts hidden)" - } else if hideWatched { - filterText = "(watched hidden)" - } - - return "No videos to show".localized() + " " + filterText.localized() - } -} - -struct EmptyItems_Previews: PreviewProvider { - static var previews: some View { - VStack { - Spacer() - EmptyItems() - Spacer() - EmptyItems(isLoading: true) - Spacer() - } - .padding() - } -} diff --git a/Shared/Views/PopularView.swift b/Shared/Views/PopularView.swift index 82f5d4a4..8ca31a6b 100644 --- a/Shared/Views/PopularView.swift +++ b/Shared/Views/PopularView.swift @@ -20,7 +20,7 @@ struct PopularView: View { } var body: some View { - VerticalCells(items: videos, isLoading: resource?.isLoading ?? false) { if shouldDisplayHeader { header } } + VerticalCells(items: videos) { if shouldDisplayHeader { header } } .onAppear { resource?.addObserver(store) resource?.loadIfNeeded()? diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 905093be..88d623cc 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -690,9 +690,6 @@ 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; 379DC3D228BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; 379DC3D328BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; - 379E7C2F2A20AF0A00AF8118 /* EmptyItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */; }; - 379E7C302A20AF0A00AF8118 /* EmptyItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */; }; - 379E7C312A20AF0A00AF8118 /* EmptyItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */; }; 379E7C332A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */; }; 379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */; }; 379E7C362A2105B900AF8118 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 379E7C352A2105B900AF8118 /* Introspect */; }; @@ -1397,7 +1394,6 @@ 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSettingsButton.swift; sourceTree = ""; }; 379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = ""; }; 379DC3D028BA4EB400B09677 /* Seek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Seek.swift; sourceTree = ""; }; - 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyItems.swift; sourceTree = ""; }; 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableSearchTextField.swift; sourceTree = ""; }; 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */ = {isa = PBXFileReference; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = HideShortsButtons.swift; sourceTree = ""; }; 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettings.swift; sourceTree = ""; }; @@ -1911,7 +1907,6 @@ 37FB285D272225E800A57617 /* ContentItemView.swift */, 372CFD14285F2E2A00B0B54B /* ControlsBar.swift */, 3748186D26A769D60084E870 /* DetailBadge.swift */, - 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */, 37599F37272B4D740087F250 /* FavoriteButton.swift */, 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */, 37758C0A2A1D1C8B001FD900 /* HideWatchedButtons.swift */, @@ -3190,7 +3185,6 @@ 374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */, 37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */, 37D836BC294927E700005E5E /* ChannelsCacheModel.swift in Sources */, - 379E7C2F2A20AF0A00AF8118 /* EmptyItems.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37BC50A82778A84700510953 /* HistorySettings.swift in Sources */, @@ -3501,7 +3495,6 @@ 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */, 376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, - 379E7C302A20AF0A00AF8118 /* EmptyItems.swift in Sources */, 377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */, 3795593727B08538007FF8F4 /* StreamControl.swift in Sources */, 37772E0E2A216F8600608BD9 /* String+ReplacingHTMLEntities.swift in Sources */, @@ -3796,7 +3789,6 @@ 37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */, 3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */, - 379E7C312A20AF0A00AF8118 /* EmptyItems.swift in Sources */, 3718B9A52921A97F0003DB2E /* InspectorView.swift in Sources */, 37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 376BE50D27349108009AD608 /* BrowsingSettings.swift in Sources */,