Files
eatvio_app/lib/features/profile/presentation/pages/profile_screen.dart
2025-06-09 17:57:01 +03:00

1382 lines
50 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/const.dart';
import 'package:eatvio_app/core/enums/complained_content.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/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: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: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(),
);
}
},
),
);
}
_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),
const SizedBox(height: 8),
_messageAndSubscribe(state),
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),
],
),
),
);
}
_myProfileScreen(ProfileStateLoaded state) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: AppBar(toolbarHeight: 0),
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),
],
),
),
);
}
_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();
},
);
}
_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(),
],
],
),
);
}
_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,
),
);
}
_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),
),
),
],
),
),
),
),
);
}
_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);
}
},
),
],
),
),
),
),
],
),
);
}
_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,
),
),
),
);
}
_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,
),
],
),
),
),
),
],
),
);
}
_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,
),
),
],
),
),
),
);
}
_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,
),
],
),
),
),
);
}
_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),
),
),
),
);
}
_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(),
],
],
),
),
],
),
),
),
);
}
_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),
),
],
),
),
),
],
),
);
}
_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'),
),
),
],
);
}
_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,
);
}
_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,
),
),
);
}
_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) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).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();
}
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).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();
}
},
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(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 {
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('Пожаловаться'),
),
],
child: const Icon(Icons.more_horiz_rounded),
),
],
),
);
}
_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),
),
),
],
);
}
_messageAndSubscribe(ProfileStateLoaded state) {
return Row(
children: [
const SizedBox(width: 16),
GestureDetector(
onTap: () async {
GoRouter.of(context).pushNamed(
'chatScreen',
extra: {
'chatId': await profileCubit.getChatRoomIdBySlug(slug: widget.author!.authorSlug),
},
);
},
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),
],
);
}
_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),
],
),
);
}
_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;
});
},
);
}
_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;
});
},
);
}
}