From b1508c3553c4299df841a18bc0f1c5a5b8b84776 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Thu, 27 Jun 2024 13:17:53 +0300 Subject: [PATCH] feat(app): Initial layouts for subscribers, feed display fix, feed photo upload front end --- .../bloc/blog_post_create_bloc.dart | 26 +- .../bloc/blog_post_create_event.dart | 7 + .../bloc/blog_post_create_state.dart | 30 ++- .../blog_post_create_screen.dart | 109 ++++++++- .../feed/feed_screen/bloc/feed_bloc.dart | 4 +- .../feed/feed_screen/feed_screen.dart | 118 +++++---- .../bloc/my_profile_bloc.dart | 231 ++++++++++-------- .../bloc/subscribers_bloc.dart | 74 ++++++ .../bloc/subscribers_event.dart | 16 ++ .../bloc/subscribers_state.dart | 49 ++++ .../models/subscribers_model.dart | 16 ++ .../subscribers_screen.dart | 213 ++++++++++++++++ 12 files changed, 709 insertions(+), 184 deletions(-) create mode 100644 lib/presentation/users/subscribers_screen/bloc/subscribers_bloc.dart create mode 100644 lib/presentation/users/subscribers_screen/bloc/subscribers_event.dart create mode 100644 lib/presentation/users/subscribers_screen/bloc/subscribers_state.dart create mode 100644 lib/presentation/users/subscribers_screen/models/subscribers_model.dart create mode 100644 lib/presentation/users/subscribers_screen/subscribers_screen.dart diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart index 3e6c5407..d2f448b1 100644 --- a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; import 'dart:convert'; import '/core/app_export.dart'; import 'package:eatvio_app/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart'; @@ -19,6 +20,7 @@ class BlogPostCreateBloc BlogPostCreateBloc(BlogPostCreateState initialState) : super(initialState) { on(_onInitialize); on(_createBlogPost); + on(_uploadImages); } _onInitialize( @@ -32,23 +34,31 @@ class BlogPostCreateBloc } if (state is BlogPostCreateLoadedState) { var currentState = state as BlogPostCreateLoadedState; - emit(currentState.copyWith( + emit( + currentState.copyWith( blog_postModelObj: currentState.blog_postModelObj, title: currentState.title, description: currentState.description, youtube_url: currentState.youtube_url, - token: currentState.token,),); + token: currentState.token, + ), + ); } else { - emit(BlogPostCreateLoadedState( + emit( + BlogPostCreateLoadedState( token: token, title: TextEditingController(), description: TextEditingController(), - youtube_url: TextEditingController(),),); + youtube_url: TextEditingController(), + ), + ); } } FutureOr _createBlogPost( - CreateBlogPost event, Emitter emit,) async { + CreateBlogPost event, + Emitter emit, + ) async { if (state is BlogPostCreateLoadedState) { var currentState = state as BlogPostCreateLoadedState; @@ -62,7 +72,8 @@ class BlogPostCreateBloc AppRouter.navigatorKey.currentContext?.goNamed('feedScreen'); } else { print( - 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}',); + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}', + ); } } } @@ -94,4 +105,7 @@ class BlogPostCreateBloc return response; } + + FutureOr _uploadImages( + UploadImages event, Emitter emit) {} } diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart index 7631fac0..778cd85d 100644 --- a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart @@ -19,3 +19,10 @@ class CreateBlogPost extends BlogPostCreateEvent { @override List get props => []; } + +class UploadImages extends BlogPostCreateEvent { + UploadImages(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart index 6012dd45..6014e3da 100644 --- a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart @@ -22,33 +22,39 @@ class BlogPostCreateState extends Equatable { } class BlogPostCreateLoadedState extends BlogPostCreateState { - BlogPostCreateLoadedState( - {this.token, - this.blog_postModelObj, - this.title, - this.description, - this.youtube_url,}); + BlogPostCreateLoadedState({ + this.token, + this.blog_postModelObj, + this.title, + this.description, + this.youtube_url, + this.isLoading = false, + }); BlogPostCreateModel? blog_postModelObj; TextEditingController? title; TextEditingController? description; TextEditingController? youtube_url; String? token; + bool isLoading; @override List get props => [blog_postModelObj, title, description, youtube_url]; - BlogPostCreateLoadedState copyWith( - {BlogPostCreateModel? blog_postModelObj, - TextEditingController? title, - TextEditingController? description, - TextEditingController? youtube_url, - String? token,}) { + BlogPostCreateLoadedState copyWith({ + BlogPostCreateModel? blog_postModelObj, + TextEditingController? title, + TextEditingController? description, + TextEditingController? youtube_url, + String? token, + bool isLoading = false, + }) { return BlogPostCreateLoadedState( blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, title: title ?? this.title, description: description ?? this.description, youtube_url: youtube_url ?? this.youtube_url, token: token ?? this.token, + isLoading: this.isLoading, ); } } diff --git a/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart index 444800a8..f02a5120 100644 --- a/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart +++ b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart @@ -2,6 +2,7 @@ import 'bloc/blog_post_create_bloc.dart'; import 'models/blog_post_create_model.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:eatvio_app/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/widgets/custom_outlined_button.dart'; @@ -11,9 +12,12 @@ class BlogPostCreateScreen extends StatelessWidget { static Widget builder(BuildContext context) { final blog_postID = ModalRoute.of(context)!.settings.arguments; return BlocProvider( - create: (context) => BlogPostCreateBloc(BlogPostCreateState(blog_postModelObj: BlogPostCreateModel(blog_postID: blog_postID.toString()))) - ..add(BlogPostCreateInitialEvent()), - child: BlogPostCreateScreen(),); + create: (context) => BlogPostCreateBloc(BlogPostCreateState( + blog_postModelObj: + BlogPostCreateModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostCreateInitialEvent()), + child: BlogPostCreateScreen(), + ); } Widget build(BuildContext context) { @@ -71,13 +75,15 @@ class BlogPostCreateScreen extends StatelessWidget { children: [ Column( children: [ - Text("Заголовок новости", style: theme.textTheme.bodyLarge), + Text("Заголовок новости", + style: theme.textTheme.bodyLarge), CustomTextFormField( controller: state.title, validator: _validateTitle, ), SizedBox(height: 8.0), - Text("Ссылка на YouTube", style: theme.textTheme.bodyLarge), + Text("Ссылка на YouTube", + style: theme.textTheme.bodyLarge), CustomTextFormField( controller: state.youtube_url, validator: _validateYouTubeLink, @@ -90,9 +96,12 @@ class BlogPostCreateScreen extends StatelessWidget { maxLines: 6, ), SizedBox(height: 16.0), + _buildAddPhoto(context, state), + SizedBox(height: 16.0), // Add widget for image upload here CustomOutlinedButton( - onPressed: () => _submitForm(context, state.title!.text, state.description!.text, state.youtube_url!.text), + onPressed: () => _submitForm(context, state.title!.text, + state.description!.text, state.youtube_url!.text), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, text: 'Создать', ), @@ -108,16 +117,20 @@ class BlogPostCreateScreen extends StatelessWidget { } } -_submitForm(BuildContext context, String title, String description, String youtubeURL) { +_submitForm( + BuildContext context, String title, String description, String youtubeURL) { String? titleError = _validateTitle(title); String? descriptionError = _validateDescription(description); String? youtubeURLError = _validateYouTubeLink(youtubeURL); - if (titleError != null || descriptionError != null || youtubeURLError != null) { + if (titleError != null || + descriptionError != null || + youtubeURLError != null) { // Display error messages or handle invalid input as needed // For example, show a snackbar with the error message ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(titleError ?? descriptionError ?? youtubeURLError!)), + SnackBar( + content: Text(titleError ?? descriptionError ?? youtubeURLError!)), ); } else { // Proceed with form submission @@ -125,6 +138,81 @@ _submitForm(BuildContext context, String title, String description, String youtu } } +_buildAddPhoto(BuildContext context, BlogPostCreateLoadedState state) { + bool isLoading = state.isLoading; + return GestureDetector( + onTap: addImage(context, isLoading), + child: Stack( + children: [ + if (!isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + // Показывайте прелоадер, если загрузка активна + + Padding( + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + "Добавить фото", + selectionColor: appTheme.blueA400, + ), + ), + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ), + ], + ), + ), + if (isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Center( + child: CircularProgressIndicator( + color: appTheme + .blueGray300), // Замените на свой прелоадер при необходимости + ), + ), + ], + ), + ); +} + +addImage(BuildContext context, bool isLoading) { + if (!isLoading) { + return () async { + final picker = ImagePicker(); + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + for (var image in images) { + context.read().add(UploadImages(image)); + } + } + }; + } +} + String? _validateTitle(value) { if (value!.isEmpty) { return 'Пожалуйста, введите заголовок'; @@ -149,6 +237,7 @@ String? _validateYouTubeLink(value) { } bool _isValidUrl(String url) { - final RegExp urlRegExp = new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$"); + final RegExp urlRegExp = + new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$"); return urlRegExp.hasMatch(url); } diff --git a/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart index 0055e61e..1eda1feb 100644 --- a/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart +++ b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart @@ -50,12 +50,12 @@ class FeedBloc extends Bloc { emit((currentState).copyWith( posts: data, myProfileModelObj: currentState.myProfileModelObj, - ),); + )); } else { print('Got unloaded'); emit(FeedLoadedState( posts: data, - ),); + )); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart index 52105c63..f6cf07a8 100644 --- a/lib/presentation/feed/feed_screen/feed_screen.dart +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -13,7 +13,10 @@ import 'package:intl/intl.dart'; class FeedScreen extends StatelessWidget { static Widget builder(BuildContext context) { - return BlocProvider(create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel()))..add(FeedInitialEvent()), child: FeedScreen()); + return BlocProvider( + create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel())) + ..add(FeedInitialEvent()), + child: FeedScreen()); } Widget build(BuildContext context) { @@ -103,7 +106,8 @@ class FeedScreen extends StatelessWidget { flex: 1, child: CustomOutlinedButton( text: "+", - onPressed: () => GoRouter.of(context).goNamed('blogPostCreateScreen'), + onPressed: () => + GoRouter.of(context).goNamed('blogPostCreateScreen'), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, ), ), @@ -132,23 +136,25 @@ class FeedScreen extends StatelessWidget { _buildFeedEntry(post, BuildContext context) { return Container( - margin: EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column(children: [ + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + children: [ ListTile( leading: CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + (post["author"]['profile_photo_path'] ?? ''), + imageUrl: dotenv.env['STORAGE_URL']! + + (post['profile_photo_path'] ?? ''), imageBuilder: (context, imageProvider) => CircleAvatar( backgroundImage: imageProvider, radius: 25, @@ -161,16 +167,17 @@ class FeedScreen extends StatelessWidget { ), ), title: Text( - post?['author']['name'] ?? '', + post?['author_name'] ?? 'Authorae', style: CustomTextStyles.titleMediumSemiBold, ), subtitle: Text( - DateFormat('dd.MM.yyyy').format(DateTime.parse(post?['post']['updated_at'])), + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(post?['updated_at'])), style: theme.textTheme.bodyMedium, ), onTap: () => GoRouter.of(context).goNamed( 'blogPostScreen', - extra: post?['post']['id'].toString(), + extra: post?['id'].toString(), ), trailing: PopupMenuButton( onSelected: (value) => print("tap"), @@ -197,7 +204,7 @@ class FeedScreen extends StatelessWidget { Align( alignment: Alignment.center, child: Text( - post?['post']['title'], + post?['title'], style: CustomTextStyles.titleMediumBlack, ), ), @@ -206,16 +213,17 @@ class FeedScreen extends StatelessWidget { ), SizedBox(height: 8.0), Text( - post?['post']['description'], + post?['description'], style: CustomTextStyles.bodyMediumBluegray800, ), SizedBox(height: 16.0), CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + (post?['post']['image'] ?? ''), + imageUrl: dotenv.env['STORAGE_URL']! + (post?['image'] ?? ''), imageBuilder: (context, imageProvider) => Stack( children: [ ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), + borderRadius: + const BorderRadius.all(Radius.circular(8.0)), child: Image( image: imageProvider, ), @@ -225,39 +233,47 @@ class FeedScreen extends StatelessWidget { placeholder: (context, url) => CircularProgressIndicator(), ), SizedBox(height: 16.0), - Row(children: [ - Padding( - padding: EdgeInsets.only(left: 4), - child: Row(children: [ - Padding( - padding: EdgeInsets.only(right: 4), - child: Icon(Icons.comment_outlined), - ), - Text( - "Комментировать", - ), - ],), - ), - Spacer(), - Row(children: [ + Row( + children: [ Padding( - padding: const EdgeInsets.only(right: 4), - child: Icon(Icons.favorite_outline), + padding: EdgeInsets.only(left: 4), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.comment_outlined), + ), + Text( + "Комментировать", + ), + ], + ), ), - Text( - "Нравится", + Spacer(), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + ), + ], ), - ],), - Spacer(), - Padding( - padding: EdgeInsets.only(right: 4), - child: Icon(Icons.share_outlined), - ), - ],), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.share_outlined), + ), + ], + ), ], ), ), - ],),); + ], + ), + ); } Function onTapBack(BuildContext context) { diff --git a/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart b/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart index cfecf5a3..806768cf 100644 --- a/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart +++ b/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart @@ -48,10 +48,10 @@ class MyProfileBloc extends Bloc { data.containsKey('userDetails') && data.containsKey('chartPoints') && data.containsKey('circumferences')) { - final avatarBox = await Hive.openBox('avatar'); - final avatar = data['user']['profile_photo_path']; - print(avatar); - avatarBox.put('avatar', avatar); + final avatarBox = await Hive.openBox('avatar'); + final avatar = data['user']['profile_photo_path']; + print(avatar); + avatarBox.put('avatar', avatar); if (state is MyProfileLoadedState) { var currentState = state as MyProfileLoadedState; var plans = [] @@ -61,46 +61,48 @@ class MyProfileBloc extends Bloc { ..addAll(data["paidPlan"]) ..addAll(data["requestedPlan"]) ..addAll(data["initedPlan"]); - emit((currentState).copyWith( - chartPoints: currentState.chartPoints, - chartDates: currentState.chartDates, - circumferences: currentState.circumferences, - user: data['user'], - userDetails: data['userDetails'], - plans: plans, - token: token, - chestCircumferenceController: TextEditingController(), - waistCircumferenceController: TextEditingController(), - hipCircumferenceController: TextEditingController(), - weightController: TextEditingController(), - chestCircumferenceFormController: - currentState.chestCircumferenceFormController, - waistCircumferenceFormController: - currentState.waistCircumferenceFormController, - hipCircumferenceFormController: - currentState.hipCircumferenceFormController, - firstNameFormController: currentState.firstNameFormController, - lastNameFormController: currentState.lastNameFormController, - countryFormController: currentState.countryFormController, - cityFormController: currentState.cityFormController, - heightFormController: currentState.heightFormController, - upPressureFormController: currentState.upPressureFormController, - downPressureFormController: - currentState.downPressureFormController, - allergiesFormController: currentState.allergiesFormController, - pillsFormController: currentState.pillsFormController, - favoriteFormController: currentState.favoriteFormController, - unfavoriteFormController: currentState.unfavoriteFormController, - additionalFormController: currentState.additionalFormController, - sportsFormController: currentState.sportsFormController, - dateFormController: currentState.dateFormController, - hypoFormController: currentState.hypoFormController, - hyperFormController: currentState.hyperFormController, - selectedCity: currentState.selectedCity, - selectedCountry: currentState.selectedCountry, - selectedActivity: currentState.selectedActivity, - myProfileModelObj: currentState.myProfileModelObj, - ),); + emit( + (currentState).copyWith( + chartPoints: currentState.chartPoints, + chartDates: currentState.chartDates, + circumferences: currentState.circumferences, + user: data['user'], + userDetails: data['userDetails'], + plans: plans, + token: token, + chestCircumferenceController: TextEditingController(), + waistCircumferenceController: TextEditingController(), + hipCircumferenceController: TextEditingController(), + weightController: TextEditingController(), + chestCircumferenceFormController: + currentState.chestCircumferenceFormController, + waistCircumferenceFormController: + currentState.waistCircumferenceFormController, + hipCircumferenceFormController: + currentState.hipCircumferenceFormController, + firstNameFormController: currentState.firstNameFormController, + lastNameFormController: currentState.lastNameFormController, + countryFormController: currentState.countryFormController, + cityFormController: currentState.cityFormController, + heightFormController: currentState.heightFormController, + upPressureFormController: currentState.upPressureFormController, + downPressureFormController: + currentState.downPressureFormController, + allergiesFormController: currentState.allergiesFormController, + pillsFormController: currentState.pillsFormController, + favoriteFormController: currentState.favoriteFormController, + unfavoriteFormController: currentState.unfavoriteFormController, + additionalFormController: currentState.additionalFormController, + sportsFormController: currentState.sportsFormController, + dateFormController: currentState.dateFormController, + hypoFormController: currentState.hypoFormController, + hyperFormController: currentState.hyperFormController, + selectedCity: currentState.selectedCity, + selectedCountry: currentState.selectedCountry, + selectedActivity: currentState.selectedActivity, + myProfileModelObj: currentState.myProfileModelObj, + ), + ); } else { var plans = [] ..addAll(data["finishedPlans"]) @@ -166,67 +168,87 @@ class MyProfileBloc extends Bloc { } } - emit(MyProfileLoadedState( - chartPoints: chartPoints, - chartDates: dates, - circumferences: circumferences, - user: data['user'], - userDetails: data['userDetails'], - plans: plans, - token: token, - chestCircumferenceController: TextEditingController(), - waistCircumferenceController: TextEditingController(), - hipCircumferenceController: TextEditingController(), - hypoFormController: data['userDetails']['has_hypo'].toString(), - hyperFormController: data['userDetails']['has_hyper'].toString(), - chestCircumferenceFormController: TextEditingController( - text: data['userDetails']['chest_circumference']?.toString(),), - waistCircumferenceFormController: TextEditingController( - text: data['userDetails']['waist_circumference']?.toString(),), - hipCircumferenceFormController: TextEditingController( - text: data['userDetails']['hip_circumference']?.toString(),), - weightController: TextEditingController( - text: data['userDetails']['weight']?.toString(),), - weightFormController: TextEditingController( - text: data['userDetails']['weight']?.toString(),), - firstNameFormController: TextEditingController( - text: data['userDetails']['first_name'],), - lastNameFormController: - TextEditingController(text: data['userDetails']['last_name']), - countryFormController: - TextEditingController(text: data['userDetails']['country']), - cityFormController: - TextEditingController(text: data['userDetails']['city']), - heightFormController: TextEditingController( - text: data['userDetails']['height']?.toString(),), - upPressureFormController: TextEditingController( - text: data['userDetails']['blood_pressure_up']?.toString(),), - downPressureFormController: TextEditingController( - text: data['userDetails']['blood_pressure_down']?.toString(),), - allergiesFormController: TextEditingController( - text: data['userDetails']['about_alergy'],), - pillsFormController: TextEditingController( - text: data['userDetails']['about_taking_pils'],), - sportsFormController: TextEditingController( - text: data['userDetails']['about_sport_hobbies'],), - favoriteFormController: TextEditingController( - text: data['userDetails']['favorite_foods'],), - unfavoriteFormController: TextEditingController( - text: data['userDetails']['unfavorite_foods'],), - additionalFormController: TextEditingController( - text: data['userDetails']['additional_info'],), - dateFormController: - TextEditingController(text: data['userDetails']['birthdate']), - selectedCity: data['userDetails']['city'], - selectedCountry: data['userDetails']['country'], - selectedActivity: determineActivityLevel( - data['userDetails']['activity_coefficient'],), - ),); + emit( + MyProfileLoadedState( + chartPoints: chartPoints, + chartDates: dates, + circumferences: circumferences, + user: data['user'], + userDetails: data['userDetails'], + plans: plans, + token: token, + chestCircumferenceController: TextEditingController(), + waistCircumferenceController: TextEditingController(), + hipCircumferenceController: TextEditingController(), + hypoFormController: data['userDetails']['has_hypo'].toString(), + hyperFormController: + data['userDetails']['has_hyper'].toString(), + chestCircumferenceFormController: TextEditingController( + text: data['userDetails']['chest_circumference']?.toString(), + ), + waistCircumferenceFormController: TextEditingController( + text: data['userDetails']['waist_circumference']?.toString(), + ), + hipCircumferenceFormController: TextEditingController( + text: data['userDetails']['hip_circumference']?.toString(), + ), + weightController: TextEditingController( + text: data['userDetails']['weight']?.toString(), + ), + weightFormController: TextEditingController( + text: data['userDetails']['weight']?.toString(), + ), + firstNameFormController: TextEditingController( + text: data['userDetails']['first_name'], + ), + lastNameFormController: TextEditingController( + text: data['userDetails']['last_name']), + countryFormController: + TextEditingController(text: data['userDetails']['country']), + cityFormController: + TextEditingController(text: data['userDetails']['city']), + heightFormController: TextEditingController( + text: data['userDetails']['height']?.toString(), + ), + upPressureFormController: TextEditingController( + text: data['userDetails']['blood_pressure_up']?.toString(), + ), + downPressureFormController: TextEditingController( + text: data['userDetails']['blood_pressure_down']?.toString(), + ), + allergiesFormController: TextEditingController( + text: data['userDetails']['about_alergy'], + ), + pillsFormController: TextEditingController( + text: data['userDetails']['about_taking_pils'], + ), + sportsFormController: TextEditingController( + text: data['userDetails']['about_sport_hobbies'], + ), + favoriteFormController: TextEditingController( + text: data['userDetails']['favorite_foods'], + ), + unfavoriteFormController: TextEditingController( + text: data['userDetails']['unfavorite_foods'], + ), + additionalFormController: TextEditingController( + text: data['userDetails']['additional_info'], + ), + dateFormController: TextEditingController( + text: data['userDetails']['birthdate']), + selectedCity: data['userDetails']['city'], + selectedCountry: data['userDetails']['country'], + selectedActivity: determineActivityLevel( + data['userDetails']['activity_coefficient'], + ), + ), + ); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку emit( - MyProfileErrorState('Некоторые необходимые данные отсутствуют.'),); + MyProfileErrorState('Некоторые необходимые данные отсутствуют.'), + ); } } else { // Статус ответа не равен 200, обрабатываем ошибку @@ -253,7 +275,9 @@ class MyProfileBloc extends Bloc { } // колхоз, но свитчкейсы в дарте странно ведут себя при сравнении double Future _uploadAvatar( - UploadAvatar event, Emitter emit,) async { + UploadAvatar event, + Emitter emit, + ) async { if (state is MyProfileLoadedState) { var currentState = state as MyProfileLoadedState; @@ -279,7 +303,8 @@ class MyProfileBloc extends Bloc { return imageUrl; } else { throw Exception( - 'Failed to upload image. Status code: ${streamedResponse.statusCode} ${streamedResponse}}',); + 'Failed to upload image. Status code: ${streamedResponse.statusCode} ${streamedResponse}}', + ); } } } diff --git a/lib/presentation/users/subscribers_screen/bloc/subscribers_bloc.dart b/lib/presentation/users/subscribers_screen/bloc/subscribers_bloc.dart new file mode 100644 index 00000000..1bb1bcf6 --- /dev/null +++ b/lib/presentation/users/subscribers_screen/bloc/subscribers_bloc.dart @@ -0,0 +1,74 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:eatvio_app/app_router.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/users/subscribers_screen/models/subscribers_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.dart'; + +part 'subscribers_event.dart'; +part 'subscribers_state.dart'; + +/// A bloc that manages the state of a Subscribers according to the event that is dispatched to it. +class SubscribersBloc extends Bloc { + SubscribersBloc(SubscribersState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + SubscribersInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/subscribers'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + print('Got response'); + print(response.statusCode); + print(response.body); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is SubscribersLoadedState) { + print('Got loaded'); + var currentState = state as SubscribersLoadedState; + emit((currentState).copyWith( + subscribers: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(SubscribersLoadedState( + subscribers: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(SubscribersErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(SubscribersErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(SubscribersErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/users/subscribers_screen/bloc/subscribers_event.dart b/lib/presentation/users/subscribers_screen/bloc/subscribers_event.dart new file mode 100644 index 00000000..3034e630 --- /dev/null +++ b/lib/presentation/users/subscribers_screen/bloc/subscribers_event.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +part of 'subscribers_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Subscribers widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class SubscribersEvent extends Equatable {} + +/// Event that is dispatched when the Subscribers widget is first created. +class SubscribersInitialEvent extends SubscribersEvent { + @override + List get props => []; +} diff --git a/lib/presentation/users/subscribers_screen/bloc/subscribers_state.dart b/lib/presentation/users/subscribers_screen/bloc/subscribers_state.dart new file mode 100644 index 00000000..151e0625 --- /dev/null +++ b/lib/presentation/users/subscribers_screen/bloc/subscribers_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'subscribers_bloc.dart'; + +class SubscribersState extends Equatable { + const SubscribersState({this.myProfileModelObj}); + + final SubscribersModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class SubscribersInitialState extends SubscribersState { + SubscribersInitialState({required SubscribersModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of Subscribers in the application. +class SubscribersLoadedState extends SubscribersState { + SubscribersLoadedState({this.myProfileModelObj, this.subscribers}) + : super(myProfileModelObj: null); + + SubscribersModel? myProfileModelObj; + dynamic subscribers; + @override + List get props => [ + myProfileModelObj, + subscribers, + ]; + SubscribersLoadedState copyWith({ + SubscribersModel? myProfileModelObj, + dynamic subscribers, + }) { + return SubscribersLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + subscribers: subscribers ?? this.subscribers, + ); + } +} + +class SubscribersErrorState extends SubscribersState { + final String errorMessage; + + const SubscribersErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/users/subscribers_screen/models/subscribers_model.dart b/lib/presentation/users/subscribers_screen/models/subscribers_model.dart new file mode 100644 index 00000000..f7d0cdab --- /dev/null +++ b/lib/presentation/users/subscribers_screen/models/subscribers_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class SubscribersModel extends Equatable { + SubscribersModel(); + + SubscribersModel copyWith() { + return SubscribersModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/users/subscribers_screen/subscribers_screen.dart b/lib/presentation/users/subscribers_screen/subscribers_screen.dart new file mode 100644 index 00000000..f2ac7e15 --- /dev/null +++ b/lib/presentation/users/subscribers_screen/subscribers_screen.dart @@ -0,0 +1,213 @@ +import 'package:go_router/go_router.dart'; + +import 'bloc/subscribers_bloc.dart'; +import 'models/subscribers_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_search_view.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class SubscribersScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => SubscribersBloc( + SubscribersState(myProfileModelObj: SubscribersModel())) + ..add(SubscribersInitialEvent()), + child: SubscribersScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is SubscribersInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is SubscribersLoadedState) { + // Данные загружены, можно строить экран. + return _buildSubscribersScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildSubscribersScreen(BuildContext context, SubscribersLoadedState state) { + return Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: + EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), + onTap: () => GoRouter.of(context).pop(), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Мои подписчики", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + _buildSearch(context), + SizedBox(height: 16), + _buildSubscribersList(context, state), + ], + ), + ), + ), + ), + ); + } + + _buildSearch(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CustomSearchView( + hintText: "Поиск", + ), + SizedBox(height: 10), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + text: "Найти", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "+", + onPressed: () => + GoRouter.of(context).goNamed('blogPostCreateScreen'), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ) + ], + ), + ), + ); + } + + _buildSubscribersList(BuildContext context, SubscribersLoadedState state) { + List subscriberWidgets = []; + + if (state.subscribers != null) { + for (var subscriber in state.subscribers!) { + subscriberWidgets.add(_buildSubscriberEntry(subscriber, context)); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: subscriberWidgets, + ); + } + + _buildSubscriberEntry(BuildContext context, subscriber) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + subscriber['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + subscriber?['title'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(subscriber?['description'] ?? ''), + trailing: GestureDetector(child: Icon(Icons.edit)), + )); + } +}