gitbay/Views/Releases/ReleaseView.swift
138 lines · 5461 bytes
1import SwiftUI
2
3struct ReleaseView: View {
4
5 @State private var model: ReleaseDetailViewModel
6 @State private var editing = false
7 @State private var draftTitle = ""
8 @State private var draftNotes = ""
9 @State private var confirmingDelete = false
10 @Environment(\.dismiss) private var dismiss
11
12 init(client: GitbayClient, repo: String, tag: String) {
13 _model = State(initialValue: ReleaseDetailViewModel(
14 client: client, repoPath: repo, tag: tag
15 ))
16 }
17
18 var body: some View {
19 List {
20 if let release = model.state.value {
21 if let error = model.actionError {
22 Section {
23 GBNotice(error, .gbWarn)
24 }
25 }
26 Section {
27 VStack(alignment: .leading, spacing: 6) {
28 Text(release.title.isEmpty ? release.tag : release.title)
29 .font(.gbSans(.headline))
30 HStack(spacing: 6) {
31 Text(release.tag)
32 .font(.gbMono(.caption))
33 if let author = release.author {
34 Text("by \(author)")
35 }
36 Text(release.createdAt, format: .relative(presentation: .named))
37 .foregroundStyle(.tertiary)
38 }
39 .font(.gbSans(.caption))
40 .foregroundStyle(.secondary)
41 }
42 .padding(.vertical, 2)
43 }
44
45 if let notes = release.notes, !notes.isEmpty {
46 Section {
47 MarkdownView(markdown: notes)
48 .padding(.vertical, 4)
49 }
50 }
51
52 if let assets = release.assets, !assets.isEmpty {
53 Section("Assets") {
54 ForEach(assets) { asset in
55 Link(destination: model.downloadURL(for: asset)) {
56 HStack {
57 VStack(alignment: .leading, spacing: 2) {
58 Text(asset.name)
59 .font(.gbMono(.caption))
60 .foregroundStyle(.primary)
61 .lineLimit(1)
62 Text(String(asset.sha256.prefix(16)))
63 .font(.gbMono(.caption2))
64 .foregroundStyle(.tertiary)
65 }
66 Spacer()
67 Text(asset.size.formatted(.byteCount(style: .file)))
68 .font(.gbSans(.caption))
69 .foregroundStyle(.secondary)
70 Image(systemName: "arrow.down.circle")
71 .foregroundStyle(.secondary)
72 }
73 }
74 }
75 }
76 }
77 }
78 }
79 .overlay { LoadStateOverlay(state: model.state) }
80 .navigationTitle(model.tag)
81 .navigationBarTitleDisplayMode(.inline)
82 .toolbar {
83 ToolbarItem(placement: .topBarTrailing) {
84 if let release = model.state.value {
85 Button("Edit") {
86 draftTitle = release.title
87 draftNotes = release.notes ?? ""
88 editing = true
89 }
90 .disabled(model.working)
91 .accessibilityIdentifier("release-edit-button")
92 }
93 }
94 ToolbarItem(placement: .topBarTrailing) {
95 if model.state.value != nil {
96 Button("Delete", role: .destructive) {
97 confirmingDelete = true
98 }
99 .disabled(model.working)
100 .accessibilityIdentifier("release-delete-button")
101 }
102 }
103 }
104 .confirmationDialog(
105 "Delete \(model.tag)?",
106 isPresented: $confirmingDelete,
107 titleVisibility: .visible
108 ) {
109 Button("Delete", role: .destructive) {
110 Task {
111 if await model.delete() {
112 dismiss()
113 }
114 }
115 }
116 Button("Cancel", role: .cancel) {}
117 } message: {
118 Text("This deletes the release's assets along with it.")
119 }
120 .sheet(isPresented: $editing) {
121 ComposeSheet(
122 heading: "Edit \(model.tag)",
123 submitLabel: "Save",
124 working: model.working,
125 errorMessage: model.actionError,
126 title: $draftTitle,
127 bodyText: $draftNotes
128 ) {
129 Task {
130 await model.edit(title: draftTitle, notes: draftNotes)
131 if model.actionError == nil { editing = false }
132 }
133 }
134 }
135 .task { await model.load() }
136 .refreshable { await model.load() }
137 }
138}