import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../theme/app_theme.dart'; class ReactionPicker extends StatefulWidget { final Function(String) onReactionSelected; final VoidCallback? onClosed; final List? reactions; final Map? reactionCounts; final Set? myReactions; const ReactionPicker({ super.key, required this.onReactionSelected, this.onClosed, this.reactions, this.reactionCounts, this.myReactions, }); @override State createState() => _ReactionPickerState(); } class _ReactionPickerState extends State with SingleTickerProviderStateMixin { late TabController _tabController; int _currentTabIndex = 0; final TextEditingController _searchController = TextEditingController(); bool _isSearching = false; List _filteredReactions = []; // Dynamic reaction sets Map> _reactionSets = {}; Map _folderCredits = {}; List _tabOrder = []; bool _isLoading = true; @override void initState() { super.initState(); _searchController.addListener(_onSearchChanged); _loadReactionSets(); } Future _loadReactionSets() async { // Start with emoji set final reactionSets = >{ 'emoji': [ '❤️', '👍', '😂', '😮', '😢', '😡', '🎉', '🔥', '👏', '🙏', '💯', '🤔', '😍', '🤣', '😊', '👌', '🙌', '💪', '🎯', '⭐', '✨', '🌟', '💫', '☀️', ], }; final folderCredits = {}; final tabOrder = ['emoji']; // Start with emoji, will add folders after // Known reaction folders to check final knownFolders = ['dotto', 'green', 'blue', 'purple']; for (final folder in knownFolders) { try { // Try to load credit file final creditContent = await _loadCreditFile(folder); folderCredits[folder] = creditContent; // Try to load files from this folder final reactions = await _loadAllFilesFromFolder(folder); if (reactions.isNotEmpty) { reactionSets[folder] = reactions; tabOrder.add(folder); } } catch (e) { // Error loading folder, skip it } } setState(() { _reactionSets = reactionSets; _folderCredits = folderCredits; _tabOrder = tabOrder; _isLoading = false; // Create TabController after setting up the data _tabController = TabController(length: _tabOrder.length, vsync: this); _tabController.addListener(() { setState(() { _currentTabIndex = _tabController.index; _clearSearch(); }); }); }); } Future> _loadAllFilesFromFolder(String folder) async { final reactions = []; // Common file names to try (comprehensive list) final possibleFiles = [ // Basic reactions 'heart', 'thumbs_up', 'laugh', 'lol', 'wow', 'sad', 'angry', 'mad', 'party', 'fire', 'clap', 'pray', 'hundred', 'thinking', 'ok', // Face expressions 'smile', 'happy', 'grinning', 'beaming', 'wink', 'kiss', 'love', 'laughing', 'crying', 'tears', 'joy', 'giggle', 'chuckle', 'frown', 'worried', 'scared', 'fear', 'shock', 'surprised', 'confused', 'thinking_face', 'face_palm', 'eyeroll', // Extended face names 'laughing_face', 'beaming_face', 'face_with_tears', 'grinning_face', 'smiling_face', 'winking_face', 'melting_face', 'upside_down_face', 'rolling_face', 'slightly_smiling_face', 'smiling_face_with_halo', 'smiling_face_with_hearts', 'face_with_monocle', 'nerd_face', 'party_face', 'sunglasses_face', 'disappointed_face', 'worried_face', 'anguished_face', 'fearful_face', 'downcast_face', 'loudly_crying_face', // Special characters 'skull', 'ghost', 'robot', 'alien', 'monster', 'devil', 'angel', 'poop', 'vomit', 'sick', 'dizzy', 'sleeping', 'zzz', // Hearts and love 'heart_with_arrow', 'broken_heart', 'sparkling_heart', 'two_hearts', 'revolving_hearts', 'heart_eyes', 'kissing_heart', // Colored hearts 'green_heart', 'blue_heart', 'purple_heart', 'yellow_heart', 'black_heart', 'white_heart', 'brown_heart', 'orange_heart', // Actions and objects 'thumbs_down', 'ok_hand', 'peace', 'victory', 'rock_on', 'call_me', 'point_up', 'point_down', 'point_left', 'point_right', 'raised_hand', 'wave', 'clap', 'high_five', 'pray', 'namaste', // Nature and elements 'fire', 'water', 'earth', 'air', 'lightning', 'storm', 'rainbow', 'sun', 'moon', 'star', 'cloud', 'tree', 'flower', 'leaf', // Food and drink 'pizza', 'burger', 'taco', 'ice_cream', 'coffee', 'tea', 'beer', 'wine', 'cocktail', 'cake', 'cookie', 'candy', 'chocolate', // Animals 'dog', 'cat', 'mouse', 'rabbit', 'bear', 'lion', 'tiger', 'elephant', 'monkey', 'bird', 'fish', 'butterfly', 'spider', 'snake', // Objects and symbols 'bomb', 'knife', 'gun', 'pistol', 'sword', 'shield', 'crown', 'gem', 'diamond', 'money', 'coin', 'dollar', 'gift', 'present', // Numbers and symbols 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'zero', 'plus', 'minus', 'multiply', 'divide', 'equal', 'check', 'x', // Common variations with underscores 'thumbs_up', 'thumbs_down', 'middle_finger', 'rock_on', 'peace_sign', 'ok_sign', 'victory_sign', 'call_me_hand', 'raised_hand', 'wave_hand', // Emoji-style names 'face_with_open_mouth', 'face_with_closed_eyes', 'face_with_tears_of_joy', 'grinning_face_with_big_eyes', 'grinning_face_with_smiling_eyes', 'grinning_face_with_sweat', 'grinning_squinting_face', 'hugging_face', 'face_with_head_bandage', 'face_with_thermometer', 'face_with_bandage', 'nauseated_face', 'sneezing_face', 'yawning_face', 'face_with_cowboy_hat', // More descriptive names 'party_popper', 'confetti_ball', 'balloon', 'ribbon', 'gift_ribbon', 'birthday_cake', 'wedding_cake', 'christmas_tree', 'pumpkin', 'ghost_halloween', // Simple variations 'like', 'dislike', 'love', 'hate', 'yes', 'no', 'maybe', 'idk', 'cool', 'hot', 'cold', 'warm', 'fresh', 'old', 'new', 'classic', // Tech and modern 'computer', 'phone', 'camera', 'video', 'music', 'game', 'controller', 'mouse', 'keyboard', 'screen', 'monitor', 'laptop', 'tablet', // Expressions 'lol', 'lmao', 'rofl', 'omg', 'wtf', 'smh', 'idc', 'ngl', 'fr', 'tbh', 'iykyk', 'rn', 'asap', 'fyi', 'btw', 'imo', 'imho', ]; // Try both PNG and SVG extensions for each possible file name for (final fileName in possibleFiles) { for (final extension in ['png', 'svg']) { final fullPath = 'reactions/$folder/$fileName.$extension'; try { // Try to load the file to check if it exists await rootBundle.load(fullPath); final assetPath = 'assets/$fullPath'; reactions.add(assetPath); break; // Found this file, don't try other extensions } catch (e) { // File doesn't exist, try next extension } } } return reactions; } Future _loadCreditFile(String folder) async { try { final creditData = await rootBundle.loadString('reactions/$folder/credit.md'); return creditData; } catch (e) { // Return default credit if file not found return '# $folder Reaction Set\n\nCustom reaction set for Sojorn'; } } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } void _clearSearch() { _searchController.clear(); setState(() { _isSearching = false; _filteredReactions = []; }); } void _onSearchChanged() { final query = _searchController.text.toLowerCase(); if (query.isEmpty) { setState(() { _isSearching = false; _filteredReactions = []; }); } else { setState(() { _isSearching = true; _filteredReactions = _filterReactions(query); }); } } List _filterReactions(String query) { final reactions = _currentReactions; return reactions.where((reaction) { // For image reactions, search by filename if (reaction.startsWith('assets/reactions/')) { final fileName = reaction.split('/').last.toLowerCase(); return fileName.contains(query); } // For emoji, search by description (you could add a mapping) return reaction.toLowerCase().contains(query); }).toList(); } List get _currentReactions { if (_tabOrder.isEmpty || _currentTabIndex >= _tabOrder.length) { return []; } final currentTab = _tabOrder[_currentTabIndex]; return _reactionSets[currentTab] ?? []; } @override Widget build(BuildContext context) { if (_isLoading) { return Dialog( backgroundColor: Colors.transparent, child: Container( width: 400, height: 300, decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppTheme.navyBlue.withValues(alpha: 0.1)), ), child: const Center( child: CircularProgressIndicator(), ), ), ); } final reactions = widget.reactions ?? (_isSearching ? _filteredReactions : _currentReactions); final reactionCounts = widget.reactionCounts ?? {}; final myReactions = widget.myReactions ?? {}; return Dialog( backgroundColor: Colors.transparent, child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(20), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.2), width: 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header with search Column( children: [ Row( children: [ Text( _isSearching ? 'Search Reactions' : 'Add Reaction', style: GoogleFonts.inter( color: AppTheme.navyBlue, fontSize: 16, fontWeight: FontWeight.w600, ), ), const Spacer(), IconButton( onPressed: () { Navigator.of(context).pop(); widget.onClosed?.call(); }, icon: Icon( Icons.close, color: AppTheme.textSecondary, size: 20, ), ), ], ), const SizedBox(height: 12), // Search bar Container( decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.1), ), ), child: TextField( controller: _searchController, style: GoogleFonts.inter( color: AppTheme.navyBlue, fontSize: 14, ), decoration: InputDecoration( hintText: 'Search reactions...', hintStyle: GoogleFonts.inter( color: AppTheme.textSecondary, fontSize: 14, ), prefixIcon: Icon( Icons.search, color: AppTheme.textSecondary, size: 20, ), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: Icon( Icons.clear, color: AppTheme.textSecondary, size: 18, ), onPressed: () { _searchController.clear(); }, ) : null, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), ), ), ], ), const SizedBox(height: 16), // Tabs Container( height: 40, decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), ), child: TabBar( controller: _tabController, onTap: (index) { setState(() { _currentTabIndex = index; _isSearching = false; _searchController.clear(); _filteredReactions = []; }); }, indicator: BoxDecoration( color: AppTheme.brightNavy, borderRadius: BorderRadius.circular(10), ), labelColor: AppTheme.textSecondary, unselectedLabelColor: AppTheme.textSecondary, labelStyle: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, ), indicatorSize: TabBarIndicatorSize.tab, tabs: _tabOrder.map((tabName) { return Tab( text: tabName.toUpperCase(), ); }).toList(), ), ), const SizedBox(height: 16), // Search results info if (_isSearching && _filteredReactions.isEmpty) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: Text( 'No reactions found for "${_searchController.text}"', style: GoogleFonts.inter( color: AppTheme.textSecondary, fontSize: 12, ), textAlign: TextAlign.center, ), ), // Reaction grid SizedBox( height: _isSearching && _filteredReactions.isNotEmpty ? (_filteredReactions.length / 6).ceil() * 60 : 240, // Dynamic height based on search results child: TabBarView( controller: _tabController, children: _tabOrder.map((tabName) { final reactions = _reactionSets[tabName] ?? []; final isEmoji = tabName == 'emoji'; final credit = _folderCredits[tabName]; return Column( children: [ // Reaction grid Expanded( child: _buildReactionGrid(reactions, widget.reactionCounts ?? {}, widget.myReactions ?? {}, isEmoji), ), // Credit section (only for non-emoji tabs) if (credit != null && credit.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(color: Colors.grey), const SizedBox(height: 8), Text( 'Credits:', style: GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.w600, color: AppTheme.textSecondary, ), ), const SizedBox(height: 4), // Parse and display credit markdown _buildCreditDisplay(credit), ], ), ), ], ); }).toList(), ), ), const SizedBox(height: 16), ], ), ), ); } Widget _buildCreditDisplay(String credit) { final lines = credit.split('\n'); final widgets = []; for (final line in lines) { if (line.trim().isEmpty) continue; if (line.startsWith('# ')) { // Title widgets.add(Text( line.substring(2).trim(), style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), )); } else if (line.startsWith('**') && line.endsWith('**')) { // Bold text widgets.add(Text( line.replaceAll('**', '').trim(), style: GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), )); } else if (line.startsWith('*') && line.endsWith('*')) { // Italic text widgets.add(Text( line.replaceAll('*', '').trim(), style: GoogleFonts.inter( fontSize: 10, fontStyle: FontStyle.italic, color: AppTheme.textPrimary, ), )); } else if (line.startsWith('- ')) { // List item widgets.add(Padding( padding: const EdgeInsets.only(left: 8), child: Text( '• ${line.substring(2).trim()}', style: GoogleFonts.inter( fontSize: 10, color: AppTheme.textPrimary, ), ), )); } else { // Regular text widgets.add(Text( line.trim(), style: GoogleFonts.inter( fontSize: 10, color: AppTheme.textPrimary, ), )); } widgets.add(const SizedBox(height: 2)); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: widgets, ); } Widget _buildReactionGrid( List reactions, Map reactionCounts, Set myReactions, bool useImages, ) { return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 1, ), itemCount: reactions.length, itemBuilder: (context, index) { final reaction = reactions[index]; final count = reactionCounts[reaction] ?? 0; final isSelected = myReactions.contains(reaction); return Material( color: Colors.transparent, child: InkWell( onTap: () { Navigator.of(context).pop(); widget.onReactionSelected(reaction); }, borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( color: isSelected ? AppTheme.brightNavy.withValues(alpha: 0.2) : AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? AppTheme.brightNavy : AppTheme.navyBlue.withValues(alpha: 0.1), width: isSelected ? 2 : 1, ), ), child: Stack( children: [ Center( child: useImages ? _buildImageReaction(reaction) : _buildEmojiReaction(reaction), ), if (count > 0) Positioned( right: 2, bottom: 2, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: AppTheme.brightNavy, borderRadius: BorderRadius.circular(8), ), child: Text( count > 99 ? '99+' : '$count', style: GoogleFonts.inter( color: Colors.white, fontSize: 8, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ), ); }, ); } Widget _buildEmojiReaction(String emoji) { return Text( emoji, style: const TextStyle(fontSize: 24), ); } Widget _buildImageReaction(String imagePath) { if (imagePath.endsWith('.svg')) { return SvgPicture.asset( imagePath, width: 32, height: 32, placeholderBuilder: (context) => Container( width: 32, height: 32, padding: const EdgeInsets.all(8), child: CircularProgressIndicator( strokeWidth: 2, color: AppTheme.textSecondary, ), ), errorBuilder: (context, error, stackTrace) { // Fallback to emoji if image not found return Icon( Icons.image_not_supported, size: 24, color: AppTheme.textSecondary, ); }, ); } return Image.asset( imagePath, width: 32, height: 32, errorBuilder: (context, error, stackTrace) { return Icon( Icons.image_not_supported, size: 24, color: AppTheme.textSecondary, ); }, ); } }