krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.0.4: Hutch/Views/Search/ScopedSearchHistoryStore.swift · raw

 1import Foundation
 2
 3struct ScopedSearchHistoryEntry: Codable, Hashable, Identifiable, Sendable {
 4    let scopeID: String
 5    let query: String
 6    let createdAt: Date
 7
 8    var id: String {
 9        "\(scopeID):\(query.lowercased())"
10    }
11}
12
13enum ScopedSearchHistoryStore {
14    private static let maximumEntriesPerScope = 8
15
16    static func load(scopeID: String, defaults: UserDefaults = .standard) -> [ScopedSearchHistoryEntry] {
17        loadAll(defaults: defaults)[scopeID] ?? []
18    }
19
20    static func record(
21        query: String,
22        scopeID: String,
23        defaults: UserDefaults = .standard,
24        now: Date = .now
25    ) {
26        let normalizedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
27        guard !normalizedQuery.isEmpty else { return }
28
29        var allEntries = loadAll(defaults: defaults)
30        var scopeEntries = allEntries[scopeID] ?? []
31        scopeEntries.removeAll {
32            $0.query.compare(normalizedQuery, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame
33        }
34        scopeEntries.insert(
35            ScopedSearchHistoryEntry(scopeID: scopeID, query: normalizedQuery, createdAt: now),
36            at: 0
37        )
38        allEntries[scopeID] = Array(scopeEntries.prefix(maximumEntriesPerScope))
39        save(allEntries, defaults: defaults)
40    }
41
42    static func clear(scopeID: String, defaults: UserDefaults = .standard) {
43        var allEntries = loadAll(defaults: defaults)
44        allEntries.removeValue(forKey: scopeID)
45        save(allEntries, defaults: defaults)
46    }
47
48    private static func loadAll(defaults: UserDefaults) -> [String: [ScopedSearchHistoryEntry]] {
49        guard let data = defaults.data(forKey: AppStorageKeys.scopedSearchHistory) else {
50            return [:]
51        }
52
53        do {
54            return try JSONDecoder().decode([String: [ScopedSearchHistoryEntry]].self, from: data)
55        } catch {
56            defaults.removeObject(forKey: AppStorageKeys.scopedSearchHistory)
57            return [:]
58        }
59    }
60
61    private static func save(_ entries: [String: [ScopedSearchHistoryEntry]], defaults: UserDefaults) {
62        guard let data = try? JSONEncoder().encode(entries) else { return }
63        defaults.set(data, forKey: AppStorageKeys.scopedSearchHistory)
64    }
65}