gitbay/Repos/RepoDetailViewModel.swift
97 lines · 3256 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 /// Re-reads `repo show` only, leaving the last good state in place on
48 /// failure. For refreshing after an action performed elsewhere
49 /// (bookmark, watch, mute) without paying for a full screen load.
50 func reloadDetail() async {
51 if let detail = try? await client.read(["repo", "show", path], as: RepoDetail.self) {
52 state = .loaded(detail)
53 }
54 }
55
56 // MARK: - Management actions
57
58 func setPinned(_ pinned: Bool) async {
59 await perform(["repo", pinned ? "pin" : "unpin", path])
60 await loadPinned()
61 }
62
63 func setArchived(_ archived: Bool) async {
64 await perform(["repo", archived ? "archive" : "unarchive", path])
65 }
66
67 private func perform(_ argv: [String]) async {
68 working = true
69 actionError = nil
70 defer { working = false }
71 do {
72 try await client.run(argv)
73 await reloadDetail()
74 } catch let error as GitbayError {
75 actionError = error.userFacingMessage
76 } catch {
77 actionError = GitbayError.transport(error).userFacingMessage
78 }
79 }
80
81 private func loadReadme() async {
82 // The README is whatever the root tree calls one, matched the way
83 // the web UI matches: README, README.md, readme.org, and so on.
84 guard let listing = try? await client.read(
85 ["repo", "tree", path], as: TreeListing.self
86 ) else { return }
87 let candidate = listing.entries.first {
88 !$0.isDirectory && $0.name.lowercased().hasPrefix("readme")
89 }
90 guard let candidate else { return }
91 guard let file = try? await client.read(
92 ["repo", "cat", path, candidate.name], as: FileContent.self
93 ), !file.binary, let content = file.content else { return }
94 readmeName = candidate.name
95 readme = content
96 }
97}