this repo has no description
at main 344 lines 14 kB view raw
1import 'package:flutter/cupertino.dart'; 2import 'package:flutter/material.dart'; 3import 'package:flutter/services.dart'; 4import 'package:grain/api.dart'; 5import 'package:grain/app_icons.dart'; 6import 'package:grain/widgets/app_image.dart'; 7import 'package:grain/widgets/gallery_preview.dart'; 8 9Future<void> showAddCommentSheet( 10 BuildContext context, { 11 String initialText = '', 12 required Future<void> Function(String text) onSubmit, 13 VoidCallback? onCancel, 14 dynamic gallery, // Pass gallery object 15 dynamic replyTo, // Pass comment/user object if replying to a comment 16}) async { 17 final theme = Theme.of(context); 18 final controller = TextEditingController(text: initialText); 19 await showCupertinoSheet( 20 context: context, 21 useNestedNavigation: false, 22 pageBuilder: (context) => Material( 23 type: MaterialType.transparency, 24 child: _AddCommentSheet( 25 controller: controller, 26 onSubmit: onSubmit, 27 onCancel: onCancel, 28 gallery: gallery, 29 replyTo: replyTo, 30 ), 31 ), 32 ); 33 SystemChrome.setSystemUIOverlayStyle( 34 theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, 35 ); 36} 37 38class _AddCommentSheet extends StatefulWidget { 39 final TextEditingController controller; 40 final Future<void> Function(String text) onSubmit; 41 final VoidCallback? onCancel; 42 final dynamic gallery; 43 final dynamic replyTo; 44 const _AddCommentSheet({ 45 required this.controller, 46 required this.onSubmit, 47 this.onCancel, 48 this.gallery, 49 this.replyTo, 50 }); 51 52 @override 53 State<_AddCommentSheet> createState() => _AddCommentSheetState(); 54} 55 56class _AddCommentSheetState extends State<_AddCommentSheet> { 57 bool _posting = false; 58 String _currentText = ''; 59 final FocusNode _focusNode = FocusNode(); 60 61 @override 62 void initState() { 63 super.initState(); 64 _currentText = widget.controller.text; 65 widget.controller.addListener(_onTextChanged); 66 // Request focus after build 67 WidgetsBinding.instance.addPostFrameCallback((_) { 68 _focusNode.requestFocus(); 69 }); 70 } 71 72 void _onTextChanged() { 73 if (_currentText != widget.controller.text) { 74 setState(() { 75 _currentText = widget.controller.text; 76 }); 77 } 78 } 79 80 @override 81 void dispose() { 82 widget.controller.removeListener(_onTextChanged); 83 widget.controller.dispose(); 84 super.dispose(); 85 } 86 87 @override 88 Widget build(BuildContext context) { 89 final theme = Theme.of(context); 90 final gallery = widget.gallery; 91 final replyTo = widget.replyTo; 92 final creator = replyTo != null 93 ? (replyTo is Map && replyTo['author'] != null ? replyTo['author'] : null) 94 : gallery?.creator; 95 final focusPhoto = replyTo != null 96 ? (replyTo is Map && replyTo['focus'] != null ? replyTo['focus'] : null) 97 : null; 98 return CupertinoPageScaffold( 99 backgroundColor: theme.colorScheme.surface, 100 navigationBar: CupertinoNavigationBar( 101 backgroundColor: theme.colorScheme.surface, 102 border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)), 103 middle: Text( 104 replyTo != null ? 'Reply' : 'Add comment', 105 style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), 106 ), 107 leading: CupertinoButton( 108 padding: EdgeInsets.zero, 109 onPressed: _posting ? null : widget.onCancel ?? () => Navigator.of(context).maybePop(), 110 child: Text( 111 'Cancel', 112 style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600), 113 ), 114 ), 115 trailing: CupertinoButton( 116 padding: EdgeInsets.zero, 117 onPressed: _posting || _currentText.trim().isEmpty 118 ? null 119 : () async { 120 setState(() => _posting = true); 121 await widget.onSubmit(_currentText.trim()); 122 if (mounted) Navigator.of(context).pop(); 123 setState(() => _posting = false); 124 }, 125 child: Row( 126 mainAxisSize: MainAxisSize.min, 127 children: [ 128 Text( 129 'Post', 130 style: TextStyle( 131 color: _posting || _currentText.trim().isEmpty 132 ? theme.disabledColor 133 : theme.colorScheme.primary, 134 fontWeight: FontWeight.w600, 135 ), 136 ), 137 if (_posting) ...[ 138 const SizedBox(width: 8), 139 SizedBox( 140 width: 16, 141 height: 16, 142 child: CircularProgressIndicator( 143 strokeWidth: 2, 144 valueColor: AlwaysStoppedAnimation<Color>(theme.colorScheme.primary), 145 semanticsLabel: 'Posting', 146 ), 147 ), 148 ], 149 ], 150 ), 151 ), 152 ), 153 child: SafeArea( 154 bottom: false, 155 child: Padding( 156 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), 157 child: Column( 158 children: [ 159 if ((gallery != null && creator != null && replyTo == null) || 160 (replyTo != null && creator != null)) ...[ 161 Row( 162 crossAxisAlignment: CrossAxisAlignment.start, 163 children: [ 164 if ((creator is Map && 165 creator['avatar'] != null && 166 creator['avatar'].isNotEmpty) || 167 (creator is! Map && creator.avatar != null && creator.avatar.isNotEmpty)) 168 ClipOval( 169 child: AppImage( 170 url: creator is Map ? creator['avatar'] : creator.avatar, 171 width: 40, 172 height: 40, 173 fit: BoxFit.cover, 174 ), 175 ) 176 else 177 CircleAvatar( 178 radius: 20, 179 backgroundColor: theme.colorScheme.surfaceContainerHighest, 180 child: Icon(AppIcons.person, size: 20, color: theme.iconTheme.color), 181 ), 182 Expanded( 183 child: Padding( 184 padding: const EdgeInsets.only(left: 20.0), 185 child: Column( 186 crossAxisAlignment: CrossAxisAlignment.start, 187 children: [ 188 Row( 189 children: [ 190 Text( 191 creator is Map 192 ? (creator['displayName'] ?? '') 193 : (creator.displayName ?? ''), 194 style: theme.textTheme.bodyMedium?.copyWith( 195 fontWeight: FontWeight.bold, 196 ), 197 ), 198 if ((creator is Map 199 ? (creator['handle'] ?? '') 200 : (creator.handle ?? '')) 201 .isNotEmpty) ...[ 202 const SizedBox(width: 8), 203 Text( 204 '@${creator is Map ? creator['handle'] : creator.handle}', 205 style: theme.textTheme.bodySmall?.copyWith( 206 color: theme.hintColor, 207 ), 208 ), 209 ], 210 ], 211 ), 212 const SizedBox(height: 8), 213 if (replyTo != null && 214 replyTo is Map && 215 replyTo['text'] != null && 216 (replyTo['text'] as String).isNotEmpty) ...[ 217 Padding( 218 padding: const EdgeInsets.only(bottom: 8.0), 219 child: Text( 220 replyTo['text'], 221 style: theme.textTheme.bodySmall?.copyWith( 222 color: theme.hintColor, 223 ), 224 maxLines: 3, 225 overflow: TextOverflow.ellipsis, 226 ), 227 ), 228 ], 229 if (replyTo != null && focusPhoto != null) ...[ 230 SizedBox( 231 height: 64, 232 child: AspectRatio( 233 aspectRatio: 234 (focusPhoto.aspectRatio != null && 235 focusPhoto.aspectRatio.width > 0 && 236 focusPhoto.aspectRatio.height > 0) 237 ? focusPhoto.aspectRatio.width / focusPhoto.aspectRatio.height 238 : 1.0, 239 child: AppImage( 240 url: focusPhoto.thumb?.isNotEmpty == true 241 ? focusPhoto.thumb 242 : focusPhoto.fullsize, 243 fit: BoxFit.cover, 244 borderRadius: BorderRadius.circular(8), 245 ), 246 ), 247 ), 248 ] else if (replyTo == null) ...[ 249 Column( 250 crossAxisAlignment: CrossAxisAlignment.start, 251 children: [ 252 Row( 253 children: [ 254 Text( 255 gallery.title ?? '', 256 style: theme.textTheme.bodyMedium?.copyWith( 257 fontWeight: FontWeight.w600, 258 ), 259 maxLines: 1, 260 overflow: TextOverflow.ellipsis, 261 ), 262 ], 263 ), 264 const SizedBox(height: 4), 265 Row( 266 children: [ 267 Align( 268 alignment: Alignment.centerLeft, 269 child: SizedBox( 270 height: 64, 271 child: GalleryPreview(gallery: gallery), 272 ), 273 ), 274 ], 275 ), 276 ], 277 ), 278 ], 279 ], 280 ), 281 ), 282 ), 283 ], 284 ), 285 const SizedBox(height: 16), 286 Divider(height: 1, thickness: 1, color: theme.dividerColor), 287 const SizedBox(height: 16), 288 ], 289 Expanded( 290 child: Row( 291 crossAxisAlignment: CrossAxisAlignment.start, 292 children: [ 293 // Current user avatar 294 if (apiService.currentUser?.avatar != null && 295 (apiService.currentUser?.avatar?.isNotEmpty ?? false)) 296 Padding( 297 padding: const EdgeInsets.only(right: 8.0, top: 4.0), 298 child: ClipOval( 299 child: AppImage( 300 url: apiService.currentUser!.avatar!, 301 width: 40, 302 height: 40, 303 fit: BoxFit.cover, 304 ), 305 ), 306 ) 307 else 308 Padding( 309 padding: const EdgeInsets.only(right: 8.0, top: 4.0), 310 child: CircleAvatar( 311 radius: 20, 312 backgroundColor: theme.colorScheme.surfaceContainerHighest, 313 child: Icon(AppIcons.person, size: 20, color: theme.iconTheme.color), 314 ), 315 ), 316 // Text input 317 Expanded( 318 child: TextField( 319 controller: widget.controller, 320 focusNode: _focusNode, 321 maxLines: 6, 322 minLines: 2, 323 style: theme.textTheme.bodyMedium, 324 decoration: InputDecoration( 325 hintText: 'Add a comment', 326 hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor), 327 border: InputBorder.none, 328 contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 329 isDense: true, 330 filled: false, 331 ), 332 ), 333 ), 334 ], 335 ), 336 ), 337 const SizedBox(height: 24), 338 ], 339 ), 340 ), 341 ), 342 ); 343 } 344}