Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Username1548
2024-12-04 22:08:00 +07:00
37 changed files with 1513 additions and 380 deletions

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/auth_screen/auth_screen.dart';
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
@@ -19,7 +20,6 @@ import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.d
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/pages/notifications_screen.dart';
@@ -41,7 +41,6 @@ import 'package:eatvio_app/features/settings/presentation/pages/physical_form_sc
import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class AppRouter {
@@ -78,7 +77,7 @@ class AppRouter {
path: '/my-profile-screen',
name: 'myProfileScreen',
redirect: (context, state) async {
await context.read<InitializationCubit>().runInitialization();
await sl<InitializationCubit>().runInitialization();
return null;
},
pageBuilder: (context, state) => NoTransitionPage(child: ProfileScreen()),
@@ -167,13 +166,15 @@ class AppRouter {
path: '/blog-post-create-screen',
name: 'blogPostCreateScreen',
builder: (context, state) => BlogPostCreateScreen(
feedCubit: state.extra != null ? state.extra as FeedCubit : null,
feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null,
),
),
GoRoute(
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) => BlogPostScreen(blogPost: state.extra as FeedBlogPostModel),
builder: (context, state) => BlogPostScreen(
blogPost: state.extra as FeedBlogPostModel,
),
),
GoRoute(
path: '/chat-screen',
@@ -273,7 +274,7 @@ class AppRouter {
path: DietPreviewScreen.path,
name: DietPreviewScreen.name,
builder: (context, state) {
final data = state.extra as Diet;
final data = state.extra as Diet;
return DietPreviewScreen(
diet: data,
);

View File

@@ -58,15 +58,15 @@ Future<void> init() async {
sl.registerLazySingleton<InitializationCubit>(() => InitializationCubit(chatRepository: sl()));
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit());
sl.registerLazySingleton<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
// Cubit factories
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(chatRepository: sl(), initializationCubit: sl()));
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl()));
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
sl.registerFactory<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
sl.registerFactory<RecipeCreateCubit>(() => RecipeCreateCubit(recipeRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));

View File

@@ -1,9 +1,11 @@
import 'dart:convert';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
@@ -191,6 +193,7 @@ class _AuthScreenState extends State<AuthScreen> {
final token = res['token'];
await TokenStorage.saveTokenInStorage(token);
GoRouter.of(context).go('/my-profile-screen');
sl<ProfileAvatarCubit>().getMyProfile();
} else {
showDialog(
context: context,

View File

@@ -3,6 +3,7 @@ import 'package:cached_network_image/cached_network_image.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/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
@@ -19,7 +20,10 @@ import 'package:share_plus/share_plus.dart';
class BlogPostScreen extends StatefulWidget {
final FeedBlogPostModel blogPost;
BlogPostScreen({super.key, required this.blogPost});
BlogPostScreen({
super.key,
required this.blogPost,
});
@override
State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -185,6 +189,40 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
appBar: AppBar(
title: Text(widget.blogPost.title),
actions: [
PopupMenuButton(
onSelected: (value) {
if (value == 'delete') {
sl<BlogPostCreateCubit>().deleteBlogPost(widget.blogPost.id);
context.read<FeedCubit>().loadFeedBlogPostList();
Navigator.of(context).pop();
}
if (value == 'edit') {
GoRouter.of(context).replaceNamed(
'blogPostCreateScreen',
extra: widget.blogPost,
);
}
},
itemBuilder: (context) => [
if (widget.blogPost.author.authorIsMe)
PopupMenuItem(
value: 'edit',
child: Text('Редактировать'),
),
if (widget.blogPost.author.authorIsMe)
PopupMenuItem(
value: 'delete',
child: Text('Удалить'),
),
if (!widget.blogPost.author.authorIsMe)
PopupMenuItem(
value: 'report',
child: Text('Пожаловаться'),
),
],
),
],
),
body: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 80),
@@ -266,7 +304,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
isLiked
? await context.read<FeedCubit>().likePost(widget.blogPost.id)
: await context.read<FeedCubit>().unlikedPost(widget.blogPost.id);
await context.read<FeedCubit>().loadFeedBlogPostList();
sl<FeedCubit>().updateFeedBlogPostList(widget.blogPost, isLiked);
},
),
SizedBox(width: 8),
@@ -318,8 +356,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
),
SizedBox(height: 16),
if (state is CommentsStateLoading)
const Loader(),
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(
id: widget.blogPost.id.toString(),

View File

@@ -25,4 +25,28 @@ class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
);
emit(BlogPostCreateState.loaded());
}
updateBlogPost({
required int id,
String? title,
String? videoUrl,
String? description,
XFile? image,
String? oldImageUrl,
}) async {
emit(BlogPostCreateState.loading());
await blogPostRepository.updateBlogPost(
id,
title,
videoUrl,
description,
image,
oldImageUrl,
);
emit(BlogPostCreateState.loaded());
}
deleteBlogPost(int id) async {
await blogPostRepository.deleteBlogPost(id);
}
}

View File

@@ -1,24 +1,25 @@
import 'dart:async';
import 'dart:io';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
class BlogPostCreateScreen extends StatefulWidget {
final FeedCubit? feedCubit;
final FeedBlogPostModel? feedBlogPostModel;
const BlogPostCreateScreen({
required this.feedCubit,
this.feedBlogPostModel,
}) : super();
@override
@@ -44,6 +45,11 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
shrinkImage = visible;
});
});
if (widget.feedBlogPostModel != null) {
titleController.text = widget.feedBlogPostModel!.title;
descriptionController.text = widget.feedBlogPostModel!.description;
videoUrlController.text = widget.feedBlogPostModel!.videoUrl;
}
}
@override
@@ -58,7 +64,8 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
child: BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (BuildContext context, BlogPostCreateState state) {
if (state is BlogPostCreateStateLoaded) {
GoRouter.of(context).pop();
context.read<FeedCubit>().loadFeedBlogPostList();
Navigator.of(context).pop();
}
},
child: _buildBlogPostCreate(),
@@ -141,13 +148,23 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
setState(() {
isLoading = true;
});
await blogPostCubit.createBlogPost(
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
);
widget.feedCubit?.loadFeedBlogPostList();
if (widget.feedBlogPostModel != null) {
await blogPostCubit.updateBlogPost(
id: widget.feedBlogPostModel!.id,
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
oldImageUrl: widget.feedBlogPostModel!.image,
);
} else {
await blogPostCubit.createBlogPost(
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
);
}
},
),
),
@@ -175,9 +192,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
if (videoUrlController.text.isEmpty) SizedBox(height: 8),
if (videoUrlController.text.isEmpty) _buildAddPhoto(),
SizedBox(height: 16),
if (image == null) Text('Ссылка на видео'),
if (image == null) SizedBox(height: 8),
if (image == null)
if (image == null && widget.feedBlogPostModel?.image == null) Text('Ссылка на видео'),
if (image == null && widget.feedBlogPostModel?.image == null) SizedBox(height: 8),
if (image == null && widget.feedBlogPostModel?.image == null)
CustomTextFormField(
controller: videoUrlController,
),
@@ -195,7 +212,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
},
child: Stack(
children: [
if (image == null)
if (image == null && widget.feedBlogPostModel?.image == null)
AnimatedContainer(
height: shrinkImage ? 52 : 128,
curve: Curves.easeIn,
@@ -233,6 +250,16 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
),
),
),
if (widget.feedBlogPostModel?.image != null && image == null)
Container(
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: NetworkImage(baseUrl + '/storage/' + widget.feedBlogPostModel!.image),
),
),
),
],
),
);

View File

@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:http/http.dart' as http;
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
class BlogPostRepository {
final BlogPostService blogPostService;
@@ -29,4 +32,58 @@ class BlogPostRepository {
formData: formData,
);
}
Future<XFile?> downloadImage(String imageUrl) async {
try {
// Загрузка изображения
final response = await http.get(Uri.parse(baseUrl + '/storage/' + imageUrl));
if (response.statusCode == 200) {
// Получение временной директории
final directory = await getTemporaryDirectory();
// Создание файла в временной директории
final filePath = '${directory.path}/image.jpg'; // Вы можете изменить название файла
final file = File(filePath);
// Запись данных в файл
await file.writeAsBytes(response.bodyBytes);
// Возвращаем объект XFile
return XFile(file.path);
} else {
print('Ошибка загрузки изображения: ${response.statusCode}');
return null;
}
} catch (e) {
print('Произошла ошибка: $e');
return null;
}
}
Future<void> updateBlogPost(
int id,
String? title,
String? videoUrl,
String? description,
XFile? image,
String? oldImageUrl,
) async {
FormData formData = FormData.fromMap({
'title': title,
'description': description,
'state': 'active',
'video_url': videoUrl,
'image': image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
: oldImageUrl != null
? await MultipartFile.fromFile((await downloadImage(oldImageUrl))?.path ?? '', filename: oldImageUrl.split('/').last)
: null,
});
return await blogPostService.updateBlogPost(
id: id,
formData: formData,
);
}
Future<void> deleteBlogPost(int id) async => await blogPostService.deleteBlogPost(id: id);
}

View File

@@ -12,4 +12,16 @@ abstract class BlogPostService {
Future<void> createBlogPost({
@Body() required FormData formData,
});
@POST('/api/blogPosts/update/{id}')
@MultiPart()
Future<void> updateBlogPost({
@Path('id') required int id,
@Body() required FormData formData,
});
@POST('/api/blogPosts/delete/{id}')
Future<void> deleteBlogPost({
@Path('id') required int id,
});
}

View File

@@ -47,6 +47,60 @@ class _BlogPostService implements BlogPostService {
await _dio.fetch<void>(_options);
}
@override
Future<void> updateBlogPost({
required int id,
required FormData formData,
}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = formData;
final _options = _setStreamType<void>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
contentType: 'multipart/form-data',
)
.compose(
_dio.options,
'/api/blogPosts/update/${id}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
await _dio.fetch<void>(_options);
}
@override
Future<void> deleteBlogPost({required int id}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/blogPosts/delete/${id}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
await _dio.fetch<void>(_options);
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

View File

@@ -10,10 +10,9 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
}) : 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()));
}

View File

@@ -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<BottomNavigationBarWidget> {
items: [
BottomNavigationBarItem(
label: '',
icon: BlocProvider<ProfileAvatarCubit>(
create: (context) => sl<ProfileAvatarCubit>()..getMyProfile(),
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return CircleAvatar(
backgroundImage: NetworkImage(state.profile.profilePhotoUrl),
radius: 16,
);
}
icon: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
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(

View File

@@ -14,6 +14,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
final ProfileRepository profileRepository;
final GlossaryRepository glossaryRepository;
static const _dietStateValues = ['active', 'personal', 'draft'];
CreateDietCubit({required this.profileRepository, required this.createDietRepository, required this.glossaryRepository}) : super(CreateDietState.initial());
void setDetails(String text) {
@@ -238,7 +239,13 @@ class CreateDietCubit extends Cubit<CreateDietState> {
),
details: state.dietData.details ?? '',
dailySchedule: state.dietData.dailySchedule ?? [],
author: Author(authorSlug: authorProfile.slug, authorName: authorProfile.name, authorProfileIcon: authorProfile.profilePhotoUrl, authorIsMe: false),
author: Author(
authorSlug: authorProfile.slug,
authorName: authorProfile.name,
authorProfileIcon: authorProfile.profilePhotoUrl,
authorIsMe: false,
isEntitySubscribedByMe: false,
),
totalLikes: 0,
totalViews: 0,
totalComments: 0,

View File

@@ -86,7 +86,7 @@ class _AddProductModalState extends State<AddProductModal> {
hintText: 'Введите название продукта/блюда',
onChanged: (query) {
EasyDebounce.debounce(
'client-typing',
'glossary-search',
Duration(milliseconds: 250),
() async {
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
@@ -133,6 +133,7 @@ class _AddProductModalState extends State<AddProductModal> {
});
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(glossariesList[i].name);
setState(() {});
FocusManager.instance.primaryFocus?.nextFocus();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),

View File

@@ -20,19 +20,31 @@ class CaloriesChartWidget extends StatelessWidget {
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Ваш КБЖУ от дневной\nсуточной нормы',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF323F4B),
fontWeight: FontWeight.w500,
fontSize: 18,
height: 1,
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(
'График отображает сколько калорий вам нужно ежедневно для похуденияи поддержания веса, согласно формуле Харриса-Бенедикта.\nИзменить коэффициент физической нагрузки можно в настройках.',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Color(0xFF323F4B),
height: 1.2,
),
),
),
);
},
child: Icon(
Icons.help_outline_rounded,
color: appTheme.blueA400,
size: 28,
),
),
const SizedBox(height: 48),
const SizedBox(height: 12),
Stack(
alignment: Alignment.bottomCenter,
children: [
@@ -60,7 +72,9 @@ class CaloriesChartWidget extends StatelessWidget {
RangePointer(
value: (state.nutrition.dailyNutritionIntake.carbohydrates / state.nutrition.dailyNutritionRecommendation.carbohydrates) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.carbohydrates < state.nutrition.dailyNutritionRecommendation.carbohydrates ? appTheme.blueA400 : Colors.red,
color: state.nutrition.dailyNutritionIntake.carbohydrates < state.nutrition.dailyNutritionRecommendation.carbohydrates
? appTheme.blueA400
: Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
@@ -78,7 +92,9 @@ class CaloriesChartWidget extends StatelessWidget {
RangePointer(
value: (state.nutrition.dailyNutritionIntake.fats / state.nutrition.dailyNutritionRecommendation.fats) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.fats < state.nutrition.dailyNutritionRecommendation.fats ? appTheme.blueA400 : Colors.red,
color: state.nutrition.dailyNutritionIntake.fats < state.nutrition.dailyNutritionRecommendation.fats
? appTheme.blueA400
: Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
@@ -97,7 +113,9 @@ class CaloriesChartWidget extends StatelessWidget {
RangePointer(
value: (state.nutrition.dailyNutritionIntake.proteins / state.nutrition.dailyNutritionRecommendation.proteins) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.proteins < state.nutrition.dailyNutritionRecommendation.proteins ? appTheme.blueA400 : Colors.red,
color: state.nutrition.dailyNutritionIntake.proteins < state.nutrition.dailyNutritionRecommendation.proteins
? appTheme.blueA400
: Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
@@ -116,7 +134,9 @@ class CaloriesChartWidget extends StatelessWidget {
RangePointer(
value: (state.nutrition.dailyNutritionIntake.energy / state.nutrition.dailyNutritionRecommendation.energy) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.energy < state.nutrition.dailyNutritionRecommendation.energy ? appTheme.blueA400 : Colors.red,
color: state.nutrition.dailyNutritionIntake.energy < state.nutrition.dailyNutritionRecommendation.energy
? appTheme.blueA400
: Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,

View File

@@ -46,6 +46,7 @@ class Author with _$Author {
@JsonKey(name: "author_name") required String authorName,
@JsonKey(name: "author_profile_icon") required String authorProfileIcon,
@JsonKey(name: "author_is_me") required bool authorIsMe,
@JsonKey(name: "is_entity_subscribed_by_me") required bool isEntitySubscribedByMe,
}) = _Author;
factory Author.fromJson(Map<String, dynamic> json) => _$AuthorFromJson(json);

View File

@@ -812,6 +812,8 @@ mixin _$Author {
String get authorProfileIcon => throw _privateConstructorUsedError;
@JsonKey(name: "author_is_me")
bool get authorIsMe => throw _privateConstructorUsedError;
@JsonKey(name: "is_entity_subscribed_by_me")
bool get isEntitySubscribedByMe => throw _privateConstructorUsedError;
/// Serializes this Author to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -831,7 +833,9 @@ abstract class $AuthorCopyWith<$Res> {
{@JsonKey(name: "author_slug") String authorSlug,
@JsonKey(name: "author_name") String authorName,
@JsonKey(name: "author_profile_icon") String authorProfileIcon,
@JsonKey(name: "author_is_me") bool authorIsMe});
@JsonKey(name: "author_is_me") bool authorIsMe,
@JsonKey(name: "is_entity_subscribed_by_me")
bool isEntitySubscribedByMe});
}
/// @nodoc
@@ -853,6 +857,7 @@ class _$AuthorCopyWithImpl<$Res, $Val extends Author>
Object? authorName = null,
Object? authorProfileIcon = null,
Object? authorIsMe = null,
Object? isEntitySubscribedByMe = null,
}) {
return _then(_value.copyWith(
authorSlug: null == authorSlug
@@ -871,6 +876,10 @@ class _$AuthorCopyWithImpl<$Res, $Val extends Author>
? _value.authorIsMe
: authorIsMe // ignore: cast_nullable_to_non_nullable
as bool,
isEntitySubscribedByMe: null == isEntitySubscribedByMe
? _value.isEntitySubscribedByMe
: isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
@@ -886,7 +895,9 @@ abstract class _$$AuthorImplCopyWith<$Res> implements $AuthorCopyWith<$Res> {
{@JsonKey(name: "author_slug") String authorSlug,
@JsonKey(name: "author_name") String authorName,
@JsonKey(name: "author_profile_icon") String authorProfileIcon,
@JsonKey(name: "author_is_me") bool authorIsMe});
@JsonKey(name: "author_is_me") bool authorIsMe,
@JsonKey(name: "is_entity_subscribed_by_me")
bool isEntitySubscribedByMe});
}
/// @nodoc
@@ -906,6 +917,7 @@ class __$$AuthorImplCopyWithImpl<$Res>
Object? authorName = null,
Object? authorProfileIcon = null,
Object? authorIsMe = null,
Object? isEntitySubscribedByMe = null,
}) {
return _then(_$AuthorImpl(
authorSlug: null == authorSlug
@@ -924,6 +936,10 @@ class __$$AuthorImplCopyWithImpl<$Res>
? _value.authorIsMe
: authorIsMe // ignore: cast_nullable_to_non_nullable
as bool,
isEntitySubscribedByMe: null == isEntitySubscribedByMe
? _value.isEntitySubscribedByMe
: isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@@ -935,7 +951,9 @@ class _$AuthorImpl implements _Author {
{@JsonKey(name: "author_slug") required this.authorSlug,
@JsonKey(name: "author_name") required this.authorName,
@JsonKey(name: "author_profile_icon") required this.authorProfileIcon,
@JsonKey(name: "author_is_me") required this.authorIsMe});
@JsonKey(name: "author_is_me") required this.authorIsMe,
@JsonKey(name: "is_entity_subscribed_by_me")
required this.isEntitySubscribedByMe});
factory _$AuthorImpl.fromJson(Map<String, dynamic> json) =>
_$$AuthorImplFromJson(json);
@@ -952,10 +970,13 @@ class _$AuthorImpl implements _Author {
@override
@JsonKey(name: "author_is_me")
final bool authorIsMe;
@override
@JsonKey(name: "is_entity_subscribed_by_me")
final bool isEntitySubscribedByMe;
@override
String toString() {
return 'Author(authorSlug: $authorSlug, authorName: $authorName, authorProfileIcon: $authorProfileIcon, authorIsMe: $authorIsMe)';
return 'Author(authorSlug: $authorSlug, authorName: $authorName, authorProfileIcon: $authorProfileIcon, authorIsMe: $authorIsMe, isEntitySubscribedByMe: $isEntitySubscribedByMe)';
}
@override
@@ -970,13 +991,15 @@ class _$AuthorImpl implements _Author {
(identical(other.authorProfileIcon, authorProfileIcon) ||
other.authorProfileIcon == authorProfileIcon) &&
(identical(other.authorIsMe, authorIsMe) ||
other.authorIsMe == authorIsMe));
other.authorIsMe == authorIsMe) &&
(identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) ||
other.isEntitySubscribedByMe == isEntitySubscribedByMe));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, authorSlug, authorName, authorProfileIcon, authorIsMe);
int get hashCode => Object.hash(runtimeType, authorSlug, authorName,
authorProfileIcon, authorIsMe, isEntitySubscribedByMe);
/// Create a copy of Author
/// with the given fields replaced by the non-null parameter values.
@@ -996,12 +1019,13 @@ class _$AuthorImpl implements _Author {
abstract class _Author implements Author {
const factory _Author(
{@JsonKey(name: "author_slug") required final String authorSlug,
@JsonKey(name: "author_name") required final String authorName,
@JsonKey(name: "author_profile_icon")
required final String authorProfileIcon,
@JsonKey(name: "author_is_me") required final bool authorIsMe}) =
_$AuthorImpl;
{@JsonKey(name: "author_slug") required final String authorSlug,
@JsonKey(name: "author_name") required final String authorName,
@JsonKey(name: "author_profile_icon")
required final String authorProfileIcon,
@JsonKey(name: "author_is_me") required final bool authorIsMe,
@JsonKey(name: "is_entity_subscribed_by_me")
required final bool isEntitySubscribedByMe}) = _$AuthorImpl;
factory _Author.fromJson(Map<String, dynamic> json) = _$AuthorImpl.fromJson;
@@ -1017,6 +1041,9 @@ abstract class _Author implements Author {
@override
@JsonKey(name: "author_is_me")
bool get authorIsMe;
@override
@JsonKey(name: "is_entity_subscribed_by_me")
bool get isEntitySubscribedByMe;
/// Create a copy of Author
/// with the given fields replaced by the non-null parameter values.

View File

@@ -74,6 +74,7 @@ _$AuthorImpl _$$AuthorImplFromJson(Map<String, dynamic> json) => _$AuthorImpl(
authorName: json['author_name'] as String,
authorProfileIcon: json['author_profile_icon'] as String,
authorIsMe: json['author_is_me'] as bool,
isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool,
);
Map<String, dynamic> _$$AuthorImplToJson(_$AuthorImpl instance) =>
@@ -82,6 +83,7 @@ Map<String, dynamic> _$$AuthorImplToJson(_$AuthorImpl instance) =>
'author_name': instance.authorName,
'author_profile_icon': instance.authorProfileIcon,
'author_is_me': instance.authorIsMe,
'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe,
};
_$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map<String, dynamic> json) =>

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -9,7 +10,24 @@ class FeedCubit extends Cubit<FeedState> {
bool isLoading = false;
String? _query;
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
FeedCubit({
required this.feedRepository,
}) : super(const FeedState.loading());
updateFeedBlogPostList(FeedBlogPostModel feedBlogPostModel, bool wasLikedByMe) {
List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexOf(feedBlogPostModel);
feedBlogPostList[index] = feedBlogPostModel.copyWith(
wasLikedByMe: wasLikedByMe,
totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1,
);
emit(
FeedState.loaded(
feedBlogPostList: feedBlogPostList,
isLoading: isLoading,
),
);
}
loadFeedBlogPostList({
String? query,

View File

@@ -34,7 +34,7 @@ class FeedScreen extends StatefulWidget {
}
class _FeedScreenState extends State<FeedScreen> {
final FeedCubit feedCubit = sl<FeedCubit>()..loadFeedBlogPostList();
final FeedCubit feedCubit = sl<FeedCubit>();
final RecipesCubit recipesCubit = sl<RecipesCubit>()..loadRecipesList();
final DietsCubit dietsCubit = sl<DietsCubit>()..loadDiets();
final PlansCubit plansCubit = sl<PlansCubit>()..loadPlans();
@@ -49,6 +49,7 @@ class _FeedScreenState extends State<FeedScreen> {
@override
void initState() {
feedCubit.loadFeedBlogPostList();
blogPostsScrollController.addListener(_onScrollBlogPosts);
recipesScrollController.addListener(_onScrollRecipes);
dietsScrollController.addListener(_onScrollDiets);
@@ -185,7 +186,7 @@ class _FeedScreenState extends State<FeedScreen> {
if (index == state.feedBlogPostList.length) {
return const Loader();
}
return BlogPostWidget(feedBlogPostModel: state.feedBlogPostList[index]);
return BlogPostWidget(index: index);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
@@ -445,7 +446,7 @@ class _FeedScreenState extends State<FeedScreen> {
GoRouter.of(context).pushNamed(CreatePlanScreen.name);
break;
default:
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedCubit);
GoRouter.of(context).pushNamed('blogPostCreateScreen');
}
},
borderRadius: BorderRadius.circular(64),
@@ -470,6 +471,7 @@ class _FeedScreenState extends State<FeedScreen> {
width: MediaQuery.of(context).size.width - 84,
child: TextFormField(
controller: searchController,
autofocus: false,
style: TextStyle(
fontSize: 12,
),

View File

@@ -4,21 +4,24 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class BlogPostWidget extends StatefulWidget {
final FeedBlogPostModel feedBlogPostModel;
final int index;
const BlogPostWidget({super.key, required this.feedBlogPostModel});
const BlogPostWidget({
super.key,
required this.index,
});
@override
State<BlogPostWidget> createState() => _BlogPostWidgetState();
@@ -28,224 +31,209 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
late bool isLiked;
late int totalLikes;
@override
void initState() {
isLiked = widget.feedBlogPostModel.wasLikedByMe;
totalLikes = widget.feedBlogPostModel.totalLikes;
super.initState();
}
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: widget.feedBlogPostModel,
),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
return BlocBuilder<FeedCubit, FeedState>(
builder: (BuildContext context, FeedState state) {
if (state is FeedStateLoaded) {
isLiked = state.feedBlogPostList[widget.index].wasLikedByMe;
totalLikes = state.feedBlogPostList[widget.index].totalLikes;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index],
),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
widget.feedBlogPostModel.author.authorProfileIcon,
SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
state.feedBlogPostList[widget.index].author.authorProfileIcon,
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.feedBlogPostList[widget.index].author.authorName,
style: TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyle(
height: 1,
color: Colors.black45,
),
),
],
),
const Spacer(),
if (!state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe && !state.feedBlogPostList[widget.index].author.authorIsMe)
SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
),
],
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.feedBlogPostModel.author.authorName,
style: TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty)
SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.feedBlogPostList[widget.index].title,
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
const SizedBox(height: 4),
Text(
calculateDifference(
widget.feedBlogPostModel.createdAt,
),
style: TextStyle(
height: 1,
color: Colors.black45,
),
),
],
),
),
const Spacer(),
InkWell(
onTap: () {
Fluttertoast.showToast(msg: 'ща доделаем');
},
borderRadius: BorderRadius.circular(8),
child: Ink(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: appTheme.gray200,
if (state.feedBlogPostList[widget.index].title.isNotEmpty && state.feedBlogPostList[widget.index].description.isNotEmpty) SizedBox(height: 8),
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
data: state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description,
style: {
"*": Style(
color: Colors.black87,
),
},
),
),
if (state.feedBlogPostList[widget.index].description.length > 200)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Подписаться',
'Читать полностью...',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
height: 1,
color: appTheme.blueA400,
decoration: TextDecoration.underline,
),
),
),
),
],
),
),
if (widget.feedBlogPostModel.title.isNotEmpty || widget.feedBlogPostModel.description.isNotEmpty) SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
widget.feedBlogPostModel.title,
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
if (widget.feedBlogPostModel.title.isNotEmpty && widget.feedBlogPostModel.description.isNotEmpty) SizedBox(height: 8),
if (widget.feedBlogPostModel.description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
data: widget.feedBlogPostModel.description.length > 200
? widget.feedBlogPostModel.description.substring(0, 200)
: widget.feedBlogPostModel.description,
style: {
"*": Style(
color: Colors.black87,
),
},
),
),
if (widget.feedBlogPostModel.description.length > 200)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Читать полностью...',
style: TextStyle(
height: 1,
color: appTheme.blueA400,
decoration: TextDecoration.underline,
),
),
),
if (widget.feedBlogPostModel.title.isNotEmpty || widget.feedBlogPostModel.description.isNotEmpty) SizedBox(height: 8),
GestureDetector(
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [baseUrl + '/storage/' + widget.feedBlogPostModel.image],
index: 0,
),
);
},
child: Hero(
tag: baseUrl + '/storage/' + widget.feedBlogPostModel.image,
child: CachedNetworkImage(
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
imageUrl: baseUrl + '/storage/' + widget.feedBlogPostModel.image,
errorWidget: (context, str, object) => SizedBox.shrink(),
),
),
),
SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
FeedIconButton(
icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withOpacity(0.8) : null,
backgroundColor: isLiked ? Colors.red.withOpacity(0.1) : null,
textColor: isLiked ? Colors.red.withOpacity(0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(widget.feedBlogPostModel.id)
: await context.read<FeedCubit>().unlikedPost(widget.feedBlogPostModel.id);
await context.read<FeedCubit>().loadFeedBlogPostList();
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) SizedBox(height: 8),
GestureDetector(
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [baseUrl + '/storage/' + state.feedBlogPostList[widget.index].image],
index: 0,
),
);
},
),
SizedBox(width: 8),
FeedIconButton(
svgIcon: SvgPicture.asset(
'assets/icons/comment.svg',
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
appTheme.gray600,
BlendMode.srcIn,
),
),
text: widget.feedBlogPostModel.totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: widget.feedBlogPostModel,
),
),
SizedBox(width: 8),
FeedIconButton(
svgIcon: SvgPicture.asset(
height: 20,
width: 20,
'assets/icons/share.svg',
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
onTap: () => Share.shareUri(
Uri.parse(
baseUrl + '/ru/blog_post/' + widget.feedBlogPostModel.id.toString(),
child: Hero(
tag: baseUrl + '/storage/' + state.feedBlogPostList[widget.index].image,
child: CachedNetworkImage(
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
imageUrl: baseUrl + '/storage/' + state.feedBlogPostList[widget.index].image,
errorWidget: (context, str, object) => SizedBox.shrink(),
),
),
),
Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
SizedBox(width: 4),
Text(
widget.feedBlogPostModel.totalViews.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
FeedIconButton(
icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withOpacity(0.8) : null,
backgroundColor: isLiked ? Colors.red.withOpacity(0.1) : null,
textColor: isLiked ? Colors.red.withOpacity(0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(state.feedBlogPostList[widget.index].id)
: await context.read<FeedCubit>().unlikedPost(state.feedBlogPostList[widget.index].id);
await context.read<FeedCubit>().loadFeedBlogPostList();
},
),
SizedBox(width: 8),
FeedIconButton(
svgIcon: SvgPicture.asset(
'assets/icons/comment.svg',
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
appTheme.gray600,
BlendMode.srcIn,
),
),
text: state.feedBlogPostList[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index],
),
),
SizedBox(width: 8),
FeedIconButton(
svgIcon: SvgPicture.asset(
height: 20,
width: 20,
'assets/icons/share.svg',
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
onTap: () => Share.shareUri(
Uri.parse(
baseUrl + '/ru/blog_post/' + state.feedBlogPostList[widget.index].id.toString(),
),
),
),
Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
SizedBox(width: 4),
Text(
state.feedBlogPostList[widget.index].totalViews.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
),
],
),
),
SizedBox(height: 8),
],
),
),
SizedBox(height: 8),
],
),
),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -77,13 +77,16 @@ class _CommentsWidgetState extends State<CommentsWidget> {
),
),
SizedBox(height: 4),
Text(
comment.body,
style: TextStyle(
height: 1,
color: appTheme.black,
fontSize: 12,
fontWeight: FontWeight.w500,
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: Text(
comment.body,
style: TextStyle(
height: 1,
color: appTheme.black,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 4),

View File

@@ -0,0 +1,57 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
import 'package:flutter/material.dart';
class SubscribeButtonWidget extends StatefulWidget {
final String slug;
const SubscribeButtonWidget({
super.key,
required this.slug,
});
@override
State<SubscribeButtonWidget> createState() => _SubscribeButtonWidgetState();
}
class _SubscribeButtonWidgetState extends State<SubscribeButtonWidget> {
bool isSubscribe = false;
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: isSubscribe ? 0 : 1,
duration: Duration(milliseconds: 250),
child: InkWell(
onTap: () async {
try {
setState(() {
isSubscribe = true;
});
await sl<SubCubit>().subscribe(widget.slug);
} catch (e) {
setState(() {
isSubscribe = false;
});
}
},
borderRadius: BorderRadius.circular(8),
child: Ink(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: appTheme.gray200,
),
child: Text(
'Подписаться',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
}

View File

@@ -15,17 +15,34 @@ 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,
@JsonKey(name: "badges") required List<BadgeModel> badges,
@JsonKey(name: "subscriptions") required List<ProfilePreviewModel> subscriptions,
@JsonKey(name: "subscribers") required List<ProfilePreviewModel> subscribers,
@JsonKey(name: "education_resource") EducationResource? educationResource,
}) = _ProfileModel;
factory ProfileModel.fromJson(Map<String, dynamic> json) => _$ProfileModelFromJson(json);
}
@freezed
class EducationResource with _$EducationResource {
const factory EducationResource({
@JsonKey(name: "has_medical_education") int? hasMedical_Education,
@JsonKey(name: "vk_link") String? vkLink,
@JsonKey(name: "insta_link") String? instaLink,
@JsonKey(name: "fb_link") String? fbLink,
@JsonKey(name: "tg_link") String? tgLink,
@JsonKey(name: "personal_site_link") String? personalSiteLink,
@JsonKey(name: "request_state") String? requestState,
}) = _EducationResource;
factory EducationResource.fromJson(Map<String, dynamic> json) => _$EducationResourceFromJson(json);
}
@freezed
class ProfileDetailsModel with _$ProfileDetailsModel {
const factory ProfileDetailsModel({

View File

@@ -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")
@@ -48,6 +50,9 @@ mixin _$ProfileModel {
@JsonKey(name: "subscribers")
List<ProfilePreviewModel> get subscribers =>
throw _privateConstructorUsedError;
@JsonKey(name: "education_resource")
EducationResource? get educationResource =>
throw _privateConstructorUsedError;
/// Serializes this ProfileModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -73,12 +78,17 @@ 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,
@JsonKey(name: "badges") List<BadgeModel> badges,
@JsonKey(name: "subscriptions") List<ProfilePreviewModel> subscriptions,
@JsonKey(name: "subscribers") List<ProfilePreviewModel> subscribers});
@JsonKey(name: "subscribers") List<ProfilePreviewModel> subscribers,
@JsonKey(name: "education_resource")
EducationResource? educationResource});
$EducationResourceCopyWith<$Res>? get educationResource;
}
/// @nodoc
@@ -103,12 +113,14 @@ 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,
Object? badges = null,
Object? subscriptions = null,
Object? subscribers = null,
Object? educationResource = freezed,
}) {
return _then(_value.copyWith(
id: null == id
@@ -139,6 +151,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
@@ -163,8 +179,26 @@ class _$ProfileModelCopyWithImpl<$Res, $Val extends ProfileModel>
? _value.subscribers
: subscribers // ignore: cast_nullable_to_non_nullable
as List<ProfilePreviewModel>,
educationResource: freezed == educationResource
? _value.educationResource
: educationResource // ignore: cast_nullable_to_non_nullable
as EducationResource?,
) as $Val);
}
/// Create a copy of ProfileModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$EducationResourceCopyWith<$Res>? get educationResource {
if (_value.educationResource == null) {
return null;
}
return $EducationResourceCopyWith<$Res>(_value.educationResource!, (value) {
return _then(_value.copyWith(educationResource: value) as $Val);
});
}
}
/// @nodoc
@@ -183,12 +217,18 @@ 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,
@JsonKey(name: "badges") List<BadgeModel> badges,
@JsonKey(name: "subscriptions") List<ProfilePreviewModel> subscriptions,
@JsonKey(name: "subscribers") List<ProfilePreviewModel> subscribers});
@JsonKey(name: "subscribers") List<ProfilePreviewModel> subscribers,
@JsonKey(name: "education_resource")
EducationResource? educationResource});
@override
$EducationResourceCopyWith<$Res>? get educationResource;
}
/// @nodoc
@@ -211,12 +251,14 @@ class __$$ProfileModelImplCopyWithImpl<$Res>
Object? createdAt = freezed,
Object? updatedAt = freezed,
Object? isPublicProfile = null,
Object? isEntitySubscribedByMe = null,
Object? slug = null,
Object? profilePhotoUrl = null,
Object? role = null,
Object? badges = null,
Object? subscriptions = null,
Object? subscribers = null,
Object? educationResource = freezed,
}) {
return _then(_$ProfileModelImpl(
id: null == id
@@ -247,6 +289,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
@@ -271,6 +317,10 @@ class __$$ProfileModelImplCopyWithImpl<$Res>
? _value._subscribers
: subscribers // ignore: cast_nullable_to_non_nullable
as List<ProfilePreviewModel>,
educationResource: freezed == educationResource
? _value.educationResource
: educationResource // ignore: cast_nullable_to_non_nullable
as EducationResource?,
));
}
}
@@ -286,6 +336,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,
@@ -293,7 +345,8 @@ class _$ProfileModelImpl implements _ProfileModel {
@JsonKey(name: "subscriptions")
required final List<ProfilePreviewModel> subscriptions,
@JsonKey(name: "subscribers")
required final List<ProfilePreviewModel> subscribers})
required final List<ProfilePreviewModel> subscribers,
@JsonKey(name: "education_resource") this.educationResource})
: _badges = badges,
_subscriptions = subscriptions,
_subscribers = subscribers;
@@ -323,6 +376,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
@@ -358,9 +414,13 @@ class _$ProfileModelImpl implements _ProfileModel {
return EqualUnmodifiableListView(_subscribers);
}
@override
@JsonKey(name: "education_resource")
final EducationResource? educationResource;
@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, educationResource: $educationResource)';
}
@override
@@ -379,6 +439,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) &&
@@ -387,7 +449,9 @@ class _$ProfileModelImpl implements _ProfileModel {
const DeepCollectionEquality()
.equals(other._subscriptions, _subscriptions) &&
const DeepCollectionEquality()
.equals(other._subscribers, _subscribers));
.equals(other._subscribers, _subscribers) &&
(identical(other.educationResource, educationResource) ||
other.educationResource == educationResource));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -401,12 +465,14 @@ class _$ProfileModelImpl implements _ProfileModel {
createdAt,
updatedAt,
isPublicProfile,
isEntitySubscribedByMe,
slug,
profilePhotoUrl,
role,
const DeepCollectionEquality().hash(_badges),
const DeepCollectionEquality().hash(_subscriptions),
const DeepCollectionEquality().hash(_subscribers));
const DeepCollectionEquality().hash(_subscribers),
educationResource);
/// Create a copy of ProfileModel
/// with the given fields replaced by the non-null parameter values.
@@ -433,6 +499,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,
@@ -440,8 +508,9 @@ abstract class _ProfileModel implements ProfileModel {
@JsonKey(name: "subscriptions")
required final List<ProfilePreviewModel> subscriptions,
@JsonKey(name: "subscribers")
required final List<ProfilePreviewModel>
subscribers}) = _$ProfileModelImpl;
required final List<ProfilePreviewModel> subscribers,
@JsonKey(name: "education_resource")
final EducationResource? educationResource}) = _$ProfileModelImpl;
factory _ProfileModel.fromJson(Map<String, dynamic> json) =
_$ProfileModelImpl.fromJson;
@@ -468,6 +537,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
@@ -485,6 +557,9 @@ abstract class _ProfileModel implements ProfileModel {
@override
@JsonKey(name: "subscribers")
List<ProfilePreviewModel> get subscribers;
@override
@JsonKey(name: "education_resource")
EducationResource? get educationResource;
/// Create a copy of ProfileModel
/// with the given fields replaced by the non-null parameter values.
@@ -494,6 +569,307 @@ abstract class _ProfileModel implements ProfileModel {
throw _privateConstructorUsedError;
}
EducationResource _$EducationResourceFromJson(Map<String, dynamic> json) {
return _EducationResource.fromJson(json);
}
/// @nodoc
mixin _$EducationResource {
@JsonKey(name: "has_medical_education")
int? get hasMedical_Education => throw _privateConstructorUsedError;
@JsonKey(name: "vk_link")
String? get vkLink => throw _privateConstructorUsedError;
@JsonKey(name: "insta_link")
String? get instaLink => throw _privateConstructorUsedError;
@JsonKey(name: "fb_link")
String? get fbLink => throw _privateConstructorUsedError;
@JsonKey(name: "tg_link")
String? get tgLink => throw _privateConstructorUsedError;
@JsonKey(name: "personal_site_link")
String? get personalSiteLink => throw _privateConstructorUsedError;
@JsonKey(name: "request_state")
String? get requestState => throw _privateConstructorUsedError;
/// Serializes this EducationResource to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of EducationResource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$EducationResourceCopyWith<EducationResource> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $EducationResourceCopyWith<$Res> {
factory $EducationResourceCopyWith(
EducationResource value, $Res Function(EducationResource) then) =
_$EducationResourceCopyWithImpl<$Res, EducationResource>;
@useResult
$Res call(
{@JsonKey(name: "has_medical_education") int? hasMedical_Education,
@JsonKey(name: "vk_link") String? vkLink,
@JsonKey(name: "insta_link") String? instaLink,
@JsonKey(name: "fb_link") String? fbLink,
@JsonKey(name: "tg_link") String? tgLink,
@JsonKey(name: "personal_site_link") String? personalSiteLink,
@JsonKey(name: "request_state") String? requestState});
}
/// @nodoc
class _$EducationResourceCopyWithImpl<$Res, $Val extends EducationResource>
implements $EducationResourceCopyWith<$Res> {
_$EducationResourceCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of EducationResource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? hasMedical_Education = freezed,
Object? vkLink = freezed,
Object? instaLink = freezed,
Object? fbLink = freezed,
Object? tgLink = freezed,
Object? personalSiteLink = freezed,
Object? requestState = freezed,
}) {
return _then(_value.copyWith(
hasMedical_Education: freezed == hasMedical_Education
? _value.hasMedical_Education
: hasMedical_Education // ignore: cast_nullable_to_non_nullable
as int?,
vkLink: freezed == vkLink
? _value.vkLink
: vkLink // ignore: cast_nullable_to_non_nullable
as String?,
instaLink: freezed == instaLink
? _value.instaLink
: instaLink // ignore: cast_nullable_to_non_nullable
as String?,
fbLink: freezed == fbLink
? _value.fbLink
: fbLink // ignore: cast_nullable_to_non_nullable
as String?,
tgLink: freezed == tgLink
? _value.tgLink
: tgLink // ignore: cast_nullable_to_non_nullable
as String?,
personalSiteLink: freezed == personalSiteLink
? _value.personalSiteLink
: personalSiteLink // ignore: cast_nullable_to_non_nullable
as String?,
requestState: freezed == requestState
? _value.requestState
: requestState // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$EducationResourceImplCopyWith<$Res>
implements $EducationResourceCopyWith<$Res> {
factory _$$EducationResourceImplCopyWith(_$EducationResourceImpl value,
$Res Function(_$EducationResourceImpl) then) =
__$$EducationResourceImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@JsonKey(name: "has_medical_education") int? hasMedical_Education,
@JsonKey(name: "vk_link") String? vkLink,
@JsonKey(name: "insta_link") String? instaLink,
@JsonKey(name: "fb_link") String? fbLink,
@JsonKey(name: "tg_link") String? tgLink,
@JsonKey(name: "personal_site_link") String? personalSiteLink,
@JsonKey(name: "request_state") String? requestState});
}
/// @nodoc
class __$$EducationResourceImplCopyWithImpl<$Res>
extends _$EducationResourceCopyWithImpl<$Res, _$EducationResourceImpl>
implements _$$EducationResourceImplCopyWith<$Res> {
__$$EducationResourceImplCopyWithImpl(_$EducationResourceImpl _value,
$Res Function(_$EducationResourceImpl) _then)
: super(_value, _then);
/// Create a copy of EducationResource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? hasMedical_Education = freezed,
Object? vkLink = freezed,
Object? instaLink = freezed,
Object? fbLink = freezed,
Object? tgLink = freezed,
Object? personalSiteLink = freezed,
Object? requestState = freezed,
}) {
return _then(_$EducationResourceImpl(
hasMedical_Education: freezed == hasMedical_Education
? _value.hasMedical_Education
: hasMedical_Education // ignore: cast_nullable_to_non_nullable
as int?,
vkLink: freezed == vkLink
? _value.vkLink
: vkLink // ignore: cast_nullable_to_non_nullable
as String?,
instaLink: freezed == instaLink
? _value.instaLink
: instaLink // ignore: cast_nullable_to_non_nullable
as String?,
fbLink: freezed == fbLink
? _value.fbLink
: fbLink // ignore: cast_nullable_to_non_nullable
as String?,
tgLink: freezed == tgLink
? _value.tgLink
: tgLink // ignore: cast_nullable_to_non_nullable
as String?,
personalSiteLink: freezed == personalSiteLink
? _value.personalSiteLink
: personalSiteLink // ignore: cast_nullable_to_non_nullable
as String?,
requestState: freezed == requestState
? _value.requestState
: requestState // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$EducationResourceImpl implements _EducationResource {
const _$EducationResourceImpl(
{@JsonKey(name: "has_medical_education") this.hasMedical_Education,
@JsonKey(name: "vk_link") this.vkLink,
@JsonKey(name: "insta_link") this.instaLink,
@JsonKey(name: "fb_link") this.fbLink,
@JsonKey(name: "tg_link") this.tgLink,
@JsonKey(name: "personal_site_link") this.personalSiteLink,
@JsonKey(name: "request_state") this.requestState});
factory _$EducationResourceImpl.fromJson(Map<String, dynamic> json) =>
_$$EducationResourceImplFromJson(json);
@override
@JsonKey(name: "has_medical_education")
final int? hasMedical_Education;
@override
@JsonKey(name: "vk_link")
final String? vkLink;
@override
@JsonKey(name: "insta_link")
final String? instaLink;
@override
@JsonKey(name: "fb_link")
final String? fbLink;
@override
@JsonKey(name: "tg_link")
final String? tgLink;
@override
@JsonKey(name: "personal_site_link")
final String? personalSiteLink;
@override
@JsonKey(name: "request_state")
final String? requestState;
@override
String toString() {
return 'EducationResource(hasMedical_Education: $hasMedical_Education, vkLink: $vkLink, instaLink: $instaLink, fbLink: $fbLink, tgLink: $tgLink, personalSiteLink: $personalSiteLink, requestState: $requestState)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$EducationResourceImpl &&
(identical(other.hasMedical_Education, hasMedical_Education) ||
other.hasMedical_Education == hasMedical_Education) &&
(identical(other.vkLink, vkLink) || other.vkLink == vkLink) &&
(identical(other.instaLink, instaLink) ||
other.instaLink == instaLink) &&
(identical(other.fbLink, fbLink) || other.fbLink == fbLink) &&
(identical(other.tgLink, tgLink) || other.tgLink == tgLink) &&
(identical(other.personalSiteLink, personalSiteLink) ||
other.personalSiteLink == personalSiteLink) &&
(identical(other.requestState, requestState) ||
other.requestState == requestState));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, hasMedical_Education, vkLink,
instaLink, fbLink, tgLink, personalSiteLink, requestState);
/// Create a copy of EducationResource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$EducationResourceImplCopyWith<_$EducationResourceImpl> get copyWith =>
__$$EducationResourceImplCopyWithImpl<_$EducationResourceImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$EducationResourceImplToJson(
this,
);
}
}
abstract class _EducationResource implements EducationResource {
const factory _EducationResource(
{@JsonKey(name: "has_medical_education") final int? hasMedical_Education,
@JsonKey(name: "vk_link") final String? vkLink,
@JsonKey(name: "insta_link") final String? instaLink,
@JsonKey(name: "fb_link") final String? fbLink,
@JsonKey(name: "tg_link") final String? tgLink,
@JsonKey(name: "personal_site_link") final String? personalSiteLink,
@JsonKey(name: "request_state")
final String? requestState}) = _$EducationResourceImpl;
factory _EducationResource.fromJson(Map<String, dynamic> json) =
_$EducationResourceImpl.fromJson;
@override
@JsonKey(name: "has_medical_education")
int? get hasMedical_Education;
@override
@JsonKey(name: "vk_link")
String? get vkLink;
@override
@JsonKey(name: "insta_link")
String? get instaLink;
@override
@JsonKey(name: "fb_link")
String? get fbLink;
@override
@JsonKey(name: "tg_link")
String? get tgLink;
@override
@JsonKey(name: "personal_site_link")
String? get personalSiteLink;
@override
@JsonKey(name: "request_state")
String? get requestState;
/// Create a copy of EducationResource
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$EducationResourceImplCopyWith<_$EducationResourceImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ProfileDetailsModel _$ProfileDetailsModelFromJson(Map<String, dynamic> json) {
return _ProfileDetailsModel.fromJson(json);
}

View File

@@ -21,6 +21,7 @@ _$ProfileModelImpl _$$ProfileModelImplFromJson(Map<String, dynamic> 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,
@@ -33,6 +34,10 @@ _$ProfileModelImpl _$$ProfileModelImplFromJson(Map<String, dynamic> json) =>
subscribers: (json['subscribers'] as List<dynamic>)
.map((e) => ProfilePreviewModel.fromJson(e as Map<String, dynamic>))
.toList(),
educationResource: json['education_resource'] == null
? null
: EducationResource.fromJson(
json['education_resource'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$ProfileModelImplToJson(_$ProfileModelImpl instance) =>
@@ -44,12 +49,38 @@ Map<String, dynamic> _$$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,
'badges': instance.badges,
'subscriptions': instance.subscriptions,
'subscribers': instance.subscribers,
'education_resource': instance.educationResource,
};
_$EducationResourceImpl _$$EducationResourceImplFromJson(
Map<String, dynamic> json) =>
_$EducationResourceImpl(
hasMedical_Education: (json['has_medical_education'] as num?)?.toInt(),
vkLink: json['vk_link'] as String?,
instaLink: json['insta_link'] as String?,
fbLink: json['fb_link'] as String?,
tgLink: json['tg_link'] as String?,
personalSiteLink: json['personal_site_link'] as String?,
requestState: json['request_state'] as String?,
);
Map<String, dynamic> _$$EducationResourceImplToJson(
_$EducationResourceImpl instance) =>
<String, dynamic>{
'has_medical_education': instance.hasMedical_Education,
'vk_link': instance.vkLink,
'insta_link': instance.instaLink,
'fb_link': instance.fbLink,
'tg_link': instance.tgLink,
'personal_site_link': instance.personalSiteLink,
'request_state': instance.requestState,
};
_$ProfileDetailsModelImpl _$$ProfileDetailsModelImplFromJson(

View File

@@ -24,8 +24,7 @@ class SubModel with _$SubModel {
required String name,
required String city,
required DateTime birthdate,
@JsonKey(name: 'is_subscribe')
required bool isSubscribe,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
required String email,
@JsonKey(name: 'profile_photo_url') required String profilePhotoUrl,
required String role,

View File

@@ -249,8 +249,8 @@ mixin _$SubModel {
String get name => throw _privateConstructorUsedError;
String get city => throw _privateConstructorUsedError;
DateTime get birthdate => throw _privateConstructorUsedError;
@JsonKey(name: 'is_subscribe')
bool get isSubscribe => throw _privateConstructorUsedError;
@JsonKey(name: 'is_entity_subscribed_by_me')
bool get isEntitySubscribedByMe => throw _privateConstructorUsedError;
String get email => throw _privateConstructorUsedError;
@JsonKey(name: 'profile_photo_url')
String get profilePhotoUrl => throw _privateConstructorUsedError;
@@ -283,7 +283,7 @@ abstract class $SubModelCopyWith<$Res> {
String name,
String city,
DateTime birthdate,
@JsonKey(name: 'is_subscribe') bool isSubscribe,
@JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe,
String email,
@JsonKey(name: 'profile_photo_url') String profilePhotoUrl,
String role,
@@ -312,7 +312,7 @@ class _$SubModelCopyWithImpl<$Res, $Val extends SubModel>
Object? name = null,
Object? city = null,
Object? birthdate = null,
Object? isSubscribe = null,
Object? isEntitySubscribedByMe = null,
Object? email = null,
Object? profilePhotoUrl = null,
Object? role = null,
@@ -341,9 +341,9 @@ class _$SubModelCopyWithImpl<$Res, $Val extends SubModel>
? _value.birthdate
: birthdate // ignore: cast_nullable_to_non_nullable
as DateTime,
isSubscribe: null == isSubscribe
? _value.isSubscribe
: isSubscribe // ignore: cast_nullable_to_non_nullable
isEntitySubscribedByMe: null == isEntitySubscribedByMe
? _value.isEntitySubscribedByMe
: isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable
as bool,
email: null == email
? _value.email
@@ -387,7 +387,7 @@ abstract class _$$SubModelImplCopyWith<$Res>
String name,
String city,
DateTime birthdate,
@JsonKey(name: 'is_subscribe') bool isSubscribe,
@JsonKey(name: 'is_entity_subscribed_by_me') bool isEntitySubscribedByMe,
String email,
@JsonKey(name: 'profile_photo_url') String profilePhotoUrl,
String role,
@@ -414,7 +414,7 @@ class __$$SubModelImplCopyWithImpl<$Res>
Object? name = null,
Object? city = null,
Object? birthdate = null,
Object? isSubscribe = null,
Object? isEntitySubscribedByMe = null,
Object? email = null,
Object? profilePhotoUrl = null,
Object? role = null,
@@ -443,9 +443,9 @@ class __$$SubModelImplCopyWithImpl<$Res>
? _value.birthdate
: birthdate // ignore: cast_nullable_to_non_nullable
as DateTime,
isSubscribe: null == isSubscribe
? _value.isSubscribe
: isSubscribe // ignore: cast_nullable_to_non_nullable
isEntitySubscribedByMe: null == isEntitySubscribedByMe
? _value.isEntitySubscribedByMe
: isEntitySubscribedByMe // ignore: cast_nullable_to_non_nullable
as bool,
email: null == email
? _value.email
@@ -484,7 +484,8 @@ class _$SubModelImpl implements _SubModel {
required this.name,
required this.city,
required this.birthdate,
@JsonKey(name: 'is_subscribe') required this.isSubscribe,
@JsonKey(name: 'is_entity_subscribed_by_me')
required this.isEntitySubscribedByMe,
required this.email,
@JsonKey(name: 'profile_photo_url') required this.profilePhotoUrl,
required this.role,
@@ -506,8 +507,8 @@ class _$SubModelImpl implements _SubModel {
@override
final DateTime birthdate;
@override
@JsonKey(name: 'is_subscribe')
final bool isSubscribe;
@JsonKey(name: 'is_entity_subscribed_by_me')
final bool isEntitySubscribedByMe;
@override
final String email;
@override
@@ -527,7 +528,7 @@ class _$SubModelImpl implements _SubModel {
@override
String toString() {
return 'SubModel(id: $id, slug: $slug, name: $name, city: $city, birthdate: $birthdate, isSubscribe: $isSubscribe, email: $email, profilePhotoUrl: $profilePhotoUrl, role: $role, isPublicProfile: $isPublicProfile, createdAt: $createdAt, updatedAt: $updatedAt)';
return 'SubModel(id: $id, slug: $slug, name: $name, city: $city, birthdate: $birthdate, isEntitySubscribedByMe: $isEntitySubscribedByMe, email: $email, profilePhotoUrl: $profilePhotoUrl, role: $role, isPublicProfile: $isPublicProfile, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
@@ -541,8 +542,8 @@ class _$SubModelImpl implements _SubModel {
(identical(other.city, city) || other.city == city) &&
(identical(other.birthdate, birthdate) ||
other.birthdate == birthdate) &&
(identical(other.isSubscribe, isSubscribe) ||
other.isSubscribe == isSubscribe) &&
(identical(other.isEntitySubscribedByMe, isEntitySubscribedByMe) ||
other.isEntitySubscribedByMe == isEntitySubscribedByMe) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.profilePhotoUrl, profilePhotoUrl) ||
other.profilePhotoUrl == profilePhotoUrl) &&
@@ -564,7 +565,7 @@ class _$SubModelImpl implements _SubModel {
name,
city,
birthdate,
isSubscribe,
isEntitySubscribedByMe,
email,
profilePhotoUrl,
role,
@@ -595,7 +596,8 @@ abstract class _SubModel implements SubModel {
required final String name,
required final String city,
required final DateTime birthdate,
@JsonKey(name: 'is_subscribe') required final bool isSubscribe,
@JsonKey(name: 'is_entity_subscribed_by_me')
required final bool isEntitySubscribedByMe,
required final String email,
@JsonKey(name: 'profile_photo_url') required final String profilePhotoUrl,
required final String role,
@@ -618,8 +620,8 @@ abstract class _SubModel implements SubModel {
@override
DateTime get birthdate;
@override
@JsonKey(name: 'is_subscribe')
bool get isSubscribe;
@JsonKey(name: 'is_entity_subscribed_by_me')
bool get isEntitySubscribedByMe;
@override
String get email;
@override

View File

@@ -31,7 +31,7 @@ _$SubModelImpl _$$SubModelImplFromJson(Map<String, dynamic> json) =>
name: json['name'] as String,
city: json['city'] as String,
birthdate: DateTime.parse(json['birthdate'] as String),
isSubscribe: json['is_subscribe'] as bool,
isEntitySubscribedByMe: json['is_entity_subscribed_by_me'] as bool,
email: json['email'] as String,
profilePhotoUrl: json['profile_photo_url'] as String,
role: json['role'] as String,
@@ -47,7 +47,7 @@ Map<String, dynamic> _$$SubModelImplToJson(_$SubModelImpl instance) =>
'name': instance.name,
'city': instance.city,
'birthdate': instance.birthdate.toIso8601String(),
'is_subscribe': instance.isSubscribe,
'is_entity_subscribed_by_me': instance.isEntitySubscribedByMe,
'email': instance.email,
'profile_photo_url': instance.profilePhotoUrl,
'role': instance.role,

View File

@@ -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';
@@ -15,11 +18,13 @@ class ProfileCubit extends Cubit<ProfileState> {
getProfileBySlug(String slug) async {
try {
emit(ProfileState.loaded(
profile: await profileRepository.getProfileBySlug(slug),
profileDetails: await profileRepository.getMyProfileDetails(),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
),);
emit(
ProfileState.loaded(
profile: await profileRepository.getProfileBySlug(slug),
profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
),
);
} catch (e) {
emit(ProfileState.error(errorMessage: e.toString()));
}
@@ -31,13 +36,15 @@ class ProfileCubit extends Cubit<ProfileState> {
_currentPeriod = period;
try {
final profile = await profileRepository.getMyProfile();
emit(ProfileState.loaded(
profile: profile,
profileDetails: await profileRepository.getMyProfileDetails(),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug),
weightPoints: await profileRepository.getWeight(period: period),
circumferencePoints: await profileRepository.getCircumferences(period: period),
),);
emit(
ProfileState.loaded(
profile: profile,
profileDetails: await profileRepository.getMyProfileDetails(),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug),
weightPoints: await profileRepository.getWeight(period: period),
circumferencePoints: await profileRepository.getCircumferences(period: period),
),
);
} catch (e) {
emit(ProfileState.error(errorMessage: e.toString()));
}
@@ -80,4 +87,14 @@ class ProfileCubit extends Cubit<ProfileState> {
rethrow;
}
}
uploadPhoto(File file) async {
await profileRepository.uploadPhoto(await ImageCompressUtility.compressFile(file));
return await getMyProfile();
}
deletePhoto() async {
await profileRepository.deletePhoto();
return await getMyProfile();
}
}

View File

@@ -1,22 +1,33 @@
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';
import 'package:url_launcher/url_launcher.dart';
class ProfileScreen extends StatefulWidget {
final Author? author;
@@ -40,6 +51,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
List<CircumferenceModel> circumferencePoints = [];
int _selectedChipIndex = 0;
int _selectedIntervalChipIndex = 0;
late bool canSub;
bool isFirstFetch = true;
@override
void initState() {
@@ -57,6 +70,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
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,17 +116,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
_header(state),
if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state),
const SizedBox(height: 8),
_messageAndSubscribe(),
_messageAndSubscribe(state),
const SizedBox(height: 16),
],
),
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) _badges(state),
_socialLinks(state),
_education(state),
_diplomasAndCertificates(state),
_activePlans(state),
if (state.profile.badges.isNotEmpty) Padding(padding: EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _socialLinks(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _education(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _diplomasAndCertificates(state)),
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _activePlans(state)),
_blogPosts(state),
],
),
@@ -444,11 +461,38 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
_socialLinkButton(() {}, 'assets/icons/vk.svg'),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.vkLink ?? '',
),
);
},
'assets/icons/vk.svg',
),
const SizedBox(width: 12),
_socialLinkButton(() {}, 'assets/icons/telegram.svg'),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.tgLink ?? '',
),
);
},
'assets/icons/telegram.svg',
),
const SizedBox(width: 12),
_socialLinkButton(() {}, 'assets/icons/web.svg'),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.personalSiteLink ?? '',
),
);
},
'assets/icons/web.svg',
),
],
),
),
@@ -662,7 +706,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => BlogPostWidget(
feedBlogPostModel: state.blogPosts[index],
index: index,
),
separatorBuilder: (context, index) => SizedBox(height: 8),
itemCount: state.blogPosts.length,
@@ -692,16 +736,100 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 32,
backgroundImage: NetworkImage(state.profile.profilePhotoUrl),
PopupMenuButton(
onSelected: (value) async {
if (value == 'view') {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [state.profile.profilePhotoUrl],
index: 0,
),
);
}
if (value == 'replace') {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
child: const Row(
children: 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<ProfileAvatarCubit>().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('Отмена'),
),
),
);
}
if (value == 'delete') {
await profileCubit.deletePhoto();
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
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 +838,110 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
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 +969,26 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
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<SubCubit>().unsubscribe(state.profile.slug) : sl<SubCubit>().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 +1035,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
return ImtChartWidget(
weightPoints: weightPoints,
);
default: return SizedBox.shrink();
default:
return SizedBox.shrink();
}
},
),

