import 'dart:convert'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../theme/app_theme.dart'; class ReactionStrip extends StatelessWidget { final Map reactions; final Set myReactions; final Map>? reactionUsers; final ValueChanged onToggle; final VoidCallback onAdd; final bool dense; const ReactionStrip({ super.key, required this.reactions, required this.myReactions, required this.onToggle, required this.onAdd, this.reactionUsers, this.dense = true, }); @override Widget build(BuildContext context) { final keys = reactions.keys.toList()..sort(); return Wrap( spacing: dense ? 6 : 8, runSpacing: dense ? 6 : 8, children: [ for (final reaction in keys) _ReactionChip( reactionId: reaction, count: reactions[reaction] ?? 0, isSelected: myReactions.contains(reaction), tooltipNames: reactionUsers?[reaction], onTap: () => onToggle(reaction), ), _ReactionAddButton(onTap: onAdd), ], ); } } class _ReactionChip extends StatefulWidget { final String reactionId; final int count; final bool isSelected; final List? tooltipNames; final VoidCallback onTap; const _ReactionChip({ required this.reactionId, required this.count, required this.isSelected, required this.onTap, this.tooltipNames, }); @override State<_ReactionChip> createState() => _ReactionChipState(); } class _ReactionChipState extends State<_ReactionChip> { int _tapCount = 0; void _handleTap() { HapticFeedback.selectionClick(); setState(() => _tapCount += 1); widget.onTap(); } @override Widget build(BuildContext context) { final background = widget.isSelected ? AppTheme.brightNavy.withValues(alpha: 0.14) : AppTheme.navyBlue.withValues(alpha: 0.08); final borderColor = widget.isSelected ? AppTheme.brightNavy : AppTheme.navyBlue.withValues(alpha: 0.2); final chip = InkWell( onTap: _handleTap, borderRadius: BorderRadius.circular(14), child: AnimatedContainer( duration: const Duration(milliseconds: 140), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: background, borderRadius: BorderRadius.circular(14), border: Border.all(color: borderColor, width: 1.2), boxShadow: widget.isSelected ? [ BoxShadow( color: AppTheme.brightNavy.withValues(alpha: 0.22), blurRadius: 10, offset: const Offset(0, 2), ), ] : null, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _ReactionIcon(reactionId: widget.reactionId), const SizedBox(width: 6), Text( widget.count.toString(), style: TextStyle( color: AppTheme.navyBlue, fontSize: 12, fontWeight: FontWeight.w700, ), ), ], ), ), ) .animate(key: ValueKey(_tapCount)) .scale( begin: const Offset(1, 1), end: const Offset(1.08, 1.08), duration: 120.ms, curve: Curves.easeOut, ) .then() .scale( begin: const Offset(1.08, 1.08), end: const Offset(1, 1), duration: 180.ms, curve: Curves.easeOutBack, ); final names = widget.tooltipNames; if (names == null || names.isEmpty) { return chip; } return Tooltip( message: names.take(3).join(', '), child: chip, ); } } class _ReactionAddButton extends StatelessWidget { final VoidCallback onTap; const _ReactionAddButton({required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(14), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.2), width: 1.2, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add, size: 16, color: AppTheme.navyBlue), const SizedBox(width: 6), Text( 'Add', style: TextStyle( color: AppTheme.navyBlue, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ); } } class _ReactionIcon extends StatelessWidget { final String reactionId; const _ReactionIcon({required this.reactionId}); @override Widget build(BuildContext context) { if (reactionId.startsWith('asset:')) { final assetPath = reactionId.replaceFirst('asset:', ''); return Image.asset( assetPath, width: 18, height: 18, fit: BoxFit.contain, ); } return Text(reactionId, style: const TextStyle(fontSize: 16)); } } Future showReactionPicker( BuildContext context, { required List baseItems, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _ReactionPickerSheet(baseItems: baseItems), ); } class _ReactionPickerSheet extends StatefulWidget { final List baseItems; const _ReactionPickerSheet({required this.baseItems}); @override State<_ReactionPickerSheet> createState() => _ReactionPickerSheetState(); } class _ReactionPickerSheetState extends State<_ReactionPickerSheet> { final TextEditingController _controller = TextEditingController(); List _assetItems = []; @override void initState() { super.initState(); _loadAssetReactions(); } Future _loadAssetReactions() async { try { final manifest = await DefaultAssetBundle.of(context) .loadString('AssetManifest.json'); final map = jsonDecode(manifest) as Map; final keys = map.keys .where((key) => key.startsWith('assets/reactions/')) .toList() ..sort(); final items = keys .map((path) => ReactionItem( id: 'asset:$path', label: _labelForAsset(path), )) .toList(); if (mounted) { setState(() => _assetItems = items); } } catch (_) { // Ignore manifest parsing errors; picker will show base items only. } } String _labelForAsset(String path) { final fileName = path.split('/').last; final name = fileName.split('.').first; return name.replaceAll('_', ' '); } @override Widget build(BuildContext context) { final query = _controller.text.trim().toLowerCase(); final items = [...widget.baseItems, ..._assetItems]; final filtered = query.isEmpty ? items : items .where((item) => item.label.toLowerCase().contains(query) || item.id.toLowerCase().contains(query)) .toList(); return Container( height: MediaQuery.of(context).size.height * 0.55, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppTheme.cardSurface.withValues(alpha: 0.75), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: ClipRRect( borderRadius: BorderRadius.circular(20), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pick a reaction', style: TextStyle( color: AppTheme.navyBlue, fontSize: 16, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 12), TextField( controller: _controller, decoration: InputDecoration( hintText: 'Search reactions', prefixIcon: Icon(Icons.search, color: AppTheme.navyBlue), filled: true, fillColor: Colors.white.withValues(alpha: 0.2), border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none, ), ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 12), Expanded( child: GridView.builder( itemCount: filtered.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, mainAxisSpacing: 8, crossAxisSpacing: 8, ), itemBuilder: (context, index) { final item = filtered[index]; return InkWell( onTap: () => Navigator.of(context).pop(item.id), borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.15), ), ), alignment: Alignment.center, child: _ReactionIcon(reactionId: item.id), ), ); }, ), ), ], ), ), ), ); } } class ReactionItem { final String id; final String label; const ReactionItem({ required this.id, required this.label, }); }