refactor: scroll

This commit is contained in:
mso7nueh
2024-09-09 12:58:25 +03:00
parent f9c10047b2
commit 3ccf5b6d92
31 changed files with 610 additions and 608 deletions

67
assets/icons/filter.svg Normal file
View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Creator: CorelDRAW X8 -->
<svg
xml:space="preserve"
width="500px"
height="500px"
version="1.1"
style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 5000 5000"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<style
type="text/css"
id="style1">
<![CDATA[
.fil0 {fill:#43A6F2;fill-rule:nonzero}
]]>
</style>
</defs>
<g
id="Слой_x0020_1">
<metadata
id="CorelCorpID_0Corel-Layer" />
<g
id="g3"><path
class="fil0"
d="m 1560.7457,700.42366 c 61.6477,182.17783 43.4185,372.49804 -35.5334,532.21314 -78.7129,160.4212 -219.0937,289.7702 -401.2715,351.4179 -62.8442,21.2661 -125.73139,33.1029 -188.42249,36.2168 l -257.58342,-35.6034 c -29.27533,-9.7676 -58.56148,-21.8927 -86.20715,-36.1468 C 431.30648,1469.8083 301.9575,1329.4275 240.3098,1147.2497 178.66221,965.07202 196.18518,774.99073 275.84324,615.03669 354.79505,455.32156 494.9369,325.26645 677.11471,263.61876 c 182.1778,-61.64759 372.49799,-43.41849 532.21309,35.53343 159.7151,78.95191 289.7702,219.09376 351.4179,401.27147 z m -268.9631,416.82184 c 48.814,-99.9374 60.5318,-218.8006 21.8228,-333.19127 C 1275.1354,670.36962 1193.4069,582.341 1093.9367,532.58196 993.99911,483.76798 874.42991,472.28915 760.7452,510.75919 646.35454,549.46814 559.03194,630.95776 509.27291,730.42789 c -49.52012,100.1764 -60.29281,219.50675 -21.82277,333.19131 38.70906,114.3907 119.49243,201.9522 219.6688,251.4723 100.17629,49.5201 218.80067,60.5317 333.19136,21.8228 113.6846,-38.4702 201.9521,-119.4924 251.4723,-219.6688 z"
id="path1-2-61"
style="clip-rule:evenodd;fill:#43a6f2;fill-rule:nonzero;stroke-width:0.745448;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" /><rect
style="fill:#43a6f2;fill-opacity:1;stroke:#000000;stroke-width:25.7141;stroke-opacity:0"
id="rect3"
width="3217.4019"
height="281.70447"
x="1668.475"
y="853.69214" /></g><g
id="g3-3"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
transform="matrix(-1.039655,0,0,1.039655,5289.2522,1773.6118)"><path
class="fil0"
d="m 1675.5155,461.38571 c 59.2963,175.22913 41.7624,358.29003 -34.1781,511.91323 -75.7106,154.30236 -210.7369,278.71766 -385.966,338.01396 -60.4472,20.4549 -120.9357,31.8403 -181.2356,34.8354 l -247.7586,-34.2454 c -28.1587,-9.3951 -56.3278,-21.0577 -82.919,-34.7681 -154.3024,-75.7107 -278.7177,-210.737 -338.014,-385.96606 -59.2962,-175.229 -42.4416,-358.06013 34.1781,-511.91313 75.9404,-153.6232 210.7369,-278.7177 385.966,-338.013995 175.2291,-59.2962 358.29,-41.76240041 511.9132,34.1781 153.6232,75.940495 278.7177,210.736995 338.014,385.965995 z m -258.7042,400.92323 c 46.9521,-96.1255 58.2229,-210.455 20.9904,-320.48253 -37.0027,-109.3484 -115.6138,-194.0194 -211.29,-241.8805 -96.1257,-46.9521 -211.1343,-57.9931 -320.48278,-20.9904 -110.02752,37.2325 -194.01942,115.6139 -241.88052,211.29 -47.6313,96.35543 -57.9931,211.13423 -20.9904,320.48263 37.2326,110.0275 114.9347,194.24916 211.2901,241.88046 96.35532,47.6313 210.4551,58.2229 320.4826,20.9904 109.3484,-37.0028 194.2492,-114.93466 241.8806,-211.29006 z"
id="path1-2-61-6"
style="clip-rule:evenodd;fill:#43a6f2;fill-rule:nonzero;stroke-width:0.717015;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" /><rect
style="fill:#43a6f2;fill-opacity:1;stroke:#000000;stroke-width:24.7333;stroke-opacity:0"
id="rect3-1"
width="3094.6824"
height="270.95956"
x="1779.1356"
y="608.80817" /></g><g
id="g3-1"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
transform="matrix(1.039655,0,0,1.039655,-155.31078,3382.0033)"><path
class="fil0"
d="m 1675.5155,461.38571 c 59.2963,175.22913 41.7624,358.29003 -34.1781,511.91323 -75.7106,154.30236 -210.7369,278.71766 -385.966,338.01396 -60.4472,20.4549 -120.9357,31.8403 -181.2356,34.8354 l -247.7586,-34.2454 c -28.1587,-9.3951 -56.3278,-21.0577 -82.919,-34.7681 -154.3024,-75.7107 -278.7177,-210.737 -338.014,-385.96606 -59.2962,-175.229 -42.4416,-358.06013 34.1781,-511.91313 75.9404,-153.6232 210.7369,-278.7177 385.966,-338.013995 175.2291,-59.2962 358.29,-41.76240041 511.9132,34.1781 153.6232,75.940495 278.7177,210.736995 338.014,385.965995 z m -258.7042,400.92323 c 46.9521,-96.1255 58.2229,-210.455 20.9904,-320.48253 -37.0027,-109.3484 -115.6138,-194.0194 -211.29,-241.8805 -96.1257,-46.9521 -211.1343,-57.9931 -320.48278,-20.9904 -110.02752,37.2325 -194.01942,115.6139 -241.88052,211.29 -47.6313,96.35543 -57.9931,211.13423 -20.9904,320.48263 37.2326,110.0275 114.9347,194.24916 211.2901,241.88046 96.35532,47.6313 210.4551,58.2229 320.4826,20.9904 109.3484,-37.0028 194.2492,-114.93466 241.8806,-211.29006 z"
id="path1-2-61-9"
style="clip-rule:evenodd;fill:#43a6f2;fill-rule:nonzero;stroke-width:0.717015;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" /><rect
style="fill:#43a6f2;fill-opacity:1;stroke:#000000;stroke-width:24.7333;stroke-opacity:0"
id="rect3-4"
width="3094.6824"
height="270.95956"
x="1779.1356"
y="608.80817" /></g></g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1,22 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Creator: CorelDRAW X8 -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="500px" height="500px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 5000 5000"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<svg
xml:space="preserve"
width="500px"
height="500px"
version="1.1"
style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 5000 5000"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<style
type="text/css"
id="style1">
<![CDATA[
.fil1 {fill:none}
.fil0 {fill:#43A6F2;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_3124155549248">
<path class="fil0" d="M2291 4109c29,26 61,46 97,60 35,14 73,21 112,21 39,0 77,-7 112,-21 37,-15 71,-36 100,-63l3 -2 0 0 1542 -1372c126,-112 225,-238 292,-379 66,-140 101,-298 101,-475 0,-297 -112,-566 -293,-760 -178,-190 -424,-308 -694,-308 -271,0 -517,118 -695,308 -181,194 -293,463 -293,760 0,97 -78,175 -175,175 -97,0 -175,-78 -175,-175 0,-297 -112,-566 -293,-760 -178,-190 -424,-308 -695,-308 -270,0 -516,118 -694,308 -181,194 -293,463 -293,760 0,177 35,335 101,475 67,141 166,267 292,379l1542 1372 6 5zm-32 385c-74,-29 -142,-72 -202,-126l-1546 -1375c-161,-143 -288,-306 -375,-491 -89,-187 -136,-394 -136,-624 0,-389 148,-742 388,-998 242,-260 578,-420 949,-420 372,0 708,160 950,420 83,88 154,188 213,297 59,-109 130,-209 213,-297 242,-260 578,-420 950,-420 371,0 707,160 949,420 240,256 388,609 388,998 0,230 -47,437 -136,624 -87,185 -214,348 -375,491l-1542 1372 0 0c-61,55 -131,99 -206,129 -75,30 -157,46 -241,46 -84,0 -166,-16 -241,-46z"/>
<rect class="fil1" width="5000" height="5000"/>
<g
id="Слой_x0020_1">
<metadata
id="CorelCorpID_0Corel-Layer" />
<g
id="_3124155549248">
<path
class="fil0"
d="M2291 4109c29,26 61,46 97,60 35,14 73,21 112,21 39,0 77,-7 112,-21 37,-15 71,-36 100,-63l3 -2 0 0 1542 -1372c126,-112 225,-238 292,-379 66,-140 101,-298 101,-475 0,-297 -112,-566 -293,-760 -178,-190 -424,-308 -694,-308 -271,0 -517,118 -695,308 -181,194 -293,463 -293,760 0,97 -78,175 -175,175 -97,0 -175,-78 -175,-175 0,-297 -112,-566 -293,-760 -178,-190 -424,-308 -695,-308 -270,0 -516,118 -694,308 -181,194 -293,463 -293,760 0,177 35,335 101,475 67,141 166,267 292,379l1542 1372 6 5zm-32 385c-74,-29 -142,-72 -202,-126l-1546 -1375c-161,-143 -288,-306 -375,-491 -89,-187 -136,-394 -136,-624 0,-389 148,-742 388,-998 242,-260 578,-420 949,-420 372,0 708,160 950,420 83,88 154,188 213,297 59,-109 130,-209 213,-297 242,-260 578,-420 950,-420 371,0 707,160 949,420 240,256 388,609 388,998 0,230 -47,437 -136,624 -87,185 -214,348 -375,491l-1542 1372 0 0c-61,55 -131,99 -206,129 -75,30 -157,46 -241,46 -84,0 -166,-16 -241,-46z"
id="path1" />
<rect
class="fil1"
width="5000"
height="5000"
id="rect1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,22 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Creator: CorelDRAW X8 -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="500px" height="500px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 5000 5000"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<svg
xml:space="preserve"
width="500px"
height="500px"
version="1.1"
style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 5000 5000"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<style
type="text/css"
id="style1">
<![CDATA[
.fil1 {fill:none}
.fil0 {fill:#43A6F2}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_3124216520000">
<path class="fil0" d="M2259 4494c-74,-29 -142,-72 -202,-126l-1546 -1375c-161,-143 -288,-306 -375,-491 -89,-187 -136,-394 -136,-624 0,-389 148,-742 388,-998 242,-260 578,-420 949,-420 372,0 708,160 950,420 83,88 155,188 213,297 59,-109 130,-209 213,-297 242,-260 578,-420 950,-420 371,0 707,160 949,420 240,256 388,609 388,998 0,230 -47,437 -136,624 -87,185 -214,348 -375,491l-1542 1372 0 0c-61,55 -131,99 -206,129 -75,30 -157,46 -241,46 -84,0 -166,-16 -241,-46z"/>
<rect class="fil1" width="5000" height="5000"/>
<g
id="Слой_x0020_1">
<metadata
id="CorelCorpID_0Corel-Layer" />
<g
id="_3124216520000">
<path
class="fil0"
d="M2259 4494c-74,-29 -142,-72 -202,-126l-1546 -1375c-161,-143 -288,-306 -375,-491 -89,-187 -136,-394 -136,-624 0,-389 148,-742 388,-998 242,-260 578,-420 949,-420 372,0 708,160 950,420 83,88 155,188 213,297 59,-109 130,-209 213,-297 242,-260 578,-420 950,-420 371,0 707,160 949,420 240,256 388,609 388,998 0,230 -47,437 -136,624 -87,185 -214,348 -375,491l-1542 1372 0 0c-61,55 -131,99 -206,129 -75,30 -157,46 -241,46 -84,0 -166,-16 -241,-46z"
id="path1" />
<rect
class="fil1"
width="5000"
height="5000"
id="rect1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -2,11 +2,11 @@ import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/models/comment_model.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';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/locator_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -31,7 +31,6 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
@override
void initState() {
commentsCubit.getBlogPostComments(widget.blogPost.id);
isLiked = widget.blogPost.wasLikedByMe;
totalLikes = widget.blogPost.totalLikes;
super.initState();
@@ -40,8 +39,20 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit,
child: BlocBuilder<CommentsCubit, CommentsState>(
create: (BuildContext context) => commentsCubit
..getComments(
widget.blogPost.id.toString(),
CommentSourceType.blogPost,
true,
),
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
return Scaffold(
resizeToAvoidBottomInset: true,
@@ -61,6 +72,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
@@ -83,7 +95,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
if (replyId != null) SizedBox(width: 8),
SizedBox(
width: mediaQueryData.size.width - (replyId == null ? 64 : 104),
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: TextFormField(
onChanged: (_) {
setState(() {});
@@ -137,11 +149,13 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendBlogPostComment(
id: widget.blogPost.id,
commentsCubit.sendComment(
id: widget.blogPost.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
@@ -310,7 +324,13 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
padding: const EdgeInsets.all(32),
child: CupertinoActivityIndicator(),
),
if (state is CommentsStateLoaded) _buildComments(state.comment),
if (state is CommentsStateLoaded)
CommentsWidget(
id: widget.blogPost.id.toString(),
commentSourceType: CommentSourceType.blogPost,
comment: state.comment,
replyId: replyId,
),
],
),
),
@@ -320,102 +340,6 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
);
}
_buildComments(CommentModel comment) {
return Column(
children: [
for (int i = 0; i < comment.data.length; i++) ...[
_comment(comment.data[i]),
if (comment.data[i].childrenComments.isNotEmpty) SizedBox(height: 8),
for (int j = 0; j < comment.data[i].childrenComments.length; j++)
Padding(
padding: EdgeInsets.only(left: 40),
child: _comment(
comment.data[i].childrenComments[j],
12,
),
),
SizedBox(height: 8),
],
],
);
}
_comment(Comment comment, [double radius = 16]) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: replyId == comment.id ? appTheme.blueA400.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon),
radius: radius,
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4),
Text(
comment.body,
style: TextStyle(
height: 1,
color: Colors.black,
),
),
SizedBox(height: 4),
Row(
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1,
),
),
SizedBox(width: 16),
GestureDetector(
onTap: () {
setState(() {
replyId = comment.id;
});
},
child: Text(
'Ответить',
style: TextStyle(
height: 1,
fontSize: 12,
color: Colors.grey.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
],
),
),
);
}
_iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) {
return InkWell(
onTap: onTap ?? () {},

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
@@ -7,7 +8,6 @@ import 'package:eatvio_app/locator_service.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
class ChatScreen extends StatefulWidget {
final String chatId;
@@ -25,6 +25,24 @@ class _ChatScreenState extends State<ChatScreen> {
final scrollController = ScrollController();
final TextEditingController textEditingController = TextEditingController();
@override
void initState() {
scrollController.addListener(_onScroll);
super.initState();
}
_onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
context.read<ChatRoomCubit>().loadNextPage(widget.chatId);
}
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return BlocProvider<ChatRoomCubit>(
create: (BuildContext context) => sl()..initChat(widget.chatId),
@@ -116,40 +134,35 @@ class _ChatScreenState extends State<ChatScreen> {
child: Container(
color: Color(0xFFF0F2F5),
width: double.maxFinite,
child: LazyLoadScrollView(
onEndOfPage: () => context.read<ChatRoomCubit>().loadNextPage(widget.chatId),
isLoading: state.isLoading,
scrollOffset: MediaQuery.of(context).size.height.toInt(),
child: CustomScrollView(
physics: BouncingScrollPhysics(),
controller: scrollController,
reverse: true,
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index < state.chatMessages.length) {
return ChatBubbleWidget(
sendTime: state.chatMessages[index].updatedAt,
text: state.chatMessages[index].data.trim(),
isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId,
mediaData: state.chatMessages[index].mediaData ?? [],
);
} else {
return Padding(
padding: const EdgeInsets.all(8),
child: Center(child: CupertinoActivityIndicator()),
);
}
},
childCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
),
child: CustomScrollView(
physics: BouncingScrollPhysics(),
controller: scrollController,
reverse: true,
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index < state.chatMessages.length) {
return ChatBubbleWidget(
sendTime: state.chatMessages[index].updatedAt,
text: state.chatMessages[index].data.trim(),
isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId,
mediaData: state.chatMessages[index].mediaData ?? [],
);
} else {
return Padding(
padding: const EdgeInsets.all(8),
child: Center(child: CupertinoActivityIndicator()),
);
}
},
childCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
),
),
],
),
),
],
),
),
),

