gitbay/Views/Releases/ReleaseView.swift
111 lines · 4475 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
10 init(client: GitbayClient, repo: String, tag: String) {
11 _model = State(initialValue: ReleaseDetailViewModel(
12 client: client, repoPath: repo, tag: tag
13 ))
14 }
15
16 var body: some View {
17 List {
18 if let release = model.state.value {
19 if let error = model.actionError {
20 Section {
21 GBNotice(error, .gbWarn)
22 }
23 }
24 Section {
25 VStack(alignment: .leading, spacing: 6) {
26 Text(release.title.isEmpty ? release.tag : release.title)
27 .font(.gbSans(.headline))
28 HStack(spacing: 6) {
29 Text(release.tag)
30 .font(.gbMono(.caption))
31 if let author = release.author {
32 Text("by \(author)")
33 }
34 Text(release.createdAt, format: .relative(presentation: .named))
35 .foregroundStyle(.tertiary)
36 }
37 .font(.gbSans(.caption))
38 .foregroundStyle(.secondary)
39 }
40 .padding(.vertical, 2)
41 }
42
43 if let notes = release.notes, !notes.isEmpty {
44 Section {
45 MarkdownView(markdown: notes)
46 .padding(.vertical, 4)
47 }
48 }
49
50 if let assets = release.assets, !assets.isEmpty {
51 Section("Assets") {
52 ForEach(assets) { asset in
53 Link(destination: model.downloadURL(for: asset)) {
54 HStack {
55 VStack(alignment: .leading, spacing: 2) {
56 Text(asset.name)
57 .font(.gbMono(.caption))
58 .foregroundStyle(.primary)
59 .lineLimit(1)
60 Text(String(asset.sha256.prefix(16)))
61 .font(.gbMono(.caption2))
62 .foregroundStyle(.tertiary)
63 }
64 Spacer()
65 Text(asset.size.formatted(.byteCount(style: .file)))
66 .font(.gbSans(.caption))
67 .foregroundStyle(.secondary)
68 Image(systemName: "arrow.down.circle")
69 .foregroundStyle(.secondary)
70 }
71 }
72 }
73 }
74 }
75 }
76 }
77 .overlay { LoadStateOverlay(state: model.state) }
78 .navigationTitle(model.tag)
79 .navigationBarTitleDisplayMode(.inline)
80 .toolbar {
81 ToolbarItem(placement: .topBarTrailing) {
82 if let release = model.state.value {
83 Button("Edit") {
84 draftTitle = release.title
85 draftNotes = release.notes ?? ""
86 editing = true
87 }
88 .disabled(model.working)
89 .accessibilityIdentifier("release-edit-button")
90 }
91 }
92 }
93 .sheet(isPresented: $editing) {
94 ComposeSheet(
95 heading: "Edit \(model.tag)",
96 submitLabel: "Save",
97 working: model.working,
98 errorMessage: model.actionError,
99 title: $draftTitle,
100 bodyText: $draftNotes
101 ) {
102 Task {
103 await model.edit(title: draftTitle, notes: draftNotes)
104 if model.actionError == nil { editing = false }
105 }
106 }
107 }
108 .task { await model.load() }
109 .refreshable { await model.load() }
110 }
111}