View File

@@ -27,6 +27,7 @@ class _SubWidgetState extends State<SubWidget> {
authorName: widget.subModel.name,
authorProfileIcon: widget.subModel.profilePhotoUrl,
authorIsMe: false,
isEntitySubscribedByMe: false,
),
),
child: Ink(
@@ -65,7 +66,7 @@ class _SubWidgetState extends State<SubWidget> {
),
const Spacer(),
SubActionButtonWidget(
subActionType: widget.subModel.isSubscribe ? SubActionType.SendMessage : SubActionType.Subscribe,
subActionType: widget.subModel.isEntitySubscribedByMe ? SubActionType.SendMessage : SubActionType.Subscribe,
slug: widget.subModel.slug,
),
],

View File

@@ -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';
@@ -20,6 +22,14 @@ class ProfileRepository {
}
}
Future<ProfileDetailsModel> getProfileDetailsBySlug(String slug) async {
try {
return await profileService.getProfileDetailsBySlug(slug: slug);
} catch (e) {
rethrow;
}
}
Future<ProfileModel> getMyProfile() async {
try {
return await profileService.getMyProfile();
@@ -145,4 +155,20 @@ class ProfileRepository {
rethrow;
}
}
Future<void> uploadPhoto(File file) async {
try {
return await profileService.uploadPhoto(photo: file);
} catch (e) {
rethrow;
}
}
Future<void> deletePhoto() async {
try {
return await profileService.deletePhoto();
} catch (e) {
rethrow;
}
}
}

