manager-app/lib/Screens/Configurations/listView_card_section.dart
Thomas Fransolet 0054122829 Carte : filtre par categorie, editeur de point, et corrections users/quotas
Regroupe la configuration de carte en deux blocs (Carte / Points d'interet),
ajoute le filtre par categorie et l'editeur de point geographique, et passe
les ecrans Users et les jauges de quota par AppLocalizations (FR/EN/NL).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 16:22:39 +02:00

405 lines
14 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/fetch_section_icon.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_api_new/api.dart';
/// Tuile d'une section dans la grille d'une configuration.
///
/// Elle faisait 150 × 150 dans une liste horizontale : carrée, trop grande, et
/// tout au centre. Format 16/9, badge d'ordre en haut à gauche, poignée de
/// réordonnancement en haut à droite, nom et type en dessous.
class SectionCard extends StatelessWidget {
const SectionCard({
Key? key,
required this.section,
required this.order,
required this.onTap,
this.dragHandle,
this.isDropTarget = false,
this.onToggleVisibility,
}) : super(key: key);
final SectionDTO section;
final int order;
final VoidCallback onTap;
final Widget? dragHandle;
final bool isDropTarget;
/// Null pour un utilisateur qui ne peut pas éditer : la tuile perd le bouton.
final VoidCallback? onToggleVisibility;
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final hasImage = section.imageSource != null && section.imageSource!.isNotEmpty;
final isHidden = section.isActive == false;
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(kRadiusCard),
onTap: onTap,
child: Opacity(
opacity: isHidden ? 0.5 : 1,
child: Container(
decoration: BoxDecoration(
color: kSurface,
border: Border.all(
color: isDropTarget ? kPrimaryColor : kLine,
width: isDropTarget ? 1.5 : 1,
),
borderRadius: BorderRadius.circular(kRadiusCard),
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
if (hasImage)
Image.network(section.imageSource!, fit: BoxFit.cover)
else
ColoredBox(
color: kGround,
child: Center(
child: Icon(getSectionIcon(section.type),
size: 26, color: kLine),
),
),
Positioned(
top: kSpace2,
left: kSpace2,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: kSpace2, vertical: 1),
decoration: BoxDecoration(
color: kPrimaryColor,
borderRadius: BorderRadius.circular(kRadiusInput),
),
child: Text("$order",
style: const TextStyle(
color: kWhite,
fontSize: 11,
fontWeight: FontWeight.w600)),
),
),
if (isHidden)
Positioned(
bottom: kSpace2,
left: kSpace2,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: kSpace2, vertical: 1),
decoration: BoxDecoration(
color: kSurface,
border: Border.all(color: kLine),
borderRadius: BorderRadius.circular(kRadiusInput),
),
child: Text(l.sectionHiddenBadge,
style: const TextStyle(
fontSize: 10, color: kInk3)),
),
),
if (onToggleVisibility != null)
Positioned(
bottom: 2,
right: 2,
child: _OverlayIconButton(
icon: isHidden
? Icons.visibility_off
: Icons.visibility,
tooltip: isHidden
? l.sectionShowAction
: l.sectionHideAction,
onPressed: onToggleVisibility!,
),
),
if (dragHandle != null)
Positioned(top: 2, right: 2, child: dragHandle!),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(kSpace3, kSpace3, kSpace3, kSpace4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
section.label ?? "",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w500,
height: 1.25,
color: kInk),
),
const SizedBox(height: 3),
Row(
children: [
Icon(getSectionIcon(section.type),
size: 12, color: kInk3),
const SizedBox(width: kSpace1),
Expanded(
child: Text(
getSectionTypeName(l, section.type),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 10.5, color: kInk3),
),
),
],
),
],
),
),
],
),
),
),
),
);
}
}
/// Bouton posé sur l'image de la tuile : l'ombre porte le contraste, parce que
/// l'image derrière peut être de n'importe quelle couleur.
class _OverlayIconButton extends StatelessWidget {
const _OverlayIconButton({
required this.icon,
required this.tooltip,
required this.onPressed,
});
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(kRadiusInput),
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.all(kSpace1),
child: Icon(icon,
size: 17,
color: kWhite,
shadows: const [Shadow(color: kBlack, blurRadius: 3)]),
),
),
),
);
}
}
/// La grille de sections, réordonnable, terminée par la tuile « Ajouter ».
///
/// Partagée par les sections d'une configuration et par les sous-sections d'un
/// Menu — ce sont les mêmes objets, avec le même geste. `ReorderableListView`
/// ne sait pas faire de grille, d'où le `Draggable` sur la poignée et le
/// `DragTarget` sur chaque tuile.
class SectionGrid extends StatefulWidget {
const SectionGrid({
Key? key,
required this.sections,
required this.onReorder,
required this.onTap,
required this.onAdd,
this.onToggleVisibility,
}) : super(key: key);
final List<SectionDTO> sections;
final void Function(int oldIndex, int newIndex) onReorder;
final void Function(SectionDTO section, int index) onTap;
final VoidCallback onAdd;
final void Function(SectionDTO section)? onToggleVisibility;
@override
State<SectionGrid> createState() => _SectionGridState();
}
class _SectionGridState extends State<SectionGrid> {
int? dropTargetIndex;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
const spacing = kSpace3;
const minWidth = 146.0;
final columns = ((constraints.maxWidth + spacing) / (minWidth + spacing))
.floor()
.clamp(1, widget.sections.length + 1);
final width = (constraints.maxWidth - spacing * (columns - 1)) / columns;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: [
for (var index = 0; index < widget.sections.length; index++)
SizedBox(
width: width,
child: DragTarget<int>(
onWillAcceptWithDetails: (details) {
if (details.data == index) return false;
setState(() => dropTargetIndex = index);
return true;
},
onLeave: (_) => setState(() => dropTargetIndex = null),
onAcceptWithDetails: (details) {
setState(() => dropTargetIndex = null);
widget.onReorder(details.data, index);
},
builder: (context, candidate, rejected) => SectionCard(
section: widget.sections[index],
order: index + 1,
isDropTarget: dropTargetIndex == index,
dragHandle: SectionDragHandle(index: index, width: width),
onTap: () => widget.onTap(widget.sections[index], index),
onToggleVisibility: widget.onToggleVisibility == null
? null
: () => widget.onToggleVisibility!(widget.sections[index]),
),
),
),
SizedBox(
width: width,
child: AddSectionCard(onTap: widget.onAdd),
),
],
);
},
);
}
}
class SectionDragHandle extends StatelessWidget {
const SectionDragHandle({Key? key, required this.index, required this.width})
: super(key: key);
final int index;
final double width;
@override
Widget build(BuildContext context) {
return Draggable<int>(
data: index,
feedback: Container(
width: width,
height: width * 9 / 16,
decoration: BoxDecoration(
color: kPrimaryColor.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(kRadiusCard),
),
child: const Icon(Icons.drag_indicator, color: kWhite),
),
childWhenDragging: const SizedBox.shrink(),
child: const MouseRegion(
cursor: SystemMouseCursors.grab,
child: Padding(
padding: EdgeInsets.all(kSpace1),
child: Icon(Icons.drag_indicator,
size: 17,
color: kWhite,
shadows: [Shadow(color: kBlack, blurRadius: 3)]),
),
),
);
}
}
/// La tuile « Ajouter » prend la place du bouton vert flottant : elle est dans
/// la grille, à la fin, là où le regard finit de lire les sections.
class AddSectionCard extends StatelessWidget {
const AddSectionCard({Key? key, required this.onTap}) : super(key: key);
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(kRadiusCard),
onTap: onTap,
child: DottedBorderBox(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add, size: 22, color: kPrimaryColor),
const SizedBox(height: 3),
Text(
l.addSectionTile,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w500,
color: kPrimaryColor),
),
],
),
),
),
);
}
}
/// Cadre en pointillés — Flutter n'a pas de `border-style: dashed`, donc c'est
/// dessiné à la main.
class DottedBorderBox extends StatelessWidget {
const DottedBorderBox({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _DashedBorderPainter(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: kSpace3, vertical: kSpace5),
child: child,
),
);
}
}
class _DashedBorderPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = kBrandSoft
..style = PaintingStyle.stroke
..strokeWidth = 1;
final path = Path()
..addRRect(RRect.fromRectAndRadius(
Offset.zero & size, const Radius.circular(kRadiusCard)));
const dash = 4.0;
const gap = 3.0;
for (final metric in path.computeMetrics()) {
double distance = 0;
while (distance < metric.length) {
canvas.drawPath(
metric.extractPath(distance, distance + dash), paint);
distance += dash + gap;
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}