Files
eatvio_app/lib/features/plans/presentation/pages/about_plan_screen.dart
2026-10-12 08:50:33 +03:00

544 lines
27 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:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart';
import 'package:eatvio_app/features/plans/presentation/widgets/confirm_start_plan_bottom_sheet.dart';
import 'package:eatvio_app/features/plans/presentation/widgets/options_tab.dart';
import 'package:eatvio_app/generated/l10n.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/widgets/option_widget.dart';
class AboutPlanScreen extends StatefulWidget {
final String slug;
final bool navigateToComments;
final int? index;
final int? highlightedCommentId;
const AboutPlanScreen({
super.key,
required this.slug,
required this.navigateToComments,
required this.index,
this.highlightedCommentId,
});
@override
State<AboutPlanScreen> createState() => _AboutPlanScreenState();
}
class _AboutPlanScreenState extends State<AboutPlanScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>();
int? replyId;
final ScrollController scrollController = ScrollController();
final textEditingController = TextEditingController();
bool isMoreOptionsVisible = false;
bool _jumped = false;
@override
void initState() {
super.initState();
context.read<FeedCubit>().viewPlan(widget.slug);
scrollController.addListener(() {
if (isMoreOptionsVisible) {
setState(() {
isMoreOptionsVisible = false;
});
}
});
}
void _scrollToBottom() {
_jumped = true;
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
Future<void> _onOrderPressed(BuildContext context) async {
final cubit = context.read<AboutPlanCubit>();
final publicDiet = await cubit.getActivePublicDiet();
if (!context.mounted) {
return;
}
if (publicDiet != null) {
showModalBottomSheet(
context: context,
builder: (_) => ConfirmStartPlanBottomSheet(
onConfirm: () => _submitOrder(context, cubit, publicDietId: publicDiet.dietId),
),
);
return;
}
await _submitOrder(context, cubit);
}
Future<void> _submitOrder(BuildContext context, AboutPlanCubit cubit, {int? publicDietId}) async {
final s = S.of(context);
if (publicDietId != null) {
final cancelled = await cubit.cancelPublicDiet(publicDietId);
if (!cancelled) {
showErrorToast('Не удалось отменить диету');
return;
}
}
final (chosenPlan, error) = await cubit.chosePlan();
if (!context.mounted) {
return;
}
if (error == 'hasPlan') {
showErrorDialog(context, title: s.end_previous_plan, description: s.you_can_not_start_plan_until_end_of_prev_one);
return;
}
if (chosenPlan == null) {
showErrorDialog(context, title: 'Не удалось заказать план');
return;
}
GoRouter.of(context).pushReplacementNamed('chatScreen', extra: {'uuid': chosenPlan.chatId, 'sendInitialMessage': true});
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return PopScope(
child: MultiBlocProvider(
providers: [
BlocProvider<CommentsCubit>(create: (context) => commentsCubit..getComments(widget.slug, CommentSourceType.plan, true)),
BlocProvider<AboutPlanCubit>(create: (context) => sl<AboutPlanCubit>()..getPlanWithLoading(widget.slug)),
],
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState commentsState) {
if (commentsState is CommentsStateLoaded) {
setState(() {
replyId = commentsState.replyId;
});
}
},
builder: (BuildContext context, CommentsState commentsState) {
return BlocBuilder<AboutPlanCubit, AboutPlanState>(
builder: (context, state) {
if (state.isLoading) {
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
centerTitle: true,
title: Text(
'',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: const Loader(),
);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.navigateToComments && !_jumped) {
_scrollToBottom();
}
});
if (state.error != null) {
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
centerTitle: true,
title: Text(
state.planModel?.name ?? '',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
actions: [
CustomPopup(
isAuthor: state.planModel?.author.authorIsMe ?? false,
isAuthorAdmin: state.planModel?.author.isAdmin ?? false,
path: 'plan/${widget.slug}',
slug: widget.slug,
id: state.planModel?.id,
plan: state.planModel,
complainedContentType: ComplainedContent.plan,
userSlug: state.planModel?.author.authorSlug ?? '',
),
const SizedBox(width: 16),
],
),
body: DataErrorWidget(onReload: () => context.read<AboutPlanCubit>().getPlan(slug: widget.slug)),
);
}
return Scaffold(
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: SizedBox(
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: appTheme.greenA700,
onPressed: () => _onOrderPressed(context),
label: Text(s.to_order, style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700)),
),
),
),
const SizedBox(width: 8),
BookmarkButton(
isFavorite: state.planModel?.isFavorite ?? false,
onTap: () {
final planModel = state.planModel;
if (planModel == null) return;
if (planModel.isFavorite) {
context.read<AboutPlanCubit>().removeFromFavorite(slug: planModel.slug, index: widget.index);
} else {
context.read<AboutPlanCubit>().addToFavorite(slug: planModel.slug, index: widget.index);
}
},
width: 52,
height: 52,
size: 24,
shape: BoxShape.circle,
color: appTheme.gray50,
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false, // убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.slug,
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.plan,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.read<AboutPlanCubit>().getPlan(slug: widget.slug, index: widget.index);
}
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(
color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
shape: BoxShape.circle,
),
height: 32,
width: 32,
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
),
),
],
),
],
),
),
),
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
state.planModel?.name ?? '',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
actions: [
CustomPopup(
isAuthor: state.planModel?.author.authorIsMe ?? false,
isAuthorAdmin: state.planModel?.author.isAdmin ?? false,
path: 'plan/${widget.slug}',
slug: widget.slug,
id: state.planModel?.id,
plan: state.planModel,
complainedContentType: ComplainedContent.plan,
userSlug: state.planModel?.author.authorSlug ?? '',
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
controller: scrollController,
padding: EdgeInsets.only(
bottom: 220 + MediaQuery.of(context).padding.bottom,
), // кнопка (52) + отступ (8) + поле комментария (52-200) + padding (12+12+56) + safe area
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
MainBackgroundBouble(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
CachedNetworkImage(
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.planModel?.image}',
height: 300,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,
),
Positioned(
bottom: 16,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InfoBouble(
backgroundColor: appTheme.whiteA700,
children: [
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
const SizedBox(width: 8),
Text(
state.planModel?.totalViews.toString() ?? '',
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
],
),
const SizedBox(width: 12),
GestureDetector(
onTap: () {
context.read<AboutPlanCubit>().likePlan(slug: widget.slug, index: widget.index);
},
child: InfoBouble(
backgroundColor: appTheme.whiteA700,
children: [
SvgPicture.asset(
'assets/icons/heart.svg',
width: 14,
height: 12,
colorFilter: state.planModel?.wasLikedByMe == true
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: null,
),
const SizedBox(width: 8),
Text(
state.planModel?.totalLikes.toString() ?? '0',
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
],
),
),
const SizedBox(width: 12),
InfoBouble(
backgroundColor: appTheme.whiteA700,
children: [
SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12),
const SizedBox(width: 8),
Text(
state.planModel?.totalComments.toString() ?? '',
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
],
),
],
),
),
],
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(s.about_plan, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
InfoRow(plan: state.planModel, type: ProductType.plan),
ExpandableText(text: state.planModel?.details ?? ''),
if (state.planModel != null) ...[
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(author: (state.planModel?.author)!, title: s.plan_author),
),
],
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 16),
if (state.planModel?.packetsCount == 3) ...[
OptionsTab(
cost: [
state.planModel?.packet1Options?.price ?? 0,
state.planModel?.packet2Options?.price ?? 0,
state.planModel?.packet3Options?.price ?? 0,
],
),
const SizedBox(height: 12),
],
switch (state.chosenOptionIndex) {
1 => OptionWidget(option: state.planModel?.packet2Options, duration: state.planModel?.duration ?? 0),
2 => OptionWidget(option: state.planModel?.packet3Options, duration: state.planModel?.duration ?? 0),
_ => OptionWidget(option: state.planModel?.packet1Options, duration: state.planModel?.duration ?? 0),
},
const SizedBox(height: 16),
MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
'${state.planModel?.totalComments ?? 0} ${getCommentForm(state.planModel?.totalComments ?? 0, s)}',
style: TextStyles.textMMedium.copyWith(color: appTheme.gray500),
),
),
const SizedBox(height: 16),
if (commentsState is CommentsStateLoading) const Loader(),
if (commentsState is CommentsStateLoaded)
CommentsWidget(
id: state.planModel?.id.toString() ?? '',
commentSourceType: CommentSourceType.plan,
comment: commentsState.comment,
replyId: replyId,
highlightedCommentId: widget.highlightedCommentId,
),
],
),
),
],
),
),
);
},
);
},
),
),
);
}
}