diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 19:44:44 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 19:44:44 -0500 |
| commit | fe1ccc69661603d419a642a450e4ac9e1258adb0 (patch) | |
| tree | 386455b6b5468abdd9dfa46dd4802b10da8b77a2 /Hutch/Views | |
| parent | 6ec9754ce34f331641b000eddef3d44bca2631a5 (diff) | |
| download | hutch-3.1.3.tar.gz hutch-3.1.3.tar.bz2 hutch-3.1.3.zip | |
fix: centralize URLs, tighten models, and polish SwiftUI bindingsv3.1.3
- 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.
Diffstat (limited to 'Hutch/Views')
| -rw-r--r-- | Hutch/Views/Home/HomeView.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/More/AccountSwitcherView.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/More/ProfileView.swift | 47 | ||||
| -rw-r--r-- | Hutch/Views/Projects/ProjectsListView.swift | 14 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift | 6 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryACLView.swift | 13 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryRowView.swift | 19 | ||||
| -rw-r--r-- | Hutch/Views/Settings/SettingsView.swift | 4 |
8 files changed, 71 insertions, 40 deletions
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index b10926a..b4b809d 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -171,7 +171,7 @@ struct HomeView: View { ) { ForEach(items) { item in Button { - openPinnedItem(item, viewModel: viewModel) + openPinnedItem(item) } label: { HomePinnedCard(item: item) } @@ -313,7 +313,7 @@ struct HomeView: View { } } - private func openPinnedItem(_ item: HomePinnedItem, viewModel: HomeViewModel) { + private func openPinnedItem(_ item: HomePinnedItem) { switch item.pin.kind { case .project: guard let project = item.project else { return } 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() diff --git a/Hutch/Views/Projects/ProjectsListView.swift b/Hutch/Views/Projects/ProjectsListView.swift index 12a5149..f6d5766 100644 --- a/Hutch/Views/Projects/ProjectsListView.swift +++ b/Hutch/Views/Projects/ProjectsListView.swift @@ -67,8 +67,6 @@ struct ProjectsListView: View { @ViewBuilder private func content(_ viewModel: ProjectsListViewModel) -> some View { - @Bindable var vm = viewModel - List { ForEach(viewModel.filteredProjects) { project in NavigationLink { @@ -84,7 +82,10 @@ struct ProjectsListView: View { .themedList() .listStyle(.plain) .searchable( - text: $vm.searchText, + text: Binding( + get: { viewModel.searchText }, + set: { viewModel.searchText = $0 } + ), placement: .navigationBarDrawer(displayMode: .always), prompt: "Search projects" ) @@ -107,7 +108,12 @@ struct ProjectsListView: View { ) } } - .srhtErrorBanner(error: $vm.error) + .srhtErrorBanner( + error: Binding( + get: { viewModel.error }, + set: { viewModel.error = $0 } + ) + ) .refreshable { await viewModel.loadProjects() } diff --git a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift index 7b8d7f7..6df40db 100644 --- a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift +++ b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift @@ -261,11 +261,7 @@ final class HgRepositorySettingsViewModel { } newACLEntity = "" } catch { - if error.matchesGraphQLErrorClassification(.serviceNotProvisioned) { - self.error = error.userFacingMessage - } else { - self.error = error.userFacingMessage - } + self.error = error.userFacingMessage } } diff --git a/Hutch/Views/Repositories/RepositoryACLView.swift b/Hutch/Views/Repositories/RepositoryACLView.swift index 29ded84..ede770c 100644 --- a/Hutch/Views/Repositories/RepositoryACLView.swift +++ b/Hutch/Views/Repositories/RepositoryACLView.swift @@ -51,8 +51,6 @@ struct RepositoryACLView: View { @ViewBuilder private func content(_ viewModel: RepositoryACLViewModel) -> some View { - @Bindable var vm = viewModel - Group { if viewModel.isLoading && !viewModel.hasEntries && viewModel.loadError == nil { SRHTLoadingStateView(message: "Loading access…") @@ -98,7 +96,12 @@ struct RepositoryACLView: View { } } } - .srhtErrorBanner(error: $vm.error) + .srhtErrorBanner( + error: Binding( + get: { viewModel.error }, + set: { viewModel.error = $0 } + ) + ) .alert("Remove Access?", isPresented: Binding( get: { pendingDeletion != nil }, set: { isPresented in @@ -107,7 +110,9 @@ struct RepositoryACLView: View { } } )) { - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { + /* Dismiss only; removal is confirmed separately. */ + } Button("Remove Access", role: .destructive) { guard let entry = pendingDeletion else { return } Task { diff --git a/Hutch/Views/Repositories/RepositoryRowView.swift b/Hutch/Views/Repositories/RepositoryRowView.swift index afbd740..a2d120b 100644 --- a/Hutch/Views/Repositories/RepositoryRowView.swift +++ b/Hutch/Views/Repositories/RepositoryRowView.swift @@ -17,9 +17,6 @@ struct RepositoryRowView: View { RepositoryForgeBadge(service: repository.service) - if buildStatus != .none { - RepositoryBuildStatusIndicator(status: buildStatus) - } VisibilityBadge(visibility: repository.visibility) } @@ -44,9 +41,19 @@ struct RepositoryRowView: View { Spacer() - Text(repository.updated.relativeDescription) - .font(.caption) - .foregroundStyle(.tertiary) + HStack(spacing: 6) { + ZStack { + if buildStatus != .none { + RepositoryBuildStatusIndicator(status: buildStatus) + } + } + .frame(width: 8, height: 8) + .accessibilityHidden(buildStatus == .none) + + Text(repository.updated.relativeDescription) + .font(.caption) + .foregroundStyle(.tertiary) + } } } .padding(.vertical, 2) diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift index 4ed9216..577de8a 100644 --- a/Hutch/Views/Settings/SettingsView.swift +++ b/Hutch/Views/Settings/SettingsView.swift @@ -32,7 +32,9 @@ struct SettingsView: 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 |
