From 4e0d7b60f72047b6d975f60f8232e8c7900ac534 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 25 Sep 2021 14:17:58 +0200 Subject: [PATCH] Search improvements for iOS --- Model/{Recents.swift => RecentsModel.swift} | 6 +- Model/SearchModel.swift | 42 ++++---- Model/SearchQuery.swift | 4 +- Pearvidious.xcodeproj/project.pbxproj | 16 +-- Shared/Defaults.swift | 4 - Shared/Navigation/AppSidebarNavigation.swift | 32 ++---- Shared/Navigation/AppSidebarRecents.swift | 4 +- Shared/Navigation/AppTabNavigation.swift | 6 +- Shared/Navigation/ContentView.swift | 3 +- Shared/PearvidiousApp.swift | 5 +- Shared/Views/SearchView.swift | 106 +++++++++++++++---- Shared/Views/VideoContextMenuView.swift | 2 +- 12 files changed, 138 insertions(+), 92 deletions(-) rename Model/{Recents.swift => RecentsModel.swift} (95%) diff --git a/Model/Recents.swift b/Model/RecentsModel.swift similarity index 95% rename from Model/Recents.swift rename to Model/RecentsModel.swift index a0cb9dba..69910588 100644 --- a/Model/Recents.swift +++ b/Model/RecentsModel.swift @@ -1,7 +1,7 @@ import Defaults import Foundation -final class Recents: ObservableObject { +final class RecentsModel: ObservableObject { @Default(.recentlyOpened) var items var isEmpty: Bool { @@ -28,6 +28,10 @@ final class Recents: ObservableObject { } } + func addQuery(_ query: String) { + open(.init(from: query)) + } + var presentedChannel: Channel? { if let recent = items.last(where: { $0.type == .channel }) { return recent.channel diff --git a/Model/SearchModel.swift b/Model/SearchModel.swift index a27b3ff9..c49cf5ac 100644 --- a/Model/SearchModel.swift +++ b/Model/SearchModel.swift @@ -14,24 +14,7 @@ final class SearchModel: ObservableObject { private var resource: Resource! var isLoading: Bool { - resource.isLoading - } - - func loadSuggestions(_ query: String) { - let resource = api.searchSuggestions(query: query) - - resource.addObserver(querySuggestions) - resource.loadIfNeeded() - - if let request = resource.loadIfNeeded() { - request.onSuccess { response in - if let suggestions: [String] = response.typedContent() { - self.querySuggestions = Store<[String]>(suggestions) - } - } - } else { - querySuggestions = Store<[String]>(querySuggestions.collection) - } + resource?.isLoading ?? false } func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) { @@ -87,4 +70,27 @@ final class SearchModel: ObservableObject { store = Store<[Video]>(videos) } } + + private var suggestionsDebounceTimer: Timer? + + func loadSuggestions(_ query: String) { + suggestionsDebounceTimer?.invalidate() + + suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in + let resource = self.api.searchSuggestions(query: query) + + resource.addObserver(self.querySuggestions) + resource.loadIfNeeded() + + if let request = resource.loadIfNeeded() { + request.onSuccess { response in + if let suggestions: [String] = response.typedContent() { + self.querySuggestions = Store<[String]>(suggestions) + } + } + } else { + self.querySuggestions = Store<[String]>(self.querySuggestions.collection) + } + } + } } diff --git a/Model/SearchQuery.swift b/Model/SearchQuery.swift index 3b34b57b..2bcfda7e 100644 --- a/Model/SearchQuery.swift +++ b/Model/SearchQuery.swift @@ -36,9 +36,9 @@ final class SearchQuery: ObservableObject { var name: String { switch self { case .uploadDate: - return "Upload Date" + return "Date" case .viewCount: - return "View Count" + return "Views" default: return rawValue.capitalized } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 2703fa68..dfa865e4 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -221,9 +221,9 @@ 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; 37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B76E95268747C900CE5671 /* OptionsView.swift */; }; - 37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* Recents.swift */; }; - 37C194C826F6A9C8005D3B96 /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* Recents.swift */; }; - 37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* Recents.swift */; }; + 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; + 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; + 37C194C926F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; @@ -379,7 +379,7 @@ 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; - 37C194C626F6A9C8005D3B96 /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = ""; }; + 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = ""; }; 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = ""; }; 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = ""; }; 37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = ""; }; @@ -760,7 +760,7 @@ 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */, 376578882685471400D4EA09 /* Playlist.swift */, 37BA794226DBA973002A0235 /* PlaylistsModel.swift */, - 37C194C626F6A9C8005D3B96 /* Recents.swift */, + 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */, 3711403E26B206A6005B3555 /* SearchModel.swift */, 373CFACA26966264003CB2C6 /* SearchQuery.swift */, 37EAD86E267B9ED100D9E01B /* Segment.swift */, @@ -1079,7 +1079,7 @@ 37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */, 37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37754C9D26B7500000DBD602 /* VideosView.swift in Sources */, - 37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */, + 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */, 3711403F26B206A6005B3555 /* SearchModel.swift in Sources */, 37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */, @@ -1152,7 +1152,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 37C194C826F6A9C8005D3B96 /* Recents.swift in Sources */, + 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, 3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, @@ -1306,7 +1306,7 @@ 37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */, - 37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */, + 37C194C926F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37F64FE626FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */, 37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */, 37484C2B26FC83FF00287258 /* AccountFormView.swift in Sources */, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 09a9b627..a0e10912 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -7,10 +7,6 @@ extension Defaults.Keys { static let instances = Key<[Instance]>("instances", default: []) - static let searchSortOrder = Key("searchSortOrder", default: .relevance) - static let searchDate = Key("searchDate") - static let searchDuration = Key("searchDuration") - static let selectedPlaylistID = Key("selectedPlaylistID") static let showingAddToPlaylist = Key("showingAddToPlaylist", default: false) static let videoIDToAddToPlaylist = Key("videoIDToAddToPlaylist") diff --git a/Shared/Navigation/AppSidebarNavigation.swift b/Shared/Navigation/AppSidebarNavigation.swift index 9fc27a91..7d408860 100644 --- a/Shared/Navigation/AppSidebarNavigation.swift +++ b/Shared/Navigation/AppSidebarNavigation.swift @@ -16,8 +16,7 @@ struct AppSidebarNavigation: View { @EnvironmentObject private var instances @EnvironmentObject private var navigation @EnvironmentObject private var playlists - @EnvironmentObject private var recents - @EnvironmentObject private var search + @EnvironmentObject private var recents @EnvironmentObject private var subscriptions @State private var didApplyPrimaryViewWorkAround = false @@ -56,23 +55,6 @@ struct AppSidebarNavigation: View { Text("Select section") } .environment(\.navigationStyle, .sidebar) - .searchable(text: $search.queryText, placement: .sidebar) { - ForEach(search.querySuggestions.collection, id: \.self) { suggestion in - Text(suggestion) - .searchCompletion(suggestion) - } - } - .onChange(of: search.queryText) { query in - search.loadSuggestions(query) - } - .onSubmit(of: .search) { - search.changeQuery { query in - query.query = search.queryText - } - recents.open(RecentItem(from: search.queryText)) - - navigation.tabSelection = .search - } } var sidebar: some View { @@ -87,12 +69,6 @@ struct AppSidebarNavigation: View { scrollScrollViewToItem(scrollView: scrollView, for: navigation.tabSelection) } } - .background { - NavigationLink(destination: SearchView(), tag: TabSelection.search, selection: selection) { - Color.clear - } - .hidden() - } .listStyle(.sidebar) } .toolbar { @@ -144,6 +120,12 @@ struct AppSidebarNavigation: View { Label("Trending", systemImage: "chart.line.uptrend.xyaxis") .accessibility(label: Text("Trending")) } + + NavigationLink(destination: LazyView(SearchView()), tag: TabSelection.search, selection: selection) { + Label("Search", systemImage: "magnifyingglass") + .accessibility(label: Text("Search")) + } + .keyboardShortcut("f") } } diff --git a/Shared/Navigation/AppSidebarRecents.swift b/Shared/Navigation/AppSidebarRecents.swift index 22e590dd..be95aa72 100644 --- a/Shared/Navigation/AppSidebarRecents.swift +++ b/Shared/Navigation/AppSidebarRecents.swift @@ -5,7 +5,7 @@ struct AppSidebarRecents: View { @Binding var selection: TabSelection? @EnvironmentObject private var navigation - @EnvironmentObject private var recents + @EnvironmentObject private var recents @Default(.recentlyOpened) private var recentItems @@ -44,7 +44,7 @@ struct AppSidebarRecents: View { } struct RecentNavigationLink: View { - @EnvironmentObject private var recents + @EnvironmentObject private var recents var recent: RecentItem @Binding var selection: TabSelection? diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index cdaedd3c..a6f32139 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -4,7 +4,7 @@ import SwiftUI struct AppTabNavigation: View { @EnvironmentObject private var navigation @EnvironmentObject private var search - @EnvironmentObject private var recents + @EnvironmentObject private var recents var body: some View { TabView(selection: $navigation.tabSelection) { @@ -78,9 +78,7 @@ struct AppTabNavigation: View { query.query = search.queryText } - recents.open(RecentItem(from: search.queryText)) - - navigation.tabSelection = .search + recents.addQuery(search.queryText) } ) } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 9a9ed5b1..95cceeaf 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -4,7 +4,7 @@ import SwiftUI struct ContentView: View { @StateObject private var navigation = NavigationModel() @StateObject private var playback = PlaybackModel() - @StateObject private var recents = Recents() + @StateObject private var recents = RecentsModel() @EnvironmentObject private var api @EnvironmentObject private var instances @@ -34,6 +34,7 @@ struct ContentView: View { .sheet(isPresented: $navigation.showingVideo) { if let video = navigation.video { VideoPlayerView(video) + .environmentObject(playback) #if !os(iOS) .frame(minWidth: 550, minHeight: 720) diff --git a/Shared/PearvidiousApp.swift b/Shared/PearvidiousApp.swift index 7ee070cb..4190ad6b 100644 --- a/Shared/PearvidiousApp.swift +++ b/Shared/PearvidiousApp.swift @@ -31,15 +31,14 @@ struct PearvidiousApp: App { .onAppear(perform: configureAPI) .environmentObject(api) .environmentObject(instances) - .environmentObject(playlists) - .environmentObject(subscriptions) } #endif } fileprivate func configureAPI() { - subscriptions.api = api playlists.api = api + search.api = api + subscriptions.api = api guard api.account == nil, instances.defaultAccount != nil else { return diff --git a/Shared/Views/SearchView.swift b/Shared/Views/SearchView.swift index 9e061908..da48304c 100644 --- a/Shared/Views/SearchView.swift +++ b/Shared/Views/SearchView.swift @@ -3,19 +3,19 @@ import Siesta import SwiftUI struct SearchView: View { - @Default(.searchSortOrder) private var searchSortOrder - @Default(.searchDate) private var searchDate - @Default(.searchDuration) private var searchDuration + private var query: SearchQuery? - @EnvironmentObject private var recents - @EnvironmentObject private var state - - @Environment(\.navigationStyle) private var navigationStyle + @State private var searchSortOrder: SearchQuery.SortOrder = .relevance + @State private var searchDate: SearchQuery.Date? + @State private var searchDuration: SearchQuery.Duration? @State private var presentingClearConfirmation = false @State private var recentsChanged = false - private var query: SearchQuery? + @Environment(\.navigationStyle) private var navigationStyle + + @EnvironmentObject private var recents + @EnvironmentObject private var state init(_ query: SearchQuery? = nil) { self.query = query @@ -37,7 +37,9 @@ struct SearchView: View { if searchFiltersActive { Button("Reset search filters") { - Defaults.reset(.searchDate, .searchDuration) + self.searchSortOrder = .relevance + self.searchDate = nil + self.searchDuration = nil } } @@ -45,16 +47,74 @@ struct SearchView: View { } } } + .toolbar { + #if os(iOS) + ToolbarItemGroup(placement: .bottomBar) { + Section { + if !state.queryText.isEmpty { + Text("Sort:") + .foregroundColor(.secondary) + + Menu(searchSortOrder.name) { + ForEach(SearchQuery.SortOrder.allCases) { sortOrder in + Button(sortOrder.name) { + searchSortOrder = sortOrder + } + } + } + + Spacer() + + Text("Filter:") + .foregroundColor(.secondary) + + Menu(searchDuration?.name ?? "Duration") { + Button("All") { + searchDuration = nil + } + ForEach(SearchQuery.Duration.allCases) { duration in + Button(duration.name) { + searchDuration = duration + } + } + } + .foregroundColor(searchDuration.isNil ? .secondary : .accentColor) + + Menu(searchDate?.name ?? "Date") { + Button("All") { + searchDate = nil + } + ForEach(SearchQuery.Date.allCases) { date in + Button(date.name) { + searchDate = date + } + } + } + .foregroundColor(searchDate.isNil ? .secondary : .accentColor) + } + } + .transaction { t in t.animation = .none } + } + #endif + } .onAppear { if query != nil { - if navigationStyle == .tab { - state.queryText = query!.query - } + state.queryText = query!.query state.resetQuery(query!) } } - .onChange(of: state.query.query) { queryText in - state.changeQuery { query in query.query = queryText } + .searchable(text: $state.queryText, placement: searchFieldPlacement) { + ForEach(state.querySuggestions.collection, id: \.self) { suggestion in + Text(suggestion) + .searchCompletion(suggestion) + } + } + .onChange(of: state.queryText) { query in + state.loadSuggestions(query) + } + .onSubmit(of: .search) { + state.changeQuery { query in query.query = state.queryText } + recents.addQuery(state.queryText) } .onChange(of: searchSortOrder) { order in state.changeQuery { query in query.sortBy = order } @@ -66,7 +126,15 @@ struct SearchView: View { state.changeQuery { query in query.duration = duration } } #if !os(tvOS) - .navigationTitle(navigationTitle) + .navigationTitle("Search") + #endif + } + + var searchFieldPlacement: SearchFieldPlacement { + #if os(iOS) + .navigationBarDrawer(displayMode: .always) + #else + .automatic #endif } @@ -114,14 +182,6 @@ struct SearchView: View { } } - var navigationTitle: String { - if state.query.query.isEmpty || (navigationStyle == .tab && state.queryText.isEmpty) { - return "Search" - } - - return "Search: \"\(state.query.query)\"" - } - var searchFiltersActive: Bool { searchDate != nil || searchDuration != nil } diff --git a/Shared/Views/VideoContextMenuView.swift b/Shared/Views/VideoContextMenuView.swift index 9845b9b6..a494a974 100644 --- a/Shared/Views/VideoContextMenuView.swift +++ b/Shared/Views/VideoContextMenuView.swift @@ -4,7 +4,7 @@ import SwiftUI struct VideoContextMenuView: View { @EnvironmentObject private var api @EnvironmentObject private var navigation - @EnvironmentObject private var recents + @EnvironmentObject private var recents @EnvironmentObject private var subscriptions let video: Video