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}