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