+63
-1
lib/screens/create_gallery_page.dart
+63
-1
lib/screens/create_gallery_page.dart
···
34
34
}
35
35
36
36
class _CreateGalleryPageState extends State<CreateGalleryPage> {
37
+
bool isValidGraphemeLength(String input, int maxLength) {
38
+
return input.characters.length <= maxLength;
39
+
}
40
+
37
41
final _titleController = TextEditingController();
38
42
final _descController = TextEditingController();
39
43
final List<GalleryImage> _images = [];
···
48
52
_descController.text = widget.gallery?.description ?? '';
49
53
}
50
54
_titleController.addListener(() {
55
+
setState(() {});
56
+
});
57
+
_descController.addListener(() {
51
58
setState(() {});
52
59
});
53
60
}
···
102
109
}
103
110
104
111
Future<void> _submit() async {
112
+
final titleGraphemes = _titleController.text.characters.length;
113
+
final descGraphemes = _descController.text.characters.length;
114
+
if (titleGraphemes > 100 || descGraphemes > 1000) {
115
+
if (mounted) {
116
+
await showDialog(
117
+
context: context,
118
+
builder: (context) => AlertDialog(
119
+
title: const Text('Character Limit Exceeded'),
120
+
content: Text(
121
+
titleGraphemes > 100
122
+
? 'Title must be 100 characters or fewer.'
123
+
: 'Description must be 1000 characters or fewer.',
124
+
),
125
+
actions: [
126
+
TextButton(child: const Text('OK'), onPressed: () => Navigator.of(context).pop()),
127
+
],
128
+
),
129
+
);
130
+
}
131
+
return;
132
+
}
105
133
if (widget.gallery == null && _images.length > 10) {
106
134
if (mounted) {
107
135
await showDialog(
···
109
137
builder: (context) => AlertDialog(
110
138
title: const Text('Photo Limit'),
111
139
content: const Text(
112
-
'You can only add up to 10 photos on initial create but can add more later on.',
140
+
'You can only add up to 10 photos initially but you can add more later on.',
113
141
),
114
142
actions: [
115
143
TextButton(child: const Text('OK'), onPressed: () => Navigator.of(context).pop()),
···
236
264
controller: _titleController,
237
265
hintText: 'Enter a title',
238
266
),
267
+
Padding(
268
+
padding: const EdgeInsets.only(top: 4),
269
+
child: Row(
270
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
271
+
children: [
272
+
const SizedBox(),
273
+
Text(
274
+
'${_titleController.text.characters.length}/100',
275
+
style: theme.textTheme.bodySmall?.copyWith(
276
+
color: _titleController.text.characters.length > 100
277
+
? theme.colorScheme.error
278
+
: theme.textTheme.bodySmall?.color,
279
+
),
280
+
),
281
+
],
282
+
),
283
+
),
239
284
const SizedBox(height: 16),
240
285
PlainTextField(
241
286
label: 'Description',
242
287
controller: _descController,
243
288
maxLines: 6,
244
289
hintText: 'Enter a description',
290
+
),
291
+
Padding(
292
+
padding: const EdgeInsets.only(top: 4),
293
+
child: Row(
294
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
295
+
children: [
296
+
const SizedBox(),
297
+
Text(
298
+
'${_descController.text.characters.length}/1000',
299
+
style: theme.textTheme.bodySmall?.copyWith(
300
+
color: _descController.text.characters.length > 1000
301
+
? theme.colorScheme.error
302
+
: theme.textTheme.bodySmall?.color,
303
+
),
304
+
),
305
+
],
306
+
),
245
307
),
246
308
const SizedBox(height: 16),
247
309
if (widget.gallery == null)
+65
-22
lib/widgets/edit_profile_sheet.dart
+65
-22
lib/widgets/edit_profile_sheet.dart
···
61
61
late TextEditingController _descriptionController;
62
62
XFile? _selectedAvatar;
63
63
bool _saving = false;
64
-
bool _hasChanged = false;
64
+
static const int maxDisplayNameGraphemes = 64;
65
+
static const int maxDescriptionGraphemes = 256;
65
66
66
67
@override
67
68
void initState() {
68
69
super.initState();
69
70
_displayNameController = TextEditingController(text: widget.initialDisplayName ?? '');
70
71
_descriptionController = TextEditingController(text: widget.initialDescription ?? '');
72
+
// No need to track changes
71
73
_displayNameController.addListener(_onInputChanged);
72
74
_descriptionController.addListener(_onInputChanged);
73
75
}
74
76
75
77
void _onInputChanged() {
76
-
final displayName = _displayNameController.text.trim();
77
-
final initialDisplayName = widget.initialDisplayName ?? '';
78
-
final displayNameChanged = displayName != initialDisplayName;
79
-
final descriptionChanged =
80
-
_descriptionController.text.trim() != (widget.initialDescription ?? '');
81
-
final avatarChanged = _selectedAvatar != null;
82
-
// Only allow Save if displayName is not empty and at least one field changed
83
-
final changed =
84
-
(displayNameChanged || descriptionChanged || avatarChanged) && displayName.isNotEmpty;
85
-
if (_hasChanged != changed) {
86
-
setState(() {
87
-
_hasChanged = changed;
88
-
});
89
-
}
78
+
setState(() {
79
+
// Trigger rebuild to update character counts
80
+
});
90
81
}
91
82
92
83
@override
93
84
void dispose() {
94
-
_displayNameController.removeListener(_onInputChanged);
95
-
_descriptionController.removeListener(_onInputChanged);
96
85
_displayNameController.dispose();
97
86
_descriptionController.dispose();
98
87
super.dispose();
···
104
93
if (picked != null) {
105
94
setState(() {
106
95
_selectedAvatar = picked;
107
-
_onInputChanged();
108
96
});
109
97
}
110
98
}
···
113
101
Widget build(BuildContext context) {
114
102
final theme = Theme.of(context);
115
103
final avatarRadius = 44.0;
104
+
final displayNameGraphemes = _displayNameController.text.characters.length;
105
+
final descriptionGraphemes = _descriptionController.text.characters.length;
116
106
return CupertinoPageScaffold(
117
107
backgroundColor: theme.colorScheme.surface,
118
108
navigationBar: CupertinoNavigationBar(
···
132
122
),
133
123
trailing: CupertinoButton(
134
124
padding: EdgeInsets.zero,
135
-
onPressed: (!_hasChanged || _saving)
125
+
onPressed: _saving
136
126
? null
137
127
: () async {
128
+
if (displayNameGraphemes > maxDisplayNameGraphemes ||
129
+
descriptionGraphemes > maxDescriptionGraphemes) {
130
+
await showDialog(
131
+
context: context,
132
+
builder: (context) => AlertDialog(
133
+
title: const Text('Character Limit Exceeded'),
134
+
content: Text(
135
+
displayNameGraphemes > maxDisplayNameGraphemes
136
+
? 'Display Name must be $maxDisplayNameGraphemes characters or fewer.'
137
+
: 'Description must be $maxDescriptionGraphemes characters or fewer.',
138
+
),
139
+
actions: [
140
+
TextButton(
141
+
child: const Text('OK'),
142
+
onPressed: () => Navigator.of(context).pop(),
143
+
),
144
+
],
145
+
),
146
+
);
147
+
return;
148
+
}
138
149
if (widget.onSave != null) {
139
150
setState(() {
140
151
_saving = true;
···
155
166
Text(
156
167
'Save',
157
168
style: TextStyle(
158
-
color: (!_hasChanged || _saving)
159
-
? theme.disabledColor
160
-
: theme.colorScheme.primary,
169
+
color: _saving ? theme.disabledColor : theme.colorScheme.primary,
161
170
fontWeight: FontWeight.w600,
162
171
),
163
172
),
···
233
242
controller: _displayNameController,
234
243
maxLines: 1,
235
244
),
245
+
Padding(
246
+
padding: const EdgeInsets.only(top: 4),
247
+
child: Row(
248
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
249
+
children: [
250
+
const SizedBox(),
251
+
Text(
252
+
'$displayNameGraphemes/$maxDisplayNameGraphemes',
253
+
style: theme.textTheme.bodySmall?.copyWith(
254
+
color: displayNameGraphemes > maxDisplayNameGraphemes
255
+
? theme.colorScheme.error
256
+
: theme.textTheme.bodySmall?.color,
257
+
),
258
+
),
259
+
],
260
+
),
261
+
),
236
262
const SizedBox(height: 12),
237
263
PlainTextField(
238
264
label: 'Description',
239
265
controller: _descriptionController,
240
266
maxLines: 6,
267
+
),
268
+
Padding(
269
+
padding: const EdgeInsets.only(top: 4),
270
+
child: Row(
271
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
272
+
children: [
273
+
const SizedBox(),
274
+
Text(
275
+
'$descriptionGraphemes/$maxDescriptionGraphemes',
276
+
style: theme.textTheme.bodySmall?.copyWith(
277
+
color: descriptionGraphemes > maxDescriptionGraphemes
278
+
? theme.colorScheme.error
279
+
: theme.textTheme.bodySmall?.color,
280
+
),
281
+
),
282
+
],
283
+
),
241
284
),
242
285
],
243
286
),