a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Releases/ReleaseListView.swift

ui-smoke
gitbay-ios/gitbay/Views/Releases/ReleaseListView.swift history · blame · raw

140 lines · 5136 bytes

  1import SwiftUI
  2
  3struct ReleaseListView: View {
  4
  5    @State private var model: ReleaseListViewModel
  6    @State private var composing = false
  7    @State private var draftTag = ""
  8    @State private var draftTitle = ""
  9    @State private var draftNotes = ""
 10
 11    init(client: GitbayClient, repo: String) {
 12        _model = State(initialValue: ReleaseListViewModel(client: client, repoPath: repo))
 13    }
 14
 15    var body: some View {
 16        List {
 17            ForEach(model.state.value ?? []) { release in
 18                NavigationLink(value: ReleaseRoute.release(repo: model.repoPath, tag: release.tag)) {
 19                    VStack(alignment: .leading, spacing: 3) {
 20                        Text(release.title.isEmpty ? release.tag : release.title)
 21                            .font(.gbSans(.subheadline).weight(.medium))
 22                            .lineLimit(2)
 23                        HStack(spacing: 6) {
 24                            Text(release.tag)
 25                                .font(.gbMono(.caption))
 26                                .foregroundStyle(.secondary)
 27                            if let author = release.author {
 28                                Text("by \(author)")
 29                                    .font(.gbSans(.caption))
 30                                    .foregroundStyle(.secondary)
 31                            }
 32                            Spacer()
 33                            Text(release.createdAt, format: .relative(presentation: .named))
 34                                .font(.gbSans(.caption))
 35                                .foregroundStyle(.tertiary)
 36                        }
 37                    }
 38                    .padding(.vertical, 2)
 39                }
 40            }
 41        }
 42        .overlay { LoadStateOverlay(state: model.state) }
 43        .navigationTitle("Releases")
 44        .navigationBarTitleDisplayMode(.inline)
 45        .toolbar {
 46            ToolbarItem(placement: .topBarTrailing) {
 47                Button {
 48                    composing = true
 49                } label: {
 50                    Image(systemName: "plus")
 51                }
 52                .accessibilityIdentifier("release-create-button")
 53            }
 54        }
 55        .sheet(isPresented: $composing) {
 56            ReleaseCreateSheet(
 57                model: model,
 58                tag: $draftTag, title: $draftTitle, notes: $draftNotes
 59            ) {
 60                draftTag = ""
 61                draftTitle = ""
 62                draftNotes = ""
 63                composing = false
 64            }
 65        }
 66        .task { await model.load() }
 67        .refreshable { await model.load() }
 68    }
 69}
 70
 71/// The tag is typed and must already exist on the server  the refusal
 72/// ("push the tag first") is the explanation.
 73private struct ReleaseCreateSheet: View {
 74
 75    let model: ReleaseListViewModel
 76    @Binding var tag: String
 77    @Binding var title: String
 78    @Binding var notes: String
 79    let onCreated: () -> Void
 80
 81    @Environment(\.dismiss) private var dismiss
 82
 83    var body: some View {
 84        NavigationStack {
 85            Form {
 86                Section("Tag") {
 87                    TextField("v1.0.0", text: $tag)
 88                        .autocorrectionDisabled()
 89                        .textInputAutocapitalization(.never)
 90                        .accessibilityIdentifier("release-tag")
 91                }
 92                Section("Title") {
 93                    TextField("Title (optional)", text: $title)
 94                        .autocorrectionDisabled()
 95                }
 96                Section("Notes") {
 97                    TextEditor(text: $notes)
 98                        .frame(minHeight: 140)
 99                        .autocorrectionDisabled()
100                }
101                if let error = model.createError {
102                    Section {
103                        GBNotice(error)
104                    }
105                }
106            }
107            .navigationTitle("New Release")
108            .navigationBarTitleDisplayMode(.inline)
109            .toolbar {
110                ToolbarItem(placement: .cancellationAction) {
111                    Button("Cancel") { dismiss() }
112                }
113                ToolbarItem(placement: .confirmationAction) {
114                    if model.working {
115                        ProgressView()
116                    } else {
117                        Button("Create") {
118                            Task {
119                                if await model.create(
120                                    tag: tag.trimmingCharacters(in: .whitespaces),
121                                    title: title, notes: notes
122                                ) {
123                                    onCreated()
124                                }
125                            }
126                        }
127                        .disabled(tag.trimmingCharacters(in: .whitespaces).isEmpty)
128                        .accessibilityIdentifier("release-submit")
129                    }
130                }
131            }
132            .interactiveDismissDisabled(model.working)
133        }
134    }
135}
136
137nonisolated enum ReleaseRoute: Hashable {
138    case list(repo: String)
139    case release(repo: String, tag: String)
140}