summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-02 00:33:58 -0500
committerChristian Cleberg <[email protected]>2026-04-02 00:33:58 -0500
commit7b4a9fd51cfaf22114daa0170675102d04625791 (patch)
treeb4331b9aef4dc3f52698ad4bf64c7554ea557d9e /Hutch/Views/Repositories
parentf820109a956c627e510880a7cac88cb35032203b (diff)
downloadhutch-7b4a9fd51cfaf22114daa0170675102d04625791.tar.gz
hutch-7b4a9fd51cfaf22114daa0170675102d04625791.tar.bz2
hutch-7b4a9fd51cfaf22114daa0170675102d04625791.zip
add hg parity
Diffstat (limited to 'Hutch/Views/Repositories')
-rw-r--r--Hutch/Views/Repositories/FileTreeView.swift6
-rw-r--r--Hutch/Views/Repositories/HgRepositoryDetailView.swift141
2 files changed, 115 insertions, 32 deletions
diff --git a/Hutch/Views/Repositories/FileTreeView.swift b/Hutch/Views/Repositories/FileTreeView.swift
index adacb2e..0e7c56e 100644
--- a/Hutch/Views/Repositories/FileTreeView.swift
+++ b/Hutch/Views/Repositories/FileTreeView.swift
@@ -369,7 +369,7 @@ private struct FileTreeContentView: View {
}
}
-private struct CodeFileTextView: UIViewRepresentable {
+struct CodeFileTextView: UIViewRepresentable {
let text: String
let fileName: String
let wrapLines: Bool
@@ -396,7 +396,7 @@ private struct CodeFileTextView: UIViewRepresentable {
}
}
-private struct FileContentShareSheet: UIViewControllerRepresentable {
+struct FileContentShareSheet: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context _: Context) -> UIActivityViewController {
@@ -406,7 +406,7 @@ private struct FileContentShareSheet: UIViewControllerRepresentable {
func updateUIViewController(_ : UIActivityViewController, context _: Context) {}
}
-private final class CodeFileUIView: UIView {
+final class CodeFileUIView: UIView {
private enum Layout {
static let verticalPadding: CGFloat = 12
static let gutterLeadingPadding: CGFloat = 12
diff --git a/Hutch/Views/Repositories/HgRepositoryDetailView.swift b/Hutch/Views/Repositories/HgRepositoryDetailView.swift
index 575ce81..71fd6a0 100644
--- a/Hutch/Views/Repositories/HgRepositoryDetailView.swift
+++ b/Hutch/Views/Repositories/HgRepositoryDetailView.swift
@@ -1,4 +1,5 @@
import SwiftUI
+import UIKit
struct HgRepositoryDetailView: View {
let repository: RepositorySummary
@@ -8,11 +9,16 @@ struct HgRepositoryDetailView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
+ @AppStorage(AppStorageKeys.wrapRepositoryFileLines) private var wrapRepositoryFileLines = false
@State private var viewModel: HgRepositoryDetailViewModel?
@State private var selectedTab: HgRepositoryDetailViewModel.Tab = .summary
@State private var showSettings = false
@State private var isShowingRepositoryDetails = false
@State private var showBrowseRefPicker = false
+ @State private var showFileShareSheet = false
+ @State private var showShareUnavailableAlert = false
+ @State private var didCopyFileContents = false
+ @State private var copyResetTask: Task<Void, Never>?
private var canManageRepository: Bool {
guard let currentUser = appState.currentUser else { return false }
@@ -80,6 +86,9 @@ struct HgRepositoryDetailView: View {
HgBrowseRefPickerSheet(viewModel: viewModel, isPresented: $showBrowseRefPicker)
}
}
+ .onChange(of: viewModel?.selectedFilePath) { _, _ in
+ resetCopyConfirmation()
+ }
.task {
if viewModel == nil {
let vm = HgRepositoryDetailViewModel(repository: repository, client: appState.client)
@@ -266,39 +275,24 @@ struct HgRepositoryDetailView: View {
SRHTLoadingStateView(message: "Loading files…")
} else if let selectedFilePath = viewModel.selectedFilePath, let fileContent = viewModel.fileContent {
VStack(spacing: 0) {
- HStack {
- Spacer()
- SRHTShareButton(url: shareURL, target: .file) {
- Label("Share File", systemImage: "square.and.arrow.up")
- }
- .buttonStyle(.bordered)
- }
- .padding(.horizontal)
- .padding(.top, 12)
-
- GeometryReader { geometry in
- ScrollView([.vertical, .horizontal]) {
- Text(fileContent)
- .font(.system(.body, design: .monospaced))
- .frame(
- minWidth: geometry.size.width,
- minHeight: geometry.size.height,
- alignment: .topLeading
- )
- .padding()
- }
- }
+ CodeFileTextView(
+ text: fileContent,
+ fileName: selectedFilePath.split(separator: "/").last.map(String.init) ?? selectedFilePath,
+ wrapLines: wrapRepositoryFileLines
+ )
}
.safeAreaInset(edge: .bottom) {
- Button("Back to directory") {
- viewModel.dismissFileView()
- }
- .buttonStyle(.bordered)
- .padding(.vertical, 8)
- .frame(maxWidth: .infinity)
- .background(.bar)
+ fileActionToolbar(fileContent: fileContent, viewModel: viewModel)
}
.navigationTitle(selectedFilePath.split(separator: "/").last.map(String.init) ?? repository.name)
+ .sheet(isPresented: $showFileShareSheet) {
+ FileContentShareSheet(activityItems: [shareURL ?? fileContent])
+ }
+ .alert("Share Unavailable", isPresented: $showShareUnavailableAlert) {
+ Button("OK", role: .cancel) {}
+ } message: {
+ Text(SRHTShareTarget.file.fallbackMessage)
+ }
} else if let error = viewModel.error, viewModel.files.isEmpty {
SRHTErrorStateView(
title: "Couldn't Load Files",
@@ -508,6 +502,95 @@ struct HgRepositoryDetailView: View {
name.hasSuffix("/") ? String(name.dropLast()) : name
}
+ private func shareFileContents(_ text: String) {
+ if text.isEmpty {
+ showShareUnavailableAlert = true
+ } else {
+ showFileShareSheet = true
+ }
+ }
+
+ private func copyFileContents(_ text: String) {
+ UIPasteboard.general.string = text
+ didCopyFileContents = true
+ copyResetTask?.cancel()
+ copyResetTask = Task {
+ try? await Task.sleep(for: .seconds(2))
+ guard !Task.isCancelled else { return }
+ await MainActor.run {
+ didCopyFileContents = false
+ }
+ }
+ }
+
+ private func resetCopyConfirmation() {
+ copyResetTask?.cancel()
+ copyResetTask = nil
+ didCopyFileContents = false
+ }
+
+ private func fileActionToolbar(fileContent: String, viewModel: HgRepositoryDetailViewModel) -> some View {
+ HStack(spacing: 0) {
+ toolbarButton(
+ title: "Share",
+ systemImage: "square.and.arrow.up"
+ ) {
+ if shareURL != nil {
+ showFileShareSheet = true
+ } else {
+ shareFileContents(fileContent)
+ }
+ }
+
+ toolbarButton(
+ title: didCopyFileContents ? "Copied" : "Copy All",
+ systemImage: didCopyFileContents ? "checkmark" : "doc.on.doc"
+ ) {
+ copyFileContents(fileContent)
+ }
+
+ toolbarButton(
+ title: wrapRepositoryFileLines ? "Wrap On" : "Wrap Off",
+ systemImage: "text.word.spacing"
+ ) {
+ wrapRepositoryFileLines.toggle()
+ }
+
+ toolbarButton(
+ title: "Back",
+ systemImage: "chevron.left"
+ ) {
+ viewModel.dismissFileView()
+ }
+ }
+ .padding(.horizontal, 8)
+ .padding(.top, 10)
+ .padding(.bottom, 8)
+ .background(.bar)
+ .overlay(alignment: .top) {
+ Divider()
+ }
+ }
+
+ private func toolbarButton(
+ title: String,
+ systemImage: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 4) {
+ Image(systemName: systemImage)
+ .font(.system(size: 17, weight: .semibold))
+ Text(title)
+ .font(.caption2)
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .foregroundStyle(.primary)
+ }
}
private struct HgBrowseRefPickerSheet: View {