diff options
| author | Christian Cleberg <[email protected]> | 2026-04-02 00:27:34 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-02 00:27:34 -0500 |
| commit | f820109a956c627e510880a7cac88cb35032203b (patch) | |
| tree | 942bfad93efbb8a2a98e98b0a9590e2631613b3a /Hutch | |
| parent | 7f79cadf99ecf014834855e2e4a5c7d60dd709ac (diff) | |
| download | hutch-f820109a956c627e510880a7cac88cb35032203b.tar.gz hutch-f820109a956c627e510880a7cac88cb35032203b.tar.bz2 hutch-f820109a956c627e510880a7cac88cb35032203b.zip | |
Implement search history for Look Up page
Diffstat (limited to 'Hutch')
| -rw-r--r-- | Hutch/App/AppStorageKeys.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Lookup/LookupHistoryStore.swift | 60 | ||||
| -rw-r--r-- | Hutch/Views/Lookup/LookupView.swift | 57 |
3 files changed, 116 insertions, 2 deletions
diff --git a/Hutch/App/AppStorageKeys.swift b/Hutch/App/AppStorageKeys.swift index 87ec420..6b8c804 100644 --- a/Hutch/App/AppStorageKeys.swift +++ b/Hutch/App/AppStorageKeys.swift @@ -3,4 +3,5 @@ enum AppStorageKeys { static let swipeActionsEnabled = "swipeActionsEnabled" static let activeAccountID = "activeAccountID" static let wrapRepositoryFileLines = "wrapRepositoryFileLines" + static let lookupHistory = "lookupHistory" } diff --git a/Hutch/Views/Lookup/LookupHistoryStore.swift b/Hutch/Views/Lookup/LookupHistoryStore.swift new file mode 100644 index 0000000..e127970 --- /dev/null +++ b/Hutch/Views/Lookup/LookupHistoryStore.swift @@ -0,0 +1,60 @@ +import Foundation + +struct LookupHistoryEntry: Codable, Hashable, Identifiable, Sendable { + let type: LookupType + let query: String + let createdAt: Date + + var id: String { + "\(type.rawValue):\(query)" + } +} + +enum LookupHistoryStore { + private static let maximumEntries = 20 + + static func load(defaults: UserDefaults = .standard) -> [LookupHistoryEntry] { + guard let data = defaults.data(forKey: AppStorageKeys.lookupHistory) else { + return [] + } + + do { + return try JSONDecoder().decode([LookupHistoryEntry].self, from: data) + } catch { + defaults.removeObject(forKey: AppStorageKeys.lookupHistory) + return [] + } + } + + static func record( + type: LookupType, + query: String, + defaults: UserDefaults = .standard, + now: Date = .now + ) { + let normalizedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) + guard !normalizedQuery.isEmpty else { return } + + var entries = load(defaults: defaults) + entries.removeAll { $0.type == type && $0.query == normalizedQuery } + entries.insert( + LookupHistoryEntry(type: type, query: normalizedQuery, createdAt: now), + at: 0 + ) + + if entries.count > maximumEntries { + entries = Array(entries.prefix(maximumEntries)) + } + + save(entries, defaults: defaults) + } + + static func clear(defaults: UserDefaults = .standard) { + defaults.removeObject(forKey: AppStorageKeys.lookupHistory) + } + + private static func save(_ entries: [LookupHistoryEntry], defaults: UserDefaults) { + guard let data = try? JSONEncoder().encode(entries) else { return } + defaults.set(data, forKey: AppStorageKeys.lookupHistory) + } +} diff --git a/Hutch/Views/Lookup/LookupView.swift b/Hutch/Views/Lookup/LookupView.swift index 413ea12..bee2453 100644 --- a/Hutch/Views/Lookup/LookupView.swift +++ b/Hutch/Views/Lookup/LookupView.swift @@ -3,7 +3,7 @@ import os private let lookupLogger = Logger(subsystem: "net.cleberg.Hutch", category: "Lookup") -enum LookupType: String, CaseIterable, Identifiable { +enum LookupType: String, CaseIterable, Identifiable, Codable, Sendable { case user = "User" case gitRepo = "Git Repo" case hgRepo = "Hg Repo" @@ -74,10 +74,12 @@ final class LookupViewModel { var inputText: String = "" private(set) var result: LookupResult? private(set) var isLooking = false + private(set) var history: [LookupHistoryEntry] var error: String? private let client: SRHTClient private let appState: AppState + private let defaults: UserDefaults var resultBinding: Binding<LookupResult?> { Binding( @@ -90,9 +92,11 @@ final class LookupViewModel { ) } - init(client: SRHTClient, appState: AppState) { + init(client: SRHTClient, appState: AppState, defaults: UserDefaults = .standard) { self.client = client self.appState = appState + self.defaults = defaults + self.history = LookupHistoryStore.load(defaults: defaults) } func lookup() async { @@ -123,6 +127,17 @@ final class LookupViewModel { } } + func rerun(_ entry: LookupHistoryEntry) async { + selectedType = entry.type + inputText = entry.query + await lookup() + } + + func clearHistory() { + LookupHistoryStore.clear(defaults: defaults) + history = [] + } + private func parseOwnerAndName() -> (owner: String, name: String)? { let trimmed = inputText.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = trimmed.hasPrefix("~") ? String(trimmed.dropFirst()) : trimmed @@ -161,6 +176,7 @@ final class LookupViewModel { private func lookupUser() async throws -> LookupResult { guard let username = parseUsername() else { throw LookupError.invalidInput } + recordHistory(type: .user, query: "~\(username)") struct Response: Decodable, Sendable { let user: User @@ -216,6 +232,8 @@ final class LookupViewModel { private func lookupRepository(service: SRHTService) async throws -> LookupResult { guard let (owner, name) = parseOwnerAndName() else { throw LookupError.invalidInput } + let type: LookupType = service == .git ? .gitRepo : .hgRepo + recordHistory(type: type, query: "~\(owner)/\(name)") let repository = try await appState.resolveRepository(owner: owner, name: name, service: service) let resolvedRepository = RepositorySummary( @@ -235,6 +253,7 @@ final class LookupViewModel { private func lookupMailingList() async throws -> LookupResult { guard let (owner, name) = parseOwnerAndName() else { throw LookupError.invalidInput } + recordHistory(type: .mailingList, query: "~\(owner)/\(name)") struct Response: Decodable, Sendable { let user: UserWithList @@ -266,12 +285,14 @@ final class LookupViewModel { private func lookupTracker() async throws -> LookupResult { guard let (owner, name) = parseOwnerAndName() else { throw LookupError.invalidInput } + recordHistory(type: .tracker, query: "~\(owner)/\(name)") let tracker = try await appState.resolveTracker(owner: owner, name: name) return .tracker(tracker) } private func lookupBuildJob() async throws -> LookupResult { guard let jobId = parseBuildJobId() else { throw LookupError.invalidInput } + recordHistory(type: .buildJob, query: String(jobId)) struct Response: Decodable, Sendable { let job: JobIdOnly @@ -300,6 +321,11 @@ final class LookupViewModel { private enum LookupError: Error { case invalidInput } + + private func recordHistory(type: LookupType, query: String) { + LookupHistoryStore.record(type: type, query: query, defaults: defaults) + history = LookupHistoryStore.load(defaults: defaults) + } } struct LookupView: View { @@ -361,6 +387,33 @@ struct LookupView: View { } } } + + if !vm.history.isEmpty { + Section("Recent Searches") { + ForEach(vm.history) { entry in + Button { + Task { await vm.rerun(entry) } + } label: { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(entry.query) + .foregroundStyle(.primary) + Text(entry.type.rawValue) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + } + } + .disabled(vm.isLooking) + } + + Button("Clear History", role: .destructive) { + vm.clearHistory() + } + .disabled(vm.isLooking) + } + } } .formStyle(.grouped) .srhtErrorBanner(error: $vm.error) |
