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 | 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 | ||
| 399 | private struct FileContentShareSheet: UIViewControllerRepresentable { | 399 | struct 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 | ||
| 409 | private final class CodeFileUIView: UIView { | 409 | final 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 @@ | |||
| 1 | import SwiftUI | 1 | import SwiftUI |
| 2 | import UIKit | ||
| 2 | 3 | ||
| 3 | struct HgRepositoryDetailView: View { | 4 | struct 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 | ||
| 513 | private struct HgBrowseRefPickerSheet: View { | 596 | private struct HgBrowseRefPickerSheet: View { |