Files
eatvio_app/lib/features/profile/presentation/pages/profile_screen.dart
2025-07-09 10:20:21 +03:00

1424 lines
53 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 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.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/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<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
late final ProfileCubit profileCubit = sl<ProfileCubit>();
final PlansCubit plansCubit = sl<PlansCubit>();
final List<String> chipsLabelList = ['Вес', 'Замеры', 'ИМТ'];
final List<String> chipsIntervalLabelList = ['1М', '3М', '6М', '1Г', 'ВСЁ ВРЕМЯ'];
final List<String> chipsIntervalList = ['1', '3', '6', '12', 'all'];
final TextEditingController controller = TextEditingController();
List<WeightModel> weightPoints = [];
List<CircumferenceModel> 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<PlansCubit>(
create: (BuildContext context) => plansCubit,
child: BlocBuilder<ProfileCubit, ProfileState>(
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')) {
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'))
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.roles.contains('consultant')) _socialLinks(state),
if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant')) _activePlans(state),
_blogPosts(state),
],
),
),
);
}
Scaffold _myProfileScreen(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'))
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),
if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.roles.contains('consultant')) _activePlans(state),
_newBlogPostButton(),
_blogPosts(state),
],
),
),
);
}
Widget _activePlans(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant')) {
return const SizedBox.shrink();
}
return BlocBuilder<PlansCubit, PlansState>(
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,
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')) {
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(
'$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: '$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<String> 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.symmetric(vertical: 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(
onSelected: (value) async {
if (value == 'view') {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [baseUrl + state.profile.profilePhotoUrl],
index: 0,
),
);
}
if (value == 'replace') {
showCupertinoModalPopup<void>(
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<ProfileAvatarCubit>().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<ProfileAvatarCubit>().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('Отмена'),
),
);
},
);
}
if (value == 'delete') {
await profileCubit.deletePhoto();
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'view',
child: Text('Посмотреть'),
),
if (widget.author == null || widget.author!.authorIsMe)
const PopupMenuItem(
value: 'replace',
child: Text('Заменить'),
),
if (widget.author == null || widget.author!.authorIsMe)
const PopupMenuItem(
value: 'delete',
child: Text('Удалить'),
),
],
child: Hero(
tag: state.profile.profilePhotoUrl,
child: CircleAvatar(
radius: 32,
foregroundImage: NetworkImage(
state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : 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(
onSelected: (value) {
if (value == 'share') {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
} else if (value == 'block') {
isFirstFetch = true;
sl<BlockedUsersCubit>().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;
} else if (value == 'unblock') {
isFirstFetch = true;
sl<BlockedUsersCubit>().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;
} else {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(
complainedContentType: ComplainedContent.profile,
slug: widget.author?.authorSlug,
),
);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'share',
child: Text('Поделиться'),
),
if (widget.author != null)
const PopupMenuItem(
value: 'complain',
child: Text('Пожаловаться'),
),
if (widget.author != null && !state.profile.isBlockedByMe)
const PopupMenuItem(
value: 'block',
child: Text('Заблокировать'),
),
if (widget.author != null && state.profile.isBlockedByMe)
const PopupMenuItem(
value: 'unblock',
child: Text('Разблокировать'),
),
],
child: const Icon(Icons.more_horiz_rounded),
),
],
),
);
}
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<SubCubit>().unsubscribe(state.profile.slug) : sl<SubCubit>().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;
});
},
);
}
}