krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v2: Hutch/Views/Repositories/HgRepositorySettingsView.swift · raw

  1import SwiftUI
  2
  3struct HgRepositorySettingsView: View {
  4    let repository: RepositorySummary
  5    let client: SRHTClient
  6    let onDeleted: () -> Void
  7
  8    @Environment(\.dismiss) private var dismiss
  9    @State private var viewModel: HgRepositorySettingsViewModel?
 10    @State private var showDeleteConfirmation = false
 11    @State private var pendingACLDeletion: HgACLEntry?
 12
 13    var body: some View {
 14        NavigationStack {
 15            Group {
 16                if let viewModel {
 17                    settingsForm(viewModel)
 18                } else {
 19                    SRHTLoadingStateView(message: "Loading settings…")
 20                }
 21            }
 22            .navigationTitle("Settings")
 23            .navigationBarTitleDisplayMode(.inline)
 24            .toolbar {
 25                ToolbarItem(placement: .cancellationAction) {
 26                    Button("Done") { dismiss() }
 27                }
 28            }
 29        }
 30        .task {
 31            if viewModel == nil {
 32                let vm = HgRepositorySettingsViewModel(repository: repository, client: client)
 33                viewModel = vm
 34                async let info: () = vm.loadRepositoryInfo()
 35                async let acls: () = vm.loadACLs()
 36                _ = await (info, acls)
 37            }
 38        }
 39    }
 40
 41    @ViewBuilder
 42    private func settingsForm(_ viewModel: HgRepositorySettingsViewModel) -> some View {
 43        @Bindable var vm = viewModel
 44
 45        Form {
 46            infoSection(viewModel)
 47            accessSection(viewModel)
 48            featuresSection(viewModel)
 49            histeditSection(viewModel)
 50            deleteSection(viewModel)
 51        }
 52        .srhtErrorBanner(error: $vm.error)
 53        .alert(
 54            "Permanently delete \(repository.owner.canonicalName)/\(repository.name)?",
 55            isPresented: $showDeleteConfirmation
 56        ) {
 57            Button("Cancel", role: .cancel) {}
 58            Button("Delete", role: .destructive) {
 59                Task {
 60                    await viewModel.deleteRepository()
 61                    if viewModel.didDelete {
 62                        dismiss()
 63                        onDeleted()
 64                    }
 65                }
 66            }
 67        } message: {
 68            Text("This cannot be undone.")
 69        }
 70        .alert("Remove Access?", isPresented: Binding(
 71            get: { pendingACLDeletion != nil },
 72            set: { isPresented in
 73                if !isPresented {
 74                    pendingACLDeletion = nil
 75                }
 76            }
 77        )) {
 78            Button("Cancel", role: .cancel) {}
 79            Button("Remove Access", role: .destructive) {
 80                guard let entry = pendingACLDeletion else { return }
 81                Task {
 82                    await viewModel.deleteACL(entry)
 83                    pendingACLDeletion = nil
 84                }
 85            }
 86        } message: {
 87            if let entry = pendingACLDeletion {
 88                Text("\(entry.entity.canonicalName) will lose \(entry.mode) access to this repository.")
 89            }
 90        }
 91    }
 92
 93    @ViewBuilder
 94    private func infoSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
 95        Section("Info") {
 96            LabeledContent("Name") {
 97                Text(repository.name)
 98                    .font(.body.monospaced())
 99            }
100
101            TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical)
102                .lineLimit(3...6)
103
104            Picker("Visibility", selection: Bindable(viewModel).editedVisibility) {
105                Text("Public").tag(Visibility.public)
106                Text("Unlisted").tag(Visibility.unlisted)
107                Text("Private").tag(Visibility.private)
108            }
109
110            Button {
111                Task { await viewModel.saveInfo() }
112            } label: {
113                if viewModel.isSavingInfo {
114                    ProgressView()
115                        .frame(maxWidth: .infinity)
116                } else {
117                    Text("Save Changes")
118                        .frame(maxWidth: .infinity)
119                }
120            }
121            .disabled(viewModel.isSavingInfo)
122        }
123    }
124
125    @ViewBuilder
126    private func accessSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
127        Section("Access") {
128            if viewModel.isLoadingACLs {
129                HStack {
130                    Spacer()
131                    ProgressView()
132                    Spacer()
133                }
134            } else if viewModel.acls.isEmpty {
135                Text("No access entries yet.")
136                    .foregroundStyle(.secondary)
137            } else {
138                ForEach(viewModel.acls) { entry in
139                    HStack {
140                        Text(entry.entity.canonicalName)
141                        Spacer()
142                        Text(entry.mode)
143                            .font(.caption.monospaced())
144                            .foregroundStyle(.secondary)
145                    }
146                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
147                        Button(role: .destructive) {
148                            pendingACLDeletion = entry
149                        } label: {
150                            Label("Remove Access", systemImage: "trash")
151                        }
152                    }
153                }
154            }
155
156            HStack {
157                TextField("Username or ~username", text: Bindable(viewModel).newACLEntity)
158                    .autocorrectionDisabled()
159                    .textInputAutocapitalization(.never)
160
161                Picker("", selection: Bindable(viewModel).newACLMode) {
162                    Text("RO").tag("RO")
163                    Text("RW").tag("RW")
164                }
165                .pickerStyle(.segmented)
166                .frame(width: 100)
167
168                Button {
169                    Task { await viewModel.addACL() }
170                } label: {
171                    if viewModel.isAddingACL {
172                        ProgressView()
173                    } else {
174                        Text("Add")
175                    }
176                }
177                .disabled(viewModel.isAddingACL || viewModel.newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
178            }
179            Text("Add a SourceHut user and choose read-only or read/write access.")
180                .font(.caption)
181                .foregroundStyle(.secondary)
182        }
183    }
184
185    @ViewBuilder
186    private func featuresSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
187        Section("Features") {
188            Toggle("Hide this repository from public listings", isOn: Bindable(viewModel).editedNonPublishing)
189
190            Button {
191                Task { await viewModel.saveInfo() }
192            } label: {
193                if viewModel.isSavingInfo {
194                    ProgressView()
195                        .frame(maxWidth: .infinity)
196                } else {
197                    Text("Save Changes")
198                        .frame(maxWidth: .infinity)
199                }
200            }
201            .disabled(viewModel.isSavingInfo)
202        }
203    }
204
205    @ViewBuilder
206    private func histeditSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
207        Section("Histedit") {
208            TextField("Revision hash", text: Bindable(viewModel).histeditRevision)
209                .autocorrectionDisabled()
210                .textInputAutocapitalization(.never)
211                .disabled(true)
212
213            Text("Removing revisions is not available through the public hg.sr.ht API, so Hutch can’t do this yet.")
214                .font(.caption)
215                .foregroundStyle(.secondary)
216
217            Button("Remove Revision", role: .destructive) {}
218                .disabled(true)
219        }
220    }
221
222    @ViewBuilder
223    private func deleteSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
224        Section {
225            Button(role: .destructive) {
226                showDeleteConfirmation = true
227            } label: {
228                if viewModel.isDeleting {
229                    ProgressView()
230                        .frame(maxWidth: .infinity)
231                } else {
232                    Text("Delete Repository")
233                        .frame(maxWidth: .infinity)
234                }
235            }
236            .disabled(viewModel.isDeleting)
237        }
238    }
239}