summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/RepositoryListView.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/Repositories/RepositoryListView.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/Repositories/RepositoryListView.swift')
-rw-r--r--Hutch/Views/Repositories/RepositoryListView.swift53
1 files changed, 51 insertions, 2 deletions
diff --git a/Hutch/Views/Repositories/RepositoryListView.swift b/Hutch/Views/Repositories/RepositoryListView.swift
index d176e4b..f5ca716 100644
--- a/Hutch/Views/Repositories/RepositoryListView.swift
+++ b/Hutch/Views/Repositories/RepositoryListView.swift
@@ -4,6 +4,7 @@ struct RepositoryListView: View {
@Environment(AppState.self) private var appState
@State private var viewModel: RepositoryListViewModel?
@State private var searchTask: Task<Void, Never>?
+ @State private var immediateSearchTask: Task<Void, Never>?
@State private var showCreateRepositorySheet = false
@State private var createdRepository: RepositorySummary?
@@ -68,6 +69,17 @@ struct RepositoryListView: View {
@Bindable var vm = viewModel
List {
+ if viewModel.isSearching {
+ HStack(spacing: 12) {
+ ProgressView()
+ .controlSize(.small)
+ Text("Searching repositories…")
+ .font(.footnote)
+ .foregroundStyle(.secondary)
+ }
+ .listRowSeparator(.hidden)
+ }
+
ForEach(viewModel.repositories) { repo in
NavigationLink(value: repo) {
RepositoryRowView(
@@ -92,7 +104,28 @@ struct RepositoryListView: View {
}
.themedList()
.listStyle(.plain)
- .searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search repositories")
+ .searchable(
+ text: $vm.searchText,
+ placement: .navigationBarDrawer(displayMode: .always),
+ prompt: "Search repositories by name, owner, or description"
+ )
+ .searchSuggestions {
+ if viewModel.searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+ RecentSearchSuggestions(
+ title: "Recent Repository Searches",
+ entries: viewModel.recentSearches
+ ) { query in
+ searchTask?.cancel()
+ immediateSearchTask?.cancel()
+ vm.searchText = query
+ immediateSearchTask = Task {
+ await viewModel.loadRepositories(search: query)
+ }
+ } onClear: {
+ viewModel.clearRecentSearches()
+ }
+ }
+ }
.overlay {
if viewModel.isLoading, viewModel.repositories.isEmpty {
SRHTLoadingStateView(message: "Loading repositories…")
@@ -110,7 +143,11 @@ struct RepositoryListView: View {
description: Text("You don't have any repositories yet.")
)
} else {
- ContentUnavailableView.search
+ ContentUnavailableView(
+ "No Repository Matches",
+ systemImage: "magnifyingglass",
+ description: Text("No repositories matched “\(viewModel.searchText)”.")
+ )
}
}
}
@@ -124,9 +161,21 @@ struct RepositoryListView: View {
.task {
await viewModel.loadRepositories()
}
+ .onSubmit(of: .search) {
+ let query = viewModel.searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !query.isEmpty else { return }
+
+ searchTask?.cancel()
+ immediateSearchTask?.cancel()
+ viewModel.recordRecentSearch(query)
+ immediateSearchTask = Task {
+ await viewModel.loadRepositories(search: query)
+ }
+ }
.onChange(of: viewModel.searchText) { oldValue, newValue in
// Cancel previous search task
searchTask?.cancel()
+ immediateSearchTask?.cancel()
// Clear results immediately when search text is cleared
if newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {