forked from eatvio/eatvio_app
413 lines
15 KiB
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|