import 'package:flutter/material.dart'; import 'package:manager_app/Components/multi_string_input_and_resource_container.dart'; import 'package:manager_app/Components/number_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/l10n/app_localizations.dart'; const _kGameMessageResourceTypes = [ ResourceType.Image, ResourceType.ImageUrl, ResourceType.VideoUrl, ResourceType.Video, ResourceType.Audio, ]; /// Gabarit C : les champs à gauche, le canevas de travail à droite. /// /// Le canevas montre le découpage réel de l'image en `rows × cols` — c'est la /// seule décision de cet écran qu'un formulaire ne montre pas. /// /// ⚠️ Le `TabBar` d'avant n'en était pas un : ses deux onglets construisaient /// exactement le même formulaire, ils ne servaient qu'à écrire `gameType`. /// C'est donc un choix à deux valeurs, et il tenait dans un `SizedBox(height: 650)` /// pour un formulaire de deux cents pixels. class GameConfig extends StatefulWidget { final String? color; final String? label; final GameDTO initialValue; final ValueChanged onChanged; const GameConfig({ Key? key, this.color, this.label, required this.initialValue, required this.onChanged, }) : super(key: key); @override _GameConfigState createState() => _GameConfigState(); } class _GameConfigState extends State { late GameDTO gameDTO; @override void initState() { gameDTO = widget.initialValue; _applyDefaults(); super.initState(); } @override void didUpdateWidget(GameConfig oldWidget) { super.didUpdateWidget(oldWidget); if (widget.initialValue.id != oldWidget.initialValue.id) { setState(() { gameDTO = widget.initialValue; _applyDefaults(); }); } } void _applyDefaults() { gameDTO.rows = gameDTO.rows ?? 3; gameDTO.cols = gameDTO.cols ?? 3; gameDTO.gameType = gameDTO.gameType ?? GameTypes.Puzzle; gameDTO.messageDebut = gameDTO.messageDebut ?? []; gameDTO.messageFin = gameDTO.messageFin ?? []; } void _update(VoidCallback change) { setState(change); widget.onChanged(gameDTO); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final fields = _buildFields(context); final canvas = _buildCanvas(context); if (constraints.maxWidth < 780) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [fields, const SizedBox(height: kSpace6), canvas], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: fields), const SizedBox(width: kSpace6), SizedBox(width: 280, child: canvas), ], ); }, ); } Widget _buildFields(BuildContext context) { final l = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(l.gameTypeLabel, style: kLabelField), const SizedBox(height: kSpace2), Row( children: [ _typeChip(GameTypes.Puzzle, Icons.extension, l.puzzle), const SizedBox(width: kSpace3), _typeChip(GameTypes.values[1], Icons.grid_on, l.slidingPuzzle), ], ), const SizedBox(height: kSpace6), ResourceInputContainer( label: l.puzzleImageLabel, initialValue: gameDTO.puzzleImageId ?? '', onChanged: (ResourceDTO resourceDTO) { _update(() { if (resourceDTO.id == null) { gameDTO.puzzleImageId = null; gameDTO.puzzleImage = null; } else { gameDTO.puzzleImageId = resourceDTO.id; gameDTO.puzzleImage = resourceDTO; } }); }, ), const SizedBox(height: kSpace6), Row( children: [ SizedBox( width: 120, child: NumberInputContainer( label: l.puzzleRowsLabel, initialValue: gameDTO.rows ?? 3, isSmall: true, onChanged: (String value) { _update(() => gameDTO.rows = int.tryParse(value) ?? 3); }, ), ), const SizedBox(width: kSpace5), SizedBox( width: 120, child: NumberInputContainer( label: l.puzzleColsLabel, initialValue: gameDTO.cols ?? 3, isSmall: true, onChanged: (String value) { _update(() => gameDTO.cols = int.tryParse(value) ?? 3); }, ), ), ], ), const SizedBox(height: kSpace6), MultiStringInputAndResourceContainer( label: l.startMessageLabel, modalLabel: l.startMessageModalLabel, initialValue: gameDTO.messageDebut ?? [], resourceTypes: _kGameMessageResourceTypes, onGetResult: (value) => _update(() => gameDTO.messageDebut = value), maxLines: 1, isTitle: false, ), const SizedBox(height: kSpace5), MultiStringInputAndResourceContainer( label: l.endMessageLabel, modalLabel: l.endMessageModalLabel, initialValue: gameDTO.messageFin ?? [], resourceTypes: _kGameMessageResourceTypes, onGetResult: (value) => _update(() => gameDTO.messageFin = value), maxLines: 1, isTitle: false, ), ], ); } Widget _typeChip(GameTypes type, IconData icon, String label) { final isSelected = gameDTO.gameType == type; return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(kRadiusPill), onTap: () => _update(() => gameDTO.gameType = type), child: Container( padding: const EdgeInsets.symmetric( horizontal: kSpace5, vertical: kSpace3), decoration: BoxDecoration( color: isSelected ? kSurface3 : kSurface, border: Border.all(color: isSelected ? kPrimaryColor : kLine), borderRadius: BorderRadius.circular(kRadiusPill), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 15, color: isSelected ? kPrimaryColor : kInk3), const SizedBox(width: kSpace2), Text(label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: isSelected ? kPrimaryColor : kInk2)), ], ), ), ), ); } Widget _buildCanvas(BuildContext context) { final l = AppLocalizations.of(context)!; final rows = gameDTO.rows ?? 3; final cols = gameDTO.cols ?? 3; final url = gameDTO.puzzleImage?.url; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(l.puzzlePreviewLabel, style: kLabelField), const SizedBox(height: kSpace2), AspectRatio( aspectRatio: 4 / 3, child: Container( decoration: BoxDecoration( color: kGround, border: Border.all(color: kLine), borderRadius: BorderRadius.circular(kRadiusCard), ), clipBehavior: Clip.antiAlias, child: url == null || url.isEmpty ? Padding( padding: const EdgeInsets.all(kSpace6), child: Center( child: Text(l.puzzleNoImage, textAlign: TextAlign.center, style: kTextHint), ), ) : Stack( fit: StackFit.expand, children: [ Image.network(url, fit: BoxFit.cover), CustomPaint(painter: _PuzzleGridPainter(rows, cols)), ], ), ), ), const SizedBox(height: kSpace2), Text(l.puzzlePieces(rows, cols, rows * cols), style: kTextHint), ], ); } } class _PuzzleGridPainter extends CustomPainter { _PuzzleGridPainter(this.rows, this.cols); final int rows; final int cols; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = kWhite.withValues(alpha: 0.85) ..strokeWidth = 1; for (var col = 1; col < cols; col++) { final x = size.width * col / cols; canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); } for (var row = 1; row < rows; row++) { final y = size.height * row / rows; canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } } @override bool shouldRepaint(covariant _PuzzleGridPainter oldDelegate) => oldDelegate.rows != rows || oldDelegate.cols != cols; }