import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; import 'package:pdfx/pdfx.dart'; import 'package:universal_platform/universal_platform.dart'; import '../services/api/api_config.dart'; import '../services/user_service.dart'; /// Tons **turquoise / menthe** (charte : `#8AD0C8`) — plus doux que le vert /// Material « gazon ». abstract final class _ModaleCouleur { static const Color turquoise = Color(0xFF8AD0C8); static const Color turquoiseFonce = Color(0xFF5A9D94); static const Color mentheTresClair = Color(0xFFF3FAF8); static const Color mentheClair = Color(0xFFE2F3EF); static const Color ivoire = Color(0xFFFFFEF9); static const Color encre = Color(0xFF2F2F2F); } enum LegalDocType { cgu, privacy, } class CguPrivacyValidationDialog extends StatefulWidget { const CguPrivacyValidationDialog({super.key}); @override State createState() => _CguPrivacyValidationDialogState(); } class _CguPrivacyValidationDialogState extends State { bool _loadingLegalDocs = false; DocumentActifInfo? _cguDoc; DocumentActifInfo? _privacyDoc; LegalDocType _selectedDocType = LegalDocType.cgu; static const double _cornerRadius = 16; @override void initState() { super.initState(); _loadActiveLegalDocs(); } Future _loadActiveLegalDocs() async { setState(() => _loadingLegalDocs = true); try { final docs = await UserService.getDocumentsLegauxActifs(); if (!mounted) return; setState(() { _cguDoc = docs.cgu; _privacyDoc = docs.privacy; }); } catch (_) { // Silencieux : message dans la zone PDF si besoin. } finally { if (mounted) setState(() => _loadingLegalDocs = false); } } /// Sur le web, [PdfDocument.openFile] n’est pas implémenté : on charge les /// octets puis [PdfDocument.openData]. Future _fetchPdfBytes(String pathOrUrl) async { final absolute = ApiConfig.absoluteMediaUrl(pathOrUrl); final response = await http.get(Uri.parse(absolute)); if (response.statusCode != 200) { throw StateError('PDF indisponible (HTTP ${response.statusCode})'); } return response.bodyBytes; } Future? get _currentPdfDocumentFuture { switch (_selectedDocType) { case LegalDocType.cgu: if (_cguDoc == null) return null; return PdfDocument.openData(_fetchPdfBytes(_cguDoc!.url)); case LegalDocType.privacy: if (_privacyDoc == null) return null; return PdfDocument.openData(_fetchPdfBytes(_privacyDoc!.url)); } } String get _dialogTitle { switch (_selectedDocType) { case LegalDocType.cgu: return 'Conditions Générales d’Utilisation'; case LegalDocType.privacy: return 'Politique de Confidentialité'; } } bool get _canValidate { return (_cguDoc != null && _privacyDoc != null) || (_cguDoc != null && _privacyDoc == null && _selectedDocType == LegalDocType.cgu) || (_privacyDoc != null && _cguDoc == null && _selectedDocType == LegalDocType.privacy); } /// [PdfViewPinch] n’est pas supporté sur Windows (pdfx) : repli [PdfView]. Widget _buildPdfBody() { final doc = _currentPdfDocumentFuture!; final key = ValueKey(_selectedDocType); if (UniversalPlatform.isWindows) { return PdfView( key: key, scrollDirection: Axis.vertical, controller: PdfController(document: doc), backgroundDecoration: const BoxDecoration(color: Colors.white), ); } return _PdfPaneWithScrollbar( key: key, documentFuture: doc, ); } @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final dialogW = size.width * 0.92; final dialogH = size.height * 0.88; return Dialog( insetPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 20), backgroundColor: Colors.transparent, elevation: 0, child: ClipRRect( borderRadius: BorderRadius.circular(_cornerRadius), child: Material( color: _ModaleCouleur.ivoire, elevation: 6, shadowColor: _ModaleCouleur.encre.withOpacity(0.12), child: SizedBox( width: dialogW, height: dialogH, child: Column( children: [ _buildHeader(context), Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _tabButton( context, label: 'CGU', selected: _selectedDocType == LegalDocType.cgu, enabled: !_loadingLegalDocs && _cguDoc != null, onTap: () => setState( () => _selectedDocType = LegalDocType.cgu, ), ), const SizedBox(width: 10), _tabButton( context, label: 'Confidentialité', selected: _selectedDocType == LegalDocType.privacy, enabled: !_loadingLegalDocs && _privacyDoc != null, onTap: () => setState( () => _selectedDocType = LegalDocType.privacy, ), ), ], ), ), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(12, 4, 12, 8), child: DecoratedBox( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: _ModaleCouleur.turquoiseFonce.withOpacity(0.35), ), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: _loadingLegalDocs ? const Center( child: CircularProgressIndicator( color: _ModaleCouleur.turquoiseFonce, strokeWidth: 3, ), ) : _currentPdfDocumentFuture != null ? _buildPdfBody() : Center( child: Text( 'Document non disponible.', style: GoogleFonts.merienda( color: _ModaleCouleur.encre .withOpacity(0.75), ), ), ), ), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: FilledButton( onPressed: _canValidate ? () => Navigator.of(context).pop(true) : null, style: FilledButton.styleFrom( backgroundColor: _ModaleCouleur.turquoiseFonce, foregroundColor: Colors.white, disabledBackgroundColor: _ModaleCouleur.mentheClair.withOpacity(0.7), disabledForegroundColor: _ModaleCouleur.encre.withOpacity(0.45), padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 14, ), ), child: Text( 'Valider les CGU et la Politique ' 'de Confidentialité', textAlign: TextAlign.center, style: GoogleFonts.merienda( fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ), ), ), ], ), ), ), ), ); } Widget _buildHeader(BuildContext context) { return DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ _ModaleCouleur.mentheTresClair, _ModaleCouleur.mentheClair, ], ), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only(left: 8), child: Text( _dialogTitle, style: GoogleFonts.merienda( fontSize: 17, fontWeight: FontWeight.w600, color: _ModaleCouleur.encre, height: 1.25, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ), IconButton( tooltip: 'Fermer', icon: Icon( Icons.close, color: _ModaleCouleur.encre.withOpacity(0.55), ), onPressed: () => Navigator.of(context).pop(false), ), ], ), ), ); } Widget _tabButton( BuildContext context, { required String label, required bool selected, required bool enabled, required VoidCallback onTap, }) { return OutlinedButton( onPressed: enabled ? onTap : null, style: OutlinedButton.styleFrom( foregroundColor: _ModaleCouleur.encre, backgroundColor: selected ? _ModaleCouleur.turquoise : _ModaleCouleur.mentheTresClair, side: BorderSide( color: _ModaleCouleur.turquoiseFonce.withOpacity(0.55), width: 1.5, ), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), ), child: Text( label, style: GoogleFonts.merienda( fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, ), ), ); } } /// [PdfViewPinch] pour le web / mobile avec `pdfx` 2.6 (pas de /// `documentProgress` / `minScale` dans cette API). class _PdfPaneWithScrollbar extends StatefulWidget { const _PdfPaneWithScrollbar({ super.key, required this.documentFuture, }); final Future documentFuture; @override State<_PdfPaneWithScrollbar> createState() => _PdfPaneWithScrollbarState(); } class _PdfPaneWithScrollbarState extends State<_PdfPaneWithScrollbar> { late final PdfControllerPinch _ctrl; @override void initState() { super.initState(); _ctrl = PdfControllerPinch(document: widget.documentFuture); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return PdfViewPinch( padding: 10, scrollDirection: Axis.vertical, controller: _ctrl, backgroundDecoration: const BoxDecoration(color: Colors.white), ); } }