summaryrefslogtreecommitdiff
path: root/Hutch/Views/Search/ScopedSearchHistoryStore.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 22:52:33 -0500
committerChristian Cleberg <[email protected]>2026-04-12 22:52:33 -0500
commit72f860e0e171f6aac4e7d896e7a571147dbcb5c5 (patch)
treec62c9c9325292f65e6f72372a38b35760cc937b1 /Hutch/Views/Search/ScopedSearchHistoryStore.swift
parent576bb6c2432a15f5138771c06019b55fd2244e15 (diff)
downloadhutch-72f860e0e171f6aac4e7d896e7a571147dbcb5c5.tar.gz
hutch-72f860e0e171f6aac4e7d896e7a571147dbcb5c5.tar.bz2
hutch-72f860e0e171f6aac4e7d896e7a571147dbcb5c5.zip
feat: add search and recent queries
Implements: https://todo.sr.ht/~ccleberg/hutch/47 Implements: https://todo.sr.ht/~ccleberg/hutch/48
Diffstat (limited to 'Hutch/Views/Search/ScopedSearchHistoryStore.swift')
-rw-r--r--Hutch/Views/Search/ScopedSearchHistoryStore.swift65
1 files changed, 65 insertions, 0 deletions
diff --git a/Hutch/Views/Search/ScopedSearchHistoryStore.swift b/Hutch/Views/Search/ScopedSearchHistoryStore.swift
new file mode 100644
index 0000000..e009376
--- /dev/null
+++ b/Hutch/Views/Search/ScopedSearchHistoryStore.swift
@@ -0,0 +1,65 @@
+import Foundation
+
+struct ScopedSearchHistoryEntry: Codable, Hashable, Identifiable, Sendable {
+ let scopeID: String
+ let query: String
+ let createdAt: Date
+
+ var id: String {
+ "\(scopeID):\(query.lowercased())"
+ }
+}
+
+enum ScopedSearchHistoryStore {
+ private static let maximumEntriesPerScope = 8
+
+ static func load(scopeID: String, defaults: UserDefaults = .standard) -> [ScopedSearchHistoryEntry] {
+ loadAll(defaults: defaults)[scopeID] ?? []
+ }
+
+ static func record(
+ query: String,
+ scopeID: String,
+ defaults: UserDefaults = .standard,
+ now: Date = .now
+ ) {
+ let normalizedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !normalizedQuery.isEmpty else { return }
+
+ var allEntries = loadAll(defaults: defaults)
+ var scopeEntries = allEntries[scopeID] ?? []
+ scopeEntries.removeAll {
+ $0.query.compare(normalizedQuery, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame
+ }
+ scopeEntries.insert(
+ ScopedSearchHistoryEntry(scopeID: scopeID, query: normalizedQuery, createdAt: now),
+ at: 0
+ )
+ allEntries[scopeID] = Array(scopeEntries.prefix(maximumEntriesPerScope))
+ save(allEntries, defaults: defaults)
+ }
+
+ static func clear(scopeID: String, defaults: UserDefaults = .standard) {
+ var allEntries = loadAll(defaults: defaults)
+ allEntries.removeValue(forKey: scopeID)
+ save(allEntries, defaults: defaults)
+ }
+
+ private static func loadAll(defaults: UserDefaults) -> [String: [ScopedSearchHistoryEntry]] {
+ guard let data = defaults.data(forKey: AppStorageKeys.scopedSearchHistory) else {
+ return [:]
+ }
+
+ do {
+ return try JSONDecoder().decode([String: [ScopedSearchHistoryEntry]].self, from: data)
+ } catch {
+ defaults.removeObject(forKey: AppStorageKeys.scopedSearchHistory)
+ return [:]
+ }
+ }
+
+ private static func save(_ entries: [String: [ScopedSearchHistoryEntry]], defaults: UserDefaults) {
+ guard let data = try? JSONEncoder().encode(entries) else { return }
+ defaults.set(data, forKey: AppStorageKeys.scopedSearchHistory)
+ }
+}