gitbay/Views/Releases/ReleaseView.swift

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

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