gitbay/Repos/RepoListViewModel.swift
69 lines · 2361 bytes
1import Foundation
2import Observation
3
4/// The `repo list` screen, paginated. A typed query searches server-side
5/// (`repo search` covers name, description and topics across everything
6/// reachable — including repos not in your list), with the loaded pages
7/// filtered client-side while the search is in flight.
8@Observable
9@MainActor
10final class RepoListViewModel {
11
12 let list: PagedListModel<RepoSummary>
13 var searchText = "" {
14 didSet {
15 guard searchText != oldValue else { return }
16 scheduleSearch()
17 }
18 }
19 /// Server results for the current query; nil while none has landed.
20 private(set) var searchResults: [RepoSummary]?
21
22 private let client: GitbayClient
23 private var searchTask: Task<Void, Never>?
24
25 init(client: GitbayClient) {
26 self.client = client
27 list = PagedListModel(
28 client: client,
29 argv: ["repo", "list"],
30 emptyMessage: "No repositories yet. Create one over SSH: gitbay repo create <name>",
31 pageSize: 100
32 )
33 }
34
35 var state: LoadState<[RepoSummary]> { list.state }
36
37 var visibleRepos: [RepoSummary] {
38 let query = searchText.trimmingCharacters(in: .whitespaces).lowercased()
39 guard !query.isEmpty else { return list.state.value ?? [] }
40 if let searchResults {
41 return searchResults
42 }
43 // The server search has not answered yet; filter what is loaded.
44 return (list.state.value ?? []).filter {
45 $0.path.lowercased().contains(query)
46 || ($0.description ?? "").lowercased().contains(query)
47 }
48 }
49
50 func load() async {
51 await list.reload()
52 }
53
54 private func scheduleSearch() {
55 searchTask?.cancel()
56 searchResults = nil
57 let query = searchText.trimmingCharacters(in: .whitespaces)
58 guard !query.isEmpty else { return }
59 searchTask = Task { [client] in
60 // Debounce a person typing; a cancelled sleep throws.
61 guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return }
62 let results = try? await client.readList(
63 ["repo", "search", query], of: RepoSummary.self
64 )
65 guard !Task.isCancelled, let results else { return }
66 self.searchResults = results
67 }
68 }
69}