gitbay/Repos/RepoDetailViewModel.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Repos/RepoDetailViewModel.swift history · blame · raw

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}