import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import 'package:manager_api_new/api.dart'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_audio_player.dart'; import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_end_view.dart'; import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_geo.dart'; import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_step_challenge.dart'; import 'package:mymuseum_visitapp/Services/pushNotificationService.dart'; import 'package:mymuseum_visitapp/constants.dart'; class GuidedPathMapProgressionPage extends StatefulWidget { final GuidedPathDTO path; final MapDTO mapDTO; final VisitAppContext visitAppContext; const GuidedPathMapProgressionPage({ Key? key, required this.path, required this.mapDTO, required this.visitAppContext, }) : super(key: key); @override State createState() => _GuidedPathMapProgressionPageState(); } class _GuidedPathMapProgressionPageState extends State with SingleTickerProviderStateMixin { late List _steps; int _index = 0; final Set _completedStepIds = {}; GuidedStepChallengeController? _challenge; bool _expiredShown = false; bool _showDetail = false; LatLng? _userPosition; bool _inGeoZone = false; double? _distanceMeters; bool _geoUnavailable = false; StreamSubscription? _positionSub; final MapController _mapController = MapController(); late AnimationController _pulse; late Animation _pulseAnim; static const _gold = Color(0xFFD4AF37); static const _green = Color(0xFF2E9E6B); bool get _isGame => widget.path.isGameMode == true; Color get _accent => _isGame ? _gold : kMainColor; Color get _ink => _isGame ? const Color(0xFFF4ECD8) : const Color(0xFF1E2A33); Color get _muted => _isGame ? const Color(0xFF8a8068) : const Color(0xFF6B7B86); Color get _detailBg => _isGame ? const Color(0xFF0B1018) : const Color(0xFFF6F3EE); GuidedStepDTO? get _currentStep => _steps.isEmpty ? null : _steps[_index]; @override void initState() { super.initState(); _steps = [...(widget.path.steps ?? [])] ..sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); _pulse = AnimationController(vsync: this, duration: const Duration(milliseconds: 900)) ..repeat(reverse: true); _pulseAnim = Tween(begin: 1.0, end: 1.4) .animate(CurvedAnimation(parent: _pulse, curve: Curves.easeInOut)); _initStepState(); _startLocationTracking(); } @override void dispose() { _positionSub?.cancel(); _pulse.dispose(); _challenge?.removeListener(_onChallengeChanged); _challenge?.dispose(); super.dispose(); } // ─── État d'étape ─────────────────────────────────────────────────────────── void _initStepState() { _challenge?.removeListener(_onChallengeChanged); _challenge?.dispose(); _expiredShown = false; _showDetail = false; _inGeoZone = false; _distanceMeters = null; final step = _currentStep; if (step == null) return; final questions = GuidedStepChallengeController.buildQuestions(step); final hasTimer = step.isStepTimer == true && (step.timerSeconds ?? 0) > 0; _challenge = GuidedStepChallengeController( questions: questions, isGame: _isGame, requireSuccess: widget.path.requireSuccessToAdvance ?? false, hasTimer: hasTimer, timerSeconds: step.timerSeconds ?? 0, visitAppContext: widget.visitAppContext, )..addListener(_onChallengeChanged); if (_userPosition != null) _checkGeoZone(_userPosition!); _centerOnCurrent(); } void _onChallengeChanged() { if (!mounted) return; setState(() {}); final c = _challenge; final step = _currentStep; if (c != null && c.expired && !_expiredShown) { final msg = TranslationHelper.get(step?.timerExpiredMessage, widget.visitAppContext); if (msg.isNotEmpty) { _expiredShown = true; WidgetsBinding.instance.addPostFrameCallback((_) => _showExpiredModal(msg)); } } } String _translate(List? list) => TranslationHelper.get(list, widget.visitAppContext); bool _hasGeoTrigger(GuidedStepDTO step) => step.isGeoTriggered == true && step.geometry != null && (step.zoneRadiusMeters ?? 0) > 0; bool get _canAdvance { final step = _currentStep; if (step == null || step.isStepLocked == true) return false; final requireSuccess = widget.path.requireSuccessToAdvance ?? false; final zoneOk = !_hasGeoTrigger(step) || _inGeoZone || !requireSuccess || _geoUnavailable; return (_challenge?.canAdvance ?? true) && zoneOk; } // ─── Navigation ─────────────────────────────────────────────────────────── void _advance() { final step = _currentStep; if (step == null || !_canAdvance) return; if (step.id != null) _completedStepIds.add(step.id!); if (_index < _steps.length - 1) { setState(() { _index++; _initStepState(); }); } else { _showEnd(); } } void _goBack() { if (_index > 0 && !(widget.path.isLinear ?? false)) { setState(() { _index--; _initStepState(); }); } } void _openChallenge() { setState(() => _showDetail = true); final started = _challenge?.openChallenge() ?? false; if (started) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('Le chrono a démarré !'), behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 2), backgroundColor: _isGame ? const Color(0xFF141a24) : const Color(0xFF1E2A33), )); } } void _showEnd() { final outro = _stripHtml( TranslationHelper.getWithResource(widget.path.gameMessageFin, widget.visitAppContext)); Navigator.of(context).push(MaterialPageRoute( builder: (_) => GuidedPathEndView( isGame: _isGame, primaryColor: kMainColor, pathTitle: _translate(widget.path.title), stepsCount: _steps.length, estimatedDurationMinutes: widget.path.estimatedDurationMinutes, gameOutro: outro, onBack: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, ), )); } // ─── Géoloc ─────────────────────────────────────────────────────────────── Future _startLocationTracking() async { final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { if (mounted) setState(() => _geoUnavailable = true); return; } var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { if (mounted) setState(() => _geoUnavailable = true); return; } if (mounted) setState(() => _geoUnavailable = false); _positionSub = Geolocator.getPositionStream( locationSettings: const LocationSettings(accuracy: LocationAccuracy.high, distanceFilter: 5), ).listen((pos) { if (!mounted) return; final p = LatLng(pos.latitude, pos.longitude); setState(() => _userPosition = p); _checkGeoZone(p); }); } void _retryGeo() { _positionSub?.cancel(); _startLocationTracking(); } void _checkGeoZone(LatLng position) { if (!mounted) return; final step = _currentStep; if (step == null || !_hasGeoTrigger(step)) return; final center = GuidedPathGeo.center(step.geometry); if (center == null) return; final radius = step.zoneRadiusMeters ?? 0; final dist = const Distance().distance(position, center); final inZone = dist <= radius; if (inZone && !_inGeoZone) { PushNotificationService.showGeoZoneNotification(stepTitle: _translate(step.title)); } setState(() { _distanceMeters = dist; _inGeoZone = inZone; }); } // ─── Carte ────────────────────────────────────────────────────────────────── List get _visibleSteps => (widget.path.hideNextStepsUntilComplete ?? false) ? _steps.sublist(0, _index + 1) : _steps; List get _polylinePositions => _visibleSteps .map((s) => GuidedPathGeo.center(s.geometry)) .whereType() .toList(); void _centerOnCurrent() { final center = GuidedPathGeo.center(_currentStep?.geometry); if (center == null) return; WidgetsBinding.instance.addPostFrameCallback((_) { try { _mapController.move(center, _mapController.camera.zoom); } catch (_) {} }); } void _fitToSteps() { final pts = _polylinePositions; if (pts.length < 2) return; try { _mapController.fitCamera( CameraFit.bounds( bounds: LatLngBounds.fromPoints(pts), padding: const EdgeInsets.all(70), maxZoom: 17, ), ); } catch (_) {} } String _formatDistance(double m) { if (m < 1000) return '${m.round()} m'; return '${(m / 1000).toStringAsFixed(m < 10000 ? 1 : 0)} km'; } String _stripHtml(String html) => html .replaceAll(RegExp(r'<[^>]*>'), ' ') .replaceAll(' ', ' ') .replaceAll(RegExp(r'\s+'), ' ') .trim(); // ─── Build ──────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final step = _currentStep; if (step == null) { return const Scaffold(body: Center(child: Text('Aucune étape'))); } final challenge = _challenge!; final fallback = widget.mapDTO.latitude != null && widget.mapDTO.longitude != null ? LatLng(double.tryParse(widget.mapDTO.latitude!) ?? 50.4655, double.tryParse(widget.mapDTO.longitude!) ?? 4.8651) : const LatLng(50.465503, 4.865105); final initialCenter = _polylinePositions.isNotEmpty ? _polylinePositions.first : fallback; final zoom = widget.mapDTO.zoom?.toDouble() ?? 15.0; return Scaffold( body: Stack( children: [ FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: initialCenter, initialZoom: zoom, onMapReady: _fitToSteps, ), children: [ TileLayer( urlTemplate: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', userAgentPackageName: 'be.unov.myinfomate.visitapp', ), ..._buildShapeLayers(), if (_polylinePositions.length > 1) PolylineLayer( polylines: [ Polyline( points: _polylinePositions, color: _accent.withOpacity(0.85), strokeWidth: 4, pattern: StrokePattern.dashed(segments: const [8.0, 6.0]), ), ], ), MarkerLayer(markers: _buildMarkers()), ], ), _buildTopHeader(), if (!_showDetail) _buildBottomSheet(step), if (_showDetail) _buildDetailOverlay(step), GuidedStepChallengeOverlay( controller: challenge, stepIndex: _index, accentColor: _accent, isGame: _isGame, visitAppContext: widget.visitAppContext, ), ], ), ); } List _buildShapeLayers() { final polylines = []; final polygons = []; for (final s in _visibleSteps) { final shape = GuidedPathGeo.shape(s.geometry); if (shape == null) continue; final completed = s.id != null && _completedStepIds.contains(s.id); final current = s.id == _currentStep?.id; final color = completed ? _green : (current ? _accent : Colors.grey); final opacity = (!completed && !current) ? 0.5 : 0.9; if (shape.type == 'LineString') { polylines.add(Polyline( points: shape.positions, color: color.withOpacity(opacity), strokeWidth: 5, )); } else { polygons.add(Polygon( points: shape.positions, color: color.withOpacity(0.15), borderColor: color.withOpacity(opacity), borderStrokeWidth: 3, )); } } return [ if (polygons.isNotEmpty) PolygonLayer(polygons: polygons), if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), ]; } List _buildMarkers() { final markers = []; var visibleOrder = 0; for (int i = 0; i < _steps.length; i++) { final step = _steps[i]; final visible = _visibleSteps.contains(step); if (!visible) continue; visibleOrder++; final center = GuidedPathGeo.center(step.geometry); if (center == null) continue; final completed = step.id != null && _completedStepIds.contains(step.id); final current = i == _index; final locked = step.isStepLocked == true; Widget pin; if (current) { pin = AnimatedBuilder( animation: _pulseAnim, builder: (_, __) => Transform.scale( scale: _pulseAnim.value, child: _numberedPin('$visibleOrder', _accent, _isGame), ), ); } else if (completed) { pin = _iconPin(Icons.check, _green); } else if (locked) { pin = _iconPin(Icons.lock, Colors.grey.shade500); } else { pin = _numberedPin('$visibleOrder', Colors.grey.shade500, false); } markers.add(Marker( point: center, width: 44, height: 44, child: pin, )); } if (_userPosition != null) { markers.add(Marker( point: _userPosition!, width: 26, height: 26, child: Container( decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow(color: Colors.blue.withOpacity(0.4), blurRadius: 8, spreadRadius: 2) ], ), ), )); } return markers; } Widget _numberedPin(String label, Color color, bool goldText) { return Container( width: 34, height: 34, alignment: Alignment.center, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 5)], ), child: Text(label, style: TextStyle( color: goldText ? const Color(0xFF1a1305) : Colors.white, fontWeight: FontWeight.w800, fontSize: 14)), ); } Widget _iconPin(IconData icon, Color color) { return Container( width: 34, height: 34, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 5)], ), child: Icon(icon, color: Colors.white, size: 18), ); } Widget _buildTopHeader() { return Positioned( top: MediaQuery.of(context).padding.top + 12, left: 14, right: 14, child: Row( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(12), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 8)], ), child: const Icon(Icons.arrow_back_ios_new, size: 16, color: Color(0xFF1E2A33)), ), ), const SizedBox(width: 10), Expanded( child: Container( height: 40, padding: const EdgeInsets.only(left: 14, right: 8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(12), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 8)], ), child: Row( children: [ Expanded( child: Text(_translate(widget.path.title), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1E2A33))), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: _accent.withOpacity(0.12), borderRadius: BorderRadius.circular(9), ), child: Text('${_index + 1} / ${_steps.length}', style: TextStyle( color: _accent, fontSize: 12, fontWeight: FontWeight.w700)), ), ], ), ), ), ], ), ); } Widget _buildBottomSheet(GuidedStepDTO step) { final title = _translate(step.title); final hasQuiz = _challenge?.hasQuiz ?? false; final progress = _steps.length > 1 ? _index / (_steps.length - 1) : 0.0; return Positioned( left: 0, right: 0, bottom: 0, child: Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(22)), boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 24, offset: Offset(0, -8))], ), padding: EdgeInsets.only(bottom: 18 + MediaQuery.of(context).padding.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only(top: 10, bottom: 4), child: Container( width: 40, height: 5, decoration: BoxDecoration( color: const Color(0xFFE2DCD2), borderRadius: BorderRadius.circular(3), ), ), ), GestureDetector( onTap: () => setState(() => _showDetail = true), child: Padding( padding: const EdgeInsets.fromLTRB(18, 4, 18, 0), child: Row( children: [ _stepThumb(step), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('ÉTAPE EN COURS', style: TextStyle( color: _accent, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 0.5)), const SizedBox(height: 3), Text(title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Color(0xFF1E2A33), fontSize: 18, fontWeight: FontWeight.w600, height: 1.15)), ], ), ), const Icon(Icons.chevron_right, color: Color(0xFF6B7B86)), ], ), ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 12, 18, 0), child: ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progress.clamp(0.0, 1.0), minHeight: 6, backgroundColor: const Color(0xFFEEE9E0), valueColor: AlwaysStoppedAnimation(_accent), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 12, 18, 0), child: Row( children: [ if (_index > 0 && !(widget.path.isLinear ?? false)) ...[ Expanded( child: OutlinedButton( onPressed: _goBack, style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF46555F), side: const BorderSide(color: Color(0xFFE2DCD2), width: 1.5), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), child: const Text('Précédente', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700)), ), ), const SizedBox(width: 11), ], if (hasQuiz) ...[ Expanded( child: OutlinedButton.icon( onPressed: () => setState(() => _showDetail = true), style: OutlinedButton.styleFrom( foregroundColor: _accent, side: BorderSide(color: _accent.withOpacity(0.5), width: 1.5), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), icon: Icon(Icons.star, size: 13, color: _accent), label: const Text('Défi', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700)), ), ), const SizedBox(width: 11), ], Expanded( flex: 2, child: ElevatedButton.icon( onPressed: _canAdvance ? _advance : () => setState(() => _showDetail = true), style: ElevatedButton.styleFrom( backgroundColor: _canAdvance ? _accent : _accent.withOpacity(0.35), foregroundColor: _isGame ? const Color(0xFF1a1305) : Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), elevation: 0, ), icon: const Icon(Icons.flag, size: 14), label: Text(_index < _steps.length - 1 ? 'Étape suivante' : 'Terminer', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700)), ), ), ], ), ), ], ), ), ); } Widget _stepThumb(GuidedStepDTO step) { return Container( width: 72, height: 72, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_accent.withOpacity(0.6), _accent], ), ), clipBehavior: Clip.antiAlias, child: step.imageUrl != null ? Image.network(step.imageUrl!, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _thumbBadge()) : _thumbBadge(), ); } Widget _thumbBadge() => Center( child: Text('${_index + 1}', style: TextStyle( color: _isGame ? const Color(0xFF1a1305) : Colors.white, fontSize: 22, fontWeight: FontWeight.w800)), ); Widget _buildDetailOverlay(GuidedStepDTO step) { final title = _translate(step.title); final desc = _translate(step.description); final hasQuiz = _challenge?.hasQuiz ?? false; final hasGeo = _hasGeoTrigger(step); final radius = (step.zoneRadiusMeters ?? 0).round(); final audioUrl = () { final list = step.audioIds ?? []; if (list.isEmpty) return null; final match = list.firstWhere( (a) => a.language == widget.visitAppContext.language, orElse: () => list.first, ); final v = match.value; return (v != null && v.startsWith('http')) ? v : null; }(); return Positioned.fill( child: Container( color: _detailBg, child: SafeArea( child: Column( children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(12, 8, 14, 8), child: Row( children: [ GestureDetector( onTap: () => setState(() => _showDetail = false), child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: _isGame ? Colors.white.withOpacity(0.08) : Colors.white, ), child: Icon(Icons.arrow_back_ios_new, size: 16, color: _isGame ? _gold : const Color(0xFF1E2A33)), ), ), const SizedBox(width: 10), Expanded( child: Row( children: List.generate(_steps.length, (i) { return Expanded( child: Container( margin: const EdgeInsets.symmetric(horizontal: 2), height: 5, decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), color: i < _index ? (_isGame ? _gold : _green) : i == _index ? _accent : (_isGame ? Colors.white.withOpacity(0.1) : const Color(0xFFE2DCD2)), ), ), ); }), ), ), const SizedBox(width: 10), Text('${_index + 1} / ${_steps.length}', style: TextStyle( color: _isGame ? _gold : const Color(0xFF46555F), fontSize: 13, fontWeight: FontWeight.w700)), ], ), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (step.imageUrl != null) ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network(step.imageUrl!, height: 200, width: double.infinity, fit: BoxFit.cover), ), if (step.imageUrl != null) const SizedBox(height: 16), Text(title, style: TextStyle( color: _ink, fontSize: 22, fontWeight: FontWeight.w600, height: 1.15)), const SizedBox(height: 12), if (audioUrl != null) GuidedPathAudioPlayer( key: ValueKey('audio-${step.id}'), url: audioUrl, accentColor: _accent, isGame: _isGame, ), if (desc.isNotEmpty) HtmlWidget(desc, textStyle: TextStyle(color: _muted, fontSize: 15.5, height: 1.65)), if (hasGeo) ...[ const SizedBox(height: 16), _buildGeoIndicator(radius), ], ], ), ), ), _buildDetailBottomBar(hasQuiz), ], ), ), ), ); } Widget _buildGeoIndicator(int radius) { final String label; if (_inGeoZone) { label = 'Vous êtes dans la zone'; } else if (_distanceMeters != null) { label = 'À ${_formatDistance(_distanceMeters!)} de la zone ($radius m)'; } else { label = 'Zone à atteindre · $radius m'; } final color = _inGeoZone ? _green : _accent; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), color: color.withOpacity(_inGeoZone ? 0.14 : 0.12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.location_on, size: 14, color: color), const SizedBox(width: 6), Text(label, style: TextStyle(color: color, fontSize: 12.5)), ], ), ), if (_geoUnavailable) ...[ const SizedBox(height: 8), GestureDetector( onTap: _retryGeo, child: const Text('Localisation indisponible — réessayer', style: TextStyle( color: Color(0xFFD14343), fontSize: 12, decoration: TextDecoration.underline)), ), ], ], ); } Widget _buildDetailBottomBar(bool hasQuiz) { return Container( padding: EdgeInsets.fromLTRB(18, 12, 18, 18 + MediaQuery.of(context).padding.bottom), child: Row( children: [ if (hasQuiz) ...[ Expanded( child: OutlinedButton.icon( onPressed: _openChallenge, style: OutlinedButton.styleFrom( foregroundColor: _accent, backgroundColor: _isGame ? Colors.white.withOpacity(0.04) : Colors.white, side: BorderSide( color: _isGame ? _gold.withOpacity(0.4) : const Color(0xFFD4C9B8), width: 1.5), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), ), icon: Icon(Icons.star, size: 14, color: _accent), label: const Text('Voir le défi', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700)), ), ), const SizedBox(width: 10), ], Expanded( flex: 2, child: ElevatedButton( onPressed: _canAdvance ? () { setState(() => _showDetail = false); _advance(); } : null, style: ElevatedButton.styleFrom( backgroundColor: _accent, foregroundColor: _isGame ? const Color(0xFF1a1305) : Colors.white, disabledBackgroundColor: _accent.withOpacity(0.4), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 0, ), child: Text(_index < _steps.length - 1 ? 'Étape suivante' : 'Terminer', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700)), ), ), ], ), ); } void _showExpiredModal(String html) { showDialog( context: context, builder: (_) => Dialog( backgroundColor: _isGame ? const Color(0xFF0B1018) : Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), child: Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xFFD14343).withOpacity(0.14), ), child: const Icon(Icons.timer_off, size: 30, color: Color(0xFFD14343)), ), const SizedBox(height: 16), Text('Temps écoulé !', style: TextStyle(color: _ink, fontSize: 19, fontWeight: FontWeight.w700)), const SizedBox(height: 10), HtmlWidget(html, textStyle: TextStyle(color: _muted, fontSize: 14.5, height: 1.6)), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( backgroundColor: _accent, foregroundColor: _isGame ? const Color(0xFF1a1305) : Colors.white, padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), elevation: 0, ), child: const Text('Fermer', style: TextStyle(fontSize: 14.5, fontWeight: FontWeight.w700)), ), ), ], ), ), ), ); } }