diff --git a/Extensions/String+Format.swift b/Extensions/String+Format.swift new file mode 100644 index 00000000..888ba2c8 --- /dev/null +++ b/Extensions/String+Format.swift @@ -0,0 +1,11 @@ +import Foundation + +extension String { + var serializationSafe: String { + let serializationUnsafe = ":;" + let forbidden = CharacterSet(charactersIn: serializationUnsafe) + let result = unicodeScalars.filter { !forbidden.contains($0) } + + return String(String.UnicodeScalarView(result)) + } +} diff --git a/Model/InstanceAccountValidator.swift b/Model/AccountValidator.swift similarity index 95% rename from Model/InstanceAccountValidator.swift rename to Model/AccountValidator.swift index 038de316..98aa1f3a 100644 --- a/Model/InstanceAccountValidator.swift +++ b/Model/AccountValidator.swift @@ -2,7 +2,7 @@ import Foundation import Siesta import SwiftUI -final class InstanceAccountValidator: Service { +final class AccountValidator: Service { let url: String let account: Instance.Account? @@ -14,14 +14,14 @@ final class InstanceAccountValidator: Service { init( url: String, account: Instance.Account? = nil, - formObjectID: Binding, + id: Binding, valid: Binding, validated: Binding, error: Binding? = nil ) { self.url = url self.account = account - self.formObjectID = formObjectID + formObjectID = id self.valid = valid self.validated = validated self.error = error diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 1d821a32..45fb8245 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -74,9 +74,15 @@ 37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; }; 37484C2E26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; }; 37484C2F26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; }; - 37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; }; - 37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; }; - 37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; }; + 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; }; + 37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; }; + 37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; }; + 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; + 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; + 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; + 375168DB27010806008F96A6 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D92701070E008F96A6 /* String+Format.swift */; }; + 375168DC27010807008F96A6 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D92701070E008F96A6 /* String+Format.swift */; }; + 375168DD27010808008F96A6 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D92701070E008F96A6 /* String+Format.swift */; }; 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; }; 375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; }; 375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; }; @@ -324,7 +330,9 @@ 37484C2426FC83E000287258 /* InstanceFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceFormView.swift; sourceTree = ""; }; 37484C2826FC83FF00287258 /* AccountFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFormView.swift; sourceTree = ""; }; 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceDetailsSettingsView.swift; sourceTree = ""; }; - 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAccountValidator.swift; sourceTree = ""; }; + 37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = ""; }; + 375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; + 375168D92701070E008F96A6 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = ""; }; 375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = ""; }; @@ -640,6 +648,7 @@ 379775922689365600DD52A8 /* Array+Next.swift */, 376578842685429C00D4EA09 /* CaseIterable+Next.swift */, 37BA794E26DC3E0E002A0235 /* Int+Format.swift */, + 375168D92701070E008F96A6 /* String+Format.swift */, 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */, ); path = Extensions; @@ -676,6 +685,7 @@ 371AAE2726CEBF4700901972 /* Videos */, 371AAE2826CEC7D900901972 /* Views */, 3788AC2126F683AB00F6BAA9 /* Watch Now */, + 375168D52700FAFF008F96A6 /* Debounce.swift */, 372915E52687E3B900F5A35B /* Defaults.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, 37D4B0C22671614700C925CA /* PearvidiousApp.swift */, @@ -741,7 +751,7 @@ 37AAF28F26740715007FC770 /* Channel.swift */, 37141672267A8E10006CA35D /* Country.swift */, 378E50FA26FE8B9F00F49626 /* Instance.swift */, - 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */, + 37484C3026FCB8F900287258 /* AccountValidator.swift */, 375DFB5726F9DA010013F468 /* InstancesModel.swift */, 37977582268922F600DD52A8 /* InvidiousAPI.swift */, 371F2F19269B43D300E4A7AB /* NavigationModel.swift */, @@ -1069,6 +1079,7 @@ 37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37754C9D26B7500000DBD602 /* VideosView.swift in Sources */, 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */, + 375168DD27010808008F96A6 /* String+Format.swift in Sources */, 37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */, 3711403F26B206A6005B3555 /* SearchModel.swift in Sources */, 37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */, @@ -1084,6 +1095,7 @@ 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, 3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */, 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */, + 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */, 37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */, 376578892685471400D4EA09 /* Playlist.swift in Sources */, 37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */, @@ -1103,7 +1115,7 @@ 376578912685490700D4EA09 /* PlaylistsView.swift in Sources */, 37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */, 377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, - 37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */, + 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */, 376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */, 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, @@ -1161,7 +1173,7 @@ 37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */, 37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */, 37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */, - 37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */, + 37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */, 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, 3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */, @@ -1202,9 +1214,11 @@ 37754C9E26B7500000DBD602 /* VideosView.swift in Sources */, 3797758C2689345500DD52A8 /* Store.swift in Sources */, 37141674267A8E10006CA35D /* Country.swift in Sources */, + 375168DC27010807008F96A6 /* String+Format.swift in Sources */, 37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */, 37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */, + 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */, 37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */, 37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */, @@ -1263,6 +1277,7 @@ 376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, 3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */, + 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */, 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, @@ -1280,6 +1295,7 @@ 37AAF27E26737323007FC770 /* PopularView.swift in Sources */, 37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */, 37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */, + 375168DB27010806008F96A6 /* String+Format.swift in Sources */, 37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */, 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, 376578932685490700D4EA09 /* PlaylistsView.swift in Sources */, @@ -1291,7 +1307,7 @@ 37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */, - 37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */, + 37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */, 37C194C926F6A9C8005D3B96 /* RecentsModel.swift in Sources */, diff --git a/Shared/Debounce.swift b/Shared/Debounce.swift new file mode 100644 index 00000000..c92fefca --- /dev/null +++ b/Shared/Debounce.swift @@ -0,0 +1,15 @@ +import Foundation + +struct Debounce { + private var timer: Timer? + + mutating func debouncing(_ interval: TimeInterval, action: @escaping () -> Void) { + timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + action() + } + } + + func invalidate() { + timer?.invalidate() + } +} diff --git a/Shared/Navigation/AccountsMenuView.swift b/Shared/Navigation/AccountsMenuView.swift index a9bb0fa1..abd6b28e 100644 --- a/Shared/Navigation/AccountsMenuView.swift +++ b/Shared/Navigation/AccountsMenuView.swift @@ -8,12 +8,12 @@ struct AccountsMenuView: View { var body: some View { Menu { - ForEach(instances, id: \.self) { instance in + ForEach(instances) { instance in Button(accountButtonTitle(instance: instance, account: instance.anonymousAccount)) { api.setAccount(instance.anonymousAccount) } - ForEach(instance.accounts, id: \.self) { account in + ForEach(instance.accounts) { account in Button(accountButtonTitle(instance: instance, account: account)) { api.setAccount(account) } diff --git a/Shared/Settings/AccountFormView.swift b/Shared/Settings/AccountFormView.swift index 13ed3024..78dff109 100644 --- a/Shared/Settings/AccountFormView.swift +++ b/Shared/Settings/AccountFormView.swift @@ -10,6 +10,7 @@ struct AccountFormView: View { @State private var valid = false @State private var validated = false + @State private var validationDebounce = Debounce() @FocusState private var focused: Bool @@ -19,55 +20,10 @@ struct AccountFormView: View { var body: some View { VStack { - HStack(alignment: .center) { - Text("Add Account") - .font(.title2.bold()) - - Spacer() - - Button("Cancel") { - dismiss() - } - #if !os(tvOS) - .keyboardShortcut(.cancelAction) - #endif - } - .padding(.horizontal) - - Form { - TextField("Name", text: $name, prompt: Text("Account Name (optional)")) - .focused($focused) - - TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie")) - } - .onAppear(perform: initializeForm) - .onChange(of: sid) { _ in validate() } - - #if os(macOS) - .padding(.horizontal) - #endif - - HStack { - HStack(spacing: 4) { - Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill") - .foregroundColor(valid ? .green : .red) - VStack(alignment: .leading) { - Text(valid ? "Account found" : "Invalid account details") - } - } - .opacity(validated ? 1 : 0) - Spacer() - - Button("Save", action: submitForm) - .disabled(!valid) - #if !os(tvOS) - .keyboardShortcut(.defaultAction) - #endif - } - .frame(minHeight: 35) - .padding(.horizontal) + header + form + footer } - #if os(iOS) .padding(.vertical) #else @@ -75,35 +31,97 @@ struct AccountFormView: View { #endif } - func initializeForm() { + var header: some View { + HStack(alignment: .center) { + Text("Add Account") + .font(.title2.bold()) + + Spacer() + + Button("Cancel") { + dismiss() + } + #if !os(tvOS) + .keyboardShortcut(.cancelAction) + #endif + } + .padding(.horizontal) + } + + var form: some View { + Form { + TextField("Name", text: $name, prompt: Text("Account Name (optional)")) + .focused($focused) + + TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie")) + } + .onAppear(perform: initializeForm) + .onChange(of: sid) { _ in validate() } + #if os(macOS) + .padding(.horizontal) + #endif + } + + var footer: some View { + HStack { + validationStatus + + Spacer() + + Button("Save", action: submitForm) + .disabled(!valid) + #if !os(tvOS) + .keyboardShortcut(.defaultAction) + #endif + } + .frame(minHeight: 35) + .padding(.horizontal) + } + + var validationStatus: some View { + HStack(spacing: 4) { + Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill") + .foregroundColor(valid ? .green : .red) + VStack(alignment: .leading) { + Text(valid ? "Account found" : "Invalid account details") + } + } + .opacity(validated ? 1 : 0) + } + + private func initializeForm() { focused = true } - func validate() { + private func validate() { + validationDebounce.invalidate() + guard !sid.isEmpty else { validator.reset() return } - validator.validateAccount() + validationDebounce.debouncing(2) { + validator.validateAccount() + } } - func submitForm() { + private func submitForm() { guard valid else { return } - let account = instances.addAccount(instance: instance, name: name, sid: sid) + let account = instances.addAccount(instance: instance, name: name.serializationSafe, sid: sid) selectedAccount?.wrappedValue = account dismiss() } - private var validator: InstanceAccountValidator { - InstanceAccountValidator( + private var validator: AccountValidator { + AccountValidator( url: instance.url, account: Instance.Account(url: instance.url, sid: sid), - formObjectID: $sid, + id: $sid, valid: $valid, validated: $validated ) diff --git a/Shared/Settings/InstanceDetailsSettingsView.swift b/Shared/Settings/InstanceDetailsSettingsView.swift index ef9cebc5..64e82df6 100644 --- a/Shared/Settings/InstanceDetailsSettingsView.swift +++ b/Shared/Settings/InstanceDetailsSettingsView.swift @@ -15,7 +15,7 @@ struct InstanceDetailsSettingsView: View { var body: some View { List { Section(header: Text("Accounts")) { - ForEach(instance.accounts, id: \.self) { account in + ForEach(instance.accounts) { account in Text(account.description) #if !os(tvOS) .swipeActions(edge: .trailing, allowsFullSwipe: false) { diff --git a/Shared/Settings/InstanceFormView.swift b/Shared/Settings/InstanceFormView.swift index aebc040a..1ff3699c 100644 --- a/Shared/Settings/InstanceFormView.swift +++ b/Shared/Settings/InstanceFormView.swift @@ -9,6 +9,7 @@ struct InstanceFormView: View { @State private var valid = false @State private var validated = false @State private var validationError: String? + @State private var validationDebounce = Debounce() @FocusState private var nameFieldFocused: Bool @@ -78,14 +79,13 @@ struct InstanceFormView: View { .padding(.vertical) #else .frame(width: 400, height: 150) - #endif } - var validator: InstanceAccountValidator { - InstanceAccountValidator( + var validator: AccountValidator { + AccountValidator( url: url, - formObjectID: $url, + id: $url, valid: $valid, validated: $validated, error: $validationError @@ -93,15 +93,16 @@ struct InstanceFormView: View { } func validate() { - valid = false - validated = false - validationError = nil + validationDebounce.invalidate() guard !url.isEmpty else { + validator.reset() return } - validator.validateInstance() + validationDebounce.debouncing(2) { + validator.validateInstance() + } } func initializeForm() { diff --git a/Shared/Settings/InstancesSettingsView.swift b/Shared/Settings/InstancesSettingsView.swift index 36e41a6f..acfd75e6 100644 --- a/Shared/Settings/InstancesSettingsView.swift +++ b/Shared/Settings/InstancesSettingsView.swift @@ -27,7 +27,7 @@ struct InstancesSettingsView: View { Group { #if os(iOS) Section(header: instancesHeader) { - ForEach(instances, id: \.self) { instance in + ForEach(instances) { instance in Button(action: { self.selectedInstanceID = instance.id self.presentingInstanceDetails = true @@ -62,7 +62,7 @@ struct InstancesSettingsView: View { if !instances.isEmpty { Picker("Instance", selection: $selectedInstanceID) { - ForEach(instances, id: \.url) { instance in + ForEach(instances) { instance in Text(instance.description).tag(Optional(instance.id)) } } @@ -81,7 +81,7 @@ struct InstancesSettingsView: View { } else { Text("Accounts") List(selection: $selectedAccount) { - ForEach(instance.accounts, id: \.self) { account in + ForEach(instance.accounts) { account in AccountSettingsView(instance: instance, account: account, selectedAccount: $selectedAccount) } diff --git a/Shared/Views/SearchView.swift b/Shared/Views/SearchView.swift index 8537be96..82fdc967 100644 --- a/Shared/Views/SearchView.swift +++ b/Shared/Views/SearchView.swift @@ -12,21 +12,21 @@ struct SearchView: View { @State private var presentingClearConfirmation = false @State private var recentsChanged = false + @State private var searchDebounce = Debounce() + @State private var recentsDebounce = Debounce() + @Environment(\.navigationStyle) private var navigationStyle @EnvironmentObject private var recents @EnvironmentObject private var state - @State private var searchDebounceTimer: Timer? - @State private var recentSearchDebounceTimer: Timer? - init(_ query: SearchQuery? = nil) { self.query = query } var body: some View { VStack { - if navigationStyle == .tab && state.queryText.isEmpty { + if showRecentQueries { recentQueries } else { #if os(tvOS) @@ -40,15 +40,11 @@ struct SearchView: View { VideosView(videos: state.store.collection) #endif - if state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty { + if noResults { Text("No results") if searchFiltersActive { - Button("Reset search filters") { - self.searchSortOrder = .relevance - self.searchDate = .any - self.searchDuration = .any - } + Button("Reset search filters", action: resetFilters) } Spacer() @@ -101,14 +97,14 @@ struct SearchView: View { state.loadSuggestions(newQuery) #if os(tvOS) - searchDebounceTimer?.invalidate() - recentSearchDebounceTimer?.invalidate() + searchDebounce.invalidate() + recentsDebounce.invalidate() - searchDebounceTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in + searchDebounce.debouncing(2) { state.changeQuery { query in query.query = newQuery } } - recentSearchDebounceTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in + recentsDebounce.debouncing(10) { recents.addQuery(newQuery) } #endif @@ -147,10 +143,24 @@ struct SearchView: View { #endif } - var filtersActive: Bool { + fileprivate var showRecentQueries: Bool { + navigationStyle == .tab && state.queryText.isEmpty + } + + fileprivate var filtersActive: Bool { searchDuration != .any || searchDate != .any } + fileprivate func resetFilters() { + searchSortOrder = .relevance + searchDate = .any + searchDuration = .any + } + + fileprivate var noResults: Bool { + state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty + } + var recentQueries: some View { VStack { List { @@ -282,6 +292,7 @@ struct SearchView: View { searchSortOrderButton } .frame(maxWidth: .infinity, alignment: .trailing) + HStack(spacing: 30) { Text("Duration") .foregroundColor(.secondary)