289 lines
9.0 KiB
Dart
289 lines
9.0 KiB
Dart
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;
|
||
}
|