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'; import 'package:tatlock_ui/features/front_hall/presentation/providers/front_hall_state_provider.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/quick_links_provider.dart'; import 'package:tatlock_ui/shared/layouts/widgets/panel_header.dart'; import 'package:tatlock_ui/shared/widgets/icon_picker.dart'; import 'package:url_launcher/url_launcher.dart'; /// Left-side panel for quick link navigation in Front Hall. /// /// Shows categorized links that can open in iframe or new tab. /// Adapts behavior based on current mode (normal vs settings). class QuickLinksPanel extends ConsumerWidget { const QuickLinksPanel({ super.key, this.width = 280, }); final double width; @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; final frontHallState = ref.watch(frontHallStateProvider); final quickLinksAsync = ref.watch(quickLinksProvider); final isSettingsMode = frontHallState.mode == FrontHallMode.settings; return SizedBox( width: width, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header changes based on mode if (isSettingsMode) _SettingsModeHeader( onBack: () => ref.read(frontHallStateProvider.notifier).exitSettings(), ) else PanelHeader( title: 'Quick Links', icon: Icons.link, dockToBottom: true, actions: [ IconButton( icon: const Icon(Icons.refresh, size: 20), tooltip: 'Refresh', onPressed: () => ref.invalidate(quickLinksProvider), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), // Add new link button (settings mode only) if (isSettingsMode) ...[ Padding( padding: const EdgeInsets.all(8), child: FilledButton.icon( onPressed: () => ref .read(frontHallStateProvider.notifier) .clearSelectedLink(), icon: const Icon(Icons.add, size: 18), label: const Text('Add New Link'), ), ), Divider(height: 1, color: colorScheme.outlineVariant), ], // Links list Expanded( child: quickLinksAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => _ErrorState( error: error, onRetry: () => ref.invalidate(quickLinksProvider), ), data: (links) { // Use default links if empty final displayLinks = links.isEmpty ? getDefaultQuickLinks() : links; return _QuickLinksList( links: displayLinks, selectedLinkId: frontHallState.selectedLinkId, isSettingsMode: isSettingsMode, ); }, ), ), // Settings button (normal mode only) if (!isSettingsMode) ...[ Divider(height: 1, color: colorScheme.outlineVariant), Padding( padding: const EdgeInsets.all(8), child: TextButton.icon( onPressed: () => ref .read(frontHallStateProvider.notifier) .enterSettings(), icon: const Icon(Icons.settings, size: 18), label: const Text('Settings'), ), ), ], ], ), ); } } /// Header for settings mode with back button. class _SettingsModeHeader extends StatelessWidget { const _SettingsModeHeader({required this.onBack}); final VoidCallback onBack; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, border: Border( bottom: BorderSide(color: colorScheme.outlineVariant), ), ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), tooltip: 'Back to Dashboard', onPressed: onBack, ), const SizedBox(width: 4), Icon(Icons.link, size: 20, color: colorScheme.primary), const SizedBox(width: 8), Text( 'Quick Links', style: textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), ], ), ); } } /// 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, required this.selectedLinkId, required this.isSettingsMode, }); final List links; final String? selectedLinkId; final bool isSettingsMode; /// Groups links by category, preserving order. List<(String?, List)> get _groupedLinks { final groups = >{}; final order = []; for (final link in links) { if (!groups.containsKey(link.category)) { groups[link.category] = []; order.add(link.category); } groups[link.category]!.add(link); } return order.map((category) => (category, groups[category]!)).toList(); } bool get _showCategoryHeaders { final categories = links.map((l) => l.category).where((c) => c != null).toSet(); return categories.length > 1; } @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; for (final (category, categoryLinks) in _groupedLinks) { // Add category header if multiple categories exist if (showHeaders && category != null) { widgets.add(_CategoryHeader(title: category)); } // Add links for (final link in categoryLinks) { widgets.add( _QuickLinkTile( link: link, isSelected: link.id == selectedLinkId, isSettingsMode: isSettingsMode, onTap: () => _handleLinkTap(ref, link), onEdit: isSettingsMode ? () => ref .read(frontHallStateProvider.notifier) .selectLink(link.id) : null, onDelete: isSettingsMode ? () => _handleDelete(context, ref, link) : null, ), ); } } return widgets; } void _handleLinkTap(WidgetRef ref, QuickLink link) { if (isSettingsMode) { // In settings mode, select link for editing ref.read(frontHallStateProvider.notifier).selectLink(link.id); } else { // Close any open content first to ensure clean state ref.read(frontHallStateProvider.notifier).showDashboard(); // Then open the new link if (link.isIframe) { ref .read(frontHallStateProvider.notifier) .showIframe(link.url, link.name); } else { // Open in new tab _openInNewTab(link.url); } } } Future _openInNewTab(String url) async { final uri = Uri.tryParse(url); if (uri != null && await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } } Future _handleDelete( BuildContext context, WidgetRef ref, QuickLink link, ) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete Link'), content: Text('Delete "${link.name}"?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: const Text('Delete'), ), ], ), ); if (confirmed == true) { await ref.read(quickLinkActionsProvider.notifier).delete(link.id); } } } /// 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}); final String title; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Container( margin: const EdgeInsets.only(top: 8, bottom: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: colorScheme.surfaceContainerHigh, border: Border( left: BorderSide( color: colorScheme.primary, width: 2, ), ), ), child: Text( title.toUpperCase(), style: textTheme.labelSmall?.copyWith( color: colorScheme.onSurfaceVariant, letterSpacing: 0.5, fontWeight: FontWeight.w600, ), ), ); } } /// Individual quick link tile. class _QuickLinkTile extends StatelessWidget { const _QuickLinkTile({ required this.link, required this.isSelected, required this.isSettingsMode, required this.onTap, this.onEdit, this.onDelete, }); final QuickLink link; final bool isSelected; final bool isSettingsMode; 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: 12, vertical: 10), child: Row( children: [ Icon( getIconData(link.iconName), size: 20, color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), const SizedBox(width: 12), 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, ), ), if (isSettingsMode) Text( link.isNewTab ? 'Opens in new tab' : 'Opens in iframe', style: textTheme.bodySmall?.copyWith( color: colorScheme.outline, fontSize: 11, ), ), ], ), ), // New tab indicator (normal mode) if (!isSettingsMode && link.isNewTab) Icon( Icons.open_in_new, size: 14, color: colorScheme.outline, ), // Overflow menu (settings mode) if (isSettingsMode) 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?.call(); case 'delete': onDelete?.call(); } }, ), ], ), ), ), ), ); } } /// Error state widget. class _ErrorState extends StatelessWidget { const _ErrorState({ required this.error, required this.onRetry, }); final Object error; final VoidCallback onRetry; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, color: colorScheme.error, size: 32, ), const SizedBox(height: 8), Text( 'Failed to load links', style: TextStyle(color: colorScheme.error), ), const SizedBox(height: 8), TextButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh, size: 16), label: const Text('Retry'), ), ], ), ), ); } }