From 0e461a382257979037e8927e5f6b468635b0a7fe Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 13 Apr 2026 09:57:23 -0500 Subject: feat: add power user actions Implements: https://todo.sr.ht/~ccleberg/hutch/52 Implements: https://todo.sr.ht/~ccleberg/hutch/53 Implements: https://todo.sr.ht/~ccleberg/hutch/54 --- Hutch/Views/Repositories/CommitDetailView.swift | 36 ++++++++++- Hutch/Views/Repositories/CommitLogView.swift | 2 +- Hutch/Views/Repositories/CommitRowView.swift | 17 ++++- Hutch/Views/Repositories/ReadmeView.swift | 23 +++++++ .../Views/Repositories/RepositoryDetailView.swift | 75 ++++++++++++++++++---- Hutch/Views/Repositories/RepositoryRowView.swift | 45 ++++++++----- 6 files changed, 162 insertions(+), 36 deletions(-) (limited to 'Hutch/Views/Repositories') diff --git a/Hutch/Views/Repositories/CommitDetailView.swift b/Hutch/Views/Repositories/CommitDetailView.swift index d303542..1dcbf68 100644 --- a/Hutch/Views/Repositories/CommitDetailView.swift +++ b/Hutch/Views/Repositories/CommitDetailView.swift @@ -5,6 +5,7 @@ struct CommitDetailView: View { let repository: RepositorySummary @Environment(AppState.self) private var appState + @Environment(\.openURL) private var openURL @State private var viewModel: CommitDetailViewModel? var body: some View { @@ -18,7 +19,38 @@ struct CommitDetailView: View { .navigationTitle(commitSummary.shortId) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + if let commitURL = SRHTWebURL.commit(repository: repository, commitId: commitSummary.id) { + Button { + openURL(commitURL) + } label: { + Label("Open in Browser", systemImage: "safari") + } + + Button { + appState.copyToPasteboard(commitURL.absoluteString, label: "commit URL") + } label: { + Label("Copy URL", systemImage: "doc.on.doc") + } + } + + Button { + appState.copyToPasteboard(commitSummary.id, label: "commit SHA") + } label: { + Label("Copy Full SHA", systemImage: "doc.on.doc") + } + + Button { + appState.copyToPasteboard(commitSummary.shortId, label: "short commit SHA") + } label: { + Label("Copy Short SHA", systemImage: "number") + } + } label: { + Image(systemName: "ellipsis.circle") + } + .accessibilityLabel("Commit actions") + SRHTShareButton(url: SRHTWebURL.commit(repository: repository, commitId: commitSummary.id), target: .commit) { Image(systemName: "square.and.arrow.up") } @@ -109,7 +141,7 @@ struct CommitDetailView: View { VStack(alignment: .leading, spacing: 8) { // Full hash — tappable to copy Button { - UIPasteboard.general.string = commit.id + appState.copyToPasteboard(commit.id, label: "commit SHA") } label: { HStack(spacing: 4) { Text(commit.id) diff --git a/Hutch/Views/Repositories/CommitLogView.swift b/Hutch/Views/Repositories/CommitLogView.swift index d63c5eb..bc8ccc9 100644 --- a/Hutch/Views/Repositories/CommitLogView.swift +++ b/Hutch/Views/Repositories/CommitLogView.swift @@ -7,7 +7,7 @@ struct CommitLogView: View { List { ForEach(viewModel.commits) { commit in NavigationLink(value: commit) { - CommitRowView(commit: commit) + CommitRowView(commit: commit, repository: viewModel.repository) } .task { await viewModel.loadMoreCommitsIfNeeded(currentItem: commit) diff --git a/Hutch/Views/Repositories/CommitRowView.swift b/Hutch/Views/Repositories/CommitRowView.swift index 362ab4d..6f40b57 100644 --- a/Hutch/Views/Repositories/CommitRowView.swift +++ b/Hutch/Views/Repositories/CommitRowView.swift @@ -1,8 +1,11 @@ import SwiftUI -import UIKit struct CommitRowView: View { + @Environment(AppState.self) private var appState + @Environment(\.openURL) private var openURL + let commit: CommitSummary + let repository: RepositorySummary var body: some View { VStack(alignment: .leading, spacing: 4) { @@ -28,14 +31,22 @@ struct CommitRowView: View { } .padding(.vertical, 2) .contextMenu { + if let url = SRHTWebURL.commit(repository: repository, commitId: commit.id) { + Button { + openURL(url) + } label: { + Label("Open in Browser", systemImage: "safari") + } + } + Button { - UIPasteboard.general.string = commit.id + appState.copyToPasteboard(commit.id, label: "commit SHA") } label: { Label("Copy Full SHA", systemImage: "doc.on.doc") } Button { - UIPasteboard.general.string = commit.shortId + appState.copyToPasteboard(commit.shortId, label: "short commit SHA") } label: { Label("Copy Short SHA", systemImage: "doc.on.doc.fill") } diff --git a/Hutch/Views/Repositories/ReadmeView.swift b/Hutch/Views/Repositories/ReadmeView.swift index 5742811..f5c0c9f 100644 --- a/Hutch/Views/Repositories/ReadmeView.swift +++ b/Hutch/Views/Repositories/ReadmeView.swift @@ -2,6 +2,7 @@ import SwiftUI import WebKit struct ReadmeView: View { + @Environment(AppState.self) private var appState let viewModel: RepositoryDetailViewModel @Environment(\.colorScheme) private var colorScheme @@ -15,6 +16,9 @@ struct ReadmeView: View { repositoryDetailsSection latestChangeSection readmeSection + if appState.isDebugModeEnabled { + debugSection + } } .padding() } @@ -150,6 +154,25 @@ struct ReadmeView: View { .plainText(text) } } + + private var debugSection: some View { + DebugTextBlock( + title: "Debug", + content: """ + repositoryId: \(viewModel.repository.id) + rid: \(viewModel.repository.rid) + service: \(viewModel.repository.service.rawValue) + defaultBranch: \(viewModel.repository.defaultBranchName ?? "none") + webURL: \(SRHTWebURL.repository(viewModel.repository)?.absoluteString ?? "unavailable") + httpsClone: \(SRHTWebURL.httpsCloneURL(viewModel.repository) ?? "unavailable") + sshClone: \(SRHTWebURL.sshCloneURL(viewModel.repository)) + readmePath: \(viewModel.readmePath ?? "none") + commitsLoaded: \(viewModel.commits.count) + branchesLoaded: \(viewModel.branches.count) + tagsLoaded: \(viewModel.tags.count) + """ + ) + } } enum RenderedMarkupContent: Sendable { diff --git a/Hutch/Views/Repositories/RepositoryDetailView.swift b/Hutch/Views/Repositories/RepositoryDetailView.swift index 0031e47..2715134 100644 --- a/Hutch/Views/Repositories/RepositoryDetailView.swift +++ b/Hutch/Views/Repositories/RepositoryDetailView.swift @@ -1,6 +1,8 @@ import SwiftUI struct RepositoryDetailView: View { + @Environment(\.openURL) private var openURL + let onRepositoryUpdated: ((RepositorySummary) -> Void)? var onDeleted: (() -> Void)? @@ -42,23 +44,11 @@ struct RepositoryDetailView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { + repositoryActionsMenu + SRHTShareButton(url: SRHTWebURL.repository(currentRepository), target: .repository) { Image(systemName: "square.and.arrow.up") } - - if canManageRepository { - Button { - showACLs = true - } label: { - Image(systemName: "person.2") - } - - Button { - showSettings = true - } label: { - Image(systemName: "gear") - } - } } } .sheet(isPresented: $showSettings) { @@ -137,4 +127,61 @@ struct RepositoryDetailView: View { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.hasPrefix("~") ? String(trimmed.dropFirst()) : trimmed } + + private var repositoryActionsMenu: some View { + Menu { + if let repositoryURL = SRHTWebURL.repository(currentRepository) { + Button { + openURL(repositoryURL) + } label: { + Label("Open in Browser", systemImage: "safari") + } + + Button { + appState.copyToPasteboard(repositoryURL.absoluteString, label: "repository URL") + } label: { + Label("Copy URL", systemImage: "doc.on.doc") + } + } + + if let httpsURL = SRHTWebURL.httpsCloneURL(currentRepository) { + Button { + appState.copyToPasteboard(httpsURL, label: "HTTPS clone URL") + } label: { + Label("Copy HTTPS URL", systemImage: "doc.on.doc") + } + } + + Button { + appState.copyToPasteboard(SRHTWebURL.sshCloneURL(currentRepository), label: "SSH clone URL") + } label: { + Label("Copy SSH URL", systemImage: "terminal") + } + + Button { + appState.copyToPasteboard(currentRepository.rid, label: "repository RID") + } label: { + Label("Copy RID", systemImage: "number") + } + + if canManageRepository { + Divider() + + Button { + showACLs = true + } label: { + Label("Manage ACLs", systemImage: "person.2") + } + + Button { + showSettings = true + } label: { + Label("Repository Settings", systemImage: "gear") + } + } + } label: { + Image(systemName: "ellipsis.circle") + } + .accessibilityLabel("Repository actions") + } } diff --git a/Hutch/Views/Repositories/RepositoryRowView.swift b/Hutch/Views/Repositories/RepositoryRowView.swift index 814ea76..0da6cbc 100644 --- a/Hutch/Views/Repositories/RepositoryRowView.swift +++ b/Hutch/Views/Repositories/RepositoryRowView.swift @@ -1,7 +1,9 @@ import SwiftUI -import UIKit struct RepositoryRowView: View { + @Environment(AppState.self) private var appState + @Environment(\.openURL) private var openURL + let repository: RepositorySummary let buildStatus: RepositoryBuildStatus @@ -56,34 +58,45 @@ struct RepositoryRowView: View { } .padding(.vertical, 2) .contextMenu { + if let url = SRHTWebURL.repository(repository) { + Button { + openURL(url) + } label: { + Label("Open in Browser", systemImage: "safari") + } + } + + Button { + if let url = SRHTWebURL.repository(repository)?.absoluteString { + appState.copyToPasteboard(url, label: "repository URL") + } + } label: { + Label("Copy URL", systemImage: "doc.on.doc") + } + Button { - UIPasteboard.general.string = httpsCloneURL(for: repository) + if let url = SRHTWebURL.httpsCloneURL(repository) { + appState.copyToPasteboard(url, label: "HTTPS clone URL") + } } label: { Label("Copy HTTPS URL", systemImage: "doc.on.doc") } Button { - UIPasteboard.general.string = sshCloneURL(for: repository) + appState.copyToPasteboard(SRHTWebURL.sshCloneURL(repository), label: "SSH clone URL") } label: { Label("Copy SSH URL", systemImage: "terminal") } + + Button { + appState.copyToPasteboard(repository.rid, label: "repository RID") + } label: { + Label("Copy RID", systemImage: "number") + } } } } -private func httpsCloneURL(for repository: RepositorySummary) -> String { - let host = "\(repository.service.rawValue).sr.ht" - let owner = repository.owner.canonicalName - return "https://\(host)/\(owner)/\(repository.name)" -} - -private func sshCloneURL(for repository: RepositorySummary) -> String { - let host = "\(repository.service.rawValue).sr.ht" - let user = repository.service == .hg ? "hg" : "git" - let owner = repository.owner.canonicalName - return "\(user)@\(host):\(owner)/\(repository.name)" -} - private struct RepositoryBuildStatusIndicator: View { let status: RepositoryBuildStatus -- cgit v1.2.3