diff --git a/Model/SearchState.swift b/Model/SearchState.swift new file mode 100644 index 00000000..1ac04ab8 --- /dev/null +++ b/Model/SearchState.swift @@ -0,0 +1,63 @@ +import Defaults +import Siesta +import SwiftUI + +final class SearchState: ObservableObject { + @Published var query = SearchQuery() + @Default(.searchQuery) private var queryText + + private var previousResource: Resource? + private var resource: Resource! + + @Published var store = Store<[Video]>() + + init() { + let newQuery = query + newQuery.query = queryText + query = newQuery + + resource = InvidiousAPI.shared.search(newQuery) + } + + var isLoading: Bool { + resource.isLoading + } + + func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) { + changeHandler(query) + + let newResource = InvidiousAPI.shared.search(query) + guard newResource != previousResource else { + return + } + + previousResource?.removeObservers(ownedBy: store) + previousResource = newResource + + queryText = query.query + + resource = newResource + resource.addObserver(store) + loadResourceIfNeededAndReplaceStore() + } + + func loadResourceIfNeededAndReplaceStore() { + let currentResource = resource! + + if let request = resource.loadIfNeeded() { + request.onSuccess { response in + if let videos: [Video] = response.typedContent() { + self.replace(videos, for: currentResource) + } + } + } else { + replace(store.collection, for: currentResource) + } + } + + func replace(_ videos: [Video], for resource: Resource) { + if self.resource == resource { + store = Store<[Video]>(videos) + } + } +} diff --git a/Model/Store.swift b/Model/Store.swift index c014bb8a..08e3e7f3 100644 --- a/Model/Store.swift +++ b/Model/Store.swift @@ -7,6 +7,12 @@ final class Store: ResourceObserver, ObservableObject { var collection: Data { all ?? ([] as! Data) } var item: Data? { all } + init(_ data: Data? = nil) { + if data != nil { + replace(data!) + } + } + func resourceChanged(_ resource: Resource, event _: ResourceEvent) { if let items: Data = resource.typedContent() { replace(items) diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 3d10928c..29c3f6a5 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; }; 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; }; 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B181267B4E4900704544 /* TrendingCategory.swift */; }; + 3711403F26B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; }; + 3711404026B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; }; + 3711404126B206A6005B3555 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchState.swift */; }; 371231842683E62F0000B307 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371231832683E62F0000B307 /* VideosView.swift */; }; 371231852683E7820000B307 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371231832683E62F0000B307 /* VideosView.swift */; }; 371231862683E7820000B307 /* VideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371231832683E62F0000B307 /* VideosView.swift */; }; @@ -209,6 +212,7 @@ /* Begin PBXFileReference section */ 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountrySelectionView.swift; sourceTree = ""; }; 3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = ""; }; + 3711403E26B206A6005B3555 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = ""; }; 371231832683E62F0000B307 /* VideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = ""; }; 3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; 37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; @@ -488,6 +492,7 @@ 376578882685471400D4EA09 /* Playlist.swift */, 37C7A1DB267CE9D90010EAD6 /* Profile.swift */, 373CFACA26966264003CB2C6 /* SearchQuery.swift */, + 3711403E26B206A6005B3555 /* SearchState.swift */, 37EAD86E267B9ED100D9E01B /* Segment.swift */, 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */, 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */, @@ -756,6 +761,7 @@ 37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */, 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */, + 3711403F26B206A6005B3555 /* SearchState.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, 37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */, @@ -846,6 +852,7 @@ 37D4B19826717E1500C925CA /* Video.swift in Sources */, 37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */, 37BD07C12698AD3B003EBB87 /* TrendingCountrySelectionView.swift in Sources */, + 3711404026B206A6005B3555 /* SearchState.swift in Sources */, 37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */, @@ -915,6 +922,7 @@ 372F954A26A4D27000502766 /* VideoLoading.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */, + 3711404126B206A6005B3555 /* SearchState.swift in Sources */, 379775952689365600DD52A8 /* Array+Next.swift in Sources */, 37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */, 3705B180267B4DFB00704544 /* TrendingCountrySelectionView.swift in Sources */, diff --git a/Shared/ContentView.swift b/Shared/ContentView.swift index 54ec8101..978a0485 100644 --- a/Shared/ContentView.swift +++ b/Shared/ContentView.swift @@ -2,6 +2,7 @@ import SwiftUI struct ContentView: View { @StateObject private var navigationState = NavigationState() + @StateObject private var searchState = SearchState() #if os(iOS) @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -20,7 +21,9 @@ struct ContentView: View { #elseif os(tvOS) TVNavigationView() #endif - }.environmentObject(navigationState) + } + .environmentObject(navigationState) + .environmentObject(searchState) } } diff --git a/Shared/SearchView.swift b/Shared/SearchView.swift index 53b1831c..9789773a 100644 --- a/Shared/SearchView.swift +++ b/Shared/SearchView.swift @@ -8,16 +8,13 @@ struct SearchView: View { @Default(.searchDate) private var searchDate @Default(.searchDuration) private var searchDuration - @ObservedObject private var store = Store<[Video]>() - @ObservedObject private var query = SearchQuery() + @EnvironmentObject private var state var body: some View { VStack { - if !store.collection.isEmpty { - VideosView(videos: store.collection) - } + VideosView(videos: state.store.collection) - if store.collection.isEmpty && !resource.isLoading && !query.isEmpty { + if state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty { Text("No results") if searchFiltersActive { @@ -31,7 +28,7 @@ struct SearchView: View { } .searchable(text: $queryText) .onAppear { - changeQuery { + state.changeQuery { query in query.query = queryText query.sortBy = searchSortOrder query.date = searchDate @@ -39,34 +36,22 @@ struct SearchView: View { } } .onChange(of: queryText) { queryText in - changeQuery { query.query = queryText } + state.changeQuery { query in query.query = queryText } } .onChange(of: searchSortOrder) { order in - changeQuery { query.sortBy = order } + state.changeQuery { query in query.sortBy = order } } .onChange(of: searchDate) { date in - changeQuery { query.date = date } + state.changeQuery { query in query.date = date } } .onChange(of: searchDuration) { duration in - changeQuery { query.duration = duration } + state.changeQuery { query in query.duration = duration } } #if !os(tvOS) .navigationTitle("Search") #endif } - func changeQuery(_ change: @escaping () -> Void = {}) { - resource.removeObservers(ownedBy: store) - change() - - resource.addObserver(store) - resource.loadIfNeeded() - } - - var resource: Resource { - InvidiousAPI.shared.search(query) - } - var searchFiltersActive: Bool { searchDate != nil || searchDuration != nil }