diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index b2f64bd..5374cfc 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -16,6 +16,8 @@ public class PluginManager: ObservableObject { @Published var availableSources: [SourceJson] = [] @Published var availableActions: [ActionJson] = [] + @Published var filteredInstalledSources: [Source] = [] + @Published var showActionErrorAlert = false @Published var actionErrorAlertMessage: String = "" @@ -263,17 +265,6 @@ public class PluginManager: ObservableObject { return Application.shared.appVersion >= minVersion } - // Fetches sources using the background context - public func fetchInstalledSources() -> [Source] { - let backgroundContext = PersistenceController.shared.backgroundContext - - if let sources = try? backgroundContext.fetch(Source.fetchRequest()) { - return sources.compactMap { $0 } - } else { - return [] - } - } - @MainActor public func runDefaultAction(urlString: String?, navModel: NavigationViewModel) { let context = PersistenceController.shared.backgroundContext diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 16ca3c0..2392601 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -61,8 +61,6 @@ class ScrapingViewModel: ObservableObject { logManager?.updateIndeterminateToast("Loading sources", cancelAction: nil) } - @Published var filteredSource: Source? - // Utility function to print source specific errors func sendSourceError(_ description: String) async { await logManager?.error(description, showToast: false) @@ -84,6 +82,7 @@ class ScrapingViewModel: ObservableObject { await debridManager.clearIAValues() } + await clearCurrentSourceNames() await clearSearchResults() await logManager?.updateIndeterminateToast("Loading sources", cancelAction: { diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift index dd75d81..d4e6a37 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -10,32 +10,36 @@ import SwiftUI struct SearchFilterHeaderView: View { @Environment(\.verticalSizeClass) var verticalSizeClass - @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var pluginManager: PluginManager - @FetchRequest( - entity: Source.entity(), - sortDescriptors: [] - ) var sources: FetchedResults + var sources: FetchedResults var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { + // MARK: - Source filter picker + + // TODO: Make this use multiple selections Menu { - Picker("", selection: $scrapingModel.filteredSource) { - Text("None").tag(nil as Source?) + Picker("", selection: $pluginManager.filteredInstalledSources) { + Text("All").tag([] as [Source]) ForEach(sources, id: \.self) { source in if source.enabled { Text(source.name) - .tag(Source?.some(source)) + .tag([source]) } } } } label: { - FilterLabelView(name: scrapingModel.filteredSource?.name ?? "Source") + FilterLabelView( + name: pluginManager.filteredInstalledSources.first?.name ?? "Source" + ) } - .id(scrapingModel.filteredSource) + .id(pluginManager.filteredInstalledSources) + + // MARK: - Selected debrid picker DebridPickerView { FilterLabelView(name: debridManager.selectedDebridType?.toString() ?? "Debrid") diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift index 45ba1d9..9f0fff6 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift @@ -13,12 +13,13 @@ struct SearchResultsView: View { @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var pluginManager: PluginManager @AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false var body: some View { ForEach(scrapingModel.searchResults, id: \.self) { result in - if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil { + if pluginManager.filteredInstalledSources.isEmpty || pluginManager.filteredInstalledSources.contains(where: { result.source == $0.name }) { SearchResultButtonView(result: result) } } @@ -49,14 +50,5 @@ struct SearchResultsView: View { scrapingModel.runningSearchTask = nil } } - .overlay { - if - scrapingModel.searchResults.isEmpty, - isSearching, - scrapingModel.runningSearchTask == nil - { - Text("No results found") - } - } } } diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 2bce38f..22e9da3 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -16,7 +16,13 @@ struct ContentView: View { @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false + @FetchRequest( + entity: Source.entity(), + sortDescriptors: [] + ) var sources: FetchedResults + @State private var isSearching = false + @State private var isEditingSearch = false @State private var dismissAction: () -> Void = {} var body: some View { @@ -27,30 +33,40 @@ struct ContentView: View { .listStyle(.insetGrouped) .inlinedList(inset: 20) .navigationTitle("Search") + .overlay { + if + scrapingModel.searchResults.isEmpty, + isSearching, + scrapingModel.runningSearchTask == nil, + !isEditingSearch + { + Text( + pluginManager.filteredInstalledSources.isEmpty ? + "No results found" : + "No results found. Check your source filter and redo your search." + ) + .padding(.horizontal) + } + } .expandedSearchable( text: $scrapingModel.searchText, isSearching: $isSearching, + isEditingSearch: $isEditingSearch, prompt: navModel.searchPrompt, dismiss: $dismissAction, scopeBarContent: { - SearchFilterHeaderView() + SearchFilterHeaderView(sources: sources) }, onSubmit: { - if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled { + if + let runningSearchTask = scrapingModel.runningSearchTask, + runningSearchTask.isCancelled + { scrapingModel.runningSearchTask = nil return } - scrapingModel.runningSearchTask = Task { - let sources = pluginManager.fetchInstalledSources() - await scrapingModel.scanSources( - sources: sources, - debridManager: debridManager - ) - - logManager.hideIndeterminateToast() - scrapingModel.runningSearchTask = nil - } + executeSearch() } ) .autocorrectionDisabled(!autocorrectSearch) @@ -58,6 +74,26 @@ struct ContentView: View { .onAppear { navModel.getSearchPrompt() } + .onChange(of: isEditingSearch) { newVal in + print(newVal) + } + } + } + + func executeSearch() { + scrapingModel.runningSearchTask = Task { + await scrapingModel.scanSources( + sources: + scrapingModel.searchResults.isEmpty ? + sources.compactMap { $0 } : + (pluginManager.filteredInstalledSources.isEmpty ? + sources.compactMap { $0 } : + pluginManager.filteredInstalledSources), + debridManager: debridManager + ) + + logManager.hideIndeterminateToast() + scrapingModel.runningSearchTask = nil } } } diff --git a/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift b/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift index 7843815..2570c58 100644 --- a/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift +++ b/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift @@ -11,6 +11,7 @@ public extension View { // A dismissAction must be added in the parent view struct due to lifecycle issues func expandedSearchable(text: Binding, isSearching: Binding? = nil, + isEditingSearch: Binding? = nil, prompt: String? = nil, dismiss: Binding<() -> Void>? = nil, scopeBarContent: @escaping () -> some View = { @@ -23,6 +24,7 @@ public extension View { SearchBar( searchText: text, isSearching: isSearching ?? Binding(get: { true }, set: { _, _ in }), + isEditingSearch: isEditingSearch ?? Binding(get: { true }, set: { _, _ in }), prompt: prompt ?? "Search", dismiss: dismiss ?? Binding(get: { {} }, set: { _, _ in }), scopeBarContent: scopeBarContent, @@ -84,6 +86,7 @@ struct SearchBar: UIViewControllerRepresentable { // Passed in vars @Binding var searchText: String @Binding var isSearching: Bool + @Binding var isEditingSearch: Bool var prompt: String @Binding var dismiss: () -> Void let scopeBarContent: () -> ScopeContent @@ -101,6 +104,14 @@ struct SearchBar: UIViewControllerRepresentable { parent.searchText = searchText } + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + parent.isEditingSearch = true + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + parent.isEditingSearch = false + } + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { if let onSubmit = parent.onSubmit { onSubmit()