import Foundation import Observation /// The `repo list` screen, paginated. A typed query searches server-side /// (`repo search` covers name, description and topics across everything /// reachable — including repos not in your list), with the loaded pages /// filtered client-side while the search is in flight. @Observable @MainActor final class RepoListViewModel { let list: PagedListModel var searchText = "" { didSet { guard searchText != oldValue else { return } scheduleSearch() } } /// Server results for the current query; nil while none has landed. private(set) var searchResults: [RepoSummary]? private let client: GitbayClient private var searchTask: Task? init(client: GitbayClient) { self.client = client list = PagedListModel( client: client, argv: ["repo", "list"], emptyMessage: "No repositories yet. Create one over SSH: gitbay repo create ", pageSize: 100 ) } var state: LoadState<[RepoSummary]> { list.state } var visibleRepos: [RepoSummary] { let query = searchText.trimmingCharacters(in: .whitespaces).lowercased() guard !query.isEmpty else { return list.state.value ?? [] } if let searchResults { return searchResults } // The server search has not answered yet; filter what is loaded. return (list.state.value ?? []).filter { $0.path.lowercased().contains(query) || ($0.description ?? "").lowercased().contains(query) } } func load() async { await list.reload() } private func scheduleSearch() { searchTask?.cancel() searchResults = nil let query = searchText.trimmingCharacters(in: .whitespaces) guard !query.isEmpty else { return } searchTask = Task { [client] in // Debounce a person typing; a cancelled sleep throws. guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return } let results = try? await client.readList( ["repo", "search", query], of: RepoSummary.self ) guard !Task.isCancelled, let results else { return } self.searchResults = results } } }