Files
eatvio_app/lib/features/create_plan/presentation/widgets/option_widget.dart

413 lines
15 KiB
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/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/presentation/widgets/add_item_widget.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
class OptionWidget extends StatefulWidget {
const OptionWidget({super.key});
@override
State<OptionWidget> createState() => _OptionWidgetState();
}
class _OptionWidgetState extends State<OptionWidget> {
static const _optionsCount = 3;
final List<TextEditingController> _customerPriceControllers = List.generate(
_optionsCount,
(_) => TextEditingController(text: '1200₽'),
);
final List<TextEditingController> _titleControllers = List.generate(
_optionsCount,
(_) => TextEditingController(),
);
final List<TextEditingController> _descriptionControllers = List.generate(
_optionsCount,
(_) => TextEditingController(),
);
final creatorPriceController = TextEditingController(text: '1000₽');
int? _syncedOptionIndex;
bool firstExplanationVisible = false;
bool isChecked = false;
bool secondExplanationVisible = false;
@override
void initState() {
super.initState();
for (final controller in _customerPriceControllers) {
controller.addListener(() => _ensureRubleSuffix(controller));
}
}
@override
void dispose() {
for (final controller in _customerPriceControllers) {
controller.dispose();
}
for (final controller in _titleControllers) {
controller.dispose();
}
for (final controller in _descriptionControllers) {
controller.dispose();
}
creatorPriceController.dispose();
super.dispose();
}
void _ensureRubleSuffix(TextEditingController controller) {
if (!controller.text.endsWith('₽')) {
final text = controller.text.replaceAll('₽', '');
controller.text = '$text₽';
controller.selection = TextSelection.collapsed(offset: text.length);
}
}
void _syncController(TextEditingController controller, String value) {
if (controller.text == value) {
return;
}
controller.value = TextEditingValue(
text: value,
selection: TextSelection.collapsed(offset: value.length),
);
}
String _formatCustomerPrice(dynamic price) {
if (price is num) {
return '${price.toStringAsFixed(0)}₽';
}
final parsed = double.tryParse(price.toString().replaceAll('₽', ''));
return '${(parsed ?? 0).toStringAsFixed(0)}₽';
}
void _applyOptionToControllers(int index, PacketOptions option) {
_syncController(_titleControllers[index], option.short);
_syncController(_descriptionControllers[index], option.description ?? '');
_syncController(
_customerPriceControllers[index],
_formatCustomerPrice(option.price),
);
}
void _syncSelectedOption(CreatePlanState state) {
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
final option = state.planData.options[selectedOptionIndex];
final price = double.tryParse(
_customerPriceControllers[selectedOptionIndex].text.replaceAll('₽', ''),
);
final creatorPrice = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
if (_syncedOptionIndex != selectedOptionIndex) {
_syncedOptionIndex = selectedOptionIndex;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_applyOptionToControllers(selectedOptionIndex, option);
creatorPriceController.text = creatorPrice;
});
return;
}
if (creatorPriceController.text != creatorPrice) {
creatorPriceController.text = creatorPrice;
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
builder: (context, state) {
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
_syncSelectedOption(state);
final optionNameHintText = switch (selectedOptionIndex) {
0 => s.economy,
2 => s.comfort,
_ => s.standard,
};
final optionTitleController = _titleControllers[selectedOptionIndex];
final optionDescriptionController =
_descriptionControllers[selectedOptionIndex];
final customerPriceController =
_customerPriceControllers[selectedOptionIndex];
final optionItems = state.planData.options[selectedOptionIndex].items;
final optionValidation =
state.validationState.optionsValidationState[selectedOptionIndex];
return MainBackgroundBouble(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.option_title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
CustomTextFormField(
hintText: optionNameHintText,
hasError: optionValidation.titleHasError,
controller: optionTitleController,
onChanged: (value) {
context.read<CreatePlanCubit>().setOptionTitle(value);
},
maxLength: 150,
),
if (optionValidation.titleHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
const SizedBox(height: 12),
Text(
s.option_desc,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
SizedBox(
height: 120,
child: CustomTextFormField(
controller: optionDescriptionController,
textAlignVertical: TextAlignVertical.top,
hasError: optionValidation.descriptionHasError,
maxLength: 1000,
minLines: null,
maxLines: null,
expands: true,
hintText: s.option_desc_hint_text,
onChanged: (value) {
context.read<CreatePlanCubit>().setOptionDescription(value);
},
),
),
if (optionValidation.descriptionHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
const SizedBox(height: 12),
Row(
children: [
Text(
s.you_will_get,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(
color: appTheme.black,
),
),
const SizedBox(width: 4),
const _PriceExplanationWidget(),
],
),
const SizedBox(height: 4),
CustomTextFormField(
controller: creatorPriceController,
readOnly: true,
textStyle: TextStyle(color: appTheme.gray),
maxLength: 20,
),
const SizedBox(height: 12),
Row(
children: [
Text(
s.customer_price,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(
color: appTheme.black,
),
),
const SizedBox(width: 4),
const _PriceExplanationWidget(),
],
),
const SizedBox(height: 4),
CustomTextFormField(
controller: customerPriceController,
textInputType: const TextInputType.numberWithOptions(
decimal: true,
),
hasError: optionValidation.priceHasError,
onChanged: (value) {
final price = double.tryParse(value.replaceAll('₽', ''));
if (price != null) {
context.read<CreatePlanCubit>().setPrice(price);
}
},
maxLength: 20,
),
const SizedBox(height: 10),
Text(
s.minimal_customer_price,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 16),
const CustomDivider(),
const SizedBox(height: 16),
Text(
s.option_content,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 4),
ListView.separated(
padding: EdgeInsets.zero,
separatorBuilder: (context, index) => const SizedBox(height: 8),
reverse: false,
itemBuilder: (context, index) {
if (optionItems.isEmpty) {
return AddItemWidget(
key: ValueKey(
'plan-option-$selectedOptionIndex-item-$index',
),
index: index,
canBeDeleted: false,
enableHint: true,
items: optionItems,
);
} else {
if (index < optionItems.length) {
return AddItemWidget(
key: ValueKey(
'plan-option-$selectedOptionIndex-item-$index',
),
canBeDeleted: true,
enableHint: false,
index: index,
items: optionItems,
);
} else {
return AddItemWidget(
key: ValueKey(
'plan-option-$selectedOptionIndex-item-$index',
),
canBeDeleted: false,
enableHint: false,
index: index,
items: optionItems,
);
}
}
},
itemCount: optionItems.length + 1,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
),
const SizedBox(height: 16),
const CustomDivider(),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: state
.planData
.options[selectedOptionIndex]
.hasPersonalDiet,
activeColor: appTheme.gray,
checkColor: appTheme.gray,
fillColor: const WidgetStatePropertyAll(Colors.transparent),
side: WidgetStateBorderSide.resolveWith((
Set<WidgetState> states,
) {
return BorderSide(width: 1, color: appTheme.gray);
}),
onChanged: (bool? value) {
context
.read<CreatePlanCubit>()
.togglePersonalizedDietOptionState();
},
),
Expanded(
child: Text(
s.option_personal_diet_desc,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
),
],
),
],
),
);
},
);
}
}
class _PriceExplanationWidget extends StatefulWidget {
const _PriceExplanationWidget();
@override
State<_PriceExplanationWidget> createState() =>
_PriceExplanationWidgetState();
}
class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
bool visible = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return PortalTarget(
closeDuration: const Duration(milliseconds: 300),
portalFollower: AnimatedContainer(
padding: const EdgeInsets.all(10),
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: visible ? appTheme.black : Colors.transparent,
),
child: AnimatedDefaultTextStyle(
style: TextStyles.textMMediumAuto.copyWith(
color: visible ? appTheme.whiteA700 : Colors.transparent,
),
duration: const Duration(milliseconds: 300),
child: Text(s.price_explanation),
),
),
visible: visible,
anchor: const Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.bottomLeft,
target: Alignment.topRight,
),
child: GestureDetector(
onTap: () {
setState(() {
visible = !visible;
});
},
child: SvgPicture.asset(
'assets/icons/question_mark.svg',
height: 12,
width: 12,
),
),
);
}
}