feat(feed): Search field (static), menu bar added. Feed and comment scroll fixed

This commit is contained in:
2024-05-22 15:23:55 +03:00
parent 6ad23e240f
commit e262c6993a
4 changed files with 423 additions and 207 deletions

View File

@@ -1,6 +1,5 @@
import 'bloc/blog_post_bloc.dart';
import 'models/blog_post_model.dart';
import 'models/comment_model.dart';
import 'widgets/comment_widget.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:flutter/material.dart';
@@ -150,15 +149,10 @@ class BlogPostScreen extends StatelessWidget {
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Spacer(),
Align(
alignment: Alignment.center,
child: Text(
state.blog_post?['title'],
style: CustomTextStyles.titleMediumBlack,
),
Text(
state.blog_post?['title'],
style: CustomTextStyles.titleMediumBlack,
),
Spacer(),
PopupMenuButton<String>(
@@ -361,20 +355,26 @@ class BlogPostScreen extends StatelessWidget {
}
_buildCommentSection(BuildContext context, BlogPostLoadedState state) {
return ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return SizedBox(
height: 10.v,
);
},
itemCount: state.comments?.length ?? 0,
itemBuilder: (context, index) {
CommentModel model = state.comments?[index];
return CommentWidget(
model,
);
});
if (state.comments != null && state.comments!.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (int i = 0; i < state.comments!.length; i++)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CommentWidget(state.comments![i]),
SizedBox(height: 10.0), // Separator
],
),
],
);
} else {
// Return a message or placeholder widget when there are no comments
return Center(
child: Text('No comments available'),
);
}
}
_buildBottomBar(BuildContext context) {

View File

@@ -1,11 +1,11 @@
import 'bloc/feed_bloc.dart';
import 'models/feed_model.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart';
import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart';
import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart';
import 'package:eatvio_app/widgets/custom_bottom_bar.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_search_view.dart';
import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_scroll_item.dart';
import 'package:eatvio_app/widgets/custom_bottom_bar.dart';
import 'package:eatvio_app/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@@ -29,7 +29,7 @@ class FeedScreen extends StatelessWidget {
);
} else if (state is FeedLoadedState) {
// Данные загружены, можно строить экран.
return _buildSetingsScreen(context, state);
return _buildFeedScreen(context, state);
} else {
// Другие состояния, если они есть.
return Container(); // Пустой контейнер для необработанных состояний.
@@ -38,7 +38,7 @@ class FeedScreen extends StatelessWidget {
);
}
_buildSetingsScreen(BuildContext context, FeedLoadedState state) {
_buildFeedScreen(BuildContext context, FeedLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
@@ -53,53 +53,10 @@ class FeedScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 10),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: CustomAppBar(
height: 54.v,
leadingWidth: 28.h,
leading: AppbarLeadingImage(
imagePath: ImageConstant.imgArrowLeft,
margin: EdgeInsets.only(
left: 16.h, top: 15.v, bottom: 15.v),
onTap: onTapBack(context),
),
title: Padding(
padding: EdgeInsets.only(left: 20.h),
child: Row(
children: [
AppbarSubtitle(
text: "Лента",
margin: EdgeInsets.only(
left: 80.h, top: 12.v, bottom: 9.v),
),
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: GestureDetector(
onTap: () => Navigator.pushNamed(
context,
AppRoutes.blogPostCreateScreen,
),
child: Icon(Icons.create_outlined)),
)
],
),
),
),
),
SizedBox(height: 20),
_buildSearch(),
SizedBox(height: 20),
_buildMenu(context, state),
SizedBox(height: 20),
_buildBlogPostList(context, state)
],
@@ -112,148 +69,202 @@ class FeedScreen extends StatelessWidget {
);
}
_buildSearch() {
return Container(
margin: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
CustomSearchView(
hintText: "Поиск записей",
),
SizedBox(height: 10),
Row(
children: [
Flexible(
flex: 5,
child: CustomOutlinedButton(
text: "Найти запись",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
),
SizedBox(width: 8), // Add some spacing between the buttons
Flexible(
flex: 1,
child: CustomOutlinedButton(
text: "+",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
),
],
)
],
),
),
);
}
_buildBlogPostList(BuildContext context, FeedLoadedState state) {
return ListView.builder(
shrinkWrap: true, // Add this line
itemCount: state.posts?.length ?? 0,
itemBuilder: (context, index) {
final post = state.posts?[index];
return Container(
margin: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
List<Widget> postWidgets = [];
if (state.posts != null) {
for (var post in state.posts!) {
postWidgets.add(_buildFeedEntry(post, context));
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: postWidgets,
);
}
_buildFeedEntry(post, BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(children: [
ListTile(
leading: CachedNetworkImage(
imageUrl: dotenv.env['STORAGE_URL']! +
(post["author"]['profile_photo_path'] ?? ''),
imageBuilder: (context, imageProvider) => CircleAvatar(
backgroundImage: imageProvider,
radius: 25,
),
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => CircleAvatar(
child: Icon(Icons.person),
backgroundColor: Colors.white,
radius: 25,
),
),
title: Text(
post?['author']['name'] ?? '',
style: CustomTextStyles.titleMediumSemiBold,
),
subtitle: Text(
DateFormat('dd.MM.yyyy')
.format(DateTime.parse(post?['post']['updated_at'])),
style: theme.textTheme.bodyMedium,
),
onTap: () => Navigator.pushNamed(
context,
AppRoutes.blogPostScreen,
arguments: post?['post']['id'].toString(),
),
trailing: PopupMenuButton<String>(
onSelected: (value) => print("tap"),
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'edit',
child: Text('Редактировать'),
),
PopupMenuItem<String>(
value: 'delete',
child: Text('Удалить'),
),
],
),
child: Column(children: [
ListTile(
leading: CachedNetworkImage(
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Spacer(),
Align(
alignment: Alignment.center,
child: Text(
post?['post']['title'],
style: CustomTextStyles.titleMediumBlack,
),
),
Spacer(),
],
),
SizedBox(height: 8.0),
Text(
post?['post']['description'],
style: CustomTextStyles.bodyMediumBluegray800,
),
SizedBox(height: 16.0),
CachedNetworkImage(
imageUrl: dotenv.env['STORAGE_URL']! +
(post["author"]['profile_photo_path'] ?? ''),
imageBuilder: (context, imageProvider) => CircleAvatar(
backgroundImage: imageProvider,
radius: 25,
(post?['post']['image'] ?? ''),
imageBuilder: (context, imageProvider) => Stack(
children: [
ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(8.0)),
child: Image(
image: imageProvider,
),
),
],
),
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => CircleAvatar(
child: Icon(Icons.person),
backgroundColor: Colors.white,
radius: 25,
),
),
title: Text(
post?['author']['name'] ?? '',
style: CustomTextStyles.titleMediumSemiBold,
),
subtitle: Text(
DateFormat('dd.MM.yyyy')
.format(DateTime.parse(post?['post']['updated_at'])),
style: theme.textTheme.bodyMedium,
),
onTap: () => Navigator.pushNamed(
context,
AppRoutes.blogPostScreen,
arguments: post?['post']['id'].toString(),
),
trailing: PopupMenuButton<String>(
onSelected: (value) => print("tap"),
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'edit',
child: Text('Редактировать'),
),
PopupMenuItem<String>(
value: 'delete',
child: Text('Удалить'),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Spacer(),
Align(
alignment: Alignment.center,
child: Text(
post?['post']['title'],
style: CustomTextStyles.titleMediumBlack,
),
),
Spacer(),
],
),
SizedBox(height: 8.0),
Text(
post?['post']['description'],
style: CustomTextStyles.bodyMediumBluegray800,
),
SizedBox(height: 16.0),
CachedNetworkImage(
imageUrl: dotenv.env['STORAGE_URL']! +
(post?['post']['image'] ?? ''),
imageBuilder: (context, imageProvider) => Stack(
children: [
ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(8.0)),
child: Image(
image: imageProvider,
),
),
],
),
placeholder: (context, url) =>
CircularProgressIndicator(),
),
SizedBox(height: 16.0),
Row(children: [
Padding(
padding: EdgeInsets.only(left: 4),
child: Row(children: [
Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(Icons.comment_outlined),
),
Text(
"Комментировать",
)
]),
),
Spacer(),
Row(children: [
Padding(
padding: const EdgeInsets.only(right: 4),
child: Icon(Icons.favorite_outline),
),
Text(
"Нравится",
)
]),
Spacer(),
SizedBox(height: 16.0),
Row(children: [
Padding(
padding: EdgeInsets.only(left: 4),
child: Row(children: [
Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(Icons.share_outlined),
child: Icon(Icons.comment_outlined),
),
Text(
"Комментировать",
)
]),
],
),
),
]));
},
);
),
Spacer(),
Row(children: [
Padding(
padding: const EdgeInsets.only(right: 4),
child: Icon(Icons.favorite_outline),
),
Text(
"Нравится",
)
]),
Spacer(),
Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(Icons.share_outlined),
),
]),
],
),
),
]));
}
Function onTapBack(BuildContext context) {
@@ -269,4 +280,35 @@ class FeedScreen extends StatelessWidget {
onTapArrowLeft(BuildContext context) {
NavigatorService.goBack();
}
_buildMenu(BuildContext context, FeedLoadedState state) {
return Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
ScrollItem(title: 'Все'),
ScrollItem(title: 'Рецепты'),
ScrollItem(title: 'Диеты'),
ScrollItem(title: 'Планы'),
ScrollItem(title: 'Записи'),
],
),
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:eatvio_app/core/app_export.dart';
class ScrollItem extends StatelessWidget {
final String title;
const ScrollItem({required this.title});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
margin: EdgeInsets.only(right: 10),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Text(
title,
style: CustomTextStyles.titleMediumSemiBold,
),
);
}
}

