feat(feed): edit and delete posts

This commit is contained in:
Alex
2024-12-02 14:50:11 +03:00
parent 61ede95609
commit 2b3ecf0bf5
14 changed files with 257 additions and 29 deletions

View File

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

View File

@@ -60,11 +60,11 @@ Future<void> init() async {
sl.registerLazySingleton<InitializationCubit>(() => InitializationCubit(chatRepository: sl()));
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit());
sl.registerLazySingleton<FeedCubit>(() => FeedCubit(feedRepository: 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()));

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),
@@ -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

@@ -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) {
@@ -236,7 +237,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

@@ -9,7 +9,7 @@ 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());
loadFeedBlogPostList({
String? query,

View File

@@ -34,7 +34,7 @@ class FeedScreen extends StatefulWidget {
}
class _FeedScreenState extends State<FeedScreen> {
final FeedCubit feedCubit = sl<FeedCubit>()..loadFeedBlogPostList();
late final FeedCubit feedCubit;
final RecipesCubit recipesCubit = sl<RecipesCubit>()..loadRecipesList();
final DietsCubit dietsCubit = sl<DietsCubit>()..loadDiets();
final PlansCubit plansCubit = sl<PlansCubit>()..loadPlans();
@@ -49,6 +49,8 @@ class _FeedScreenState extends State<FeedScreen> {
@override
void initState() {
feedCubit = context.read<FeedCubit>();
feedCubit.loadFeedBlogPostList();
blogPostsScrollController.addListener(_onScrollBlogPosts);
recipesScrollController.addListener(_onScrollRecipes);
dietsScrollController.addListener(_onScrollDiets);
@@ -444,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),
@@ -469,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

@@ -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<BlogPostWidget> createState() => _BlogPostWidgetState();

View File

@@ -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<InitializationCubit>(create: (context) => sl<InitializationCubit>()),
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
],
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