krz/hutch

an ios client for sourcehut

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

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