gitbay/Releases/ReleaseViewModels.swift
112 lines · 3412 bytes
1import Foundation
2import Observation
3
4/// `release list <owner/name>`.
5@Observable
6@MainActor
7final class ReleaseListViewModel {
8
9 private(set) var state: LoadState<[Release]> = .loading
10 private(set) var createError: String?
11 private(set) var working = false
12
13 private let client: GitbayClient
14 let repoPath: String
15
16 init(client: GitbayClient, repoPath: String) {
17 self.client = client
18 self.repoPath = repoPath
19 }
20
21 func load() async {
22 do {
23 let releases = try await client.readList(["release", "list", repoPath], of: Release.self)
24 state = releases.isEmpty
25 ? .empty("No releases. Tag a commit, push the tag, then release it.")
26 : .loaded(releases)
27 } catch {
28 state = .from(error)
29 }
30 }
31
32 /// `release create <owner/name> <tag> [--title <t>] [--file -]` —
33 /// the tag must already exist; the server's refusal says so.
34 func create(tag: String, title: String, notes: String) async -> Bool {
35 working = true
36 createError = nil
37 defer { working = false }
38 do {
39 var argv = ["release", "create", repoPath, tag]
40 if !title.isEmpty { argv.append(contentsOf: ["--title", title]) }
41 var stdin: String?
42 if !notes.isEmpty {
43 argv.append(contentsOf: ["--file", "-"])
44 stdin = notes
45 }
46 try await client.run(argv, stdin: stdin)
47 await load()
48 return true
49 } catch let error as GitbayError {
50 createError = error.userFacingMessage
51 } catch {
52 createError = GitbayError.transport(error).userFacingMessage
53 }
54 return false
55 }
56}
57
58/// One release: `release show`, plus title/notes editing.
59@Observable
60@MainActor
61final class ReleaseDetailViewModel {
62
63 private(set) var state: LoadState<Release> = .loading
64 private(set) var actionError: String?
65 private(set) var working = false
66
67 private let client: GitbayClient
68 let repoPath: String
69 let tag: String
70
71 init(client: GitbayClient, repoPath: String, tag: String) {
72 self.client = client
73 self.repoPath = repoPath
74 self.tag = tag
75 }
76
77 func load() async {
78 do {
79 state = .loaded(try await client.read(
80 ["release", "show", repoPath, tag], as: Release.self))
81 } catch {
82 state = .from(error)
83 }
84 }
85
86 func edit(title: String, notes: String) async {
87 working = true
88 actionError = nil
89 defer { working = false }
90 do {
91 try await client.run(
92 ["release", "edit", repoPath, tag, "--title", title, "--file", "-"],
93 stdin: notes
94 )
95 await load()
96 } catch let error as GitbayError {
97 actionError = error.userFacingMessage
98 } catch {
99 actionError = GitbayError.transport(error).userFacingMessage
100 }
101 }
102
103 /// Where a browser downloads this asset — the web's own download
104 /// route; the JSON surface has no binary-safe asset read.
105 func downloadURL(for asset: Release.Asset) -> URL {
106 client.instance.baseURL
107 .appending(path: repoPath)
108 .appending(path: "releases/download")
109 .appending(path: tag)
110 .appending(path: asset.name)
111 }
112}