import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:flutter_animate/flutter_animate.dart'; import '../models/post.dart'; import '../models/thread_node.dart'; import '../providers/api_provider.dart'; import '../theme/app_theme.dart'; import '../widgets/media/signed_media_image.dart'; import '../widgets/post/post_actions.dart'; /// Kinetic Spatial Engine widget for layer-based thread navigation class KineticThreadWidget extends ConsumerStatefulWidget { final ThreadNode rootNode; final Function(ThreadNode)? onLayerChanged; final Function()? onReplyPosted; const KineticThreadWidget({ super.key, required this.rootNode, this.onLayerChanged, this.onReplyPosted, }); @override ConsumerState createState() => _KineticThreadWidgetState(); } class _KineticThreadWidgetState extends ConsumerState with TickerProviderStateMixin { // Layer navigation state ThreadNode? _currentFocusNode; List _layerStack = []; final PageController _pageController = PageController(); // Animation controllers late AnimationController _layerTransitionController; late AnimationController _lineageBarController; late AnimationController _impactController; // Animations late Animation _slideAnimation; late Animation _scaleAnimation; late Animation _fadeAnimation; // Reply state bool _showInlineReply = false; final TextEditingController _replyController = TextEditingController(); bool _isPostingReply = false; // Rail collapsing state final Set _collapsedRails = {}; @override void initState() { super.initState(); _currentFocusNode = widget.rootNode; _layerStack = [widget.rootNode]; _initializeAnimations(); } void _initializeAnimations() { // Layer transition animations _layerTransitionController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _slideAnimation = Tween( begin: const Offset(1.0, 0.0), end: Offset.zero, ).animate(CurvedAnimation( parent: _layerTransitionController, curve: Curves.easeOutCubic, )); _scaleAnimation = Tween( begin: 0.8, end: 1.0, ).animate(CurvedAnimation( parent: _layerTransitionController, curve: Curves.easeOutBack, )); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _layerTransitionController, curve: Curves.easeInOut, )); // Lineage bar animation _lineageBarController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); // Impact animation for new replies _impactController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); } @override void dispose() { _layerTransitionController.dispose(); _lineageBarController.dispose(); _impactController.dispose(); _pageController.dispose(); _replyController.dispose(); super.dispose(); } void _drillDownToLayer(ThreadNode targetNode) { setState(() { _currentFocusNode = targetNode; _layerStack.add(targetNode); _showInlineReply = false; }); _layerTransitionController.forward().then((_) { _layerTransitionController.reset(); }); _lineageBarController.forward(); widget.onLayerChanged?.call(targetNode); } void _scrubToLayer(int layerIndex) { if (layerIndex < 0 || layerIndex >= _layerStack.length) return; setState(() { _currentFocusNode = _layerStack[layerIndex]; _layerStack = _layerStack.sublist(0, layerIndex + 1); _showInlineReply = false; }); _layerTransitionController.forward().then((_) { _layerTransitionController.reset(); }); widget.onLayerChanged?.call(_currentFocusNode!); } void _toggleRailCollapse(ThreadNode node) { setState(() { if (_collapsedRails.contains(node.post.id)) { _collapsedRails.remove(node.post.id); } else { _collapsedRails.add(node.post.id); // Shatter animation _impactController.forward().then((_) { _impactController.reset(); }); } }); } bool _isRailCollapsed(ThreadNode node) { return _collapsedRails.contains(node.post.id); } Future _submitInlineReply() async { if (_replyController.text.trim().isEmpty || _currentFocusNode == null) return; setState(() => _isPostingReply = true); try { final api = ref.read(apiServiceProvider); await api.publishComment( postId: _currentFocusNode!.post.id, body: _replyController.text.trim(), ); _replyController.clear(); setState(() { _showInlineReply = false; _isPostingReply = false; }); // Impact animation _impactController.forward().then((_) { _impactController.reset(); }); widget.onReplyPosted?.call(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Reply posted!'), backgroundColor: Colors.green, duration: const Duration(seconds: 2), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to post reply: $e'), backgroundColor: Colors.red, ), ); } setState(() => _isPostingReply = false); } } @override Widget build(BuildContext context) { return Column( children: [ // Kinetic Trace Bar (Lineage Scrubber) _buildKineticTraceBar(), // Main content area with layer navigation Expanded( child: _buildLayerContent(), ), // Inline reply composer if (_showInlineReply && _currentFocusNode != null) _buildInlineReplyComposer(), ], ); } Widget _buildKineticTraceBar() { return AnimatedBuilder( animation: _lineageBarController, builder: (context, child) { return Container( height: 60, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.2), width: 1, ), ), child: Row( children: [ // Back button if (_layerStack.length > 1) GestureDetector( onTap: () => _scrubToLayer(_layerStack.length - 2), child: Container( margin: const EdgeInsets.only(left: 12), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.brightNavy.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.arrow_back, size: 20, color: AppTheme.brightNavy, ), ), ), // Lineage avatars Expanded( child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _layerStack.length, itemBuilder: (context, index) { final node = _layerStack[index]; final isCurrentLayer = index == _layerStack.length - 1; return GestureDetector( onTap: () => _scrubToLayer(index), child: AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: isCurrentLayer ? AppTheme.brightNavy : AppTheme.navyBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), border: isCurrentLayer ? null : Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.3), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ // Mini avatar Container( width: 24, height: 24, decoration: BoxDecoration( color: isCurrentLayer ? Colors.white : AppTheme.brightNavy.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(6), ), child: Center( child: Text( 'L${node.depth}', style: GoogleFonts.inter( color: isCurrentLayer ? AppTheme.brightNavy : AppTheme.brightNavy, fontSize: 10, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(width: 6), // Author name Text( node.post.author?.displayName ?? 'Anonymous', style: GoogleFonts.inter( color: isCurrentLayer ? Colors.white : AppTheme.navyBlue, fontSize: 12, fontWeight: isCurrentLayer ? FontWeight.w600 : FontWeight.w500, ), ), ], ), ), ); }, ), ), ], ), ); }, ); } Widget _buildLayerContent() { if (_currentFocusNode == null) { return const Center(child: Text('No content')); } return PageView.builder( controller: _pageController, physics: const NeverScrollableScrollPhysics(), // Disable manual swiping itemCount: 1, itemBuilder: (context, index) { return AnimatedBuilder( animation: _layerTransitionController, builder: (context, child) { return SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: ScaleTransition( scale: _scaleAnimation, child: _buildFocusLayer(_currentFocusNode!), ), ), ); }, ); }, ); } Widget _buildFocusLayer(ThreadNode focusNode) { return Column( children: [ // Focus post (blooms to full width) _buildFocusPost(focusNode), // Children list (high-velocity scroll) if (focusNode.hasChildren) Expanded( child: _buildChildrenList(focusNode.children), ), ], ); } Widget _buildFocusPost(ThreadNode node) { return Hero( tag: 'post_${node.post.id}', child: Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(16), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.2), width: 2, ), boxShadow: [ BoxShadow( color: AppTheme.navyBlue.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Author and metadata Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppTheme.brightNavy.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( 'L${node.depth}', style: GoogleFonts.inter( color: AppTheme.brightNavy, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( node.post.author?.displayName ?? 'Anonymous', style: GoogleFonts.inter( color: AppTheme.textPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), ), Text( timeago.format(node.post.createdAt), style: GoogleFonts.inter( color: AppTheme.textSecondary, fontSize: 12, ), ), ], ), ), ], ), ), // Post content Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( node.post.body, style: GoogleFonts.inter( fontSize: 16, color: AppTheme.navyText, height: 1.5, ), ), ), // Media if present if (node.post.imageUrl != null) ...[ const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: SignedMediaImage( url: node.post.imageUrl!, width: double.infinity, height: 200, fit: BoxFit.cover, ), ), ), ], // Actions Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => setState(() => _showInlineReply = !_showInlineReply), icon: Icon(Icons.reply, size: 18), label: Text('Reply'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.brightNavy, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 12), if (node.hasChildren) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppTheme.egyptianBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.chat_bubble_outline, size: 16, color: AppTheme.egyptianBlue, ), const SizedBox(width: 4), Text( '${node.totalDescendants}', style: GoogleFonts.inter( color: AppTheme.egyptianBlue, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), ), ], ), ), ); } Widget _buildChildrenList(List children) { // Filter out collapsed rails final visibleChildren = children.where((child) => !_isRailCollapsed(child)).toList(); return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: visibleChildren.length, itemBuilder: (context, index) { final child = visibleChildren[index]; return _buildChildItem(child, index); }, ); } Widget _buildChildItem(ThreadNode child, int index) { return AnimatedContainer( duration: const Duration(milliseconds: 300), margin: const EdgeInsets.only(bottom: 12), transform: Matrix4.translationValues(0, 0, 0), child: GestureDetector( onTap: () => _drillDownToLayer(child), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.1), width: 1, ), ), child: Row( children: [ // Power Rail (collapsible thread indicator) GestureDetector( onLongPress: () => _toggleRailCollapse(child), onDoubleTap: () => _toggleRailCollapse(child), child: AnimatedContainer( duration: const Duration(milliseconds: 300), width: 4, height: 40, decoration: BoxDecoration( color: _isRailCollapsed(child) ? Colors.red.withValues(alpha: 0.5) : AppTheme.brightNavy.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(width: 12), // Child content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: AppTheme.brightNavy.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Center( child: Text( 'L${child.depth}', style: GoogleFonts.inter( color: AppTheme.brightNavy, fontSize: 8, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(width: 8), Expanded( child: Text( child.post.author?.displayName ?? 'Anonymous', style: GoogleFonts.inter( color: AppTheme.textPrimary, fontSize: 14, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), if (child.hasChildren) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppTheme.egyptianBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Text( '${child.totalDescendants}', style: GoogleFonts.inter( color: AppTheme.egyptianBlue, fontSize: 10, fontWeight: FontWeight.w600, ), ), ), ], ], ), const SizedBox(height: 4), Text( child.post.body, style: GoogleFonts.inter( color: AppTheme.navyText, fontSize: 14, height: 1.4, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), // Drill down indicator Icon( Icons.arrow_forward_ios, size: 16, color: AppTheme.textSecondary, ), ], ), ), ), ); } Widget _buildInlineReplyComposer() { return AnimatedBuilder( animation: _impactController, builder: (context, child) { return Transform.scale( scale: 1.0 + (_impactController.value * 0.05), child: Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.3), width: 2, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.reply, size: 20, color: AppTheme.navyBlue), const SizedBox(width: 8), Text( 'Replying to ${_currentFocusNode!.post.author?.displayName ?? 'Anonymous'}', style: GoogleFonts.inter( fontSize: 14, color: AppTheme.navyBlue, fontWeight: FontWeight.w600, ), ), const Spacer(), GestureDetector( onTap: () => setState(() => _showInlineReply = false), child: Icon(Icons.close, size: 20, color: AppTheme.navyBlue), ), ], ), const SizedBox(height: 12), TextField( controller: _replyController, maxLines: 3, minLines: 1, style: GoogleFonts.inter( fontSize: 14, color: AppTheme.navyText, ), decoration: InputDecoration( hintText: 'Write your reply...', hintStyle: GoogleFonts.inter( fontSize: 14, color: AppTheme.textSecondary, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), filled: true, fillColor: AppTheme.cardSurface, contentPadding: const EdgeInsets.all(12), ), ), const SizedBox(height: 12), Row( children: [ Text( '${_replyController.text.length}/500', style: GoogleFonts.inter( fontSize: 12, color: AppTheme.textSecondary, ), ), const Spacer(), ElevatedButton( onPressed: (_replyController.text.trim().isNotEmpty && !_isPostingReply) ? _submitInlineReply : null, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.brightNavy, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: _isPostingReply ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( 'Reply', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ], ), ], ), ), ); }, ); } }