gitbay/Repos/RepoDetailViewModel.swift

v1.6.2
gitbay-ios/gitbay/Repos/RepoDetailViewModel.swift history · blame · raw

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}