diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9f456ad2..2631ff92 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -61,6 +61,7 @@ Future init() async { sl.registerLazySingleton(() => NewMessagesCubit()); sl.registerLazySingleton(() => NotificationsCubit()); sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); + sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); @@ -68,7 +69,6 @@ Future init() async { sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); - sl.registerFactory(() => ProfileAvatarCubit(profileRepository: sl())); sl.registerFactory(() => RecipeCreateCubit(recipeRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl())); diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index 2990a507..4655d87d 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -10,10 +10,9 @@ class ProfileAvatarCubit extends Cubit { }) : super(const ProfileAvatarState.loading()); getMyProfile() async { + emit(const ProfileAvatarState.loading()); try { - emit(ProfileAvatarState.loaded( - profile: await profileRepository.getMyProfile(), - ),); + emit(ProfileAvatarState.loaded(profile: await profileRepository.getMyProfile())); } catch (e) { emit(ProfileAvatarState.error(errorMessage: e.toString())); } diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index fc7c278e..48b7474d 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -7,7 +7,6 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/noti import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/generated/l10n.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'; @@ -72,22 +71,19 @@ class _BottomNavigationBarWidgetState extends State { items: [ BottomNavigationBarItem( label: '', - icon: BlocProvider( - create: (context) => sl()..getMyProfile(), - child: BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return CircleAvatar( - backgroundImage: NetworkImage(state.profile.profilePhotoUrl), - radius: 16, - ); - } + icon: BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { return CircleAvatar( + backgroundImage: NetworkImage(state.profile.profilePhotoUrl), radius: 16, - backgroundColor: Colors.black12, ); - }, - ), + } + return CircleAvatar( + radius: 16, + backgroundColor: Colors.black12, + ); + }, ), ), BottomNavigationBarItem( diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 075604fc..df06dee5 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -15,6 +15,7 @@ class ProfileModel with _$ProfileModel { @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile") required bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: "slug") required String slug, @JsonKey(name: "profile_photo_url") required String profilePhotoUrl, @JsonKey(name: "role") required String role, diff --git a/lib/features/profile/models/profile_model.freezed.dart b/lib/features/profile/models/profile_model.freezed.dart index 862e19e6..d5ec2486 100644 --- a/lib/features/profile/models/profile_model.freezed.dart +++ b/lib/features/profile/models/profile_model.freezed.dart @@ -34,6 +34,8 @@ mixin _$ProfileModel { DateTime? get updatedAt => throw _privateConstructorUsedError; @JsonKey(name: "is_public_profile") bool get isPublicProfile => throw _privateConstructorUsedError; + @JsonKey(name: 'is_entity_subscribed_by_me') + bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; @JsonKey(name: "slug") String get slug => throw _privateConstructorUsedError; @JsonKey(name: "profile_photo_url") @@ -73,6 +75,7 @@ abstract class $ProfileModelCopyWith<$Res> { @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile") bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, @JsonKey(name: "slug") String slug, @JsonKey(name: "profile_photo_url") String profilePhotoUrl, @JsonKey(name: "role") String role, @@ -103,6 +106,7 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel> Object? createdAt = freezed, Object? updatedAt = freezed, Object? isPublicProfile = null, + Object? isEntitySubscribedByMe = null, Object? slug = null, Object? profilePhotoUrl = null, Object? role = null, @@ -139,6 +143,10 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel> ? _value.isPublicProfile : isPublicProfile // ignore: cast_nullable_to_non_nullable as bool, + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable + as bool, slug: null == slug ? _value.slug : slug // ignore: cast_nullable_to_non_nullable @@ -183,6 +191,7 @@ abstract class _$$ProfileModelImplCopyWith<$Res> @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile") bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, @JsonKey(name: "slug") String slug, @JsonKey(name: "profile_photo_url") String profilePhotoUrl, @JsonKey(name: "role") String role, @@ -211,6 +220,7 @@ class __$$ProfileModelImplCopyWithImpl<$Res> Object? createdAt = freezed, Object? updatedAt = freezed, Object? isPublicProfile = null, + Object? isEntitySubscribedByMe = null, Object? slug = null, Object? profilePhotoUrl = null, Object? role = null, @@ -247,6 +257,10 @@ class __$$ProfileModelImplCopyWithImpl<$Res> ? _value.isPublicProfile : isPublicProfile // ignore: cast_nullable_to_non_nullable as bool, + isEntitySubscribedByMe: null == isEntitySubscribedByMe + ? _value.isEntitySubscribedByMe + : isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable + as bool, slug: null == slug ? _value.slug : slug // ignore: cast_nullable_to_non_nullable @@ -286,6 +300,8 @@ class _$ProfileModelImpl implements _ProfileModel { @JsonKey(name: "created_at") this.createdAt, @JsonKey(name: "updated_at") this.updatedAt, @JsonKey(name: "is_public_profile") required this.isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') + required this.isEntitySubscribedByMe, @JsonKey(name: "slug") required this.slug, @JsonKey(name: "profile_photo_url") required this.profilePhotoUrl, @JsonKey(name: "role") required this.role, @@ -323,6 +339,9 @@ class _$ProfileModelImpl implements _ProfileModel { @JsonKey(name: "is_public_profile") final bool isPublicProfile; @override + @JsonKey(name: 'is_entity_subscribed_by_me') + final bool isEntitySubscribedByMe; + @override @JsonKey(name: "slug") final String slug; @override @@ -360,7 +379,7 @@ class _$ProfileModelImpl implements _ProfileModel { @override String toString() { - return 'ProfileModel(id: $id, name: $name, email: $email, emailVerifiedAt: $emailVerifiedAt, createdAt: $createdAt, updatedAt: $updatedAt, isPublicProfile: $isPublicProfile, slug: $slug, profilePhotoUrl: $profilePhotoUrl, role: $role, badges: $badges, subscriptions: $subscriptions, subscribers: $subscribers)'; + return 'ProfileModel(id: $id, name: $name, email: $email, emailVerifiedAt: $emailVerifiedAt, createdAt: $createdAt, updatedAt: $updatedAt, isPublicProfile: $isPublicProfile, isEntitySubscribedByMe: $isEntitySubscribedByMe, slug: $slug, profilePhotoUrl: $profilePhotoUrl, role: $role, badges: $badges, subscriptions: $subscriptions, subscribers: $subscribers)'; } @override @@ -379,6 +398,8 @@ class _$ProfileModelImpl implements _ProfileModel { other.updatedAt == updatedAt) && (identical(other.isPublicProfile, isPublicProfile) || other.isPublicProfile == isPublicProfile) && + (identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) || + other.isEntitySubscribedByMe == isEntitySubscribedByMe) && (identical(other.slug, slug) || other.slug == slug) && (identical(other.profilePhotoUrl, profilePhotoUrl) || other.profilePhotoUrl == profilePhotoUrl) && @@ -401,6 +422,7 @@ class _$ProfileModelImpl implements _ProfileModel { createdAt, updatedAt, isPublicProfile, + isEntitySubscribedByMe, slug, profilePhotoUrl, role, @@ -433,6 +455,8 @@ abstract class _ProfileModel implements ProfileModel { @JsonKey(name: "created_at") final DateTime? createdAt, @JsonKey(name: "updated_at") final DateTime? updatedAt, @JsonKey(name: "is_public_profile") required final bool isPublicProfile, + @JsonKey(name: 'is_entity_subscribed_by_me') + required final bool isEntitySubscribedByMe, @JsonKey(name: "slug") required final String slug, @JsonKey(name: "profile_photo_url") required final String profilePhotoUrl, @JsonKey(name: "role") required final String role, @@ -468,6 +492,9 @@ abstract class _ProfileModel implements ProfileModel { @JsonKey(name: "is_public_profile") bool get isPublicProfile; @override + @JsonKey(name: 'is_entity_subscribed_by_me') + bool get isEntitySubscribedByMe; + @override @JsonKey(name: "slug") String get slug; @override diff --git a/lib/features/profile/models/profile_model.g.dart b/lib/features/profile/models/profile_model.g.dart index aa2431ca..47755454 100644 --- a/lib/features/profile/models/profile_model.g.dart +++ b/lib/features/profile/models/profile_model.g.dart @@ -21,6 +21,7 @@ _$ProfileModelImpl _$$ProfileModelImplFromJson(Map json) => ? null : DateTime.parse(json['updated_at'] as String), isPublicProfile: json['is_public_profile'] as bool, + isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool, slug: json['slug'] as String, profilePhotoUrl: json['profile_photo_url'] as String, role: json['role'] as String, @@ -44,6 +45,7 @@ Map _$$ProfileModelImplToJson(_$ProfileModelImpl instance) => 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'is_public_profile': instance.isPublicProfile, + 'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe, 'slug': instance.slug, 'profile_photo_url': instance.profilePhotoUrl, 'role': instance.role, diff --git a/lib/features/profile/models/sub_model.freezed.dart b/lib/features/profile/models/sub_model.freezed.dart index 5aeb0237..34d6ae49 100644 --- a/lib/features/profile/models/sub_model.freezed.dart +++ b/lib/features/profile/models/sub_model.freezed.dart @@ -249,7 +249,7 @@ mixin _$SubModel { String get name => throw _privateConstructorUsedError; String get city => throw _privateConstructorUsedError; DateTime get birthdate => throw _privateConstructorUsedError; - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') bool get isEntitySubscribedByMe => throw _privateConstructorUsedError; String get email => throw _privateConstructorUsedError; @JsonKey(name: 'profile_photo_url') @@ -283,7 +283,7 @@ abstract class $SubModelCopyWith<$Res> { String name, String city, DateTime birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") bool isEntitySubscribedByMe, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, String email, @JsonKey(name: 'profile_photo_url') String profilePhotoUrl, String role, @@ -387,7 +387,7 @@ abstract class _$$SubModelImplCopyWith<$Res> String name, String city, DateTime birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") bool isEntitySubscribedByMe, + @JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe, String email, @JsonKey(name: 'profile_photo_url') String profilePhotoUrl, String role, @@ -484,7 +484,7 @@ class _$SubModelImpl implements _SubModel { required this.name, required this.city, required this.birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') required this.isEntitySubscribedByMe, required this.email, @JsonKey(name: 'profile_photo_url') required this.profilePhotoUrl, @@ -507,7 +507,7 @@ class _$SubModelImpl implements _SubModel { @override final DateTime birthdate; @override - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') final bool isEntitySubscribedByMe; @override final String email; @@ -596,7 +596,7 @@ abstract class _SubModel implements SubModel { required final String name, required final String city, required final DateTime birthdate, - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') required final bool isEntitySubscribedByMe, required final String email, @JsonKey(name: 'profile_photo_url') required final String profilePhotoUrl, @@ -620,7 +620,7 @@ abstract class _SubModel implements SubModel { @override DateTime get birthdate; @override - @JsonKey(name: "is_entity_subscribed_by_me") + @JsonKey(name: 'is_entity_subscribed_by_me') bool get isEntitySubscribedByMe; @override String get email; diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index bdd4e0f4..235a16bf 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; @@ -80,4 +83,9 @@ class ProfileCubit extends Cubit { rethrow; } } + + uploadPhoto(File file) async { + await profileRepository.uploadPhoto(await ImageCompressUtility.compressFile(file)); + return await getMyProfile(); + } } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 51825cfd..2abd3feb 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,22 +1,32 @@ +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/utils/date_time_utils.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/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/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/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:go_router/go_router.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:share_plus/share_plus.dart'; class ProfileScreen extends StatefulWidget { final Author? author; @@ -40,6 +50,8 @@ class _ProfileScreenState extends State { List circumferencePoints = []; int _selectedChipIndex = 0; int _selectedIntervalChipIndex = 0; + late bool canSub; + bool isFirstFetch = true; @override void initState() { @@ -57,6 +69,10 @@ class _ProfileScreenState extends State { circumferencePoints = state.circumferencePoints!; weightPoints = state.weightPoints!; } + if (isFirstFetch) { + canSub = !state.profile.isEntitySubscribedByMe; + isFirstFetch = false; + } return widget.author == null ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -99,7 +115,7 @@ class _ProfileScreenState extends State { _header(state), if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state), const SizedBox(height: 8), - _messageAndSubscribe(), + _messageAndSubscribe(state), const SizedBox(height: 16), ], ), @@ -692,16 +708,96 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.all(16), child: Row( children: [ - CircleAvatar( - radius: 32, - backgroundImage: NetworkImage(state.profile.profilePhotoUrl), + PopupMenuButton( + onSelected: (value) { + if (value == 'view') { + context.pushTransparentRoute( + rootNavigator: true, + PhotoViewScreen( + imageUrls: [state.profile.profilePhotoUrl], + index: 0, + ), + ); + } + if (value == 'replace') { + showCupertinoModalPopup( + 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().getMyProfile(); + } + }, + child: const Row( + children: const [ + 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().getMyProfile(); + } + }, + child: const Row( + children: const [ + Icon(Icons.photo), + SizedBox(width: 8), + Text('Галерея'), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ), + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'view', + child: Text('Посмотреть'), + ), + PopupMenuItem( + value: 'replace', + child: Text('Заменить'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], + child: Hero( + tag: state.profile.profilePhotoUrl, + child: CircleAvatar( + radius: 32, + backgroundImage: NetworkImage(state.profile.profilePhotoUrl), + ), + ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - state.profile.name, + state.profileDetails.firstName + ' ' + state.profileDetails.lastName, style: TextStyle( color: Colors.black, fontWeight: FontWeight.w600, @@ -710,37 +806,110 @@ class _ProfileScreenState extends State { ), ), 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, + 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: [ + 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: 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), + PopupMenuButton( + onSelected: (value) { + Share.shareUri( + Uri.parse( + baseUrl + '/ru/profile/' + state.profile.slug, + ), + ); + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'share', + child: Text('Поделиться'), + ), + ], + child: Icon(Icons.more_horiz_rounded), ), ], ), ); } - _messageAndSubscribe() { + _additional(String title, String value) { + return Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF6D6F71), + ), + ), + Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF323F4B), + ), + ), + ], + ); + } + + _messageAndSubscribe(ProfileStateLoaded state) { return Row( children: [ SizedBox(width: 16), @@ -768,18 +937,26 @@ class _ProfileScreenState extends State { ), ), 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, + GestureDetector( + onTap: () { + setState(() { + canSub = !canSub; + }); + canSub ? sl().unsubscribe(state.profile.slug) : sl().subscribe(state.profile.slug); + }, + child: Container( + alignment: Alignment.center, + padding: 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, + ), ), ), ), @@ -826,7 +1003,8 @@ class _ProfileScreenState extends State { return ImtChartWidget( weightPoints: weightPoints, ); - default: return SizedBox.shrink(); + default: + return SizedBox.shrink(); } }, ), diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 022a768e..5bb2e00f 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -145,4 +147,12 @@ class ProfileRepository { rethrow; } } + + Future uploadPhoto(File file) async { + try { + return await profileService.uploadPhoto(photo: file); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 7d9f6480..4517fee2 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; @@ -83,4 +85,10 @@ abstract class ProfileService { Future unsubscribe({ @Path() required String slug, }); + + @POST('/api/profile/photo/upload') + @MultiPart() + Future uploadPhoto({ + @Part(name: 'photo') required File photo, + }); } diff --git a/lib/features/profile/services/profile_service.g.dart b/lib/features/profile/services/profile_service.g.dart index 366ab7de..44a66b83 100644 --- a/lib/features/profile/services/profile_service.g.dart +++ b/lib/features/profile/services/profile_service.g.dart @@ -504,6 +504,39 @@ class _ProfileService implements ProfileService { await _dio.fetch(_options); } + @override + Future uploadPhoto({required File photo}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = FormData(); + _data.files.add(MapEntry( + 'photo', + MultipartFile.fromFileSync( + photo.path, + filename: photo.path.split(Platform.pathSeparator).last, + ), + )); + final _options = _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/profile/photo/upload', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/main.dart b/lib/main.dart index c166a0cf..195c45de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -31,6 +32,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), + BlocProvider(create: (context) => sl()..getMyProfile()), ], child: Portal( child: KeyboardDismisser(