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: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. 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) { return ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: _buildLinkList(context, ref), ); } 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 { // In normal mode, open 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); } } } /// 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; IconData _getIconData(String iconName) { // Map common icon names to Material icons return switch (iconName.toLowerCase()) { 'home' => Icons.home, 'movie' => Icons.movie, 'chat' => Icons.chat, 'search' => Icons.search, 'videogame_asset' => Icons.videogame_asset, 'code' => Icons.code, 'terminal' => Icons.terminal, 'monitoring' => Icons.show_chart, 'dns' => Icons.dns, 'public' => Icons.public, 'api' => Icons.api, 'settings' => Icons.settings, 'storage' => Icons.storage, 'cloud' => Icons.cloud, 'security' => Icons.security, 'person' => Icons.person, 'folder' => Icons.folder, 'link' => Icons.link, _ => Icons.link, }; } @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'), ), ], ), ), ); } }