import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; import 'package:mymuseum_visitapp/Components/SliderImages.dart'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/resourceModel.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_step_challenge.dart'; import 'package:mymuseum_visitapp/constants.dart'; /// Vue de progression centrée contenu (escape game, ou parcours sans carte). /// Miroir de `ProgressView` (non-carte) de visitapp-web. class GuidedPathContentProgressionPage extends StatefulWidget { final GuidedPathDTO path; final VisitAppContext visitAppContext; const GuidedPathContentProgressionPage({ Key? key, required this.path, required this.visitAppContext, }) : super(key: key); @override State createState() => _GuidedPathContentProgressionPageState(); } class _GuidedPathContentProgressionPageState extends State { late List _steps; int _index = 0; final Set _completedStepIds = {}; GuidedStepChallengeController? _challenge; bool _expiredShown = false; static const _gold = Color(0xFFD4AF37); bool get _isGame => widget.path.isGameMode == true; Color get _accent => _isGame ? _gold : kMainColor; Color get _bg => _isGame ? const Color(0xFF0B1018) : const Color(0xFFF6F3EE); Color get _ink => _isGame ? const Color(0xFFF4ECD8) : const Color(0xFF1E2A33); Color get _muted => _isGame ? const Color(0xFF8a8068) : const Color(0xFF6B7B86); 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)); _initStepState(); } @override void dispose() { _challenge?.removeListener(_onChallengeChanged); _challenge?.dispose(); super.dispose(); } void _initStepState() { _challenge?.removeListener(_onChallengeChanged); _challenge?.dispose(); _expiredShown = false; 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); } 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 get _canAdvance => _challenge?.canAdvance ?? true; 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() { 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 _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)), ), ), ], ), ), ), ); } 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(); // ferme l'écran de fin Navigator.of(context).pop(); // revient à la liste des parcours }, ), )); } String _stripHtml(String html) => html .replaceAll(RegExp(r'<[^>]*>'), ' ') .replaceAll(' ', ' ') .replaceAll(RegExp(r'\s+'), ' ') .trim(); @override Widget build(BuildContext context) { final step = _currentStep; if (step == null) { return const Scaffold(body: Center(child: Text('Aucune étape'))); } final challenge = _challenge!; final hasQuiz = challenge.hasQuiz; return Scaffold( backgroundColor: _bg, body: Stack( children: [ SafeArea( child: Column( children: [ _buildHeader(step), Expanded(child: _buildContent(step)), _buildBottomBar(hasQuiz), ], ), ), GuidedStepChallengeOverlay( controller: challenge, stepIndex: _index, accentColor: _accent, isGame: _isGame, visitAppContext: widget.visitAppContext, ), ], ), ); } Widget _buildHeader(GuidedStepDTO step) { return Container( padding: const EdgeInsets.fromLTRB(12, 12, 14, 12), color: _isGame ? const Color(0xFF0B1018) : const Color(0xFFF6F3EE), child: Row( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), 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 : const Color(0xFF2E9E6B)) : i == _index ? _accent : (_isGame ? Colors.white.withOpacity(0.1) : const Color(0xFFE2DCD2)), ), ), ); }), ), ), const SizedBox(width: 10), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(11), color: _isGame ? Colors.white.withOpacity(0.08) : Colors.white, ), child: Text('${_index + 1} / ${_steps.length}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: _isGame ? _gold : const Color(0xFF46555F))), ), ], ), ); } Widget _buildContent(GuidedStepDTO step) { final title = _translate(step.title); final desc = _translate(step.description); final hasContents = step.contents?.isNotEmpty == true; 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; }(); final contentResources = hasContents ? step.contents! .map((c) => ResourceModel( id: c.resourceId, source: c.resource?.url, type: ResourceType.Image)) .toList() : []; return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (hasContents) ClipRRect( borderRadius: BorderRadius.circular(16), child: SizedBox( height: 200, child: SliderImagesWidget( resources: contentResources, height: 200, contentsDTO: step.contents!, ), ), ) else if (step.imageUrl != null) ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network(step.imageUrl!, height: 200, width: double.infinity, fit: BoxFit.cover), ), if (hasContents || 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), Container( padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: _isGame ? _gold.withOpacity(0.12) : _accent.withOpacity(0.1), ), child: Text(_translate(widget.path.title).toUpperCase(), style: TextStyle( color: _accent, fontSize: 11.5, fontWeight: FontWeight.w700, letterSpacing: 0.3)), ), const SizedBox(height: 14), 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)), ], ), ); } Widget _buildBottomBar(bool hasQuiz) { return Container( padding: EdgeInsets.fromLTRB(18, 12, 18, 18 + MediaQuery.of(context).padding.bottom), color: _bg, child: Row( children: [ if (_index > 0 && !(widget.path.isLinear ?? false)) ...[ GestureDetector( onTap: _goBack, child: Container( width: 50, height: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), color: _isGame ? Colors.white.withOpacity(0.06) : Colors.white, border: Border.all( color: _isGame ? _gold.withOpacity(0.3) : const Color(0xFFE2DCD2), width: 1.5), ), child: Icon(Icons.chevron_left, color: _isGame ? _gold : const Color(0xFF54636E)), ), ), const SizedBox(width: 10), ], 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: 16, 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 ? _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)), ), ), ], ), ); } }