import Foundation import Observation /// `release list `. @Observable @MainActor final class ReleaseListViewModel { private(set) var state: LoadState<[Release]> = .loading private(set) var createError: String? private(set) var working = false private let client: GitbayClient let repoPath: String init(client: GitbayClient, repoPath: String) { self.client = client self.repoPath = repoPath } func load() async { do { let releases = try await client.readList(["release", "list", repoPath], of: Release.self) state = releases.isEmpty ? .empty("No releases. Tag a commit, push the tag, then release it.") : .loaded(releases) } catch { state = .from(error) } } /// `release create [--title ] [--file -]` — /// the tag must already exist; the server's refusal says so. func create(tag: String, title: String, notes: String) async -> Bool { working = true createError = nil defer { working = false } do { var argv = ["release", "create", repoPath, tag] if !title.isEmpty { argv.append(contentsOf: ["--title", title]) } var stdin: String? if !notes.isEmpty { argv.append(contentsOf: ["--file", "-"]) stdin = notes } try await client.run(argv, stdin: stdin) await load() return true } catch let error as GitbayError { createError = error.userFacingMessage } catch { createError = GitbayError.transport(error).userFacingMessage } return false } } /// One release: `release show`, plus title/notes editing. @Observable @MainActor final class ReleaseDetailViewModel { private(set) var state: LoadState = .loading private(set) var actionError: String? private(set) var working = false private let client: GitbayClient let repoPath: String let tag: String init(client: GitbayClient, repoPath: String, tag: String) { self.client = client self.repoPath = repoPath self.tag = tag } func load() async { do { state = .loaded(try await client.read( ["release", "show", repoPath, tag], as: Release.self)) } catch { state = .from(error) } } func edit(title: String, notes: String) async { working = true actionError = nil defer { working = false } do { try await client.run( ["release", "edit", repoPath, tag, "--title", title, "--file", "-"], stdin: notes ) await load() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } /// Where a browser downloads this asset — the web's own download /// route; the JSON surface has no binary-safe asset read. func downloadURL(for asset: Release.Asset) -> URL { client.instance.baseURL .appending(path: repoPath) .appending(path: "releases/download") .appending(path: tag) .appending(path: asset.name) } }