gitbay/Repos/RepoActionsViewModel.swift
110 lines · 3303 bytes
8 symbols in this file
1import Foundation
2import Observation
3
4/// Fork, watch, mute and bookmark — repository-level actions performed
5/// against a `RepoDetail` already loaded elsewhere:
6///
7/// - Watch/mute/bookmark state now comes back on `repo show`
8/// (`RepoDetail.watch`/`.bookmarked`), so this view model only sends
9/// the write commands; the caller rereads `repo show` to see the result.
10/// - `fork_of` is also on `repo fork`'s own response, which is why `fork`
11/// hands its result back to the caller directly.
12@Observable
13@MainActor
14final class RepoActionsViewModel {
15
16 /// `repo fork`'s result — the only place `fork_of` is reported.
17 nonisolated struct ForkResult: Decodable, Sendable, Hashable {
18 let path: String
19 let forkOf: String
20
21 enum CodingKeys: String, CodingKey {
22 case path
23 case forkOf = "fork_of"
24 }
25 }
26
27 private(set) var actionError: String?
28 private(set) var notice: String?
29 private(set) var working = false
30
31 private let client: GitbayClient
32 let repoPath: String
33
34 init(client: GitbayClient, repoPath: String) {
35 self.client = client
36 self.repoPath = repoPath
37 }
38
39 // MARK: - Bookmarks
40
41 func setBookmarked(_ bookmarked: Bool) async {
42 await perform(["repo", bookmarked ? "bookmark" : "unbookmark", repoPath])
43 }
44
45 // MARK: - Watch / mute
46
47 func watch() async {
48 await perform(["repo", "watch", repoPath])
49 if actionError == nil {
50 notice = "You are now watching this repository."
51 }
52 }
53
54 func mute() async {
55 await perform(["repo", "mute", repoPath])
56 if actionError == nil {
57 notice = "Notifications for this repository are muted."
58 }
59 }
60
61 func unwatch() async {
62 await perform(["repo", "unwatch", repoPath])
63 if actionError == nil {
64 notice = "Notifications for this repository are back to the default."
65 }
66 }
67
68 // MARK: - Fork
69
70 func fork(named name: String?) async -> ForkResult? {
71 var argv = ["repo", "fork", repoPath]
72 if let name {
73 let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
74 if !trimmed.isEmpty {
75 argv += ["--name", trimmed]
76 }
77 }
78 working = true
79 actionError = nil
80 notice = nil
81 defer { working = false }
82 do {
83 guard let result = try await client.run(argv, as: ForkResult.self) else {
84 actionError = "The fork succeeded, but the server reported nothing back."
85 return nil
86 }
87 return result
88 } catch let error as GitbayError {
89 actionError = error.userFacingMessage
90 return nil
91 } catch {
92 actionError = GitbayError.transport(error).userFacingMessage
93 return nil
94 }
95 }
96
97 private func perform(_ argv: [String]) async {
98 working = true
99 actionError = nil
100 notice = nil
101 defer { working = false }
102 do {
103 try await client.run(argv)
104 } catch let error as GitbayError {
105 actionError = error.userFacingMessage
106 } catch {
107 actionError = GitbayError.transport(error).userFacingMessage
108 }
109 }
110}