289 lines
9.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>[
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<GameDTO> 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<GameConfig> {
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;
}