import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; import 'package:eatvio_app/features/profile/models/weight_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/profile_blog_post_widget.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; class ProfileScreen extends StatefulWidget { final Author? author; const ProfileScreen({super.key, this.author}); @override State createState() => _ProfileScreenState(); } class _ProfileScreenState extends State { late final ProfileCubit profileCubit = sl(); final PlansCubit plansCubit = sl(); final List chipsLabelList = ['Вес', 'Замеры', 'ИМТ']; final List chipsIntervalLabelList = ['1М', '3М', '6М', '1Г', 'ВСЁ ВРЕМЯ']; final List chipsIntervalList = ['1', '3', '6', '12', 'all']; final TextEditingController controller = TextEditingController(); List weightPoints = []; List circumferencePoints = []; int _selectedChipIndex = 0; int _selectedIntervalChipIndex = 0; late bool canSub; bool isFirstFetch = true; int _currentBullet = 0; @override void initState() { super.initState(); widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); } @override Widget build(BuildContext context) { FlutterNativeSplash.remove(); return BlocProvider( create: (BuildContext context) => plansCubit, child: BlocBuilder( bloc: profileCubit, builder: (BuildContext context, ProfileState state) { if (state is ProfileStateLoaded) { if (state.circumferencePoints != null) { circumferencePoints = state.circumferencePoints!; weightPoints = state.weightPoints!; } if (isFirstFetch) { canSub = !state.profile.isEntitySubscribedByMe; if (state.profile.roles.contains('consultant') && !hidePlans) { plansCubit.getPlans(state.profile.slug); } isFirstFetch = false; } return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( appBar: AppBar(), body: DataErrorWidget( onReload: () async { widget.author == null || widget.author!.authorIsMe ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.author!.authorSlug); }, ), ); } else { return Scaffold(appBar: AppBar(), body: const Loader()); } }, ), ); } Scaffold _profileScreen(ProfileStateLoaded state) { return Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar(), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ _header(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const SizedBox(width: 16), const Text( 'Консультант', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), child: Text( '${state.consultantInfo!.ratingPlace}-й в рейтинге', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400), ), ), ], ), if (state.consultantInfo!.educationModel.hasMedicalEducation == 1) const Row( children: [ SizedBox(width: 16), Icon(Icons.done_rounded, color: Colors.green), Text( 'Медицинское образование', style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600), ), ], ), ], ), if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state), if (!state.profile.isBlockedByMe) const SizedBox(height: 8), if (!state.profile.isBlockedByMe) _messageAndSubscribe(state), if (!state.profile.isBlockedByMe) const SizedBox(height: 16), ], ), ), _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.isBlockedByAuthor) Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.block, size: 48, color: appTheme.grey7B), const SizedBox(height: 16), Center( child: Text( 'Пользователь ограничил доступ\nк своей странице', textAlign: TextAlign.center, style: TextStyles.textMSemibold.copyWith(color: appTheme.grey7B), ), ), ], ), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) _socialLinks(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) _education(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) _activePlans(state), if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) const Padding( padding: EdgeInsets.only(left: 8, top: 16, bottom: 8), child: Text( 'Блог', style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), ), ), if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) _blogPosts(state), ], ), ), ); } Scaffold _myProfileScreen(ProfileStateLoaded state) { return Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar(toolbarHeight: widget.author == null ? 0 : null), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ _header(state), if (state.profile.roles.contains('consultant')) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const SizedBox(width: 16), const Text( 'Консультант', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), child: Text( '${state.consultantInfo!.ratingPlace}-й в рейтинге', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400), ), ), ], ), if (state.consultantInfo!.educationModel.hasMedicalEducation == 1) const Row( children: [ SizedBox(width: 16), Icon(Icons.done_rounded, color: Colors.green), Text( 'Медицинское образование', style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600), ), ], ), ], ), if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state), ], ), ), _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.roles.contains('consultant')) _socialLinks(state), Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant')) _education(state), if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), _newBlogPostButton(), _blogPosts(state), ], ), ), ); } Widget _activePlans(ProfileStateLoaded state) { if (!state.profile.roles.contains('consultant') || hidePlans) { return const SizedBox.shrink(); } return BlocBuilder( bloc: plansCubit, builder: (BuildContext context, PlansState state) { if (state is PlansStateLoaded) { if (state.plansList.isEmpty) { return const SizedBox.shrink(); } return Column( children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Активные планы', style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600), ), ], ), ), const SizedBox(height: 8), SizedBox( height: 781, child: PageView.builder( itemBuilder: (BuildContext context, int index) { return PlanWidget(plan: state.plansList[index], index: index); }, onPageChanged: (bullet) { setState(() { _currentBullet = bullet; }); }, itemCount: state.plansList.length, ), ), const SizedBox(height: 8), _bullets(_currentBullet, state.plansList.length), const SizedBox(height: 8), ], ); } return const Loader(); }, ); } Container _bullets(int currentBullet, int bulletCount) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), child: Row( mainAxisSize: MainAxisSize.min, children: [ for (int i = 0; i < bulletCount; i++) ...[_bullet(i == currentBullet), i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink()], ], ), ); } AnimatedContainer _bullet(bool isActive) { return AnimatedContainer( width: isActive ? 10 : 8, height: isActive ? 10 : 8, duration: const Duration(milliseconds: 150), decoration: BoxDecoration(color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC), shape: BoxShape.circle), ); } Material _newBlogPostButton() { return Material( color: Colors.white, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: InkWell( onTap: () { GoRouter.of(context).pushNamed('blogPostCreateScreen'); }, borderRadius: BorderRadius.circular(8), child: Ink( padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(8)), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_circle_outline_rounded, color: Color(0xFF2F80ED)), SizedBox(width: 12), Text( 'Новая запись', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF2F80ED)), ), ], ), ), ), ), ); } SingleChildRenderObjectWidget _diplomasAndCertificates(ProfileStateLoaded state) { if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Дипломы\nи сертификаты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600), ), GestureDetector( onTap: () { GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug); }, child: Text( widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '', style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500), ), ), ], ), ), const SizedBox(height: 8), Container( width: MediaQuery.of(context).size.width - 2, padding: const EdgeInsets.symmetric(horizontal: 4), child: Material( color: Colors.white, borderRadius: BorderRadius.circular(12), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ if (state.consultantInfo!.photos.isNotEmpty) ...List.generate(state.consultantInfo!.photos.length * 2 - 1, (index) { if (index.isEven) { final photo = state.consultantInfo!.photos[index ~/ 2]; return _diplomaImage(photo.url); } else { return const SizedBox(width: 8); } }), ], ), ), ), ), ], ), ); } GestureDetector _diplomaImage(String url) { return GestureDetector( onTap: () { context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0)); }, child: Hero( tag: url, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160), ), ), ); } SingleChildRenderObjectWidget _education(ProfileStateLoaded state) { if (!state.profile.roles.contains('consultant') || state.consultantInfo!.educationModel.completedCourses == null || (state.consultantInfo!.educationModel.completedCourses?.isEmpty ?? false)) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Образование', style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), ), GestureDetector( onTap: () { GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug); }, child: Text( widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '', style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500), ), ), ], ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Material( color: Colors.white, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ ListView.separated( shrinkWrap: true, padding: const EdgeInsets.all(0), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _educationBlock( state.consultantInfo!.educationModel.completedCourses![index].values.first.title, state.consultantInfo!.educationModel.completedCourses![index].values.first.date, ), separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.consultantInfo!.educationModel.completedCourses!.length, ), ], ), ), ), ), ], ), ); } SizedBox _educationBlock(String title, String date) { final parsedDate = DateTime.tryParse(date); final formattedDate = parsedDate != null ? DateFormat('d MMMM y', 'ru_RU').format(parsedDate) : date; return SizedBox( width: double.infinity, child: Material( color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontWeight: FontWeight.w500, fontSize: 16), ), const SizedBox(height: 4), Text( formattedDate, style: const TextStyle(height: 1, color: Color(0xFF6D6F71), fontWeight: FontWeight.w500, fontSize: 14), ), ], ), ), ), ); } SingleChildRenderObjectWidget _socialLinks(ProfileStateLoaded state) { if (!state.profile.roles.contains('consultant') && state.consultantInfo != null) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 4) + const EdgeInsets.only(bottom: 16), child: Material( color: Colors.white, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ _socialLinkButton( () { launchUrl(Uri.parse(state.consultantInfo!.educationModel.vkLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/vk.svg', state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false, ), const SizedBox(width: 12), _socialLinkButton( () { launchUrl(Uri.parse(state.consultantInfo!.educationModel.tgLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/telegram.svg', state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false, ), const SizedBox(width: 12), _socialLinkButton( () { launchUrl(Uri.parse(state.consultantInfo!.educationModel.personalSiteLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/web.svg', state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false, ), ], ), ), ), ); } InkWell _socialLinkButton(Function() onTap, String icon, bool disabled) { return InkWell( onTap: disabled ? null : onTap, borderRadius: BorderRadius.circular(20), child: Ink( width: MediaQuery.of(context).size.width / 3 - (56 / 3), decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16), child: SvgPicture.asset( icon, width: 24, colorFilter: disabled ? const ColorFilter.mode(Colors.black12, BlendMode.srcIn) : const ColorFilter.mode(Colors.black, BlendMode.srcIn), ), ), ), ); } Padding _badges(ProfileStateLoaded state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: InkWell( onTap: () {}, borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Достижения', style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (int i = 0; i < state.profile.badges.length; i++) ...[ state.profile.badges[i].image.contains('svg') ? Tooltip( message: state.profile.badges[i].description, triggerMode: TooltipTriggerMode.tap, textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), child: SvgPicture.network('${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ) : Tooltip( message: state.profile.badges[i].description, margin: const EdgeInsets.symmetric(horizontal: 32), triggerMode: TooltipTriggerMode.tap, textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), child: CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ), i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], ], ), ), ], ), ), ), ); } Padding _subscriptionsAndSubscribers(ProfileStateLoaded state) { return Padding( padding: const EdgeInsets.all(4), child: Row( children: [ InkWell( onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscriptions, pathParameters: {'slug': state.profile.slug}), borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width / 2 - 6, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Подписки', style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), Text( '${state.profile.subscriptions.length}', style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700), ), ], ), Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscriptions)), ], ), ), ), const SizedBox(width: 4), InkWell( onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscribers, pathParameters: {'slug': state.profile.slug}), borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width / 2 - 6, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Подписчики', style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), Text( '${state.profile.subscribers.length}', style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700), ), ], ), Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscribers)), ], ), ), ), ], ), ); } Row _miniAvatars(List urls) { int maxLength = urls.length > 2 ? 3 : urls.length; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < maxLength; i++) Align( widthFactor: 0.8, child: CircleAvatar(radius: 14, foregroundImage: NetworkImage(urls[i]), backgroundImage: const AssetImage('assets/images/no_avatar.png')), ), ], ); } ListView _blogPosts(ProfileStateLoaded state) { return ListView.separated( padding: const EdgeInsets.only(bottom: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.blogPosts.length, ); } Container _additionalInfo(ProfileStateLoaded state) { String text = state.profileDetails.additionalInfo; if (state.profileDetails.additionalInfo.length > 250) { text = state.profileDetails.additionalInfo.substring(0, 250); } return Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16), child: Text(text, style: TextStyle(color: Colors.black.withValues(alpha: 0.4), height: 1)), ); } Padding _header(ProfileStateLoaded state) { return Padding( padding: const EdgeInsets.all(16), child: Row( children: [ PopupMenuButton( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white, constraints: const BoxConstraints(minWidth: 260), surfaceTintColor: Colors.transparent, elevation: 2, itemBuilder: (context) => [ _buildCompactItem( icon: 'assets/icons/eye.svg', text: 'Посмотреть', onTap: () async { context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [sl().baseUrl + state.profile.profilePhotoUrl], index: 0)); }, ), if (widget.author == null || widget.author!.authorIsMe) ...[ _buildDivider(), _buildCompactItem( icon: 'assets/icons/edit_paint.svg', text: 'Заменить', onTap: () async { showCupertinoModalPopup( context: context, builder: (context) { final s = S.of(context); return CupertinoActionSheet( actions: [ CupertinoActionSheetAction( onPressed: () async { PermissionHandler.checkPermission( permission: Permission.camera, title: s.allowAccessToCamera, description: s.allowAccessToCameraDescription, onGranted: () async { AppRouter.navigatorKey.currentContext?.pop(); final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.camera); if (image != null) { await profileCubit.uploadPhoto(File(image.path)); await sl().getMyProfile(); } }, context: context, ); }, child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), ), CupertinoActionSheetAction( onPressed: () async { PermissionHandler.checkPermission( permission: Permission.photos, title: s.allowAcessToGalery, description: s.allowAcessToGaleryDescription, onGranted: () async { AppRouter.navigatorKey.currentContext?.pop(); final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { await profileCubit.uploadPhoto(File(image.path)); await sl().getMyProfile(); } }, context: context, ); }, child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]), ), ], cancelButton: CupertinoActionSheetAction( onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена'), ), ); }, ); }, ), _buildDivider(), _buildCompactItem( icon: 'assets/icons/red_trash.svg', text: 'Удалить', onTap: () async { await profileCubit.deletePhoto(); await sl().getMyProfile(); }, ), ], ], child: Hero( tag: state.profile.profilePhotoUrl, child: CircleAvatar( radius: 32, foregroundImage: NetworkImage( state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : sl().baseUrl + state.profile.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${state.profileDetails.firstName} ${state.profileDetails.lastName}', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600, fontSize: 16, height: 1), ), const SizedBox(height: 8), GestureDetector( onTap: () { showModalBottomSheet( useRootNavigator: true, showDragHandle: true, useSafeArea: true, context: context, builder: (context) => Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'Подробнее', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), _additional('Дата рожденя: ', state.profileDetails.birthdate), const SizedBox(height: 12), _additional('Страна: ', state.profileDetails.country), const SizedBox(height: 12), _additional('Город: ', state.profileDetails.city), const SizedBox(height: 12), _additional('Язык общения: ', state.profileDetails.lang), const SizedBox(height: 12), _additional('В Eatvio с: ', '${state.profile.createdAt?.format('d.MM.yyyy', 'ru')}'), const SizedBox(height: 12), ], ), ), ); }, child: const Row( children: [ Icon(Icons.info_outline, size: 20, color: Color(0xFF6D6F71)), SizedBox(width: 4), Text( 'Дополнительно', style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), ), ], ), const Spacer(), PopupMenuButton( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white, constraints: const BoxConstraints(minWidth: 260), surfaceTintColor: Colors.transparent, elevation: 2, itemBuilder: (context) => [ _buildCompactItem( icon: 'assets/icons/share2.svg', text: 'Поделиться', onTap: () { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams(uri: Uri.parse('${sl().baseUrl}/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), ); }, ), if (widget.author != null) ...[ _buildDivider(), _buildCompactItem( icon: 'assets/icons/complain.svg', text: 'Пожаловаться', onTap: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug), ); }, ), ], if (widget.author != null && !state.profile.isBlockedByMe) ...[ _buildDivider(), _buildCompactItem( icon: 'assets/icons/complain.svg', text: 'Заблокировать', onTap: () { isFirstFetch = true; sl().blockUser(state.profile.slug); widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); if (state.profile.roles.contains('consultant')) { plansCubit.getPlans(state.profile.slug); } isFirstFetch = false; }, ), ], if (widget.author != null && state.profile.isBlockedByMe) ...[ _buildDivider(), _buildCompactItem( icon: 'assets/icons/complain.svg', text: 'Разблокировать', onTap: () { isFirstFetch = true; sl().unblockUser(state.profile.slug); widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug); if (state.profile.roles.contains('consultant')) { plansCubit.getPlans(state.profile.slug); } isFirstFetch = false; }, ), ], ], child: const Icon(Icons.more_horiz_rounded), ), ], ), ); } PopupMenuItem _buildCompactItem({required String icon, required String text, required Function() onTap}) { return PopupMenuItem( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), height: 36, onTap: onTap, child: SizedBox( width: double.infinity, child: Row( children: [ Text(text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const Spacer(), SvgPicture.asset(icon), ], ), ), ); } PopupMenuItem _buildDivider() { return PopupMenuItem( enabled: false, height: 1, padding: EdgeInsets.zero, child: Container(margin: const EdgeInsets.symmetric(horizontal: 12), height: 1, color: appTheme.dividerColor), ); } Row _additional(String title, String value) { return Row( children: [ Text( title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6D6F71)), ), Text( value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), ), ], ); } Row _messageAndSubscribe(ProfileStateLoaded state) { return Row( children: [ const SizedBox(width: 16), GestureDetector( onTap: () async => await profileCubit.getChatRoomIdBySlug(author: widget.author!), child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), width: (MediaQuery.of(context).size.width - 40) / 2, decoration: BoxDecoration(color: appTheme.blueA400.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16)), child: Text('Сообщение', style: TextStyle(color: appTheme.blueA400)), ), ), const SizedBox(width: 8), GestureDetector( onTap: () { setState(() { canSub = !canSub; }); canSub ? sl().unsubscribe(state.profile.slug) : sl().subscribe(state.profile.slug); }, child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), width: (MediaQuery.of(context).size.width - 40) / 2, decoration: BoxDecoration(color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, borderRadius: BorderRadius.circular(16)), child: Text(!canSub ? 'Отписаться' : 'Подписаться', style: TextStyle(color: !canSub ? Colors.black : Colors.white)), ), ), const SizedBox(width: 16), ], ); } Material _charts() { return Material( color: Colors.white, borderRadius: BorderRadius.circular(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]), ), Builder( builder: (context) { switch (_selectedChipIndex) { case 0: return WeightChartWidget(weightPoints: weightPoints, profileCubit: profileCubit); case 1: return CircumferenceChartWidget(circumferencePoints: circumferencePoints, profileCubit: profileCubit); case 2: return ImtChartWidget(weightPoints: weightPoints); default: return const SizedBox.shrink(); } }, ), const SizedBox(height: 8), Row( children: [ const SizedBox(width: 16), _choiceIntervalChipWidget(0), const SizedBox(width: 8), _choiceIntervalChipWidget(1), const SizedBox(width: 8), _choiceIntervalChipWidget(2), const SizedBox(width: 8), _choiceIntervalChipWidget(3), const SizedBox(width: 8), _choiceIntervalChipWidget(4), ], ), const SizedBox(height: 8), ], ), ); } ChoiceChip _choiceIntervalChipWidget(int index) { return ChoiceChip( selectedColor: Colors.grey.withValues(alpha: 0.1), pressElevation: 0, showCheckmark: false, side: BorderSide.none, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsIntervalLabelList[index]), labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedIntervalChipIndex == index, onSelected: (bool selected) { profileCubit.getMyProfile(period: chipsIntervalList[index]); setState(() { _selectedIntervalChipIndex = index; }); }, ); } ChoiceChip _choiceChipWidget(int index) { return ChoiceChip( selectedColor: appTheme.blueA400, pressElevation: 0, showCheckmark: false, side: BorderSide.none, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsLabelList[index]), labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedChipIndex == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { setState(() { _selectedChipIndex = index; }); }, ); } }