View File

@@ -1,11 +1,10 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/models/comment_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_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';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/locator_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -28,17 +27,23 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
int? replyId;
final textEditingController = TextEditingController();
@override
void initState() {
super.initState();
commentsCubit.getDietsComments(widget.diet.id);
}
@override
Widget build(BuildContext context) {
return BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit,
child: BlocBuilder<CommentsCubit, CommentsState>(
create: (BuildContext context) => commentsCubit
..getComments(
widget.diet.id.toString(),
CommentSourceType.diet,
true,
),
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
return Scaffold(
resizeToAvoidBottomInset: true,
@@ -80,6 +85,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
@@ -156,11 +162,13 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendDietsComment(
id: widget.diet.id,
commentsCubit.sendComment(
id: widget.diet.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.diet,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
@@ -187,25 +195,6 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
],
),
),
// floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
// floatingActionButton: SizedBox(
// width: MediaQuery.of(context).size.width - 32,
// child: FloatingActionButton.extended(
// heroTag: 'nextButtonRegistration',
// highlightElevation: 0,
// elevation: 0,
// focusElevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// backgroundColor: appTheme.greenA700,
// onPressed: () {},
// label: Text(
// 'Начать диету',
// style: theme.textTheme.titleSmall,
// ),
// ),
// ),
appBar: AppBar(
actions: [
InkWell(
@@ -340,7 +329,13 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
padding: const EdgeInsets.all(32),
child: CupertinoActivityIndicator(),
),
if (state is CommentsStateLoaded) _buildComments(state.comment),
if (state is CommentsStateLoaded)
CommentsWidget(
id: widget.diet.id.toString(),
commentSourceType: CommentSourceType.diet,
comment: state.comment,
replyId: replyId,
),
],
),
),
@@ -446,100 +441,4 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
],
);
}
_buildComments(CommentModel comment) {
return Column(
children: [
for (int i = 0; i < comment.data.length; i++) ...[
_comment(comment.data[i]),
if (comment.data[i].childrenComments.isNotEmpty)
for (int j = 0; j < comment.data[i].childrenComments.length; j++)
Padding(
padding: EdgeInsets.only(left: 40),
child: _comment(
comment.data[i].childrenComments[j],
12,
),
),
SizedBox(height: 8),
],
],
);
}
_comment(Comment diet, [double radius = 16]) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: replyId == diet.id ? appTheme.blueA400.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
foregroundImage: NetworkImage(diet.authorProfileIcon),
radius: radius,
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
diet.authorName,
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4),
Text(
diet.body,
style: TextStyle(
height: 1,
color: Colors.black,
),
),
SizedBox(height: 4),
Row(
children: [
Text(
diet.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1,
),
),
SizedBox(width: 16),
GestureDetector(
onTap: () {
setState(() {
replyId = diet.id;
});
},
child: Text(
'Ответить',
style: TextStyle(
height: 1,
fontSize: 12,
color: Colors.grey.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
],
),
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
class CommentsCubit extends Cubit<CommentsState> {
@@ -9,79 +10,60 @@ class CommentsCubit extends Cubit<CommentsState> {
required this.feedRepository,
}) : super(CommentsState());
getBlogPostComments(int id) async {
emit(CommentsState.loading());
setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
}
getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
if (isFirstFetch) emit(CommentsState.loading());
try {
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id)));
switch (commentSourceType) {
case CommentSourceType.blogPost:
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id)));
break;
case CommentSourceType.diet:
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id)));
break;
case CommentSourceType.recipe:
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id)));
break;
}
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
getRecipesComments(String id) async {
emit(CommentsState.loading());
try {
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
getDietsComments(int id) async {
emit(CommentsState.loading());
try {
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
sendBlogPostComment({
required int id,
required String commentText,
int? parentCommentId,
}) async {
try {
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
sendRecipesComment({
sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,
int? parentCommentId,
}) async {
try {
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id)));
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
sendDietsComment({
required int id,
required String commentText,
int? parentCommentId,
}) async {
try {
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id)));
switch (commentSourceType) {
case CommentSourceType.blogPost:
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.diet:
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.recipe:
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
}
getComments(id, commentSourceType);
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}

View File

@@ -11,6 +11,7 @@ abstract class CommentsState with _$CommentsState {
const factory CommentsState.loaded({
required CommentModel comment,
int? replyId,
}) = CommentsStateLoaded;
const factory CommentsState.error({

View File

@@ -20,7 +20,7 @@ mixin _$CommentsState {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) =>
throw _privateConstructorUsedError;
@@ -28,7 +28,7 @@ mixin _$CommentsState {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) =>
throw _privateConstructorUsedError;
@@ -36,7 +36,7 @@ mixin _$CommentsState {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) =>
@@ -126,7 +126,7 @@ class _$CommentsStateImpl implements _CommentsState {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return $default();
@@ -137,7 +137,7 @@ class _$CommentsStateImpl implements _CommentsState {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return $default?.call();
@@ -148,7 +148,7 @@ class _$CommentsStateImpl implements _CommentsState {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
@@ -241,7 +241,7 @@ class _$CommentsStateLoadingImpl implements CommentsStateLoading {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return loading();
@@ -252,7 +252,7 @@ class _$CommentsStateLoadingImpl implements CommentsStateLoading {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return loading?.call();
@@ -263,7 +263,7 @@ class _$CommentsStateLoadingImpl implements CommentsStateLoading {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
@@ -321,7 +321,7 @@ abstract class _$$CommentsStateLoadedImplCopyWith<$Res> {
$Res Function(_$CommentsStateLoadedImpl) then) =
__$$CommentsStateLoadedImplCopyWithImpl<$Res>;
@useResult
$Res call({CommentModel comment});
$Res call({CommentModel comment, int? replyId});
$CommentModelCopyWith<$Res> get comment;
}
@@ -338,12 +338,17 @@ class __$$CommentsStateLoadedImplCopyWithImpl<$Res>
@override
$Res call({
Object? comment = null,
Object? replyId = freezed,
}) {
return _then(_$CommentsStateLoadedImpl(
comment: null == comment
? _value.comment
: comment // ignore: cast_nullable_to_non_nullable
as CommentModel,
replyId: freezed == replyId
? _value.replyId
: replyId // ignore: cast_nullable_to_non_nullable
as int?,
));
}
@@ -359,14 +364,16 @@ class __$$CommentsStateLoadedImplCopyWithImpl<$Res>
/// @nodoc
class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
const _$CommentsStateLoadedImpl({required this.comment});
const _$CommentsStateLoadedImpl({required this.comment, this.replyId});
@override
final CommentModel comment;
@override
final int? replyId;
@override
String toString() {
return 'CommentsState.loaded(comment: $comment)';
return 'CommentsState.loaded(comment: $comment, replyId: $replyId)';
}
@override
@@ -374,11 +381,12 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CommentsStateLoadedImpl &&
(identical(other.comment, comment) || other.comment == comment));
(identical(other.comment, comment) || other.comment == comment) &&
(identical(other.replyId, replyId) || other.replyId == replyId));
}
@override
int get hashCode => Object.hash(runtimeType, comment);
int get hashCode => Object.hash(runtimeType, comment, replyId);
@JsonKey(ignore: true)
@override
@@ -392,10 +400,10 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return loaded(comment);
return loaded(comment, replyId);
}
@override
@@ -403,10 +411,10 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return loaded?.call(comment);
return loaded?.call(comment, replyId);
}
@override
@@ -414,12 +422,12 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(comment);
return loaded(comment, replyId);
}
return orElse();
}
@@ -463,10 +471,12 @@ class _$CommentsStateLoadedImpl implements CommentsStateLoaded {
}
abstract class CommentsStateLoaded implements CommentsState {
const factory CommentsStateLoaded({required final CommentModel comment}) =
_$CommentsStateLoadedImpl;
const factory CommentsStateLoaded(
{required final CommentModel comment,
final int? replyId}) = _$CommentsStateLoadedImpl;
CommentModel get comment;
int? get replyId;
@JsonKey(ignore: true)
_$$CommentsStateLoadedImplCopyWith<_$CommentsStateLoadedImpl> get copyWith =>
throw _privateConstructorUsedError;
@@ -540,7 +550,7 @@ class _$CommentsStateErrorImpl implements CommentsStateError {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(CommentModel comment) loaded,
required TResult Function(CommentModel comment, int? replyId) loaded,
required TResult Function(String errorMessage) error,
}) {
return error(errorMessage);
@@ -551,7 +561,7 @@ class _$CommentsStateErrorImpl implements CommentsStateError {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(CommentModel comment)? loaded,
TResult? Function(CommentModel comment, int? replyId)? loaded,
TResult? Function(String errorMessage)? error,
}) {
return error?.call(errorMessage);
@@ -562,7 +572,7 @@ class _$CommentsStateErrorImpl implements CommentsStateError {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(CommentModel comment)? loaded,
TResult Function(CommentModel comment, int? replyId)? loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {

View File

@@ -4,8 +4,8 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
class FeedCubit extends Cubit<FeedState> {
final FeedRepository feedRepository;
int _currentPage = 1;
final int _lastPage = 7;
int currentPage = 1;
late int lastPage;
bool isLoading = false;
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
@@ -13,6 +13,7 @@ class FeedCubit extends Cubit<FeedState> {
loadFeedBlogPostList() async {
try {
final feedBlogPosts = await feedRepository.getFeedBlogPosts();
lastPage = feedBlogPosts.meta.lastPage;
emit(
FeedState.loaded(
feedBlogPostList: feedBlogPosts.feedBlogPostList,
@@ -25,26 +26,29 @@ class FeedCubit extends Cubit<FeedState> {
}
loadNextPage() async {
print('start');
try {
if (_lastPage < _currentPage) {
_currentPage++;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: _currentPage);
emit(
FeedState.loaded(
feedBlogPostList: [
...(state as FeedStateLoaded).feedBlogPostList,
...feedBlogPosts.feedBlogPostList,
],
isLoading: false,
),
);
if (currentPage < lastPage && !isLoading) {
isLoading = true;
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
try {
if (currentPage < lastPage) {
currentPage++;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage);
isLoading = false;
emit(
FeedState.loaded(
feedBlogPostList: [
...(state as FeedStateLoaded).feedBlogPostList,
...feedBlogPosts.feedBlogPostList,
],
isLoading: isLoading,
),
);
}
} catch (e) {
emit(FeedState.error(errorMessage: e.toString()));
}
} catch (e) {
print('error bro');
print(e.toString());
} finally {
print('end');
}
}

View File

@@ -26,6 +26,27 @@ class _FeedScreenState extends State<FeedScreen> {
final searchController = TextEditingController();
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
int _selectedChipIndex = 0;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
final dietsScrollController = ScrollController();
@override
void initState() {
blogPostsScrollController.addListener(_onScroll);
super.initState();
}
_onScroll() {
if (blogPostsScrollController.offset >= blogPostsScrollController.position.maxScrollExtent && !blogPostsScrollController.position.outOfRange) {
context.read<FeedCubit>().loadNextPage();
}
}
@override
void dispose() {
blogPostsScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -68,6 +89,7 @@ class _FeedScreenState extends State<FeedScreen> {
),
backgroundColor: Colors.grey.shade200,
body: NestedScrollView(
controller: blogPostsScrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
@@ -108,47 +130,6 @@ class _FeedScreenState extends State<FeedScreen> {
);
}
_buildDiets() {
return RefreshIndicator(
onRefresh: () => context.read<DietsCubit>().loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state is DietsStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.diets.data[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.diets.data.length,
);
} else if (state is DietsStateError) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage),
),
),
);
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Center(
child: CupertinoActivityIndicator(),
),
);
}
},
),
);
}
_buildAllBlogPosts() {
return RefreshIndicator(
onRefresh: () => context.read<FeedCubit>().loadFeedBlogPostList(),
@@ -158,14 +139,17 @@ class _FeedScreenState extends State<FeedScreen> {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == state.feedBlogPostList.length) {
return CupertinoActivityIndicator();
}
return BlogPostWidget(feedBlogPostModel: state.feedBlogPostList[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.feedBlogPostList.length,
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
);
} else if (state is FeedStateError) {
return Scaffold(
@@ -231,6 +215,47 @@ class _FeedScreenState extends State<FeedScreen> {
);
}
_buildDiets() {
return RefreshIndicator(
onRefresh: () => context.read<DietsCubit>().loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state is DietsStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.diets.data[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.diets.data.length,
);
} else if (state is DietsStateError) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage),
),
),
);
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Center(
child: CupertinoActivityIndicator(),
),
);
}
},
),
);
}
_chipGroup() {
return PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),

