feat(app): Initial layouts for subscribers, feed display fix, feed photo upload front end

This commit is contained in:
2024-06-27 13:17:53 +03:00
parent 6e7f1acf46
commit b1508c3553
12 changed files with 709 additions and 184 deletions

View File

@@ -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<BlogPostCreateInitialEvent>(_onInitialize);
on<CreateBlogPost>(_createBlogPost);
on<UploadImages>(_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<void> _createBlogPost(
CreateBlogPost event, Emitter<BlogPostCreateState> emit,) async {
CreateBlogPost event,
Emitter<BlogPostCreateState> 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<void> _uploadImages(
UploadImages event, Emitter<BlogPostCreateState> emit) {}
}

View File

@@ -19,3 +19,10 @@ class CreateBlogPost extends BlogPostCreateEvent {
@override
List<Object?> get props => [];
}
class UploadImages extends BlogPostCreateEvent {
UploadImages(this.image);
final XFile image;
@override
List<Object> get props => [image];
}

View File

@@ -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<Object?> 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,
);
}
}

View File

@@ -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<BlogPostCreateBloc>(
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: <Widget>[
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<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
for (var image in images) {
context.read<BlogPostCreateBloc>().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);
}

View File

@@ -50,12 +50,12 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {
emit((currentState).copyWith(
posts: data,
myProfileModelObj: currentState.myProfileModelObj,
),);
));
} else {
print('Got unloaded');
emit(FeedLoadedState(
posts: data,
),);
));
}
} else {
// Один или несколько ключевых полей отсутствуют, обрабатываем ошибку

View File

@@ -13,7 +13,10 @@ import 'package:intl/intl.dart';
class FeedScreen extends StatelessWidget {
static Widget builder(BuildContext context) {
return BlocProvider<FeedBloc>(create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel()))..add(FeedInitialEvent()), child: FeedScreen());
return BlocProvider<FeedBloc>(
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<String>(
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) {

View File

@@ -48,10 +48,10 @@ class MyProfileBloc extends Bloc<MyProfileEvent, MyProfileState> {
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<MyProfileEvent, MyProfileState> {
..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<MyProfileEvent, MyProfileState> {
}
}
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<MyProfileEvent, MyProfileState> {
} // колхоз, но свитчкейсы в дарте странно ведут себя при сравнении double
Future<void> _uploadAvatar(
UploadAvatar event, Emitter<MyProfileState> emit,) async {
UploadAvatar event,
Emitter<MyProfileState> emit,
) async {
if (state is MyProfileLoadedState) {
var currentState = state as MyProfileLoadedState;
@@ -279,7 +303,8 @@ class MyProfileBloc extends Bloc<MyProfileEvent, MyProfileState> {
return imageUrl;
} else {
throw Exception(
'Failed to upload image. Status code: ${streamedResponse.statusCode} ${streamedResponse}}',);
'Failed to upload image. Status code: ${streamedResponse.statusCode} ${streamedResponse}}',
);
}
}
}

View File

@@ -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<SubscribersEvent, SubscribersState> {
SubscribersBloc(SubscribersState initialState) : super(initialState) {
on<SubscribersInitialEvent>(_onInitialize);
}
Future<void> _onInitialize(
SubscribersInitialEvent event,
Emitter<SubscribersState> 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<dynamic>) {
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'));
}
}
}

View File

@@ -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<Object?> get props => [];
}

View File

@@ -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<Object?> 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<Object?> 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<Object> get props => [errorMessage];
}

View File

@@ -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<Object?> get props => [];
}

View File

@@ -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<SubscribersBloc>(
create: (context) => SubscribersBloc(
SubscribersState(myProfileModelObj: SubscribersModel()))
..add(SubscribersInitialEvent()),
child: SubscribersScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<SubscribersBloc, SubscribersState>(
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<Widget> 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)),
));
}
}