From 3761f25df46012f6a6e69107457af7c3fd3681ce Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 19 Aug 2024 17:41:09 +0300 Subject: [PATCH] add likes --- .../presentation/pages/blog_post_screen.dart | 35 ++++++++++++- .../chat_select_screen.dart | 3 ++ .../widgets/chat_entry_widget.dart | 2 + .../feed/models/feed_blog_post_model.dart | 10 ++-- .../models/feed_blog_post_model.freezed.dart | 39 ++++++++++++--- .../feed/models/feed_blog_post_model.g.dart | 2 + .../bloc/feed_cubit/feed_cubit.dart | 14 ++++-- .../widgets/blog_post_widget.dart | 37 ++++++++++++-- .../feed/repositories/feed_repository.dart | 16 ++++++ lib/features/feed/services/feed_service.dart | 12 ++++- .../feed/services/feed_service.g.dart | 50 +++++++++++++++++++ pubspec.yaml | 16 +++--- 12 files changed, 202 insertions(+), 34 deletions(-) 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 14281d47..183a6558 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -53,6 +53,9 @@ class _BlogPostScreenState extends State { SizedBox( width: mediaQueryData.size.width - 64, child: TextFormField( + onChanged: (_) { + setState(() {}); + }, controller: textEditingController, maxLines: 5, minLines: 1, @@ -109,11 +112,12 @@ class _BlogPostScreenState extends State { setState(() { textEditingController.clear(); }); + FocusManager.instance.primaryFocus?.unfocus(); } }, child: Container( decoration: BoxDecoration( - color: appTheme.blueA400, + color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle, ), height: 32, @@ -177,7 +181,7 @@ class _BlogPostScreenState extends State { ), ), ), - SizedBox(height: 12), + if (widget.blogPost.description.isNotEmpty) SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Align( @@ -319,6 +323,33 @@ class _BlogPostScreenState extends State { color: Colors.black, ), ), + SizedBox(height: 4), + Row( + children: [ + Text( + widget.blogPost.createdAt.format('dd MMMM', 'ru'), + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + fontWeight: FontWeight.w400, + height: 1, + ), + ), + SizedBox(width: 16), + GestureDetector( + onTap: () {}, + child: Text( + 'Ответить', + style: TextStyle( + height: 1, + fontSize: 12, + color: Colors.grey.shade800, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), ], ), ], diff --git a/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart b/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart index 9fea543a..a47c1cb8 100644 --- a/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart +++ b/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart @@ -47,6 +47,9 @@ class ChatSelectScreen extends StatelessWidget { title: Padding( padding: const EdgeInsets.only(top: 8), child: CustomSearchView( + onChanged: (query) { + + }, controller: state.searchController, fillColor: Color(0xFFF0F2F5), hintText: "Поиск", diff --git a/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart index 03c8371f..a8bd0f79 100644 --- a/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart +++ b/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart @@ -81,6 +81,8 @@ class ChatEntryWidget extends StatelessWidget { SizedBox(height: 6.v), Text( chatEntryObj.greetingText!.trim(), + maxLines: 2, + overflow: TextOverflow.ellipsis, style: CustomTextStyles.bodyMediumBluegray800, ), ], diff --git a/lib/features/feed/models/feed_blog_post_model.dart b/lib/features/feed/models/feed_blog_post_model.dart index 43faed61..f904032d 100644 --- a/lib/features/feed/models/feed_blog_post_model.dart +++ b/lib/features/feed/models/feed_blog_post_model.dart @@ -16,12 +16,10 @@ class FeedBlogPostModel with _$FeedBlogPostModel { @JsonKey(name: "created_at") required DateTime createdAt, @JsonKey(name: "updated_at") required DateTime updatedAt, required String title, - @JsonKey(name: "total_likes") - required int totalLikes, - @JsonKey(name: "total_views") - required int totalViews, - @JsonKey(name: "total_comments") - required int totalComments, + @JsonKey(name: "total_likes") required int totalLikes, + @JsonKey(name: "total_views") required int totalViews, + @JsonKey(name: "total_comments") required int totalComments, + @JsonKey(name: "was_liked_by_me") required bool wasLikedByMe, }) = _FeedBlogPostModel; factory FeedBlogPostModel.fromJson(Map json) => _$FeedBlogPostModelFromJson(json); diff --git a/lib/features/feed/models/feed_blog_post_model.freezed.dart b/lib/features/feed/models/feed_blog_post_model.freezed.dart index b6fb79ca..08d5527a 100644 --- a/lib/features/feed/models/feed_blog_post_model.freezed.dart +++ b/lib/features/feed/models/feed_blog_post_model.freezed.dart @@ -38,6 +38,8 @@ mixin _$FeedBlogPostModel { int get totalViews => throw _privateConstructorUsedError; @JsonKey(name: "total_comments") int get totalComments => throw _privateConstructorUsedError; + @JsonKey(name: "was_liked_by_me") + bool get wasLikedByMe => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -63,7 +65,8 @@ abstract class $FeedBlogPostModelCopyWith<$Res> { String title, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, - @JsonKey(name: "total_comments") int totalComments}); + @JsonKey(name: "total_comments") int totalComments, + @JsonKey(name: "was_liked_by_me") bool wasLikedByMe}); $AuthorCopyWith<$Res> get author; } @@ -93,6 +96,7 @@ class _$FeedBlogPostModelCopyWithImpl<$Res, $Val extends FeedBlogPostModel> Object? totalLikes = null, Object? totalViews = null, Object? totalComments = null, + Object? wasLikedByMe = null, }) { return _then(_value.copyWith( id: null == id @@ -143,6 +147,10 @@ class _$FeedBlogPostModelCopyWithImpl<$Res, $Val extends FeedBlogPostModel> ? _value.totalComments : totalComments // ignore: cast_nullable_to_non_nullable as int, + wasLikedByMe: null == wasLikedByMe + ? _value.wasLikedByMe + : wasLikedByMe // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -175,7 +183,8 @@ abstract class _$$FeedBlogPostModelImplCopyWith<$Res> String title, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, - @JsonKey(name: "total_comments") int totalComments}); + @JsonKey(name: "total_comments") int totalComments, + @JsonKey(name: "was_liked_by_me") bool wasLikedByMe}); @override $AuthorCopyWith<$Res> get author; @@ -204,6 +213,7 @@ class __$$FeedBlogPostModelImplCopyWithImpl<$Res> Object? totalLikes = null, Object? totalViews = null, Object? totalComments = null, + Object? wasLikedByMe = null, }) { return _then(_$FeedBlogPostModelImpl( id: null == id @@ -254,6 +264,10 @@ class __$$FeedBlogPostModelImplCopyWithImpl<$Res> ? _value.totalComments : totalComments // ignore: cast_nullable_to_non_nullable as int, + wasLikedByMe: null == wasLikedByMe + ? _value.wasLikedByMe + : wasLikedByMe // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -273,7 +287,8 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel { required this.title, @JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_views") required this.totalViews, - @JsonKey(name: "total_comments") required this.totalComments}); + @JsonKey(name: "total_comments") required this.totalComments, + @JsonKey(name: "was_liked_by_me") required this.wasLikedByMe}); factory _$FeedBlogPostModelImpl.fromJson(Map json) => _$$FeedBlogPostModelImplFromJson(json); @@ -308,10 +323,13 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel { @override @JsonKey(name: "total_comments") final int totalComments; + @override + @JsonKey(name: "was_liked_by_me") + final bool wasLikedByMe; @override String toString() { - return 'FeedBlogPostModel(id: $id, state: $state, image: $image, description: $description, author: $author, youtubeUrl: $youtubeUrl, createdAt: $createdAt, updatedAt: $updatedAt, title: $title, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments)'; + return 'FeedBlogPostModel(id: $id, state: $state, image: $image, description: $description, author: $author, youtubeUrl: $youtubeUrl, createdAt: $createdAt, updatedAt: $updatedAt, title: $title, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, wasLikedByMe: $wasLikedByMe)'; } @override @@ -337,7 +355,9 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel { (identical(other.totalViews, totalViews) || other.totalViews == totalViews) && (identical(other.totalComments, totalComments) || - other.totalComments == totalComments)); + other.totalComments == totalComments) && + (identical(other.wasLikedByMe, wasLikedByMe) || + other.wasLikedByMe == wasLikedByMe)); } @JsonKey(ignore: true) @@ -355,7 +375,8 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel { title, totalLikes, totalViews, - totalComments); + totalComments, + wasLikedByMe); @JsonKey(ignore: true) @override @@ -385,7 +406,8 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel { required final String title, @JsonKey(name: "total_likes") required final int totalLikes, @JsonKey(name: "total_views") required final int totalViews, - @JsonKey(name: "total_comments") required final int totalComments}) = + @JsonKey(name: "total_comments") required final int totalComments, + @JsonKey(name: "was_liked_by_me") required final bool wasLikedByMe}) = _$FeedBlogPostModelImpl; factory _FeedBlogPostModel.fromJson(Map json) = @@ -422,6 +444,9 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel { @JsonKey(name: "total_comments") int get totalComments; @override + @JsonKey(name: "was_liked_by_me") + bool get wasLikedByMe; + @override @JsonKey(ignore: true) _$$FeedBlogPostModelImplCopyWith<_$FeedBlogPostModelImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/features/feed/models/feed_blog_post_model.g.dart b/lib/features/feed/models/feed_blog_post_model.g.dart index 54557324..fadf4302 100644 --- a/lib/features/feed/models/feed_blog_post_model.g.dart +++ b/lib/features/feed/models/feed_blog_post_model.g.dart @@ -21,6 +21,7 @@ _$FeedBlogPostModelImpl _$$FeedBlogPostModelImplFromJson( totalLikes: (json['total_likes'] as num).toInt(), totalViews: (json['total_views'] as num).toInt(), totalComments: (json['total_comments'] as num).toInt(), + wasLikedByMe: json['was_liked_by_me'] as bool, ); Map _$$FeedBlogPostModelImplToJson( @@ -38,4 +39,5 @@ Map _$$FeedBlogPostModelImplToJson( 'total_likes': instance.totalLikes, 'total_views': instance.totalViews, 'total_comments': instance.totalComments, + 'was_liked_by_me': instance.wasLikedByMe, }; 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 904282c5..0de3af57 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -4,8 +4,8 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; class FeedCubit extends Cubit { final FeedRepository feedRepository; - int currentPage = 1; - final int lastPage = 7; + int _currentPage = 1; + final int _lastPage = 7; bool isLoading = false; FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); @@ -27,9 +27,9 @@ class FeedCubit extends Cubit { loadNextPage() async { print('start'); try { - if (lastPage < currentPage) { - currentPage++; - final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage); + if (_lastPage < _currentPage) { + _currentPage++; + final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: _currentPage); emit( FeedState.loaded( feedBlogPostList: [ @@ -47,4 +47,8 @@ class FeedCubit extends Cubit { print('end'); } } + + likePost(int id) async => await feedRepository.likeBlogPost(id); + + unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id); } diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index c75e6056..290560a6 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -3,6 +3,7 @@ import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/app_export.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:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:go_router/go_router.dart'; @@ -18,6 +19,16 @@ class BlogPostWidget extends StatefulWidget { } class _BlogPostWidgetState extends State { + 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( @@ -106,7 +117,23 @@ class _BlogPostWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ - _iconButton(icon: Icons.favorite_border_rounded, text: widget.feedBlogPostModel.totalLikes.toString()), + _iconButton( + icon: 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().likePost(widget.feedBlogPostModel.id) + : await context.read().unlikedPost(widget.feedBlogPostModel.id); + await context.read().loadFeedBlogPostList(); + }, + ), SizedBox(width: 8), _iconButton( icon: Icons.mode_comment_outlined, @@ -150,14 +177,14 @@ class _BlogPostWidgetState extends State { ); } - _iconButton({required IconData icon, String? text, Function()? onTap}) { + _iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) { return InkWell( onTap: onTap ?? () {}, borderRadius: BorderRadius.circular(16), child: Ink( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( - color: appTheme.gray100, + color: backgroundColor ?? appTheme.gray100, borderRadius: BorderRadius.circular(16), ), height: 32, @@ -167,14 +194,14 @@ class _BlogPostWidgetState extends State { Icon( icon, size: 20, - color: appTheme.gray600, + color: iconColor ?? appTheme.gray600, ), if (text != null) SizedBox(width: 8), if (text != null) Text( text, style: TextStyle( - color: appTheme.gray600, + color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500, ), ), diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 3bbaeadd..01d0dedb 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -56,4 +56,20 @@ class FeedRepository { rethrow; } } + + Future likeBlogPost(int id) async { + try { + await feedService.likeBlogPost(id: id); + } catch (e) { + rethrow; + } + } + + Future unlikedBlogPost(int id) async { + try { + await feedService.unlikedBlogPost(id: id); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 0e3248a5..19d459cf 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -26,11 +26,21 @@ abstract class FeedService { Future getBlogPostComments({ @Path() required int id, }); - + @POST('/api/blogPosts/{id}/comment/create') Future sendBlogPostComment({ @Path() required int id, @Part() required String body, @Part() int? parentCommentId, }); + + @POST('/api/blogPosts/{id}/like') + Future likeBlogPost({ + @Path() required int id, + }); + + @POST('/api/blogPosts/{id}/unliked') + Future unlikedBlogPost({ + @Path() required int id, + }); } diff --git a/lib/features/feed/services/feed_service.g.dart b/lib/features/feed/services/feed_service.g.dart index bb7171f5..f401da59 100644 --- a/lib/features/feed/services/feed_service.g.dart +++ b/lib/features/feed/services/feed_service.g.dart @@ -193,6 +193,56 @@ class _FeedService implements FeedService { await _dio.fetch(_options); } + @override + Future likeBlogPost({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/${id}/like', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + await _dio.fetch(_options); + } + + @override + Future unlikedBlogPost({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/${id}/unliked', + 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/pubspec.yaml b/pubspec.yaml index 33058e55..859b095d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,26 +17,26 @@ dependencies: shared_preferences: ^2.3.2 hive: ^2.2.3 hive_flutter: ^1.1.0 - cached_network_image: ^3.3.1 + cached_network_image: ^3.4.1 flutter_svg: ^2.0.10 equatable: ^2.0.5 - freezed_annotation: ^2.4.2 + freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 validators: ^3.0.0 isar_flutter_libs: ^3.1.0 - path_provider: ^2.1.3 + path_provider: ^2.1.4 fl_chart: ^0.68.0 flutter_spinkit: ^5.2.1 carousel_slider_plus: ^7.0.0 flutter_form_builder: ^9.3.0 image_picker: ^1.1.2 flutter_typeahead: ^5.2.0 - go_router: ^14.2.0 + go_router: ^14.2.3 keyboard_dismisser: ^3.0.0 - retrofit: ^4.1.0 + retrofit: ^4.2.0 get_it: ^7.7.0 - dio: ^5.5.0 - dart_pusher_channels: ^1.2.2+1 + dio: ^5.6.0 + dart_pusher_channels: ^1.2.3 photo_view: ^0.15.0 dismissible_page: ^1.0.2 url_launcher: ^6.3.0 @@ -56,7 +56,7 @@ dev_dependencies: build_runner: ^2.4.11 freezed: ^2.5.2 json_serializable: ^6.8.0 - retrofit_generator: ^8.1.1 + retrofit_generator: ^8.2.0 flutter_test: sdk: flutter