View File

@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import 'package:eatvio_app/core/app_export.dart';
class CustomSearchView extends StatelessWidget {
CustomSearchView(
{Key? key,
this.alignment,
this.width,
this.scrollPadding,
this.controller,
this.focusNode,
this.autofocus = false,
this.textStyle,
this.textInputType = TextInputType.text,
this.maxLines,
this.hintText,
this.hintStyle,
this.prefix,
this.prefixConstraints,
this.suffix,
this.suffixConstraints,
this.contentPadding,
this.borderDecoration,
this.fillColor,
this.filled = true,
this.validator,
this.onChanged})
: super(
key: key,
);
final Alignment? alignment;
final double? width;
final TextEditingController? scrollPadding;
final TextEditingController? controller;
final FocusNode? focusNode;
final bool? autofocus;
final TextStyle? textStyle;
final TextInputType? textInputType;
final int? maxLines;
final String? hintText;
final TextStyle? hintStyle;
final Widget? prefix;
final BoxConstraints? prefixConstraints;
final Widget? suffix;
final BoxConstraints? suffixConstraints;
final EdgeInsets? contentPadding;
final InputBorder? borderDecoration;
final Color? fillColor;
final bool? filled;
final FormFieldValidator<String>? validator;
final Function(String)? onChanged;
@override
Widget build(BuildContext context) {
return alignment != null
? Align(
alignment: alignment ?? Alignment.center,
child: searchViewWidget(context))
: searchViewWidget(context);
}
searchViewWidget(BuildContext context) => SizedBox(
width: width ?? double.maxFinite,
child: TextFormField(
scrollPadding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
controller: controller,
focusNode: focusNode,
onTapOutside: (event) => onTapOutside(),
autofocus: autofocus!,
style: textStyle ?? CustomTextStyles.bodySmallMulishBluegray800,
keyboardType: textInputType,
maxLines: maxLines ?? 1,
decoration: decoration,
validator: validator,
onChanged: (String value) => onChanged?.call(value),
),
);
void onTapOutside() {
if (focusNode != null) {
focusNode?.unfocus();
} else {
FocusManager.instance.primaryFocus?.unfocus();
}
}
InputDecoration get decoration => InputDecoration(
hintText: hintText ?? "",
hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800,
prefixIcon: prefix ??
SizedBox(
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Icon(Icons.search)),
),
prefixIconConstraints: prefixConstraints ??
BoxConstraints(
maxHeight: 40.v,
),
suffixIconConstraints: suffixConstraints ??
BoxConstraints(
maxHeight: 40.v,
),
isDense: true,
contentPadding: contentPadding ??
EdgeInsets.only(
top: 11.v,
right: 11.h,
bottom: 11.v,
),
fillColor: fillColor ?? Colors.grey[200],
filled: filled,
border: borderDecoration ??
OutlineInputBorder(
borderRadius: BorderRadius.circular(8.h),
borderSide: BorderSide.none,
),
enabledBorder: borderDecoration ??
OutlineInputBorder(
borderRadius: BorderRadius.circular(8.h),
borderSide: BorderSide.none,
),
focusedBorder: borderDecoration ??
OutlineInputBorder(
borderRadius: BorderRadius.circular(8.h),
borderSide: BorderSide.none,
),
);
}