diff --git a/mobile/lib/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart index 66c73746f1..814c706df0 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart @@ -169,77 +169,185 @@ class _ReorderableGrid extends StatefulWidget { class _ReorderableGridState extends State<_ReorderableGrid> { int? _draggingIndex; - int? _hoveringIndex; + late List _itemOrder; + + @override + void initState() { + super.initState(); + _itemOrder = List.generate(widget.items.length, (index) => index); + } + + @override + void didUpdateWidget(_ReorderableGrid oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.items.length != widget.items.length) { + _itemOrder = List.generate(widget.items.length, (index) => index); + } + } + + void _updateHover(int draggedIndex, int targetIndex) { + if (draggedIndex == targetIndex || _draggingIndex == null) return; + + setState(() { + // Temporarily reorder for visual feedback + final newOrder = List.from(_itemOrder); + final draggedOrderIndex = newOrder.indexOf(draggedIndex); + final targetOrderIndex = newOrder.indexOf(targetIndex); + + newOrder.removeAt(draggedOrderIndex); + newOrder.insert(targetOrderIndex, draggedIndex); + _itemOrder = newOrder; + }); + } @override Widget build(BuildContext context) { - return GridView.builder( - controller: widget.scrollController, - physics: widget.shouldScroll ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount, - crossAxisSpacing: widget.crossAxisSpacing, - mainAxisSpacing: widget.mainAxisSpacing, - childAspectRatio: widget.childAspectRatio, - ), - itemCount: widget.items.length, - itemBuilder: (context, index) { - final item = widget.items[index]; - final isDragging = _draggingIndex == index; - final isHovering = _hoveringIndex == index; - - return DragTarget( - onWillAcceptWithDetails: (details) { - if (details.data != index) { - setState(() => _hoveringIndex = index); - } - return details.data != index; - }, - onLeave: (_) { - setState(() => _hoveringIndex = null); - }, - onAcceptWithDetails: (details) { - final oldIndex = details.data; - if (oldIndex != index) { - widget.onReorder(oldIndex, index); - } - setState(() { - _hoveringIndex = null; - _draggingIndex = null; - }); - }, - builder: (context, candidateData, rejectedData) { - return LongPressDraggable( - data: index, - feedback: Material( - color: Colors.transparent, + return LayoutBuilder( + builder: (context, constraints) { + final tileWidth = + (constraints.maxWidth - (widget.crossAxisSpacing * (widget.crossAxisCount - 1))) / widget.crossAxisCount; + final tileHeight = tileWidth / widget.childAspectRatio; + final rows = (_itemOrder.length / widget.crossAxisCount).ceil(); + final totalHeight = rows * tileHeight + (rows - 1) * widget.mainAxisSpacing; + + return SingleChildScrollView( + controller: widget.scrollController, + physics: widget.shouldScroll ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(), + child: SizedBox( + width: constraints.maxWidth, + height: totalHeight, + child: Stack( + children: List.generate(widget.items.length, (index) { + final visualIndex = _itemOrder.indexOf(index); + final item = widget.items[index]; + final isDragging = _draggingIndex == index; + + // Calculate position + final row = visualIndex ~/ widget.crossAxisCount; + final col = visualIndex % widget.crossAxisCount; + final left = col * (tileWidth + widget.crossAxisSpacing); + final top = row * (tileHeight + widget.mainAxisSpacing); + + return _AnimatedGridItem( + key: ValueKey(index), + index: index, + item: item, + isDragging: isDragging, + tileWidth: tileWidth, + tileHeight: tileHeight, + left: left, + top: top, + onDragStarted: () { + setState(() => _draggingIndex = index); + }, + onDragUpdate: (draggedIndex, targetIndex) { + _updateHover(draggedIndex, targetIndex); + }, + onDragEnd: (draggedIndex, targetIndex) { + if (draggedIndex != targetIndex) { + final oldVisualIndex = _itemOrder.indexOf(draggedIndex); + final newVisualIndex = _itemOrder.indexOf(targetIndex); + widget.onReorder(oldVisualIndex, newVisualIndex); + } + setState(() { + _draggingIndex = null; + _itemOrder = List.generate(widget.items.length, (i) => i); + }); + }, + onDragCanceled: () { + setState(() { + _draggingIndex = null; + _itemOrder = List.generate(widget.items.length, (i) => i); + }); + }, + ); + }), + ), + ), + ); + }, + ); + } +} + +class _AnimatedGridItem extends StatelessWidget { + final int index; + final ActionButtonType item; + final bool isDragging; + final double tileWidth; + final double tileHeight; + final double left; + final double top; + final VoidCallback onDragStarted; + final Function(int draggedIndex, int targetIndex) onDragUpdate; + final Function(int draggedIndex, int targetIndex) onDragEnd; + final VoidCallback onDragCanceled; + + const _AnimatedGridItem({ + super.key, + required this.index, + required this.item, + required this.isDragging, + required this.tileWidth, + required this.tileHeight, + required this.left, + required this.top, + required this.onDragStarted, + required this.onDragUpdate, + required this.onDragEnd, + required this.onDragCanceled, + }); + + @override + Widget build(BuildContext context) { + return AnimatedPositioned( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + left: left, + top: top, + width: tileWidth, + height: tileHeight, + child: DragTarget( + onWillAcceptWithDetails: (details) { + if (details.data != index) { + onDragUpdate(details.data, index); + } + return details.data != index; + }, + onAcceptWithDetails: (details) { + onDragEnd(details.data, index); + }, + builder: (context, candidateData, rejectedData) { + Widget child = _QuickActionTile(index: index, type: item); + + if (isDragging) { + child = Opacity(opacity: 0.0, child: child); + } + + return Draggable( + data: index, + feedback: Material( + color: Colors.transparent, + child: SizedBox( + width: tileWidth, + height: tileHeight, child: Opacity( - opacity: 0.8, + opacity: 0.9, child: Transform.scale( - scale: 1.1, + scale: 1.05, child: _QuickActionTile(index: index, type: item), ), ), ), - childWhenDragging: Opacity( - opacity: 0.3, - child: _QuickActionTile(index: index, type: item), - ), - onDragStarted: () { - setState(() => _draggingIndex = index); - }, - onDragEnd: (_) { - setState(() => _draggingIndex = null); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - transform: isHovering && !isDragging ? Matrix4.translationValues(0, -4, 0) : Matrix4.identity(), - child: _QuickActionTile(index: index, type: item), - ), - ); - }, - ); - }, + ), + childWhenDragging: const SizedBox.shrink(), + onDragStarted: onDragStarted, + onDragEnd: (_) => onDragCanceled(), + onDraggableCanceled: (_, __) => onDragCanceled(), + child: child, + ); + }, + ), ); } }