tablet-app-new/lib/Screens/Game/puzzle_piece.dart
Thomas Fransolet 9f582dd8a7 Lot K : tablet-app reconstruit et rebranché sur le contrat Postgres v3
Premier vrai build depuis avril. Le diagnostic des docs — « un seul défaut,
purement Gradle, peut-être réglé par 5a6701d » — tenait parce que personne
n'avait lancé la commande.

K1 — dix crans d'outillage, chacun dicté par l'erreur du précédent :
Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 1.9.0 → 2.3.10,
enableUncompressedNativeLibs retirée (supprimée en AGP 8.1), jcenter → mavenCentral,
heap 1536M → 4096M, Jetifier coupé, et retrait du resolutionStrategy qui forçait
androidx.lifecycle à 2.4.0 « to fix mapbox issue » : il réglait un problème d'il y
a trois ans et causait celui d'aujourd'hui, mapbox_maps_flutter 2.21.1 appelant
setViewTreeLifecycleOwner. Les trois derniers crans étaient de simples alignements
sur mymuseum-visitapp, qui utilise le même plugin sans ces problèmes.

Le bloc buildscript commenté de android/build.gradle est supprimé : il déclarait
une config qui n'était pas celle appliquée et a fait conclure deux fois à tort.

K2 — les 132 erreurs Dart que le Gradle masquait. Trois causes : roundedValue,
isDate, isHour, isSectionImageBackground et screenPercentageSectionsMainPage ont
migré vers AppConfigurationLink ; GeoPointDTO.latitude/longitude sont devenus
geometry ; le reste en découlait. Deux bugs latents trouvés au passage :

- applicationInstanceDTO n'était assigné que si l'instance avait l'IA — il servait
  de drapeau d'assistant. Ce DTO portant désormais les AppConfigurationLink, les
  cinq réglages seraient retombés sur leurs défauts sur MDLF et le Fort, sans
  erreur ni log. Drapeau rendu explicite, en préservant le ET instance × canal.
- ConfigurationDTO.isTablet ayant disparu, le filtre des configurations tablette
  n'avait plus de source : il porte sur les liens du canal AppType.Tablet.

La formule de clé par coordonnées de geo_point_filter, dupliquée à quatre endroits
alors que les deux côtés de l'appariement doivent produire la même valeur, ne vit
plus qu'à un seul (Helpers/geo.dart). Ce fichier documente aussi les deux
conventions de coordonnées du projet : [lng, lat] pour les GeoPoint, [lat, lng]
pour les géométries de GuidedStep.

K4 — l'écran Game repris de mymuseum-visitapp, Screens/Puzzle supprimé. La tablette
gagne le puzzle glissant, le bouton d'indice et un dimensionnement au ratio de
l'image. Les couleurs, codées en dur par flavor client chez mymuseum, sont dérivées
de configuration.primaryColor : tablet-app n'a pas de flavor, un seul APK sert tous
les clients.

Reste à vérifier sur device : le rendu de l'écran Game, et l'écran de sélection de
configuration, qui sera vide si les configurations ne sont pas rattachées au canal
tablette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:18:07 +02:00

