NoteVocaleRow.swift 143 lignes · 5195 octets
import AVFoundation
import SwiftUI

/// Ligne d'affichage d'une note vocale (interaction `note_vocale` ou `NoteProjetEntity`).
///
/// Affiche : sujet, description/transcription, badge de statut (`en_attente` / `echec`)
/// avec le motif d'erreur quand il existe, lecture audio si le fichier est présent localement,
/// et les actions « Relancer la transcription » (echec uniquement) et « Supprimer ».
struct NoteVocaleRow: View {
    let sujet: String
    let description: String
    let audioPath: String?
    let transcriptionStatut: String?
    let transcriptionErreur: String?
    /// Rappel de relance — fourni uniquement quand le statut vaut `echec`.
    var onRelancer: (() -> Void)? = nil
    /// Rappel de suppression avec confirmation intégrée.
    var onSupprimer: (() -> Void)? = nil

    @State private var joueur: AVAudioPlayer?
    @State private var enLecture = false
    @State private var confirmSupprimer = false

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            // Titre + badge statut
            HStack(alignment: .top, spacing: 8) {
                Image(systemName: "waveform")
                    .foregroundColor(C2VColor.texteFaible)
                    .font(.system(size: 14))
                Text(sujet)
                    .font(C2VFont.bodyMedium)
                    .foregroundColor(C2VColor.ink)
                    .frame(maxWidth: .infinity, alignment: .leading)
                if let statut = transcriptionStatut, statut != "terminee" {
                    statutBadge(statut)
                }
            }

            // Description / texte transcrit
            if !description.isEmpty {
                Text(description)
                    .font(C2VFont.bodySmall)
                    .foregroundColor(C2VColor.texteFaible)
            }

            // Motif d'erreur (quand echec)
            if let erreur = transcriptionErreur, !erreur.isEmpty {
                HStack(spacing: 4) {
                    Image(systemName: "exclamationmark.circle")
                        .font(.system(size: 11))
                    Text("Erreur : \(erreur)")
                        .font(C2VFont.bodySmall)
                }
                .foregroundColor(C2VColor.brandError)
            }

            // Lecteur audio (fichier local uniquement)
            if let path = audioPath, FileManager.default.fileExists(atPath: path) {
                lecteurAudio(path: path)
            }

            // Actions
            if transcriptionStatut == "echec", let relancer = onRelancer {
                Button("Relancer la transcription") { relancer() }
                    .buttonStyle(C2VTextButtonStyle())
                    .font(C2VFont.labelSmall)
            }
            if onSupprimer != nil {
                Button("Supprimer") { confirmSupprimer = true }
                    .buttonStyle(C2VTextButtonStyle(color: C2VColor.brandError))
                    .font(C2VFont.labelSmall)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .confirmationDialog(
            "Supprimer cette note vocale ?",
            isPresented: $confirmSupprimer,
            titleVisibility: .visible
        ) {
            Button("Supprimer", role: .destructive) { onSupprimer?() }
            Button("Annuler", role: .cancel) {}
        }
        .onDisappear {
            joueur?.stop()
            joueur = nil
            enLecture = false
        }
    }

    // MARK: - Badge statut

    @ViewBuilder
    private func statutBadge(_ statut: String) -> some View {
        let (label, color): (String, Color) = switch statut {
        case "en_attente": ("En attente", C2VColor.brandWarn)
        case "echec": ("Échec", C2VColor.brandError)
        default: (statut, C2VColor.texteFaible)
        }
        Text(label)
            .font(C2VFont.labelSmall)
            .foregroundColor(color)
            .padding(.horizontal, 6)
            .padding(.vertical, 2)
            .overlay(Rectangle().stroke(color, lineWidth: 1))
    }

    // MARK: - Lecteur audio

    private func lecteurAudio(path: String) -> some View {
        Button {
            toggleLecture(path: path)
        } label: {
            HStack(spacing: 6) {
                Image(systemName: enLecture ? "pause.circle" : "play.circle")
                    .font(.system(size: 22))
                Text(enLecture ? "Pause" : "Écouter")
                    .font(C2VFont.labelSmall)
            }
            .foregroundColor(C2VColor.ink)
        }
        .buttonStyle(.plain)
        .padding(.top, 2)
    }

    private func toggleLecture(path: String) {
        if enLecture {
            joueur?.pause()
            enLecture = false
        } else {
            do {
                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
                try AVAudioSession.sharedInstance().setActive(true)
                let url = URL(fileURLWithPath: path)
                joueur = try AVAudioPlayer(contentsOf: url)
                joueur?.play()
                enLecture = true
            } catch {
                enLecture = false
            }
        }
    }
}