View File

@@ -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';
@@ -5,6 +7,7 @@ import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/models/sub_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:retrofit/retrofit.dart';
part 'profile_service.g.dart';
@RestApi()
@@ -22,6 +25,11 @@ abstract class ProfileService {
@GET('/api/profile/detailz')
Future<ProfileDetailsModel> getMyProfileDetails();
@GET('/api/profile/detailz/{slug}')
Future<ProfileDetailsModel> getProfileDetailsBySlug({
@Path() required String slug,
});
@POST('/api/profile/addCircumference')
Future<void> addCircumference({
@Part(name: 'chest_circumference') required int chestCircumference,
@@ -82,4 +90,13 @@ abstract class ProfileService {
Future<void> unsubscribe({
@Path() required String slug,
});
@POST('/api/profile/photo/upload')
@MultiPart()
Future<void> uploadPhoto({
@Part(name: 'photo') required File photo,
});
@DELETE('/api/profile/photo/delete')
Future<void> deletePhoto();
}

View File

@@ -120,6 +120,40 @@ class _ProfileService implements ProfileService {
return _value;
}
@override
Future<ProfileDetailsModel> getProfileDetailsBySlug(
{required String slug}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<ProfileDetailsModel>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/profile/detailz/${slug}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late ProfileDetailsModel _value;
try {
_value = ProfileDetailsModel.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<void> addCircumference({
required int chestCircumference,
@@ -504,6 +538,64 @@ class _ProfileService implements ProfileService {
await _dio.fetch<void>(_options);
}
@override
Future<void> uploadPhoto({required File photo}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = FormData();
_data.files.add(MapEntry(
'photo',
MultipartFile.fromFileSync(
photo.path,
filename: photo.path.split(Platform.pathSeparator).last,
),
));
final _options = _setStreamType<void>(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<void>(_options);
}
@override
Future<void> deletePhoto() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(Options(
method: 'DELETE',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/profile/photo/delete',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
await _dio.fetch<void>(_options);
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

View File

@@ -5,6 +5,8 @@ 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';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -26,9 +28,11 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<InitializationCubit>(create: (context) => sl<InitializationCubit>()),
BlocProvider<InitializationCubit>(create: (context) => sl<InitializationCubit>()..runInitialization()),
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
],
child: Portal(
child: KeyboardDismisser(

View File

@@ -48,7 +48,7 @@ dependencies:
file_picker: ^8.1.4
audioplayers: ^6.1.0
intl: ^0.19.0
rive: ^0.13.18
rive: ^0.13.19
mask_text_input_formatter: ^2.9.0
lottie: ^3.1.3
flutter_portal: ^1.1.4
@@ -58,6 +58,7 @@ dependencies:
image: ^4.3.0
flutter_native_splash: ^2.4.3
dotted_border: ^2.1.0
http: ^1.2.2
dev_dependencies:
build_runner: ^2.4.13