import Foundation import Observation /// The repo screen: header from `repo show`, README found in the root /// tree and fetched with `repo cat`. @Observable @MainActor final class RepoDetailViewModel { private(set) var state: LoadState = .loading /// README markdown, when the root tree has one. Absent is normal. private(set) var readme: String? private(set) var readmeName: String? /// Whether this repo is on the account's dashboard. nil until known — /// pin state only exists in the dashboard aggregate. private(set) var isPinned: Bool? private(set) var actionError: String? private(set) var working = false private let client: GitbayClient private let path: String init(client: GitbayClient, path: String) { self.client = client self.path = path } func load() async { do { let detail = try await client.read(["repo", "show", path], as: RepoDetail.self) state = .loaded(detail) } catch { state = .from(error) return } await loadPinned() await loadReadme() } private func loadPinned() async { guard let dashboard = try? await client.read(["dashboard"], as: DashboardData.self) else { return } isPinned = dashboard.pinned.contains { $0.path == path } } // MARK: - Management actions func setPinned(_ pinned: Bool) async { await perform(["repo", pinned ? "pin" : "unpin", path]) await loadPinned() } func setArchived(_ archived: Bool) async { await perform(["repo", archived ? "archive" : "unarchive", path]) } private func perform(_ argv: [String]) async { working = true actionError = nil defer { working = false } do { try await client.run(argv) if let detail = try? await client.read(["repo", "show", path], as: RepoDetail.self) { state = .loaded(detail) } } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } private func loadReadme() async { // The README is whatever the root tree calls one, matched the way // the web UI matches: README, README.md, readme.org, and so on. guard let listing = try? await client.read( ["repo", "tree", path], as: TreeListing.self ) else { return } let candidate = listing.entries.first { !$0.isDirectory && $0.name.lowercased().hasPrefix("readme") } guard let candidate else { return } guard let file = try? await client.read( ["repo", "cat", path, candidate.name], as: FileContent.self ), !file.binary, let content = file.content else { return } readmeName = candidate.name readme = content } }