diff --git a/lib/features/front_hall/presentation/widgets/quick_links_panel.dart b/lib/features/front_hall/presentation/widgets/quick_links_panel.dart index 2dacef3..3bf60a9 100644 --- a/lib/features/front_hall/presentation/widgets/quick_links_panel.dart +++ b/lib/features/front_hall/presentation/widgets/quick_links_panel.dart @@ -1,3 +1,5 @@ +import 'dart:ui' show lerpDouble; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tatlock_ui/features/front_hall/domain/entities/quick_link.dart'; @@ -152,6 +154,9 @@ class _SettingsModeHeader extends StatelessWidget { } /// List of quick links grouped by category. +/// +/// In settings mode, shows a flat reorderable list. +/// In normal mode, shows grouped list with category headers. class _QuickLinksList extends ConsumerWidget { const _QuickLinksList({ required this.links, @@ -187,12 +192,72 @@ class _QuickLinksList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // In settings mode, show reorderable list + if (isSettingsMode) { + return _buildReorderableList(context, ref); + } + + // In normal mode, show grouped list return ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: _buildLinkList(context, ref), ); } + Widget _buildReorderableList(BuildContext context, WidgetRef ref) { + return ReorderableListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: links.length, + onReorder: (oldIndex, newIndex) => _handleReorder(ref, oldIndex, newIndex), + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) { + final elevation = lerpDouble(0, 8, animation.value) ?? 0; + return Material( + elevation: elevation, + borderRadius: BorderRadius.circular(8), + child: child, + ); + }, + child: child, + ); + }, + itemBuilder: (context, index) { + final link = links[index]; + return _ReorderableLinkTile( + key: ValueKey(link.id), + link: link, + index: index, + isSelected: link.id == selectedLinkId, + onTap: () => _handleLinkTap(ref, link), + onEdit: () => ref + .read(frontHallStateProvider.notifier) + .selectLink(link.id), + onDelete: () => _handleDelete(context, ref, link), + ); + }, + ); + } + + void _handleReorder(WidgetRef ref, int oldIndex, int newIndex) { + // Adjust for the removal + if (newIndex > oldIndex) { + newIndex -= 1; + } + + // Create new ordered list + final reorderedLinks = List.from(links); + final item = reorderedLinks.removeAt(oldIndex); + reorderedLinks.insert(newIndex, item); + + // Extract IDs in new order + final orderedIds = reorderedLinks.map((l) => l.id).toList(); + + // Call reorder API + ref.read(quickLinkActionsProvider.notifier).reorder(orderedIds); + } + List _buildLinkList(BuildContext context, WidgetRef ref) { final widgets = []; final showHeaders = _showCategoryHeaders; @@ -280,6 +345,138 @@ class _QuickLinksList extends ConsumerWidget { } } +/// Reorderable link tile with drag handle for settings mode. +class _ReorderableLinkTile extends StatelessWidget { + const _ReorderableLinkTile({ + super.key, + required this.link, + required this.index, + required this.isSelected, + required this.onTap, + required this.onEdit, + required this.onDelete, + }); + + final QuickLink link; + final int index; + final bool isSelected; + final VoidCallback onTap; + final VoidCallback onEdit; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: Material( + color: isSelected + ? colorScheme.primaryContainer.withValues(alpha: 0.4) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Row( + children: [ + // Drag handle + ReorderableDragStartListener( + index: index, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: Icon( + Icons.drag_indicator, + size: 20, + color: colorScheme.outline, + ), + ), + ), + const SizedBox(width: 8), + // Icon + Icon( + getIconData(link.iconName), + size: 20, + color: isSelected + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + // Name and subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + link.name, + style: textTheme.bodyMedium?.copyWith( + color: isSelected + ? colorScheme.primary + : colorScheme.onSurface, + fontWeight: isSelected ? FontWeight.w600 : null, + ), + ), + Text( + link.category ?? 'No category', + style: textTheme.bodySmall?.copyWith( + color: colorScheme.outline, + fontSize: 11, + ), + ), + ], + ), + ), + // Overflow menu + PopupMenuButton( + icon: Icon( + Icons.more_vert, + size: 18, + color: colorScheme.onSurfaceVariant, + ), + padding: EdgeInsets.zero, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Icon(Icons.edit, size: 18), + SizedBox(width: 8), + Text('Edit'), + ], + ), + ), + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete, size: 18), + SizedBox(width: 8), + Text('Delete'), + ], + ), + ), + ], + onSelected: (value) { + switch (value) { + case 'edit': + onEdit(); + case 'delete': + onDelete(); + } + }, + ), + ], + ), + ), + ), + ), + ); + } +} + /// Category header with left accent bar. class _CategoryHeader extends StatelessWidget { const _CategoryHeader({required this.title});