import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_state.dart'; class SearchResult { final String type; final String id; final String title; final String subtitle; final IconData icon; final dynamic data; SearchResult({ required this.type, required this.id, required this.title, required this.subtitle, required this.icon, required this.data, }); } class GlobalSearchWidget extends StatefulWidget { final bool isOpen; final VoidCallback onClose; final Function(SearchResult) onResultSelected; const GlobalSearchWidget({ super.key, required this.isOpen, required this.onClose, required this.onResultSelected, }); @override State createState() => _GlobalSearchWidgetState(); } class _GlobalSearchWidgetState extends State { final TextEditingController _searchController = TextEditingController(); List _searchResults = []; bool _isSearching = false; @override void dispose() { _searchController.dispose(); super.dispose(); } void _performSearch(String query) { if (query.isEmpty) { setState(() { _searchResults = []; _isSearching = false; }); return; } setState(() { _isSearching = true; }); final appState = Provider.of(context, listen: false); final results = []; // Search beans for (final bean in appState.beans) { if (_matchesQuery(bean.name, query) || _matchesQuery(bean.varietal, query) || _matchesQuery(bean.originCountry?.id ?? '', query) || _matchesQuery(bean.roastLevel.name, query)) { results.add(SearchResult( type: 'Bean', id: bean.id, title: bean.name, subtitle: '${bean.varietal} • ${bean.originCountry?.id ?? 'Unknown'}', icon: Icons.coffee, data: bean, )); } } // Search machines for (final machine in appState.machines) { if (_matchesQuery(machine.model, query) || _matchesQuery(machine.type.name, query) || _matchesQuery(machine.manufacturer, query)) { results.add(SearchResult( type: 'Machine', id: machine.id, title: machine.model, subtitle: '${machine.manufacturer} • ${machine.type.name}', icon: Icons.kitchen, data: machine, )); } } // Search recipes for (final recipe in appState.recipes) { if (_matchesQuery(recipe.name, query) || _matchesQuery(recipe.brewMethod.name, query) || _matchesQuery(recipe.instructions, query) || _matchesQuery(recipe.notes ?? '', query)) { results.add(SearchResult( type: 'Recipe', id: recipe.id, title: recipe.name, subtitle: '${recipe.brewMethod.name} • ${recipe.grindSize}', icon: Icons.menu_book, data: recipe, )); } } // Search journal entries for (final entry in appState.journalEntries) { if (_matchesQuery(entry.drink.name, query) || _matchesQuery(entry.notes ?? '', query) || _matchesQuery(entry.mood ?? '', query) || _matchesQuery(entry.weather ?? '', query)) { results.add(SearchResult( type: 'Journal', id: entry.id, title: entry.drink.name, subtitle: 'Rating: ${entry.drink.rating}/5 • ${_formatDate(entry.date)}', icon: Icons.book, data: entry, )); } } // Sort results by relevance (exact matches first) results.sort((a, b) { final aExact = a.title.toLowerCase() == query.toLowerCase(); final bExact = b.title.toLowerCase() == query.toLowerCase(); if (aExact && !bExact) return -1; if (!aExact && bExact) return 1; return a.title.compareTo(b.title); }); setState(() { _searchResults = results; _isSearching = false; }); } bool _matchesQuery(String text, String query) { return text.toLowerCase().contains(query.toLowerCase()); } String _formatDate(DateTime date) { return '${date.day}/${date.month}/${date.year}'; } @override Widget build(BuildContext context) { if (!widget.isOpen) return const SizedBox.shrink(); return Container( color: Colors.black.withAlpha((0.6 * 255).toInt()), child: Center( child: Container( width: MediaQuery.of(context).size.width > 600 ? 600 : MediaQuery.of(context).size.width - 32, height: MediaQuery.of(context).size.height > 600 ? 600 : MediaQuery.of(context).size.height - 100, margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF2D2D2D), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF3A3A3A)), boxShadow: [ BoxShadow( color: Colors.black.withAlpha((0.5 * 255).toInt()), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( children: [ // Header Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: Color(0xFF3A3A3A)), ), ), child: Row( children: [ const Icon( Icons.search, color: Color(0xFFD4A574), size: 24, ), const SizedBox(width: 12), const Expanded( child: Text( 'Global Search', style: TextStyle( color: Color(0xFFF5F5DC), fontSize: 20, fontWeight: FontWeight.w600, ), ), ), IconButton( icon: const Icon( Icons.close, color: Color(0xFFF5F5DC), ), onPressed: widget.onClose, ), ], ), ), // Search input Padding( padding: const EdgeInsets.all(16), child: TextField( controller: _searchController, autofocus: true, style: const TextStyle(color: Color(0xFFF5F5DC)), onChanged: _performSearch, decoration: InputDecoration( hintText: 'Search beans, machines, recipes, journal entries...', hintStyle: const TextStyle(color: Color(0xFFD2B48C)), prefixIcon: const Icon( Icons.search, color: Color(0xFFD4A574), ), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon( Icons.clear, color: Color(0xFFD2B48C), ), onPressed: () { _searchController.clear(); _performSearch(''); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF3A3A3A)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFD4A574)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF3A3A3A)), ), filled: true, fillColor: const Color(0xFF1A1A1A), ), ), ), // Results Expanded( child: _buildSearchResults(), ), ], ), ), ), ); } Widget _buildSearchResults() { if (_isSearching) { return const Center( child: CircularProgressIndicator( color: Color(0xFFD4A574), ), ); } if (_searchController.text.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search, size: 64, color: Color(0xFF3A3A3A), ), SizedBox(height: 16), Text( 'Start typing to search...', style: TextStyle( color: Color(0xFFD2B48C), fontSize: 16, ), ), SizedBox(height: 8), Text( 'Search across beans, machines, recipes, and journal entries', style: TextStyle( color: Color(0xFF3A3A3A), fontSize: 14, ), textAlign: TextAlign.center, ), ], ), ); } if (_searchResults.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.search_off, size: 64, color: Color(0xFF3A3A3A), ), const SizedBox(height: 16), Text( 'No results found for "${_searchController.text}"', style: const TextStyle( color: Color(0xFFD2B48C), fontSize: 16, ), ), const SizedBox(height: 8), const Text( 'Try different keywords or check your spelling', style: TextStyle( color: Color(0xFF3A3A3A), fontSize: 14, ), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _searchResults.length, itemBuilder: (context, index) { final result = _searchResults[index]; return _buildSearchResultItem(result); }, ); } Widget _buildSearchResultItem(SearchResult result) { return Container( margin: const EdgeInsets.only(bottom: 8), child: Material( color: Colors.transparent, child: InkWell( onTap: () { widget.onResultSelected(result); widget.onClose(); }, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF3A3A3A)), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: const Color(0xFFD4A574).withAlpha((0.2 * 255).toInt()), borderRadius: BorderRadius.circular(8), ), child: Icon( result.icon, color: const Color(0xFFD4A574), size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: const Color(0xFF6F4E37), borderRadius: BorderRadius.circular(4), ), child: Text( result.type, style: const TextStyle( color: Color(0xFFF5F5DC), fontSize: 10, fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: 8), Expanded( child: Text( result.title, style: const TextStyle( color: Color(0xFFF5F5DC), fontSize: 16, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 4), Text( result.subtitle, style: const TextStyle( color: Color(0xFFD2B48C), fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const Icon( Icons.arrow_forward_ios, color: Color(0xFF3A3A3A), size: 16, ), ], ), ), ), ), ); } }