feat: Implement auto-scrolling to the first validation error in the create diet form and refine image validation logic.

This commit is contained in:
mso7nueh
2026-02-09 23:15:12 +03:00
parent 531f732b1a
commit d70aaf955f
2 changed files with 50 additions and 8 deletions

View File

@@ -58,6 +58,39 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
final GlobalKey _nameKey = GlobalKey();
final GlobalKey _durationKey = GlobalKey();
final GlobalKey _statusKey = GlobalKey();
final GlobalKey _typeKey = GlobalKey();
final GlobalKey _imageKey = GlobalKey();
final GlobalKey _planKey = GlobalKey();
void _scrollToFirstError(CreateDietState state) {
GlobalKey? key;
if (state.dietNameError != null) {
key = _nameKey;
} else if (state.dietDescriptionError != null) {
key = _mdKey;
} else if (state.dietDurationError != null) {
key = _durationKey;
} else if (state.dietStateError != null) {
key = _statusKey;
} else if (state.dietImageError != null) {
key = _imageKey;
} else if (state.dailyScheduleErrors.any((e) => e.isNotEmpty)) {
key = _planKey;
}
if (key != null && key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
alignment: 0.1,
);
}
}
@override
void initState() {
super.initState();
@@ -118,6 +151,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
children: [
const SizedBox(height: 16),
CustomTextFormField(
key: _nameKey,
hintText: s.enter_diet_name,
controller: nameController,
hasError: state.dietNameError != null,
@@ -143,19 +177,20 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
),
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
const SizedBox(height: 12),
const DietDurationDropDownWidget(),
DietDurationDropDownWidget(key: _durationKey),
const SizedBox(height: 12),
StatusDropDownWidget(dietState: widget.diet?.state),
StatusDropDownWidget(key: _statusKey, dietState: widget.diet?.state),
const SizedBox(height: 16),
const DietTypeChoiceWidget(),
DietTypeChoiceWidget(key: _typeKey),
const SizedBox(height: 16),
const DietImagePickerWidget(),
DietImagePickerWidget(key: _imageKey),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 16),
MainBackgroundBouble(
key: _planKey,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
@@ -185,7 +220,11 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
? () {}
: () async {
final diet = await context.read<CreateDietCubit>().getDietForPreview();
if (diet == null) return;
if (diet == null) {
// ignore: use_build_context_synchronously
_scrollToFirstError(context.read<CreateDietCubit>().state);
return;
}
if (!context.mounted) return;
GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet);
},
@@ -204,7 +243,10 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
CustomOutlinedButton(
onPressed: () async {
final result = await context.read<CreateDietCubit>().createDiet();
if (result == 'error') {
if (result == 'validation') {
// ignore: use_build_context_synchronously
_scrollToFirstError(context.read<CreateDietCubit>().state);
} else if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
}

View File

@@ -394,11 +394,11 @@ class CreateDietCubit extends Cubit<CreateDietState> {
result = false;
emit(state.copyWith(dietStateError: 'Статус диеты не выбран'));
}
if (state.dietData.image == null) {
if (state.dietData.image == null && state.dietData.imageUrl == null) {
result = false;
emit(state.copyWith(dietImageError: 'Не добавлена обложка'));
}
result = _isDailyScheduleValid();
result = _isDailyScheduleValid() && result;
return result;
}