246 lines
6.6 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class PuzzlePiece extends StatefulWidget {
final CachedNetworkImage image;
final Size imageSize;
final int row;
final int col;
final int maxRow;
final int maxCol;
final Function bringToTop;
final Function sendToBack;
final double initialTop;
final double initialLeft;
PuzzlePiece({
Key? key,
required this.image,
required this.imageSize,
required this.row,
required this.col,
required this.maxRow,
required this.maxCol,
required this.bringToTop,
required this.sendToBack,
required this.initialTop,
required this.initialLeft,
}) : super(key: key);
@override
_PuzzlePieceState createState() => _PuzzlePieceState();
}
class _PuzzlePieceState extends State<PuzzlePiece> {
// the piece initial top offset
double? top;
// the piece initial left offset
double? left;
// can we move the piece ?
bool isMovable = true;
GlobalKey _widgetPieceKey = GlobalKey();
@override
void initState() {
super.initState();
top = widget.initialTop;
left = widget.initialLeft;
if (widget.row == 0 && widget.col == 0) {
isMovable = false;
top = 0;
left = 0;
}
}
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
duration: isMovable ? Duration.zero : const Duration(milliseconds: 300),
curve: Curves.easeOutBack,
top: top,
left: left,
width: widget.imageSize.width,
child: AnimatedScale(
duration: const Duration(milliseconds: 300),
scale: isMovable ? 1.05 : 1.0,
child: Container(
key: _widgetPieceKey,
decoration: widget.col == 0 && widget.row == 0 ? BoxDecoration(
border: Border.all(
color: Colors.black.withOpacity(0.3),
width: 0.5,
),
) : null,
child: GestureDetector(
onTap: () {
if (isMovable) {
widget.bringToTop(widget);
}
},
onPanStart: (_) {
if (isMovable) {
widget.bringToTop(widget);
}
},
onPanUpdate: (dragUpdateDetails) {
if (isMovable) {
setState(() {
top = top! + dragUpdateDetails.delta.dy;
left = left! + dragUpdateDetails.delta.dx;
// Target position is (0,0) relative to its original offset in the image
if (top!.abs() < 15 && left!.abs() < 15) {
setState(() {
top = 0;
left = 0;
isMovable = false;
});
widget.sendToBack(widget);
}
});
}
},
child: PhysicalShape(
clipper: PuzzlePieceClipper(
widget.row, widget.col, widget.maxRow, widget.maxCol),
elevation: isMovable ? 4.0 : 0.0,
color: Colors.transparent,
shadowColor: Colors.black.withOpacity(0.5),
child: ClipPath(
child: CustomPaint(
foregroundPainter: PuzzlePiecePainter(
widget.row, widget.col, widget.maxRow, widget.maxCol),
child: widget.image),
clipper: PuzzlePieceClipper(
widget.row, widget.col, widget.maxRow, widget.maxCol),
),
),
),
),
),
);
}
}
// this class is used to clip the image to the puzzle piece path
class PuzzlePieceClipper extends CustomClipper<Path> {
final int row;
final int col;
final int maxRow;
final int maxCol;
PuzzlePieceClipper(this.row, this.col, this.maxRow, this.maxCol);
@override
Path getClip(Size size) {
return getPiecePath(size, row, col, maxRow, maxCol);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
// this class is used to draw a border around the clipped image
class PuzzlePiecePainter extends CustomPainter {
final int row;
final int col;
final int maxRow;
final int maxCol;
PuzzlePiecePainter(this.row, this.col, this.maxRow, this.maxCol);
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.black.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
canvas.drawPath(getPiecePath(size, row, col, maxRow, maxCol), paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
// this is the path used to clip the image and, then, to draw a border around it; here we actually draw the puzzle piece
Path getPiecePath(Size size, int row, int col, int maxRow, int maxCol) {
final width = size.width / maxCol;
final height = size.height / maxRow;
final offsetX = col * width;
final offsetY = row * height;
final bumpSize = height / 4;
var path = Path();
path.moveTo(offsetX, offsetY);
if (row == 0) {
// top side piece
path.lineTo(offsetX + width, offsetY);
} else {
// top bump
path.lineTo(offsetX + width / 3, offsetY);
path.cubicTo(
offsetX + width / 6,
offsetY - bumpSize,
offsetX + width / 6 * 5,
offsetY - bumpSize,
offsetX + width / 3 * 2,
offsetY);
path.lineTo(offsetX + width, offsetY);
}
if (col == maxCol - 1) {
// right side piece
path.lineTo(offsetX + width, offsetY + height);
} else {
// right bump
path.lineTo(offsetX + width, offsetY + height / 3);
path.cubicTo(
offsetX + width - bumpSize,
offsetY + height / 6,
offsetX + width - bumpSize,
offsetY + height / 6 * 5,
offsetX + width,
offsetY + height / 3 * 2);
path.lineTo(offsetX + width, offsetY + height);
}
if (row == maxRow - 1) {
// bottom side piece
path.lineTo(offsetX, offsetY + height);
} else {
// bottom bump
path.lineTo(offsetX + width / 3 * 2, offsetY + height);
path.cubicTo(
offsetX + width / 6 * 5,
offsetY + height - bumpSize,
offsetX + width / 6,
offsetY + height - bumpSize,
offsetX + width / 3,
offsetY + height);
path.lineTo(offsetX, offsetY + height);
}
if (col == 0) {
// left side piece
path.close();
} else {
// left bump
path.lineTo(offsetX, offsetY + height / 3 * 2);
path.cubicTo(
offsetX - bumpSize,
offsetY + height / 6 * 5,
offsetX - bumpSize,
offsetY + height / 6,
offsetX,
offsetY + height / 3);
path.close();
}
return path;
}