gitbay/Views/Releases/ReleaseView.swift

v1.0.6
gitbay-ios/gitbay/Views/Releases/ReleaseView.swift history · blame · raw

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}