806 lines
28 KiB
Dart
806 lines
28 KiB
Dart
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<KineticThreadWidget> createState() => _KineticThreadWidgetState();
|
|
}
|
|
|
|
class _KineticThreadWidgetState extends ConsumerState<KineticThreadWidget>
|
|
with TickerProviderStateMixin {
|
|
|
|
// Layer navigation state
|
|
ThreadNode? _currentFocusNode;
|
|
List<ThreadNode> _layerStack = [];
|
|
final PageController _pageController = PageController();
|
|
|
|
// Animation controllers
|
|
late AnimationController _layerTransitionController;
|
|
late AnimationController _lineageBarController;
|
|
late AnimationController _impactController;
|
|
|
|
// Animations
|
|
late Animation<Offset> _slideAnimation;
|
|
late Animation<double> _scaleAnimation;
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
// Reply state
|
|
bool _showInlineReply = false;
|
|
final TextEditingController _replyController = TextEditingController();
|
|
bool _isPostingReply = false;
|
|
|
|
// Rail collapsing state
|
|
final Set<String> _collapsedRails = <String>{};
|
|
|
|
@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<Offset>(
|
|
begin: const Offset(1.0, 0.0),
|
|
end: Offset.zero,
|
|
).animate(CurvedAnimation(
|
|
parent: _layerTransitionController,
|
|
curve: Curves.easeOutCubic,
|
|
));
|
|
|
|
_scaleAnimation = Tween<double>(
|
|
begin: 0.8,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _layerTransitionController,
|
|
curve: Curves.easeOutBack,
|
|
));
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
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<void> _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<ThreadNode> 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<Color>(Colors.white),
|
|
),
|
|
)
|
|
: Text(
|
|
'Reply',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|