gitbay/Repos/RepoDetailViewModel.swift
90 lines · 2982 bytes
1import Foundation
2import Observation
3
4/// The repo screen: header from `repo show`, README found in the root
5/// tree and fetched with `repo cat`.
6@Observable
7@MainActor
8final class RepoDetailViewModel {
9
10 private(set) var state: LoadState<RepoDetail> = .loading
11 /// README markdown, when the root tree has one. Absent is normal.
12 private(set) var readme: String?
13 private(set) var readmeName: String?
14 /// Whether this repo is on the account's dashboard. nil until known —
15 /// pin state only exists in the dashboard aggregate.
16 private(set) var isPinned: Bool?
17 private(set) var actionError: String?
18 private(set) var working = false
19
20 private let client: GitbayClient
21 private let path: String
22
23 init(client: GitbayClient, path: String) {
24 self.client = client
25 self.path = path
26 }
27
28 func load() async {
29 do {
30 let detail = try await client.read(["repo", "show", path], as: RepoDetail.self)
31 state = .loaded(detail)
32 } catch {
33 state = .from(error)
34 return
35 }
36 await loadPinned()
37 await loadReadme()
38 }
39
40 private func loadPinned() async {
41 guard let dashboard = try? await client.read(["dashboard"], as: DashboardData.self) else {
42 return
43 }
44 isPinned = dashboard.pinned.contains { $0.path == path }
45 }
46
47 // MARK: - Management actions
48
49 func setPinned(_ pinned: Bool) async {
50 await perform(["repo", pinned ? "pin" : "unpin", path])
51 await loadPinned()
52 }
53
54 func setArchived(_ archived: Bool) async {
55 await perform(["repo", archived ? "archive" : "unarchive", path])
56 }
57
58 private func perform(_ argv: [String]) async {
59 working = true
60 actionError = nil
61 defer { working = false }
62 do {
63 try await client.run(argv)
64 if let detail = try? await client.read(["repo", "show", path], as: RepoDetail.self) {
65 state = .loaded(detail)
66 }
67 } catch let error as GitbayError {
68 actionError = error.userFacingMessage
69 } catch {
70 actionError = GitbayError.transport(error).userFacingMessage
71 }
72 }
73
74 private func loadReadme() async {
75 // The README is whatever the root tree calls one, matched the way
76 // the web UI matches: README, README.md, readme.org, and so on.
77 guard let listing = try? await client.read(
78 ["repo", "tree", path], as: TreeListing.self
79 ) else { return }
80 let candidate = listing.entries.first {
81 !$0.isDirectory && $0.name.lowercased().hasPrefix("readme")
82 }
83 guard let candidate else { return }
84 guard let file = try? await client.read(
85 ["repo", "cat", path, candidate.name], as: FileContent.self
86 ), !file.binary, let content = file.content else { return }
87 readmeName = candidate.name
88 readme = content
89 }
90}