From fe1ccc69661603d419a642a450e4ac9e1258adb0 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 13 Apr 2026 19:44:44 -0500 Subject: fix: centralize URLs, tighten models, and polish SwiftUI bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared HutchDeepLinkURL constants for app, widgets, and tests. - Bump marketing version to 3.1.2 and build to 70 (app + widget extension). - Point Hutch stats default base URL at HutchStatsAPI; add SRHTWebURL status incident feed and reuse it from SystemStatusService. - Group Project into Metadata and Resources; split ContributionStatsResponse into StatsWindow and StatsTotals with updated decoding and tests. - Replace @Bindable usage with explicit Bindings in Profile, Projects list, and repository ACL flows; simplify Home pinned-item helper; add no-op bodies on cancel alert buttons where the compiler requires a statement. - Move repository row build-status indicator next to the relative-updated caption and reserve a fixed 8×8 slot so the row does not jump when status loads. - Use NSString.lastPathComponent for build artifact filenames; collapse duplicate ACL error branches; minor HutchStats HTTP and XMLParserDelegate cleanups. - Point widgets at HutchDeepLinkURL helpers; align tests with the new response and URL types. --- Hutch/Views/More/AccountSwitcherView.swift | 4 ++- Hutch/Views/More/ProfileView.swift | 47 +++++++++++++++++++----------- 2 files changed, 33 insertions(+), 18 deletions(-) (limited to 'Hutch/Views/More') diff --git a/Hutch/Views/More/AccountSwitcherView.swift b/Hutch/Views/More/AccountSwitcherView.swift index ce69279..83871a8 100644 --- a/Hutch/Views/More/AccountSwitcherView.swift +++ b/Hutch/Views/More/AccountSwitcherView.swift @@ -98,7 +98,9 @@ struct AccountSwitcherView: View { } ) ) { - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { + /* Dismiss only; removal uses the destructive button. */ + } Button("Remove", role: .destructive) { guard let pendingRemoval else { return } Task { await appState.removeAccount(id: pendingRemoval.id) } diff --git a/Hutch/Views/More/ProfileView.swift b/Hutch/Views/More/ProfileView.swift index c53a89d..8f353ee 100644 --- a/Hutch/Views/More/ProfileView.swift +++ b/Hutch/Views/More/ProfileView.swift @@ -61,8 +61,6 @@ struct ProfileView: View { @ViewBuilder private func profileContent(_ viewModel: SettingsViewModel) -> some View { - @Bindable var vm = viewModel - Form { if let profile = viewModel.profile { profileSection(profile, viewModel: viewModel) @@ -100,7 +98,10 @@ struct ProfileView: View { ) } } - .sheet(isPresented: $vm.isEditingProfile) { + .sheet(isPresented: Binding( + get: { viewModel.isEditingProfile }, + set: { viewModel.isEditingProfile = $0 } + )) { if let profile = viewModel.profile { EditProfileSheet(profile: profile, viewModel: viewModel) } @@ -130,7 +131,9 @@ struct ProfileView: View { } ) ) { - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { + /* Dismiss only; destructive action is separate. */ + } Button(pendingDestructiveAction?.confirmationLabel ?? "Confirm", role: .destructive) { guard let action = pendingDestructiveAction else { return } pendingDestructiveAction = nil @@ -158,12 +161,11 @@ struct ProfileView: View { Section("Profile") { HStack(spacing: 12) { AsyncImage(url: profile.avatar.flatMap { URL(string: $0) }) { phase in - switch phase { - case .success(let image): + if case .success(let image) = phase { image .resizable() .scaledToFill() - default: + } else { Image(systemName: "person.crop.circle.fill") .resizable() .foregroundStyle(.secondary) @@ -238,8 +240,6 @@ struct ProfileView: View { @ViewBuilder private func sshKeysSection(_ viewModel: SettingsViewModel) -> some View { - @Bindable var vm = viewModel - Section { ForEach(viewModel.sshKeys) { key in VStack(alignment: .leading, spacing: 2) { @@ -275,7 +275,14 @@ struct ProfileView: View { .themedRow() if viewModel.isAddingSSHKey { - TextField("Paste SSH public key", text: $vm.newSSHKey, axis: .vertical) + TextField( + "Paste SSH public key", + text: Binding( + get: { viewModel.newSSHKey }, + set: { viewModel.newSSHKey = $0 } + ), + axis: .vertical + ) .font(.caption.monospaced()) .lineLimit(3...6) .themedRow() @@ -310,8 +317,6 @@ struct ProfileView: View { @ViewBuilder private func pgpKeysSection(_ viewModel: SettingsViewModel) -> some View { - @Bindable var vm = viewModel - Section { ForEach(viewModel.pgpKeys) { key in VStack(alignment: .leading, spacing: 2) { @@ -333,7 +338,14 @@ struct ProfileView: View { .themedRow() if viewModel.isAddingPGPKey { - TextField("Paste PGP public key", text: $vm.newPGPKey, axis: .vertical) + TextField( + "Paste PGP public key", + text: Binding( + get: { viewModel.newPGPKey }, + set: { viewModel.newPGPKey = $0 } + ), + axis: .vertical + ) .font(.caption.monospaced()) .lineLimit(3...6) .themedRow() @@ -482,12 +494,11 @@ private struct EditProfileSheet: View { .scaledToFill() } else { AsyncImage(url: profile.avatar.flatMap { URL(string: $0) }) { phase in - switch phase { - case .success(let image): + if case .success(let image) = phase { image .resizable() .scaledToFill() - default: + } else { Image(systemName: "person.crop.circle.fill") .resizable() .foregroundStyle(.secondary) @@ -592,7 +603,9 @@ private struct EditProfileSheet: View { } } .alert("Remove Avatar?", isPresented: $isShowingRemoveAvatarConfirmation) { - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { + /* Dismiss only; removal uses the destructive button. */ + } Button("Remove Avatar", role: .destructive) { Task { await viewModel.removeAvatar() -- cgit v1.2.3