forked from eatvio/eatvio_app
feat(video_calls, welcome_screen): ios mic fix, welcome scrin layoute fix
This commit is contained in:
@@ -27,6 +27,12 @@ class AddItemWidget extends StatefulWidget {
|
||||
class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -38,14 +44,24 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
if (widget.items.length > widget.index && controller.text != widget.items[widget.index]) {
|
||||
if (widget.items.length > widget.index &&
|
||||
controller.text != widget.items[widget.index]) {
|
||||
controller.text = widget.items[widget.index];
|
||||
}
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
buildWhen: (previous, current) => previous.validationState != current.validationState,
|
||||
buildWhen: (previous, current) =>
|
||||
previous.validationState != current.validationState,
|
||||
builder: (context, state) {
|
||||
final hasError = state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty ||
|
||||
(state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false);
|
||||
final hasError =
|
||||
state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.isItemsEmpty ||
|
||||
(state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.itemsErrors[widget.index] ??
|
||||
false);
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -54,9 +70,14 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
child: CustomTextFormField(
|
||||
controller: controller,
|
||||
hasError: hasError,
|
||||
hintText: widget.enableHint ? s.option_content_hint_text : null,
|
||||
hintText: widget.enableHint
|
||||
? s.option_content_hint_text
|
||||
: null,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setOptionContent(value, widget.index);
|
||||
context.read<CreatePlanCubit>().setOptionContent(
|
||||
value,
|
||||
widget.index,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -64,7 +85,9 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreatePlanCubit>().removeOptionContentItem(widget.index);
|
||||
context.read<CreatePlanCubit>().removeOptionContentItem(
|
||||
widget.index,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
@@ -72,19 +95,30 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
color: appTheme.lightGray0,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/trash_can.svg',
|
||||
width: 20,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty) ...[
|
||||
if (state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.isItemsEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.addAtLeastOneItem)
|
||||
] else if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false) ...[
|
||||
ValidationErrorWidget(error: s.addAtLeastOneItem),
|
||||
] else if (state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.itemsErrors[widget.index] ??
|
||||
false) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols)
|
||||
]
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widge
|
||||
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';
|
||||
@@ -21,47 +22,107 @@ class OptionWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OptionWidgetState extends State<OptionWidget> {
|
||||
final firstOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
final secondOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
final thirdOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
static const _optionsCount = 3;
|
||||
|
||||
final creatorPriceController = TextEditingController()..text = '1000₽';
|
||||
|
||||
final firstOptionTitleController = TextEditingController();
|
||||
final secondOptionTitleController = TextEditingController();
|
||||
final thirdOptionTitleController = TextEditingController();
|
||||
|
||||
final firstOptionDescriptionController = TextEditingController();
|
||||
final secondOptionDescriptionController = TextEditingController();
|
||||
final thirdOptionDescriptionController = TextEditingController();
|
||||
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();
|
||||
firstOptionCustomerPriceController.addListener(() {
|
||||
if (!firstOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = firstOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
firstOptionCustomerPriceController.text = '$text₽';
|
||||
firstOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
secondOptionCustomerPriceController.addListener(() {
|
||||
if (!secondOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = secondOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
secondOptionCustomerPriceController.text = '$text₽';
|
||||
secondOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
thirdOptionCustomerPriceController.addListener(() {
|
||||
if (!thirdOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = thirdOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
thirdOptionCustomerPriceController.text = '$text₽';
|
||||
thirdOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
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
|
||||
@@ -70,31 +131,21 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
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 = switch (selectedOptionIndex) {
|
||||
0 => firstOptionTitleController,
|
||||
2 => thirdOptionTitleController,
|
||||
_ => secondOptionTitleController,
|
||||
};
|
||||
final optionDescriptionController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionDescriptionController,
|
||||
2 => thirdOptionDescriptionController,
|
||||
_ => secondOptionDescriptionController,
|
||||
};
|
||||
final customerPriceController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionCustomerPriceController,
|
||||
2 => thirdOptionCustomerPriceController,
|
||||
_ => secondOptionCustomerPriceController,
|
||||
};
|
||||
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];
|
||||
|
||||
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
|
||||
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||
firstOptionDescriptionController.text = state.planData.options[selectedOptionIndex].description ?? '';
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -110,14 +161,14 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: optionNameHintText,
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError,
|
||||
hasError: optionValidation.titleHasError,
|
||||
controller: optionTitleController,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setOptionTitle(value);
|
||||
},
|
||||
maxLength: 150,
|
||||
),
|
||||
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError) ...[
|
||||
if (optionValidation.titleHasError) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
@@ -135,7 +186,7 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
child: CustomTextFormField(
|
||||
controller: optionDescriptionController,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError,
|
||||
hasError: optionValidation.descriptionHasError,
|
||||
maxLength: 1000,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
@@ -146,7 +197,7 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError) ...[
|
||||
if (optionValidation.descriptionHasError) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
@@ -158,7 +209,9 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
style: TextStyles.textSMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _PriceExplanationWidget(),
|
||||
@@ -179,7 +232,9 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
style: TextStyles.textSMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _PriceExplanationWidget(),
|
||||
@@ -188,8 +243,10 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: customerPriceController,
|
||||
textInputType: const TextInputType.numberWithOptions(decimal: true),
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError,
|
||||
textInputType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
hasError: optionValidation.priceHasError,
|
||||
onChanged: (value) {
|
||||
final price = double.tryParse(value.replaceAll('₽', ''));
|
||||
if (price != null) {
|
||||
@@ -222,34 +279,41 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
reverse: false,
|
||||
itemBuilder: (context, index) {
|
||||
final items = state.planData.options[selectedOptionIndex].items;
|
||||
|
||||
if (items.isEmpty) {
|
||||
if (optionItems.isEmpty) {
|
||||
return AddItemWidget(
|
||||
key: ValueKey(
|
||||
'plan-option-$selectedOptionIndex-item-$index',
|
||||
),
|
||||
index: index,
|
||||
canBeDeleted: false,
|
||||
enableHint: true,
|
||||
items: items,
|
||||
items: optionItems,
|
||||
);
|
||||
} else {
|
||||
if (index < items.length) {
|
||||
if (index < optionItems.length) {
|
||||
return AddItemWidget(
|
||||
key: ValueKey(
|
||||
'plan-option-$selectedOptionIndex-item-$index',
|
||||
),
|
||||
canBeDeleted: true,
|
||||
enableHint: false,
|
||||
index: index,
|
||||
items: items,
|
||||
items: optionItems,
|
||||
);
|
||||
} else {
|
||||
return AddItemWidget(
|
||||
key: ValueKey(
|
||||
'plan-option-$selectedOptionIndex-item-$index',
|
||||
),
|
||||
canBeDeleted: false,
|
||||
enableHint: false,
|
||||
index: index,
|
||||
items: items,
|
||||
items: optionItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemCount: state.planData.options[selectedOptionIndex].items.length + 1,
|
||||
itemCount: optionItems.length + 1,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
),
|
||||
@@ -259,23 +323,30 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: state.planData.options[selectedOptionIndex].hasPersonalDiet,
|
||||
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);
|
||||
},
|
||||
),
|
||||
side: WidgetStateBorderSide.resolveWith((
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
return BorderSide(width: 1, color: appTheme.gray);
|
||||
}),
|
||||
onChanged: (bool? value) {
|
||||
context.read<CreatePlanCubit>().togglePersonalizedDietOptionState();
|
||||
context
|
||||
.read<CreatePlanCubit>()
|
||||
.togglePersonalizedDietOptionState();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
s.option_personal_diet_desc,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -292,7 +363,8 @@ class _PriceExplanationWidget extends StatefulWidget {
|
||||
const _PriceExplanationWidget();
|
||||
|
||||
@override
|
||||
State<_PriceExplanationWidget> createState() => _PriceExplanationWidgetState();
|
||||
State<_PriceExplanationWidget> createState() =>
|
||||
_PriceExplanationWidgetState();
|
||||
}
|
||||
|
||||
class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
@@ -314,9 +386,7 @@ class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
color: visible ? appTheme.whiteA700 : Colors.transparent,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
s.price_explanation,
|
||||
),
|
||||
child: Text(s.price_explanation),
|
||||
),
|
||||
),
|
||||
visible: visible,
|
||||
@@ -331,7 +401,11 @@ class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
visible = !visible;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset('assets/icons/question_mark.svg', height: 12, width: 12),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 12,
|
||||
width: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user