Commit e5f5db3bba

e5f5db3bbac0f49314972ed72e90c29c3fcd8d24

parent: fddfb1cbb5

Unsigned

cmc <hello@cleberg.net> · 2026-04-02 05:33 UTC

add hg parity

Layout: unified · split

Hutch/Views/Repositories/FileTreeView.swift +3 −3
@@ -369,7 +369,7 @@ private struct FileTreeContentView: View {
369369 }
370370}
371371
372private struct CodeFileTextView: UIViewRepresentable {
372struct CodeFileTextView: UIViewRepresentable {
373373 let text: String
374374 let fileName: String
375375 let wrapLines: Bool
@@ -396,7 +396,7 @@ private struct CodeFileTextView: UIViewRepresentable {
396396 }
397397}
398398
399private struct FileContentShareSheet: UIViewControllerRepresentable {
399struct FileContentShareSheet: UIViewControllerRepresentable {
400400 let activityItems: [Any]
401401
402402 func makeUIViewController(context _: Context) -> UIActivityViewController {
@@ -406,7 +406,7 @@ private struct FileContentShareSheet: UIViewControllerRepresentable {
406406 func updateUIViewController(_ : UIActivityViewController, context _: Context) {}
407407}
408408
409private final class CodeFileUIView: UIView {
409final class CodeFileUIView: UIView {
410410 private enum Layout {
411411 static let verticalPadding: CGFloat = 12
412412 static let gutterLeadingPadding: CGFloat = 12
Hutch/Views/Repositories/HgRepositoryDetailView.swift +112 −29
@@ -1,4 +1,5 @@
11import SwiftUI
2import UIKit
23
34struct HgRepositoryDetailView: View {
45 let repository: RepositorySummary
@@ -8,11 +9,16 @@ struct HgRepositoryDetailView: View {
89 @Environment(\.dismiss) private var dismiss
910 @Environment(\.colorScheme) private var colorScheme
1011
12 @AppStorage(AppStorageKeys.wrapRepositoryFileLines) private var wrapRepositoryFileLines = false
1113 @State private var viewModel: HgRepositoryDetailViewModel?
1214 @State private var selectedTab: HgRepositoryDetailViewModel.Tab = .summary
1315 @State private var showSettings = false
1416 @State private var isShowingRepositoryDetails = false
1517 @State private var showBrowseRefPicker = false
18 @State private var showFileShareSheet = false
19 @State private var showShareUnavailableAlert = false
20 @State private var didCopyFileContents = false
21 @State private var copyResetTask: Task<Void, Never>?
1622
1723 private var canManageRepository: Bool {
1824 guard let currentUser = appState.currentUser else { return false }
@@ -80,6 +86,9 @@ struct HgRepositoryDetailView: View {
8086 HgBrowseRefPickerSheet(viewModel: viewModel, isPresented: $showBrowseRefPicker)
8187 }
8288 }
89 .onChange(of: viewModel?.selectedFilePath) { _, _ in
90 resetCopyConfirmation()
91 }
8392 .task {
8493 if viewModel == nil {
8594 let vm = HgRepositoryDetailViewModel(repository: repository, client: appState.client)
@@ -266,39 +275,24 @@ struct HgRepositoryDetailView: View {
266275 SRHTLoadingStateView(message: "Loading files…")
267276 } else if let selectedFilePath = viewModel.selectedFilePath, let fileContent = viewModel.fileContent {
268277 VStack(spacing: 0) {
269 HStack {
270 Spacer()
271 SRHTShareButton(url: shareURL, target: .file) {
272 Label("Share File", systemImage: "square.and.arrow.up")
273 }
274 .buttonStyle(.bordered)
275 }
276 .padding(.horizontal)
277 .padding(.top, 12)
278
279 GeometryReader { geometry in
280 ScrollView([.vertical, .horizontal]) {
281 Text(fileContent)
282 .font(.system(.body, design: .monospaced))
283 .frame(
284 minWidth: geometry.size.width,
285 minHeight: geometry.size.height,
286 alignment: .topLeading
287 )
288 .padding()
289 }
290 }
278 CodeFileTextView(
279 text: fileContent,
280 fileName: selectedFilePath.split(separator: "/").last.map(String.init) ?? selectedFilePath,
281 wrapLines: wrapRepositoryFileLines
282 )
291283 }
292284 .safeAreaInset(edge: .bottom) {
293 Button("Back to directory") {
294 viewModel.dismissFileView()
295 }
296 .buttonStyle(.bordered)
297 .padding(.vertical, 8)
298 .frame(maxWidth: .infinity)
299 .background(.bar)
285 fileActionToolbar(fileContent: fileContent, viewModel: viewModel)
300286 }
301287 .navigationTitle(selectedFilePath.split(separator: "/").last.map(String.init) ?? repository.name)
288 .sheet(isPresented: $showFileShareSheet) {
289 FileContentShareSheet(activityItems: [shareURL ?? fileContent])
290 }
291 .alert("Share Unavailable", isPresented: $showShareUnavailableAlert) {
292 Button("OK", role: .cancel) {}
293 } message: {
294 Text(SRHTShareTarget.file.fallbackMessage)
295 }
302296 } else if let error = viewModel.error, viewModel.files.isEmpty {
303297 SRHTErrorStateView(
304298 title: "Couldn't Load Files",
@@ -508,6 +502,95 @@ struct HgRepositoryDetailView: View {
508502 name.hasSuffix("/") ? String(name.dropLast()) : name
509503 }
510504
505 private func shareFileContents(_ text: String) {
506 if text.isEmpty {
507 showShareUnavailableAlert = true
508 } else {
509 showFileShareSheet = true
510 }
511 }
512
513 private func copyFileContents(_ text: String) {
514 UIPasteboard.general.string = text
515 didCopyFileContents = true
516 copyResetTask?.cancel()
517 copyResetTask = Task {
518 try? await Task.sleep(for: .seconds(2))
519 guard !Task.isCancelled else { return }
520 await MainActor.run {
521 didCopyFileContents = false
522 }
523 }
524 }
525
526 private func resetCopyConfirmation() {
527 copyResetTask?.cancel()
528 copyResetTask = nil
529 didCopyFileContents = false
530 }
531
532 private func fileActionToolbar(fileContent: String, viewModel: HgRepositoryDetailViewModel) -> some View {
533 HStack(spacing: 0) {
534 toolbarButton(
535 title: "Share",
536 systemImage: "square.and.arrow.up"
537 ) {
538 if shareURL != nil {
539 showFileShareSheet = true
540 } else {
541 shareFileContents(fileContent)
542 }
543 }
544
545 toolbarButton(
546 title: didCopyFileContents ? "Copied" : "Copy All",
547 systemImage: didCopyFileContents ? "checkmark" : "doc.on.doc"
548 ) {
549 copyFileContents(fileContent)
550 }
551
552 toolbarButton(
553 title: wrapRepositoryFileLines ? "Wrap On" : "Wrap Off",
554 systemImage: "text.word.spacing"
555 ) {
556 wrapRepositoryFileLines.toggle()
557 }
558
559 toolbarButton(
560 title: "Back",
561 systemImage: "chevron.left"
562 ) {
563 viewModel.dismissFileView()
564 }
565 }
566 .padding(.horizontal, 8)
567 .padding(.top, 10)
568 .padding(.bottom, 8)
569 .background(.bar)
570 .overlay(alignment: .top) {
571 Divider()
572 }
573 }
574
575 private func toolbarButton(
576 title: String,
577 systemImage: String,
578 action: @escaping () -> Void
579 ) -> some View {
580 Button(action: action) {
581 VStack(spacing: 4) {
582 Image(systemName: systemImage)
583 .font(.system(size: 17, weight: .semibold))
584 Text(title)
585 .font(.caption2)
586 .lineLimit(1)
587 }
588 .frame(maxWidth: .infinity)
589 .contentShape(Rectangle())
590 }
591 .buttonStyle(.plain)
592 .foregroundStyle(.primary)
593 }
511594}
512595
513596private struct HgBrowseRefPickerSheet: View {