import Foundation import MediaKit import Observation /// Die Suche als Modell: entprellen, fragen, merken. /// /// **Herausgelöst wie `HomeFeed` und `LibraryFeed`**, und aus demselben /// Grund: der Fernseher stellt dieselben Fragen an denselben Server, nur ganz /// anders dargestellt. Kopiert wären dabei die drei Stellen, an denen es /// nicht offensichtlich ist — die Pause vor der Anfrage, die Mindestlänge und /// vor allem die Prüfung auf `Task.isCancelled`, ohne die jeder Tastendruck /// die Ergebnisse des vorigen löscht. @MainActor @Observable final class SearchFeed { /// Wie viele zuletzt gesuchte Begriffe gemerkt werden. private static let recentLimit = 8 private static let recentKey = "flixy.search.recent" private(set) var hubs: [MediaRowGroup] = [] private(set) var isSearching = false private(set) var recent: [String] private let defaults: UserDefaults init(defaults: UserDefaults = .standard) { self.defaults = defaults self.recent = defaults.stringArray(forKey: Self.recentKey) ?? [] } /// Sucht — nach einer kurzen Pause. /// /// Die Pause ist der Kern: aufgerufen wird das bei **jedem** Tastendruck, /// und ohne sie liefe für „Nachtzug" eine Anfrage je Buchstabe. Ein /// neuer Tastendruck bricht die laufende Aufgabe ab, und der Abbruch /// landet im `catch` der Pause — deshalb steht dort kein Fehler, sondern /// ein `return`. func search(_ text: String, from backend: (any MediaBackend)?) async { let bereinigt = text.trimmingCharacters(in: .whitespacesAndNewlines) guard let backend, bereinigt.count >= 2 else { hubs = [] return } do { try await Task.sleep(for: .milliseconds(300)) } catch { return } isSearching = true defer { isSearching = false } do { hubs = try await backend.search(bereinigt, limit: 30) if !hubs.isEmpty { remember(bereinigt) } } catch { // **`Task.isCancelled` statt `catch is CancellationError`.** Eine // abgebrochene Netzanfrage kommt als Übertragungsfehler zurück, // nicht als Abbruch; die naheliegende Prüfung lief deshalb ins // Leere, und jeder Tastendruck leerte kurz die Liste. if Task.isCancelled { return } hubs = [] } } // MARK: - Zuletzt gesucht private func remember(_ eintrag: String) { recent.removeAll { $0.caseInsensitiveCompare(eintrag) == .orderedSame } recent.insert(eintrag, at: 0) recent = Array(recent.prefix(Self.recentLimit)) defaults.set(recent, forKey: Self.recentKey) } func forgetRecent() { recent = [] defaults.removeObject(forKey: Self.recentKey) } }