ScanCarteScreen.swift 119 lignes · 4244 octets
import SwiftUI

// Port of ui/ScanCarteScreen.kt. CameraX preview + capture is replaced by the
// VisionKit document camera (perspective-corrected pages); the photo picker
// covers the simulator and imports.

struct ScanCarteScreen: View {
    @ObservedObject var viewModel: ScanCarteViewModel
    let onBack: () -> Void
    let onContactSaved: (Int64) -> Void

    @State private var showDocumentCamera = false
    @State private var showPhotoPicker = false

    var body: some View {
        Group {
            switch viewModel.state {
            case .idle:
                captureLauncher
            case .processing:
                ScanProgressView(message: "Reconnaissance du texte…")
            case .draftReady(let draft):
                ContactDraftScreen(
                    draft: draft,
                    onCardChange: { viewModel.updateDraft($0) },
                    onSaveToCarnet: { viewModel.saveToCarnet() },
                    onRetry: { viewModel.reset() },
                    isEditing: viewModel.isEditing
                )
            case .error(let message):
                errorView(message)
            }
        }
        .toolbar(.hidden, for: .navigationBar)
        .onChange(of: viewModel.saveDoneId) { id in
            guard let id else { return }
            viewModel.consumeSaveDone()
            onContactSaved(id)
        }
        .fullScreenCover(isPresented: $showDocumentCamera) {
            DocumentScannerView { outcome in
                showDocumentCamera = false
                switch outcome {
                case .page(let image):
                    viewModel.processCapture(image, source: .documentCamera)
                case .cancelled:
                    break
                case .failed:
                    viewModel.onCaptureDecodeFailed()
                }
            }
            .ignoresSafeArea()
        }
        .sheet(isPresented: $showPhotoPicker) {
            PhotoPickerView { image in
                showPhotoPicker = false
                if let image {
                    viewModel.processCapture(image, source: .photoLibrary)
                }
            }
        }
    }

    private var captureLauncher: some View {
        VStack(alignment: .leading, spacing: 0) {
            ScreenTopBar(title: "Scanner une carte", onBack: onBack)
            Text("Cadrez la carte de visite, puis capturez.")
                .font(C2VFont.bodyMedium)
                .foregroundColor(C2VColor.texteFaible)
                .padding(.horizontal, 18)
                .padding(.vertical, 8)
            Spacer()
            VStack(spacing: 10) {
                if DocumentScannerView.isSupported {
                    Button("Capturer") { showDocumentCamera = true }
                        .buttonStyle(C2VPrimaryButtonStyle())
                }
                Button("Choisir une photo") { showPhotoPicker = true }
                    .buttonStyle(
                        DocumentScannerView.isSupported
                            ? AnyButtonStyle(C2VOutlineButtonStyle())
                            : AnyButtonStyle(C2VPrimaryButtonStyle())
                    )
            }
            .padding(18)
        }
        .background(C2VColor.fond)
    }

    private func errorView(_ message: String) -> some View {
        VStack(spacing: 12) {
            Spacer()
            Text(message)
                .font(C2VFont.bodyLarge)
                .foregroundColor(C2VColor.encre)
                .multilineTextAlignment(.center)
            Button("Reprendre la capture") { viewModel.reset() }
                .buttonStyle(C2VPrimaryButtonStyle())
            Button("Retour", action: onBack)
                .buttonStyle(C2VOutlineButtonStyle())
            Spacer()
        }
        .padding(18)
        .background(C2VColor.fond)
    }
}

/// Type-erased button style (conditional primary/outline in the launcher).
struct AnyButtonStyle: ButtonStyle {
    private let makeBodyClosure: (Configuration) -> AnyView

    init<S: ButtonStyle>(_ style: S) {
        makeBodyClosure = { AnyView(style.makeBody(configuration: $0)) }
    }

    func makeBody(configuration: Configuration) -> some View {
        makeBodyClosure(configuration)
    }
}