gitbay/Views/Releases/ReleaseView.swift
113 lines · 4578 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 Label(error, systemImage: "hand.raised")
22 .foregroundStyle(.orange)
23 .font(.subheadline)
24 }
25 }
26 Section {
27 VStack(alignment: .leading, spacing: 6) {
28 Text(release.title.isEmpty ? release.tag : release.title)
29 .font(.headline)
30 HStack(spacing: 6) {
31 Text(release.tag)
32 .font(.caption.monospaced())
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(.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(.caption.monospaced())
60 .foregroundStyle(.primary)
61 .lineLimit(1)
62 Text(String(asset.sha256.prefix(16)))
63 .font(.caption2.monospaced())
64 .foregroundStyle(.tertiary)
65 }
66 Spacer()
67 Text(asset.size.formatted(.byteCount(style: .file)))
68 .font(.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 }
95 .sheet(isPresented: $editing) {
96 ComposeSheet(
97 heading: "Edit \(model.tag)",
98 submitLabel: "Save",
99 working: model.working,
100 errorMessage: model.actionError,
101 title: $draftTitle,
102 bodyText: $draftNotes
103 ) {
104 Task {
105 await model.edit(title: draftTitle, notes: draftNotes)
106 if model.actionError == nil { editing = false }
107 }
108 }
109 }
110 .task { await model.load() }
111 .refreshable { await model.load() }
112 }
113}