View File

@@ -124,6 +124,8 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
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(),
),

View File

@@ -0,0 +1,119 @@
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/models/comment_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:flutter/material.dart';
enum CommentSourceType { blogPost, recipe, diet }
class CommentsWidget extends StatefulWidget {
final String id;
final CommentSourceType commentSourceType;
final CommentModel comment;
final int? replyId;
const CommentsWidget({
super.key,
required this.id,
required this.commentSourceType,
required this.comment,
this.replyId,
});
@override
State<CommentsWidget> createState() => _CommentsWidgetState();
}
class _CommentsWidgetState extends State<CommentsWidget> {
@override
Widget build(BuildContext context) {
return Column(
children: [
for (int i = 0; i < widget.comment.data.length; i++) ...[
_comment(widget.comment.data[i]),
if (widget.comment.data[i].childrenComments.isNotEmpty) SizedBox(height: 8),
for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++)
Padding(
padding: EdgeInsets.only(left: 40),
child: _comment(widget.comment.data[i].childrenComments[j], 12, true),
),
SizedBox(height: 8),
],
],
);
}
_comment(Comment comment, [double radius = 16, bool isChild = false]) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: widget.replyId == comment.id ? appTheme.blueA400.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon),
radius: radius,
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4),
Text(
comment.body,
style: TextStyle(
height: 1,
color: Colors.black,
),
),
SizedBox(height: 4),
Row(
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1,
),
),
SizedBox(width: 16),
if (!isChild)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(comment.id);
},
child: Text(
'Ответить',
style: TextStyle(
height: 1,
fontSize: 12,
color: Colors.grey.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
],
),
),
);
}
}

