manager-app/lib/Screens/Configurations/listView_card_section.dart

347 lines
12 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,
}) : super(key: key);
final SectionDTO section;
final int order;
final VoidCallback onTap;
final Widget? dragHandle;
final bool isDropTarget;
@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 (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),
),
),
],
),
],
),
),
],
),
),
),
),
);
}
}
/// 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,
}) : 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;
@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),
),
),
),
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;
}