Commit e5f5db3bba
Unsigned
Layout: unified · split
Hutch/Views/Repositories/FileTreeView.swift +3 −3
| @@ -369,7 +369,7 @@ private struct FileTreeContentView: View { | ||
| 369 | 369 | } |
| 370 | 370 | } |
| 371 | 371 | |
| 372 | private struct CodeFileTextView: UIViewRepresentable { | |
| 372 | struct CodeFileTextView: UIViewRepresentable { | |
| 373 | 373 | let text: String |
| 374 | 374 | let fileName: String |
| 375 | 375 | let wrapLines: Bool |
| @@ -396,7 +396,7 @@ private struct CodeFileTextView: UIViewRepresentable { | ||
| 396 | 396 | } |
| 397 | 397 | } |
| 398 | 398 | |
| 399 | private struct FileContentShareSheet: UIViewControllerRepresentable { | |
| 399 | struct FileContentShareSheet: UIViewControllerRepresentable { | |
| 400 | 400 | let activityItems: [Any] |
| 401 | 401 | |
| 402 | 402 | func makeUIViewController(context _: Context) -> UIActivityViewController { |
| @@ -406,7 +406,7 @@ private struct FileContentShareSheet: UIViewControllerRepresentable { | ||
| 406 | 406 | func updateUIViewController(_ : UIActivityViewController, context _: Context) {} |
| 407 | 407 | } |
| 408 | 408 | |
| 409 | private final class CodeFileUIView: UIView { | |
| 409 | final class CodeFileUIView: UIView { | |
| 410 | 410 | private enum Layout { |
| 411 | 411 | static let verticalPadding: CGFloat = 12 |
| 412 | 412 | static let gutterLeadingPadding: CGFloat = 12 |
Hutch/Views/Repositories/HgRepositoryDetailView.swift +112 −29
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import SwiftUI |
| 2 | import UIKit | |
| 2 | 3 | |
| 3 | 4 | struct HgRepositoryDetailView: View { |
| 4 | 5 | let repository: RepositorySummary |
| @@ -8,11 +9,16 @@ struct HgRepositoryDetailView: View { | ||
| 8 | 9 | @Environment(\.dismiss) private var dismiss |
| 9 | 10 | @Environment(\.colorScheme) private var colorScheme |
| 10 | 11 | |
| 12 | @AppStorage(AppStorageKeys.wrapRepositoryFileLines) private var wrapRepositoryFileLines = false | |
| 11 | 13 | @State private var viewModel: HgRepositoryDetailViewModel? |
| 12 | 14 | @State private var selectedTab: HgRepositoryDetailViewModel.Tab = .summary |
| 13 | 15 | @State private var showSettings = false |
| 14 | 16 | @State private var isShowingRepositoryDetails = false |
| 15 | 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 | 23 | private var canManageRepository: Bool { |
| 18 | 24 | guard let currentUser = appState.currentUser else { return false } |
| @@ -80,6 +86,9 @@ struct HgRepositoryDetailView: View { | ||
| 80 | 86 | HgBrowseRefPickerSheet(viewModel: viewModel, isPresented: $showBrowseRefPicker) |
| 81 | 87 | } |
| 82 | 88 | } |
| 89 | .onChange(of: viewModel?.selectedFilePath) { _, _ in | |
| 90 | resetCopyConfirmation() | |
| 91 | } | |
| 83 | 92 | .task { |
| 84 | 93 | if viewModel == nil { |
| 85 | 94 | let vm = HgRepositoryDetailViewModel(repository: repository, client: appState.client) |
| @@ -266,39 +275,24 @@ struct HgRepositoryDetailView: View { | ||
| 266 | 275 | SRHTLoadingStateView(message: "Loading files…") |
| 267 | 276 | } else if let selectedFilePath = viewModel.selectedFilePath, let fileContent = viewModel.fileContent { |
| 268 | 277 | 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 | ) | |
| 291 | 283 | } |
| 292 | 284 | .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) | |
| 300 | 286 | } |
| 301 | 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 | 296 | } else if let error = viewModel.error, viewModel.files.isEmpty { |
| 303 | 297 | SRHTErrorStateView( |
| 304 | 298 | title: "Couldn't Load Files", |
| @@ -508,6 +502,95 @@ struct HgRepositoryDetailView: View { | ||
| 508 | 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 | |
| 513 | 596 | private struct HgBrowseRefPickerSheet: View { |