View File

@@ -34,6 +34,8 @@ class _DietsWidgetState extends State<DietsWidget> {
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + widget.diet.image,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
Positioned(

View File

@@ -34,6 +34,8 @@ class _RecipesWidgetState extends State<RecipesWidget> {
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + widget.recipes.image,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
Positioned(

View File

@@ -40,7 +40,7 @@ class FeedRepository {
}
}
Future<CommentModel> getBlogPostComments(int id) async {
Future<CommentModel> getBlogPostComments(String id) async {
try {
return (await feedService.getBlogPostComments(id: id));
} catch (e) {
@@ -49,7 +49,7 @@ class FeedRepository {
}
Future<void> sendBlogPostComment({
required int id,
required String id,
required String commentText,
int? parentCommentId,
}) async {
@@ -64,7 +64,7 @@ class FeedRepository {
}
}
Future<CommentModel> getDietsComments(int id) async {
Future<CommentModel> getDietsComments(String id) async {
try {
return (await feedService.getDietsComments(id: id));
} catch (e) {
@@ -73,7 +73,7 @@ class FeedRepository {
}
Future<void> sendDietsComment({
required int id,
required String id,
required String commentText,
int? parentCommentId,
}) async {

View File

@@ -27,24 +27,24 @@ abstract class FeedService {
@GET('/api/blogPosts/{id}/comments/show')
Future<CommentModel> getBlogPostComments({
@Path() required int id,
@Path() required String id,
});
@POST('/api/blogPosts/{id}/comment/create')
Future<void> sendBlogPostComment({
@Path() required int id,
@Path() required String id,
@Part() required String body,
@Part() int? parentCommentId,
});
@GET('/api/diets/{id}/comments/show')
Future<CommentModel> getDietsComments({
@Path() required int id,
@Path() required String id,
});
@POST('/api/diets/{id}/comment/create')
Future<void> sendDietsComment({
@Path() required int id,
@Path() required String id,
@Part() required String body,
@Part() int? parentCommentId,
});

View File

@@ -128,7 +128,7 @@ class _FeedService implements FeedService {
}
@override
Future<CommentModel> getBlogPostComments({required int id}) async {
Future<CommentModel> getBlogPostComments({required String id}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
@@ -162,7 +162,7 @@ class _FeedService implements FeedService {
@override
Future<void> sendBlogPostComment({
required int id,
required String id,
required String body,
int? parentCommentId,
}) async {
@@ -201,7 +201,7 @@ class _FeedService implements FeedService {
}
@override
Future<CommentModel> getDietsComments({required int id}) async {
Future<CommentModel> getDietsComments({required String id}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
@@ -235,7 +235,7 @@ class _FeedService implements FeedService {
@override
Future<void> sendDietsComment({
required int id,
required String id,
required String body,
int? parentCommentId,
}) async {

View File

@@ -10,6 +10,8 @@ import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_wi
import 'package:eatvio_app/locator_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class ProfileScreen extends StatefulWidget {
final Author? author;
@@ -118,13 +120,20 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
],
),
Container(
padding: EdgeInsets.only(left: 16),
child: SvgPicture.asset(
height: 24,
width: 24,
'assets/icons/share.svg',
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
GestureDetector(
onTap: () => Share.shareUri(
Uri.parse(
baseUrl + '/ru/profile/' + state.profile.user.slug,
),
),
child: Container(
padding: EdgeInsets.only(left: 16),
child: SvgPicture.asset(
height: 24,
width: 24,
'assets/icons/share.svg',
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
),
),
),
],
@@ -147,18 +156,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
Row(
children: [
SizedBox(width: 16),
Container(
alignment: Alignment.center,
padding: EdgeInsets.all(8),
width: (MediaQuery.of(context).size.width - 40) / 2,
decoration: BoxDecoration(
color: appTheme.blueA400.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Сообщение',
style: TextStyle(
color: appTheme.blueA400,
GestureDetector(
onTap: () => GoRouter.of(context).pushNamed('chatScreen', ),
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(8),
width: (MediaQuery.of(context).size.width - 40) / 2,
decoration: BoxDecoration(
color: appTheme.blueA400.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Сообщение',
style: TextStyle(
color: appTheme.blueA400,
),
),
),
),

View File

@@ -1,12 +1,11 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/feed/models/comment_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart' as step;
import 'package:eatvio_app/features/feed/models/recipes_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';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/locator_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -26,21 +25,24 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
final textEditingController = TextEditingController();
int? replyId;
@override
void initState() {
super.initState();
commentsCubit.getRecipesComments(widget.recipes.slug);
}
@override
Widget build(BuildContext context) {
return BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit,
child: BlocBuilder<CommentsCubit, CommentsState>(
builder: (BuildContext context, CommentsState state) {
if (state is CommentsStateError) {
print(state.errorMessage);
create: (BuildContext context) => commentsCubit
..getComments(
widget.recipes.slug,
CommentSourceType.recipe,
true,
),
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
@@ -59,6 +61,7 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
@@ -81,7 +84,7 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
),
if (replyId != null) SizedBox(width: 8),
SizedBox(
width: mediaQueryData.size.width - (replyId == null ? 64 : 104),
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: TextFormField(
onChanged: (_) {
setState(() {});
@@ -135,11 +138,13 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendRecipesComment(
commentsCubit.sendComment(
id: widget.recipes.slug,
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.recipe,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
@@ -300,17 +305,16 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _step(widget.recipes.steps[index], index + 1),
separatorBuilder: (context, index) =>
Padding(
padding: const EdgeInsets.all(16),
child: Container(
height: 0.4,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(16),
),
),
separatorBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(16),
child: Container(
height: 0.4,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(16),
),
),
),
itemCount: widget.recipes.steps.length,
),
SizedBox(height: 8),
@@ -343,7 +347,13 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
padding: const EdgeInsets.all(32),
child: CupertinoActivityIndicator(),
),
if (state is CommentsStateLoaded) _buildComments(state.comment),
if (state is CommentsStateLoaded)
CommentsWidget(
id: widget.recipes.slug,
commentSourceType: CommentSourceType.recipe,
comment: state.comment,
replyId: replyId,
),
],
),
),
@@ -406,103 +416,4 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
],
);
}
_buildComments(CommentModel comment) {
return Column(
children: [
for (int i = 0; i < comment.data.length; i++) ...[
_comment(comment.data[i]),
if (comment.data[i].childrenComments.isNotEmpty)
for (int j = 0; j < comment.data[i].childrenComments.length; j++)
Padding(
padding: EdgeInsets.only(left: 40),
child: _comment(
comment.data[i].childrenComments[j],
12,
),
),
SizedBox(height: 8),
],
],
);
}
_comment(Comment comment, [double radius = 16]) {
return Container(
width: MediaQuery
.of(context)
.size
.width,
decoration: BoxDecoration(
color: replyId == comment.id ? appTheme.blueA400.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon),
radius: radius,
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4),
Text(
comment.body,
style: TextStyle(
height: 1,
color: Colors.black,
),
),
SizedBox(height: 4),
Row(
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1,
),
),
SizedBox(width: 16),
GestureDetector(
onTap: () {
setState(() {
replyId = comment.id;
});
},
child: Text(
'Ответить',
style: TextStyle(
height: 1,
fontSize: 12,
color: Colors.grey.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
],
),
),
);
}
}

View File

@@ -38,7 +38,6 @@ class DietCreateScreen extends StatelessWidget {
}
_buildDietCreate(BuildContext context, DietCreateLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,

View File

@@ -39,7 +39,6 @@ class DietEditScreen extends StatelessWidget {
}
_buildDietEdit(BuildContext context, DietEditLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,

View File

@@ -40,7 +40,6 @@ class DietScreen extends StatelessWidget {
}
_buildDiet(BuildContext context, DietLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,

View File

@@ -43,7 +43,6 @@ class BlogPostEditScreen extends StatelessWidget {
}
_buildBlogPostEdit(BuildContext context, BlogPostEditLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return Scaffold(
resizeToAvoidBottomInset: false,
body: Container(

View File

@@ -38,7 +38,6 @@ class PlanCreateScreen extends StatelessWidget {
}
_buildPlanCreate(BuildContext context, PlanCreateLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,

View File

@@ -39,7 +39,6 @@ class PlanEditScreen extends StatelessWidget {
}
_buildPlanEdit(BuildContext context, PlanEditLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,

View File

@@ -40,7 +40,6 @@ class PlanScreen extends StatelessWidget {
}
_buildPlan(BuildContext context, PlanLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,

View File

@@ -58,7 +58,6 @@ class MyProfileScreen extends StatelessWidget {
}
_buildMyProfileScreen(BuildContext context, MyProfileLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,

View File

@@ -45,7 +45,6 @@ class RecipeEditScreen extends StatelessWidget {
}
_buildRecipeEdit(BuildContext context, RecipeEditLoadedState state) {
mediaQueryData = MediaQuery.of(context);
List<Widget> steps = [];
if (state.steps != null) {
for (int i = 0; i < state.steps!.length; i++) {

View File

@@ -44,7 +44,6 @@ class RecipeScreen extends StatelessWidget {
}
_buildRecipe(BuildContext context, RecipeLoadedState state) {
mediaQueryData = MediaQuery.of(context);
List<Widget> steps = [];
if (state.steps != null) {
for (int i = 0; i < state.steps!.length; i++) {

View File

@@ -38,11 +38,10 @@ dependencies:
dismissible_page: ^1.0.2
url_launcher: ^6.3.0
easy_debounce: ^2.0.3
lazy_load_scrollview: ^1.3.0
badges: ^3.1.2
google_fonts: ^6.2.1
location: ^6.0.2
syncfusion_flutter_charts: ^26.2.12
syncfusion_flutter_charts: ^26.2.13
table_calendar: ^3.1.2
flutter_keyboard_visibility: ^6.0.0
share_plus: ^10.0.0