EnregistrementNoteSheet.swift 258 lignes · 9240 octets
import SwiftUI

/// Feuille d'enregistrement de note vocale.
///
/// Commune à la fiche contact (crée une `InteractionEntity` de type `note_vocale`)
/// et à la section Notes du projet (crée une `NoteProjetEntity`).
/// La logique de sauvegarde est fournie par `sauvegarder`.
struct EnregistrementNoteSheet: View {
    @StateObject private var viewModel: EnregistrementNoteViewModel
    @Environment(\.dismiss) private var dismiss

    /// - Parameters:
    ///   - modeTranscription: mode lu depuis `PreferencesAudio` par défaut.
    ///   - sauvegarder: `(sujet, description, audioPath, demandeTranscription) async -> envoye`.
    init(
        modeTranscription: ModeTranscription = PreferencesAudio.modeTranscription,
        sauvegarder: @escaping (String, String, String, Bool) async -> Bool
    ) {
        _viewModel = StateObject(wrappedValue: EnregistrementNoteViewModel(
            modeTranscription: modeTranscription,
            sauvegarder: sauvegarder
        ))
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            barreHaut

            switch viewModel.phase {
            case .demandePermission:
                chargementView(label: "Vérification des permissions…")
            case .permissionMicRefusee:
                permissionRefuseeView(
                    icone: "mic.slash.circle",
                    titre: "Microphone",
                    message: "L'accès au microphone est requis pour enregistrer une note vocale. Activez-le dans Réglages → Confidentialité."
                )
            case .permissionSpeechRefusee:
                permissionRefuseeView(
                    icone: "waveform.slash",
                    titre: "Reconnaissance vocale",
                    message: "Autorisez la reconnaissance vocale pour activer la transcription sur l'appareil. Activez-la dans Réglages → Confidentialité."
                )
            case .pret:
                pretView
            case .enregistrement:
                enregistrementView
            case .edition:
                editionView
            case .sauvegarde:
                chargementView(label: "Sauvegarde en cours…")
            case .termine(let message):
                termineView(message: message)
            }
        }
        .background(C2VColor.fond)
        .task {
            await viewModel.demanderPermissions()
        }
    }

    // MARK: - Barre haute

    private var barreHaut: some View {
        ZStack {
            HStack {
                Button("Annuler") {
                    viewModel.annuler()
                    dismiss()
                }
                .buttonStyle(C2VTextButtonStyle(color: C2VColor.brandError))
                Spacer()
            }
            Text("Note vocale")
                .font(C2VFont.titleMedium)
                .foregroundColor(C2VColor.encre)
        }
        .padding(.horizontal, 12)
        .padding(.vertical, 10)
        .background(C2VColor.surface)
    }

    // MARK: - Bandeau d'avertissement permanent

    private var bandeauAvertissement: some View {
        HStack(spacing: 8) {
            Image(systemName: "exclamationmark.triangle.fill")
                .font(.system(size: 13))
            Text("La transcription ne distingue pas les interlocuteurs")
                .font(C2VFont.bodySmall)
        }
        .foregroundColor(C2VColor.brandError)
        .padding(.horizontal, 16)
        .padding(.vertical, 8)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(C2VColor.brandErrorSoft)
    }

    // MARK: - Phase : prêt

    private var pretView: some View {
        VStack(spacing: 0) {
            bandeauAvertissement
            Spacer()
            VStack(spacing: 20) {
                Text("Appuyez pour démarrer l'enregistrement")
                    .font(C2VFont.bodyMedium)
                    .foregroundColor(C2VColor.texteFaible)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 32)
                Button {
                    viewModel.demarrer()
                } label: {
                    Image(systemName: "mic.circle.fill")
                        .font(.system(size: 84))
                        .foregroundColor(C2VColor.ink)
                }
                .buttonStyle(.plain)
            }
            Spacer()
        }
        .frame(maxWidth: .infinity)
    }

    // MARK: - Phase : enregistrement

    private var enregistrementView: some View {
        VStack(spacing: 0) {
            bandeauAvertissement
            Spacer()
            VStack(spacing: 20) {
                Text(viewModel.dureeFormatee)
                    .font(.system(size: 60, weight: .light, design: .monospaced))
                    .foregroundColor(C2VColor.encre)
                if !viewModel.transcriptionEnDirect.isEmpty {
                    Text(viewModel.transcriptionEnDirect)
                        .font(C2VFont.bodyMedium)
                        .foregroundColor(C2VColor.texteFaible)
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 24)
                }
                Button {
                    viewModel.arreter()
                } label: {
                    VStack(spacing: 8) {
                        Image(systemName: "stop.circle.fill")
                            .font(.system(size: 72))
                            .foregroundColor(C2VColor.brandError)
                        Text("Arrêter")
                            .font(C2VFont.labelMedium)
                            .foregroundColor(C2VColor.brandError)
                    }
                }
                .buttonStyle(.plain)
            }
            Spacer()
        }
        .frame(maxWidth: .infinity)
    }

    // MARK: - Phase : édition

    private var editionView: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                bandeauAvertissement
                VStack(alignment: .leading, spacing: 12) {
                    Text("Sujet")
                        .font(C2VFont.labelMedium)
                        .foregroundColor(C2VColor.texteFaible)
                    C2VTextField(
                        label: "",
                        placeholder: "Sujet de la note",
                        text: $viewModel.sujet
                    )
                    Text("Texte / transcription")
                        .font(C2VFont.labelMedium)
                        .foregroundColor(C2VColor.texteFaible)
                    C2VTextField(
                        label: "",
                        placeholder: "Texte (optionnel)",
                        text: $viewModel.descriptionNote,
                        minLines: 3
                    )
                    Button("Enregistrer") {
                        viewModel.sauvegarderNote()
                    }
                    .buttonStyle(C2VPrimaryButtonStyle())
                    .padding(.top, 4)
                }
                .padding(16)
            }
        }
    }

    // MARK: - Phase : chargement

    private func chargementView(label: String) -> some View {
        VStack(spacing: 16) {
            Spacer()
            ProgressView()
            Text(label)
                .font(C2VFont.bodyMedium)
                .foregroundColor(C2VColor.texteFaible)
            Spacer()
        }
        .frame(maxWidth: .infinity)
    }

    // MARK: - Phase : terminé

    private func termineView(message: String) -> some View {
        VStack(spacing: 20) {
            Spacer()
            Image(systemName: "checkmark.circle.fill")
                .font(.system(size: 64))
                .foregroundColor(C2VColor.brandGreen)
            Text(message)
                .font(C2VFont.bodyLarge)
                .foregroundColor(C2VColor.ink)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 32)
            Button("Fermer") { dismiss() }
                .buttonStyle(C2VPrimaryButtonStyle())
                .padding(.horizontal, 32)
            Spacer()
        }
        .frame(maxWidth: .infinity)
    }

    // MARK: - Permission refusée

    private func permissionRefuseeView(icone: String, titre: String, message: String) -> some View {
        VStack(spacing: 20) {
            Spacer()
            Image(systemName: icone)
                .font(.system(size: 60))
                .foregroundColor(C2VColor.brandError)
            Text("Permission \(titre) refusée")
                .font(C2VFont.titleMedium)
                .foregroundColor(C2VColor.encre)
            Text(message)
                .font(C2VFont.bodyMedium)
                .foregroundColor(C2VColor.texteFaible)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 32)
            Button("Ouvrir les Réglages") {
                if let url = URL(string: UIApplication.openSettingsURLString) {
                    UIApplication.shared.open(url)
                }
            }
            .buttonStyle(C2VOutlineButtonStyle())
            .padding(.horizontal, 32)
            Spacer()
        }
        .frame(maxWidth: .infinity)
    }
}