Files
eatvio_app/lib/features/create_diet/presentation/create_diet_screen.dart
2026-03-16 10:32:12 +03:00

346 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/day_widget.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_duration_drop_down_widget.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_image_picker_widget.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/status_drop_down_widget.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_type_choice_widget.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class CreateDietScreen extends StatelessWidget {
static const name = 'createDietScreen';
static const path = '/create-diet-screen';
const CreateDietScreen({super.key, this.diet, this.isEditing});
final Diet? diet;
final bool? isEditing;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<CreateDietCubit>()..setInitialData(diet),
child: CreateDietScreenContent(diet: diet, isEditing: isEditing),
);
}
}
class CreateDietScreenContent extends StatefulWidget {
const CreateDietScreenContent({super.key, this.diet, this.isEditing});
final Diet? diet;
final bool? isEditing;
@override
State<CreateDietScreenContent> createState() => _CreateDietScreenContentState();
}
class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final scaffoldKey = GlobalKey<ScaffoldState>();
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();
if (widget.diet != null) {
nameController.text = widget.diet?.name ?? '';
descriptionController.text = widget.diet?.details ?? '';
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<CreateDietCubit, CreateDietState>(
builder: (context, state) {
if (state.published) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
);
});
} else if (state.drafted) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop(true);
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
);
});
}
return Scaffold(
backgroundColor: appTheme.gray100,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
centerTitle: true,
title: Text(
s.create_diet,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 8),
MainBackgroundBouble(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
CustomTextFormField(
key: _nameKey,
hintText: s.enter_diet_name,
controller: nameController,
hasError: state.dietNameError != null,
onChanged: (value) {
context.read<CreateDietCubit>().setName(value);
},
),
if (state.dietNameError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietNameError ?? '')],
const SizedBox(height: 12),
MarkdownEditor(
key: _mdKey,
initialText: descriptionController.text,
hintText: s.enter_diet_description,
hasError: state.dietDescriptionError != null,
minLines: 6,
maxLines: 8,
showInlineToolbar: false,
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
onChanged: (value) {
descriptionController.text = value;
context.read<CreateDietCubit>().setDetails(value);
},
),
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
const SizedBox(height: 12),
DietDurationDropDownWidget(key: _durationKey),
const SizedBox(height: 12),
StatusDropDownWidget(key: _statusKey, dietState: widget.diet?.state),
const SizedBox(height: 16),
DietTypeChoiceWidget(key: _typeKey),
const SizedBox(height: 16),
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(
child: Text(
'${s.diet_plan} (${state.dietData.duration ?? 0} ${getDayForm(state.dietData.duration ?? 0, s)})',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
),
),
const SizedBox(height: 8),
ListView.separated(
padding: const EdgeInsets.all(0),
itemBuilder: (context, index) => DayWidget(index: index),
itemCount: state.dietData.dailySchedule?.length ?? 0,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => const SizedBox(height: 8),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: state.isDietBeingEdited || state.isDietPublishing
? const Loader()
: Column(
children: [
CustomOutlinedButton(
onPressed: () async {
if (state.isDietPublishing) return;
if (widget.diet != null && widget.isEditing == true) {
final dietState = state.dietState;
if (dietState == DietState.draft) {
context.read<CreateDietCubit>().setDietState(DietState.active);
} else {
context.read<CreateDietCubit>().setDietState(DietState.draft);
}
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
return;
}
if (dietState == DietState.draft) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
);
});
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop(true);
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
);
});
}
} else {
if (state.dietState != DietState.draft) {
context.read<CreateDietCubit>().setDietState(DietState.draft);
}
final result = await context.read<CreateDietCubit>().createDiet();
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
}
}
},
text: state.dietState == DietState.draft && widget.isEditing == true ? s.publish : "В черновик",
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
),
),
const SizedBox(height: 12),
CustomOutlinedButton(
onPressed: state.isDietPublishing || state.isDietBeingEdited
? () {}
: () async {
final diet = await context.read<CreateDietCubit>().getDietForPreview();
if (diet == null) {
if (!context.mounted) return;
_scrollToFirstError(context.read<CreateDietCubit>().state);
return;
}
if (!context.mounted) return;
GoRouter.of(
context,
).pushNamed(DietPreviewScreen.name, extra: {'diet': diet, 'isImageInBase64': state.dietData.imageUrl == null});
},
text: s.preview,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
),
),
const SizedBox(height: 12),
CustomOutlinedButton(
onPressed: () async {
if (state.isDietBeingEdited) return;
if (widget.diet != null && widget.isEditing == true) {
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isUpdated: true),
);
});
}
} else {
final result = await context.read<CreateDietCubit>().createDiet();
if (result == 'validation') {
if (!context.mounted) return;
_scrollToFirstError(context.read<CreateDietCubit>().state);
} else if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
}
}
},
text: widget.diet != null && widget.isEditing == true ? s.save : s.publish,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
],
),
),
const SizedBox(height: 20),
],
),
),
);
},
);
}
}