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 {
369 } 369 }
370} 370}
371 371
372private struct CodeFileTextView: UIViewRepresentable { 372struct CodeFileTextView: UIViewRepresentable {
373 let text: String 373 let text: String
374 let fileName: String 374 let fileName: String
375 let wrapLines: Bool 375 let wrapLines: Bool
@@ -396,7 +396,7 @@ private struct CodeFileTextView: UIViewRepresentable {
396 } 396 }
397} 397}
398 398
399private struct FileContentShareSheet: UIViewControllerRepresentable { 399struct FileContentShareSheet: UIViewControllerRepresentable {
400 let activityItems: [Any] 400 let activityItems: [Any]
401 401
402 func makeUIViewController(context _: Context) -> UIActivityViewController { 402 func makeUIViewController(context _: Context) -> UIActivityViewController {
@@ -406,7 +406,7 @@ private struct FileContentShareSheet: UIViewControllerRepresentable {
406 func updateUIViewController(_ : UIActivityViewController, context _: Context) {} 406 func updateUIViewController(_ : UIActivityViewController, context _: Context) {}
407} 407}
408 408
409private final class CodeFileUIView: UIView { 409final class CodeFileUIView: UIView {
410 private enum Layout { 410 private enum Layout {
411 static let verticalPadding: CGFloat = 12 411 static let verticalPadding: CGFloat = 12
412 static let gutterLeadingPadding: CGFloat = 12 412 static let gutterLeadingPadding: CGFloat = 12
Hutch/Views/Repositories/HgRepositoryDetailView.swift +112 −29
@@ -1,4 +1,5 @@
1import SwiftUI 1import SwiftUI
2import UIKit
2 3
3struct HgRepositoryDetailView: View { 4struct HgRepositoryDetailView: View {
4 let repository: RepositorySummary 5 let repository: RepositorySummary
@@ -8,11 +9,16 @@ struct HgRepositoryDetailView: View {
8 @Environment(\.dismiss) private var dismiss 9 @Environment(\.dismiss) private var dismiss
9 @Environment(\.colorScheme) private var colorScheme 10 @Environment(\.colorScheme) private var colorScheme
10 11
12 @AppStorage(AppStorageKeys.wrapRepositoryFileLines) private var wrapRepositoryFileLines = false
11 @State private var viewModel: HgRepositoryDetailViewModel? 13 @State private var viewModel: HgRepositoryDetailViewModel?
12 @State private var selectedTab: HgRepositoryDetailViewModel.Tab = .summary 14 @State private var selectedTab: HgRepositoryDetailViewModel.Tab = .summary
13 @State private var showSettings = false 15 @State private var showSettings = false
14 @State private var isShowingRepositoryDetails = false 16 @State private var isShowingRepositoryDetails = false
15 @State private var showBrowseRefPicker = false 17 @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>?
16 22
17 private var canManageRepository: Bool { 23 private var canManageRepository: Bool {
18 guard let currentUser = appState.currentUser else { return false } 24 guard let currentUser = appState.currentUser else { return false }
@@ -80,6 +86,9 @@ struct HgRepositoryDetailView: View {
80 HgBrowseRefPickerSheet(viewModel: viewModel, isPresented: $showBrowseRefPicker) 86 HgBrowseRefPickerSheet(viewModel: viewModel, isPresented: $showBrowseRefPicker)
81 } 87 }
82 } 88 }
89 .onChange(of: viewModel?.selectedFilePath) { _, _ in
90 resetCopyConfirmation()
91 }
83 .task { 92 .task {
84 if viewModel == nil { 93 if viewModel == nil {
85 let vm = HgRepositoryDetailViewModel(repository: repository, client: appState.client) 94 let vm = HgRepositoryDetailViewModel(repository: repository, client: appState.client)
@@ -266,39 +275,24 @@ struct HgRepositoryDetailView: View {
266 SRHTLoadingStateView(message: "Loading files…") 275 SRHTLoadingStateView(message: "Loading files…")
267 } else if let selectedFilePath = viewModel.selectedFilePath, let fileContent = viewModel.fileContent { 276 } else if let selectedFilePath = viewModel.selectedFilePath, let fileContent = viewModel.fileContent {
268 VStack(spacing: 0) { 277 VStack(spacing: 0) {
269 HStack { 278 CodeFileTextView(
270 Spacer() 279 text: fileContent,
271 SRHTShareButton(url: shareURL, target: .file) { 280 fileName: selectedFilePath.split(separator: "/").last.map(String.init) ?? selectedFilePath,
272 Label("Share File", systemImage: "square.and.arrow.up") 281 wrapLines: wrapRepositoryFileLines
273 } 282 )
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 }
291 } 283 }
292 .safeAreaInset(edge: .bottom) { 284 .safeAreaInset(edge: .bottom) {
293 Button("Back to directory") { 285 fileActionToolbar(fileContent: fileContent, viewModel: viewModel)
294 viewModel.dismissFileView()
295 }
296 .buttonStyle(.bordered)
297 .padding(.vertical, 8)
298 .frame(maxWidth: .infinity)
299 .background(.bar)
300 } 286 }
301 .navigationTitle(selectedFilePath.split(separator: "/").last.map(String.init) ?? repository.name) 287 .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 }
302 } else if let error = viewModel.error, viewModel.files.isEmpty { 296 } else if let error = viewModel.error, viewModel.files.isEmpty {
303 SRHTErrorStateView( 297 SRHTErrorStateView(
304 title: "Couldn't Load Files", 298 title: "Couldn't Load Files",
@@ -508,6 +502,95 @@ struct HgRepositoryDetailView: View {
508 name.hasSuffix("/") ? String(name.dropLast()) : name 502 name.hasSuffix("/") ? String(name.dropLast()) : name
509 } 503 }
510 504
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 }
511} 594}
512 595
513private struct HgBrowseRefPickerSheet: View { 596private struct HgBrowseRefPickerSheet: View {