diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 1cb3b901..6facfbb2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -19,7 +19,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'; @@ -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, ); diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 8d6994f5..9f456ad2 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -60,11 +60,11 @@ Future init() async { sl.registerLazySingleton(() => InitializationCubit(chatRepository: sl())); sl.registerLazySingleton(() => NewMessagesCubit()); sl.registerLazySingleton(() => NotificationsCubit()); + sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), initializationCubit: sl())); - sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 1d73d7cd..dd9b1c78 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -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 createState() => _BlogPostScreenState(); @@ -185,6 +189,40 @@ class _BlogPostScreenState extends State { ), appBar: AppBar( title: Text(widget.blogPost.title), + actions: [ + PopupMenuButton( + onSelected: (value) { + if (value == 'delete') { + sl().deleteBlogPost(widget.blogPost.id); + context.read().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), @@ -318,8 +356,7 @@ class _BlogPostScreenState extends State { ), ), 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(), diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index 81784e5f..90ee1e88 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -25,4 +25,28 @@ class BlogPostCreateCubit extends Cubit { ); 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); + } } diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index ebe3593d..9e7a85d3 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -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 { 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 { child: BlocListener( listener: (BuildContext context, BlogPostCreateState state) { if (state is BlogPostCreateStateLoaded) { - GoRouter.of(context).pop(); + context.read().loadFeedBlogPostList(); + Navigator.of(context).pop(); } }, child: _buildBlogPostCreate(), @@ -141,13 +148,23 @@ class _BlogPostCreateScreenState extends State { 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 { 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 { }, 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 { ), ), ), + 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), + ), + ), + ), ], ), ); diff --git a/lib/features/blog_post_create/repositories/blog_post_repository.dart b/lib/features/blog_post_create/repositories/blog_post_repository.dart index 1f6331a0..d7247be4 100644 --- a/lib/features/blog_post_create/repositories/blog_post_repository.dart +++ b/lib/features/blog_post_create/repositories/blog_post_repository.dart @@ -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 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 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 deleteBlogPost(int id) async => await blogPostService.deleteBlogPost(id: id); } diff --git a/lib/features/blog_post_create/services/blog_post_service.dart b/lib/features/blog_post_create/services/blog_post_service.dart index 8d09a84e..1d0934d5 100644 --- a/lib/features/blog_post_create/services/blog_post_service.dart +++ b/lib/features/blog_post_create/services/blog_post_service.dart @@ -12,4 +12,16 @@ abstract class BlogPostService { Future createBlogPost({ @Body() required FormData formData, }); + + @POST('/api/blogPosts/update/{id}') + @MultiPart() + Future updateBlogPost({ + @Path('id') required int id, + @Body() required FormData formData, + }); + + @POST('/api/blogPosts/delete/{id}') + Future deleteBlogPost({ + @Path('id') required int id, + }); } diff --git a/lib/features/blog_post_create/services/blog_post_service.g.dart b/lib/features/blog_post_create/services/blog_post_service.g.dart index de8f722f..33f4251a 100644 --- a/lib/features/blog_post_create/services/blog_post_service.g.dart +++ b/lib/features/blog_post_create/services/blog_post_service.g.dart @@ -47,6 +47,60 @@ class _BlogPostService implements BlogPostService { await _dio.fetch(_options); } + @override + Future updateBlogPost({ + required int id, + required FormData formData, + }) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = formData; + final _options = _setStreamType(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(_options); + } + + @override + Future deleteBlogPost({required int id}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(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(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 949241af..1ccd21e6 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -14,6 +14,7 @@ class CreateDietCubit extends Cubit { 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) { @@ -236,7 +237,13 @@ class CreateDietCubit extends Cubit { ), 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, diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 83df5eb0..e5839487 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -9,7 +9,7 @@ class FeedCubit extends Cubit { bool isLoading = false; String? _query; - FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); + FeedCubit({required this.feedRepository,}) : super(const FeedState.loading()); loadFeedBlogPostList({ String? query, diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index f86430ef..cbaa0463 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -34,7 +34,7 @@ class FeedScreen extends StatefulWidget { } class _FeedScreenState extends State { - final FeedCubit feedCubit = sl()..loadFeedBlogPostList(); + late final FeedCubit feedCubit; final RecipesCubit recipesCubit = sl()..loadRecipesList(); final DietsCubit dietsCubit = sl()..loadDiets(); final PlansCubit plansCubit = sl()..loadPlans(); @@ -49,6 +49,8 @@ class _FeedScreenState extends State { @override void initState() { + feedCubit = context.read(); + feedCubit.loadFeedBlogPostList(); blogPostsScrollController.addListener(_onScrollBlogPosts); recipesScrollController.addListener(_onScrollRecipes); dietsScrollController.addListener(_onScrollDiets); @@ -444,7 +446,7 @@ class _FeedScreenState extends State { GoRouter.of(context).pushNamed(CreatePlanScreen.name); break; default: - GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedCubit); + GoRouter.of(context).pushNamed('blogPostCreateScreen'); } }, borderRadius: BorderRadius.circular(64), @@ -469,6 +471,7 @@ class _FeedScreenState extends State { width: MediaQuery.of(context).size.width - 84, child: TextFormField( controller: searchController, + autofocus: false, style: TextStyle( fontSize: 12, ), diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 8d547f95..ee2d66aa 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -18,7 +18,10 @@ import 'package:share_plus/share_plus.dart'; class BlogPostWidget extends StatefulWidget { final FeedBlogPostModel feedBlogPostModel; - const BlogPostWidget({super.key, required this.feedBlogPostModel}); + const BlogPostWidget({ + super.key, + required this.feedBlogPostModel, + }); @override State createState() => _BlogPostWidgetState(); diff --git a/lib/main.dart b/lib/main.dart index d32df6a1..c166a0cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/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'; @@ -29,6 +30,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), ], child: Portal( child: KeyboardDismisser( diff --git a/pubspec.yaml b/pubspec.yaml index 6a455e12..44dc695b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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