import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:myinfomate_layout/myinfomate_layout.dart'; const double _kGap = 8; /// Aperçu live de la grille bento (même placement dense que web/mobile en prod). /// Glisser le coin d'une card change ses spans (colonnes × lignes) ; le résultat /// snappe sur des cellules entières. [onSpanChanged] doit rester synchronisé avec /// le même état que le sélecteur de forme de la liste. /// Glisser la poignée (coin haut-gauche) d'une card sur une autre échange leur /// position dans la liste — le dense packing recalcule le reste automatiquement. class BentoPreviewGrid extends StatelessWidget { final List links; final int columns; final void Function(AppConfigurationLinkDTO link, int colSpan, int rowSpan) onSpanChanged; final void Function(String fromId, String toId) onSwap; const BentoPreviewGrid({ super.key, required this.links, required this.columns, required this.onSpanChanged, required this.onSwap, }); @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { final width = constraints.maxWidth; // Seuls les blocs actifs apparaissent réellement dans l'app — sinon le // switch "actif/inactif" de la liste n'aurait aucun effet sur l'aperçu. final activeLinks = links.where((l) => l.isActive == true).toList(); if (width <= 0 || activeLinks.isEmpty) return const SizedBox(); final cols = columns < 1 ? 1 : columns; final maxSpan = cols < 2 ? 1 : 2; final cellWidth = (width - (cols - 1) * _kGap) / cols; final cellHeight = cellWidth * 0.78; // Trié par `order` plutôt que par l'ordre du tableau reçu : la liste de // gauche (ReorderableCustomList) travaille sur sa propre copie interne et // ne réordonne que les DTO (leur champ `order`), jamais le tableau `links` // lui-même — sans ce tri, l'aperçu ignorerait le reorder de la liste. final orderedLinks = [...activeLinks]..sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); final result = bentoLayout( orderedLinks .map((l) => BentoItem( id: l.id!, colSpan: (l.gridColSpan ?? 1).clamp(1, maxSpan), rowSpan: (l.gridRowSpan ?? 1).clamp(1, 2), )) .toList(), cols, ); final totalHeight = result.rowCount * cellHeight + (result.rowCount - 1) * _kGap; final byId = {for (final l in activeLinks) l.id!: l}; return SizedBox( width: width, height: totalHeight < 0 ? 0 : totalHeight, child: Stack( children: result.placements.map((p) { final link = byId[p.id]!; return Positioned( left: p.col * (cellWidth + _kGap), top: p.row * (cellHeight + _kGap), width: p.colSpan * cellWidth + (p.colSpan - 1) * _kGap, height: p.rowSpan * cellHeight + (p.rowSpan - 1) * _kGap, child: DragTarget( onWillAcceptWithDetails: (details) => details.data != link.id, onAcceptWithDetails: (details) => onSwap(details.data, link.id!), builder: (context, candidateData, rejectedData) { return _BentoCard( link: link, cellWidth: cellWidth, cellHeight: cellHeight, maxSpan: maxSpan, isDropTarget: candidateData.isNotEmpty, onSpanChanged: (c, r) => onSpanChanged(link, c, r), ); }, ), ); }).toList(), ), ); }); } } class _BentoCard extends StatefulWidget { final AppConfigurationLinkDTO link; final double cellWidth; final double cellHeight; final int maxSpan; final bool isDropTarget; final void Function(int colSpan, int rowSpan) onSpanChanged; const _BentoCard({ required this.link, required this.cellWidth, required this.cellHeight, required this.maxSpan, required this.isDropTarget, required this.onSpanChanged, }); @override State<_BentoCard> createState() => _BentoCardState(); } class _BentoCardState extends State<_BentoCard> { double _accDx = 0; double _accDy = 0; int _startCol = 1; int _startRow = 1; @override Widget build(BuildContext context) { final link = widget.link; final hasImage = link.configuration?.imageSource != null; return Container( decoration: BoxDecoration( color: hasImage ? null : kPrimaryColor.withValues(alpha: 0.55), borderRadius: BorderRadius.circular(10), image: hasImage ? DecorationImage(image: NetworkImage(link.configuration!.imageSource!), fit: BoxFit.cover) : null, border: widget.isDropTarget ? Border.all(color: kPrimaryColor, width: 3) : null, ), child: Stack( children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.black.withValues(alpha: 0.7)], stops: const [0.4, 1.0], ), ), ), ), Positioned( bottom: 6, left: 8, right: 8, child: Text( link.configuration?.label ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600), ), ), // Poignée de déplacement — glisser sur une autre card échange leur position. // Coin opposé à la poignée de resize pour éviter tout conflit de geste. Positioned( left: 0, top: 0, child: Draggable( data: link.id, feedback: Material( color: Colors.transparent, child: Opacity( opacity: 0.85, child: SizedBox( width: widget.cellWidth, height: widget.cellHeight, child: _dragHandle(), ), ), ), child: _dragHandle(), ), ), // Poignée de coin — drag 2D : largeur = colonnes, hauteur = lignes. Positioned( right: 0, bottom: 0, child: MouseRegion( cursor: SystemMouseCursors.resizeDownRight, child: GestureDetector( onPanStart: (_) { _accDx = 0; _accDy = 0; _startCol = (link.gridColSpan ?? 1).clamp(1, widget.maxSpan); _startRow = (link.gridRowSpan ?? 1).clamp(1, 2); }, onPanUpdate: (details) { _accDx += details.delta.dx; _accDy += details.delta.dy; final newCol = (_startCol + (_accDx / (widget.cellWidth + _kGap)).round()) .clamp(1, widget.maxSpan); final newRow = (_startRow + (_accDy / (widget.cellHeight + _kGap)).round()) .clamp(1, 2); if (newCol != (link.gridColSpan ?? 1) || newRow != (link.gridRowSpan ?? 1)) { widget.onSpanChanged(newCol, newRow); } }, child: Container( width: 20, height: 20, margin: const EdgeInsets.all(3), decoration: BoxDecoration( color: kWhite, borderRadius: BorderRadius.circular(5), border: Border.all(color: kPrimaryColor, width: 2), ), child: Icon(Icons.open_in_full, size: 11, color: kPrimaryColor), ), ), ), ), ], ), ); } Widget _dragHandle() { return MouseRegion( cursor: SystemMouseCursors.grab, child: Container( width: 20, height: 20, margin: const EdgeInsets.all(3), decoration: BoxDecoration( color: kWhite, borderRadius: BorderRadius.circular(5), border: Border.all(color: kPrimaryColor, width: 2), ), child: Icon(Icons.drag_indicator, size: 11, color: kPrimaryColor), ), ); } }