Files
eatvio_app/lib/features/profile/presentation/pages/profile_screen.dart
2024-11-22 15:27:51 +03:00

906 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_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/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/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/weight_chart_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_svg/flutter_svg.dart';
class ProfileScreen extends StatefulWidget {
final Author? author;
const ProfileScreen({
super.key,
this.author,
});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final ProfileCubit profileCubit = sl<ProfileCubit>();
final List<String> chipsLabelList = ['Вес', 'Замеры', 'ИМТ'];
final List<String> chipsIntervalLabelList = ['1М', '3М', '6М', '1Г', 'ВСЁ ВРЕМЯ'];
final List<String> chipsIntervalList = ['1', '3', '6', '12', 'all'];
List<WeightModel> weightPoints = [];
List<CircumferenceModel> circumferencePoints = [];
int _selectedChipIndex = 0;
int _selectedIntervalChipIndex = 0;
@override
void initState() {
super.initState();
widget.author == null ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ProfileCubit, ProfileState>(
bloc: profileCubit,
builder: (BuildContext context, ProfileState state) {
if (state is ProfileStateLoaded) {
if (state.circumferencePoints != null) {
circumferencePoints = state.circumferencePoints!;
weightPoints = state.weightPoints!;
}
return widget.author == null ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(state.errorMessage),
),
),
);
} 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: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_header(state),
if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state),
const SizedBox(height: 8),
_messageAndSubscribe(),
const SizedBox(height: 16),
],
),
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) _badges(state),
_socialLinks(state),
_education(state),
_diplomasAndCertificates(state),
_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: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_header(state),
if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state),
],
),
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _socialLinks(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _education(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _diplomasAndCertificates(state)),
Padding(padding: EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _activePlans(state)),
_newBlogPostButton(),
_blogPosts(state),
],
),
),
);
}
_activePlans(ProfileStateLoaded state) {
if (state.profile.role != 'consultant') {
return const SizedBox.shrink();
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Активные планы',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 24,
height: 1,
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null ? 'Редактировать' : 'Показать все',
style: TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
const SizedBox(height: 8),
_bullets(0, 4),
],
);
}
_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: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: isActive ? appTheme.blueA400 : 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: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_circle_outline_rounded,
color: Color(0xFF2F80ED),
),
const SizedBox(width: 12),
Text(
'Новая запись',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF2F80ED),
),
),
],
),
),
),
),
);
}
_diplomasAndCertificates(ProfileStateLoaded state) {
if (state.profile.role != 'consultant') {
return const SizedBox.shrink();
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Дипломы\nи сертификаты',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 24,
height: 1,
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null ? 'Редактировать' : 'Показать все',
style: 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: [
_diplomaImage(
state.profile.profilePhotoUrl,
),
const SizedBox(width: 8),
_diplomaImage(
state.profile.profilePhotoUrl,
),
const SizedBox(width: 8),
_diplomaImage(
state.profile.profilePhotoUrl,
),
const SizedBox(width: 8),
_diplomaImage(
state.profile.profilePhotoUrl,
),
],
),
),
),
),
],
);
}
_diplomaImage(String url) {
return 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.role != 'consultant') {
return const SizedBox.shrink();
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Образование',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null ? 'Редактировать' : 'Показать все',
style: 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.symmetric(horizontal: 16, vertical: 12),
child: Column(
children: [
_educationBlock(
'Наименование диплома или сертификата в 2 строки',
'30.01.2020',
),
],
),
),
),
),
],
);
}
_educationBlock(String title, String date) {
return Material(
color: Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
height: 1,
color: Color(0xFF323F4B),
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
date,
style: TextStyle(
height: 1,
color: Color(0xFF6D6F71),
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
);
}
_socialLinks(ProfileStateLoaded state) {
if (state.profile.role != 'consultant') {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
_socialLinkButton(() {}, 'assets/icons/vk.svg'),
const SizedBox(width: 12),
_socialLinkButton(() {}, 'assets/icons/telegram.svg'),
const SizedBox(width: 12),
_socialLinkButton(() {}, 'assets/icons/web.svg'),
],
),
),
),
);
}
_socialLinkButton(Function() onTap, String icon) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width / 3 - (56 / 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Color(0xFFF6F6F6),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: SvgPicture.asset(
icon,
width: 24,
),
),
),
);
}
_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,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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')
? SvgPicture.network(
baseUrl + '/storage/' + state.profile.badges[i].image,
width: 40,
height: 40,
)
: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + state.profile.badges[i].image,
),
i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(),
],
],
),
),
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
),
),
);
}
_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,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Подписки',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Text(
'${state.profile.subscriptions.length}',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
],
),
Padding(
padding: EdgeInsets.only(right: 8),
child: _miniAvatars(state.profile.subscriptions.map((e) => e.profilePhotoUrl).toList()),
),
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
),
),
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,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Подписчики',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Text(
'${state.profile.subscribers.length}',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
],
),
Padding(
padding: EdgeInsets.only(right: 8),
child: _miniAvatars(state.profile.subscribers.map((e) => e.profilePhotoUrl).toList()),
),
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
),
),
],
),
);
}
_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,
backgroundImage: NetworkImage(
urls[i],
),
),
),
],
);
}
_blogPosts(ProfileStateLoaded state) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => BlogPostWidget(
feedBlogPostModel: state.blogPosts[index],
),
separatorBuilder: (context, index) => 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.withOpacity(0.4),
height: 1,
),
),
);
}
_header(ProfileStateLoaded state) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 32,
backgroundImage: NetworkImage(state.profile.profilePhotoUrl),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.profile.name,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 16,
height: 1,
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: Color(0xFF6D6F71),
),
const SizedBox(width: 4),
Text(
'Дополнительно',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
const Spacer(),
IconButton(
onPressed: () {},
icon: Icon(Icons.more_horiz_rounded),
),
],
),
);
}
_messageAndSubscribe() {
return Row(
children: [
SizedBox(width: 16),
GestureDetector(
onTap: () async {
GoRouter.of(context).pushNamed(
'chatScreen',
extra: await profileCubit.getChatRoomIdBySlug(slug: widget.author!.authorSlug),
);
},
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(8),
width: (MediaQuery.of(context).size.width - 40) / 2,
decoration: BoxDecoration(
color: appTheme.blueA400.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Сообщение',
style: TextStyle(
color: appTheme.blueA400,
),
),
),
),
SizedBox(width: 8),
Container(
alignment: Alignment.center,
padding: EdgeInsets.all(8),
width: (MediaQuery.of(context).size.width - 40) / 2,
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Подписаться',
style: TextStyle(
color: Colors.white,
),
),
),
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: EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
children: [
_choiceChipWidget(0),
SizedBox(width: 8),
_choiceChipWidget(1),
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,
);
}
return SizedBox.shrink();
},
),
SizedBox(height: 8),
Row(
children: [
SizedBox(width: 16),
_choiceIntervalChipWidget(0),
SizedBox(width: 8),
_choiceIntervalChipWidget(1),
SizedBox(width: 8),
_choiceIntervalChipWidget(2),
SizedBox(width: 8),
_choiceIntervalChipWidget(3),
SizedBox(width: 8),
_choiceIntervalChipWidget(4),
],
),
SizedBox(height: 8),
],
),
);
}
_choiceIntervalChipWidget(int index) {
return ChoiceChip(
selectedColor: Colors.grey.withOpacity(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;
});
},
);
}
}