Merge pull request 'Запилил глоссарий: все необходимые классы, контроль состояний, верстка, коннкет АПИ (загрузка и отображение продуктов и их категорий)' (#60) from maxxkim/eatvio_app:glossary into main

Reviewed-on: eatvio/eatvio_app#60
Reviewed-by: apoloz <anatoliy.poloz@gmail.com>
This commit is contained in:
apoloz
2024-05-24 12:57:10 +00:00
15 changed files with 884 additions and 69 deletions

View File

@@ -36,9 +36,11 @@ class BlogPostCreateBloc
blog_postModelObj: currentState.blog_postModelObj,
title: currentState.title,
description: currentState.description,
youtube_url: currentState.youtube_url));
youtube_url: currentState.youtube_url,
token: currentState.token));
} else {
emit(BlogPostCreateLoadedState(
token: token,
title: TextEditingController(),
description: TextEditingController(),
youtube_url: TextEditingController()));
@@ -71,7 +73,8 @@ class BlogPostCreateBloc
String? description,
) async {
String url = '${dotenv.env['SERVER_ROOT']!}/feed/create';
final box = await Hive.openBox('token');
final token = box.get('token');
final Map<String, dynamic> credentials = {
'title': title,
'description': description,
@@ -83,6 +86,7 @@ class BlogPostCreateBloc
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
body: json.encode(credentials),
encoding: utf8,

View File

@@ -23,26 +23,32 @@ class BlogPostCreateState extends Equatable {
class BlogPostCreateLoadedState extends BlogPostCreateState {
BlogPostCreateLoadedState(
{this.blog_postModelObj, this.title, this.description, this.youtube_url});
{this.token,
this.blog_postModelObj,
this.title,
this.description,
this.youtube_url});
BlogPostCreateModel? blog_postModelObj;
TextEditingController? title;
TextEditingController? description;
TextEditingController? youtube_url;
String? token;
@override
List<Object?> get props =>
[blog_postModelObj, title, description, youtube_url];
BlogPostCreateLoadedState copyWith({
BlogPostCreateModel? blog_postModelObj,
TextEditingController? title,
TextEditingController? description,
TextEditingController? youtube_url,
}) {
BlogPostCreateLoadedState copyWith(
{BlogPostCreateModel? blog_postModelObj,
TextEditingController? title,
TextEditingController? description,
TextEditingController? youtube_url,
String? token}) {
return BlogPostCreateLoadedState(
blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj,
title: title ?? this.title,
description: description ?? this.description,
youtube_url: youtube_url ?? this.youtube_url,
token: token ?? this.token,
);
}
}

View File

@@ -3,6 +3,8 @@ import 'models/blog_post_create_model.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:flutter/material.dart';
import 'package:eatvio_app/widgets/custom_bottom_bar.dart';
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/widgets/custom_outlined_button.dart';
class BlogPostCreateScreen extends StatelessWidget {
const BlogPostCreateScreen({Key? key}) : super(key: key);
@@ -44,61 +46,123 @@ class BlogPostCreateScreen extends StatelessWidget {
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
width: double.maxFinite,
decoration: AppDecoration.fillGray,
child: ListView(children: [
SizedBox(height: 30.v),
Container(
margin: EdgeInsets.only(
left: 16.h,
right: 20.h,
bottom: 10.h,
),
padding: EdgeInsets.all(12.h),
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),
width: double.maxFinite,
decoration: AppDecoration.fillGray,
child: ListView(children: [
SizedBox(height: 30.v),
Container(
margin: EdgeInsets.only(
left: 16.h,
right: 20.h,
bottom: 10.h,
),
],
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
SizedBox(height: 16.0),
TextFormField(
controller: state.title,
decoration: InputDecoration(labelText: 'Заголовок'),
),
SizedBox(height: 16.0),
TextFormField(
controller: state.description,
decoration: InputDecoration(labelText: 'Описание'),
),
SizedBox(height: 16.0),
TextFormField(
controller: state.youtube_url,
decoration: InputDecoration(labelText: 'Ссылка'),
),
SizedBox(height: 16.0),
// Здесь нужно добавить виджет для загрузки картинки
ElevatedButton(
onPressed: () => context
.read<BlogPostCreateBloc>()
.add(CreateBlogPost()),
child: Text('Создать'),
)
]),
)
]),
),
padding: EdgeInsets.all(12.h),
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(mainAxisSize: MainAxisSize.min, children: [
Column(
children: <Widget>[
Text("Заголовок новости",
style: theme.textTheme.bodyLarge),
CustomTextFormField(
controller: state.title,
validator: _validateTitle,
),
SizedBox(height: 8.0),
Text("Ссылка на YouTube",
style: theme.textTheme.bodyLarge),
CustomTextFormField(
controller: state.youtube_url,
validator: _validateYouTubeLink,
),
SizedBox(height: 8.0),
Text("Описание", style: theme.textTheme.bodyLarge),
CustomTextFormField(
controller: state.description,
validator: _validateDescription,
maxLines: 6,
),
SizedBox(height: 16.0),
// Add widget for image upload here
CustomOutlinedButton(
onPressed: () => _submitForm(
context,
state.title!.text,
state.description!.text,
state.youtube_url!.text),
buttonStyle:
CustomButtonStyles.outlinePrimaryTL15,
text: 'Создать',
),
],
),
]))
])),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_submitForm(
BuildContext context, String title, String description, String youtubeURL) {
String? titleError = _validateTitle(title);
String? descriptionError = _validateDescription(description);
String? youtubeURLError = _validateYouTubeLink(youtubeURL);
if (titleError != null ||
descriptionError != null ||
youtubeURLError != null) {
// Display error messages or handle invalid input as needed
// For example, show a snackbar with the error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(titleError ?? descriptionError ?? youtubeURLError!)),
);
} else {
// Proceed with form submission
context.read<BlogPostCreateBloc>().add(CreateBlogPost());
}
}
String? _validateTitle(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите заголовок';
}
return null;
}
String? _validateDescription(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите описание';
}
return null;
}
String? _validateYouTubeLink(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите ссылку';
} else if (!_isValidUrl(value)) {
return 'Пожалуйста, введите корректную ссылку';
}
return null;
}
bool _isValidUrl(String url) {
final RegExp urlRegExp =
new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$");
return urlRegExp.hasMatch(url);
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -80,7 +80,6 @@ class BlogPostBloc extends Bloc<BlogPostEvent, BlogPostState> {
// Отправляем данные по HTTP маршруту и ждем ответ
var response = await _sendCommentData(
currentState.token, event.body, currentState.blog_post?['id']);
if (response.statusCode == 200) {
var updatedComments = fillCommentList(
await _fetchComments(currentState.blog_post?['id']));
@@ -159,7 +158,6 @@ class BlogPostBloc extends Bloc<BlogPostEvent, BlogPostState> {
List<CommentModel> fillCommentList(List<dynamic> comments) {
List<CommentModel> list = [];
for (int i = 0; i < comments.length; i++) {
print(comments[i]);
list.add(CommentModel(
id: comments[i]['id'].toString(),
userImage: comments[i]['profile_photo_path'],
@@ -175,6 +173,7 @@ class BlogPostBloc extends Bloc<BlogPostEvent, BlogPostState> {
String? token, String body, int id) async {
String url = dotenv.env['SERVER_ROOT']! + '/feed/createComment/$id';
final Map<String, dynamic> credentials = {
'id': id,
'body': body,
};

View File

@@ -54,7 +54,7 @@ class FeedScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 20),
_buildSearch(),
_buildSearch(context),
SizedBox(height: 20),
_buildMenu(context, state),
SizedBox(height: 20),
@@ -69,7 +69,7 @@ class FeedScreen extends StatelessWidget {
);
}
_buildSearch() {
_buildSearch(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
@@ -106,6 +106,10 @@ class FeedScreen extends StatelessWidget {
flex: 1,
child: CustomOutlinedButton(
text: "+",
onPressed: () => Navigator.pushNamed(
context,
AppRoutes.blogPostCreateScreen,
),
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
),

View File

@@ -0,0 +1,115 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '/core/app_export.dart';
import 'package:eatvio_app/presentation/glossary/glossary_screen/models/glossary_screen_model.dart';
part 'glossary_screen_event.dart';
part 'glossary_screen_state.dart';
/// A bloc that manages the state of a Glossary according to the event that is dispatched to it.
class GlossaryBloc extends Bloc<GlossaryEvent, GlossaryState> {
GlossaryBloc(GlossaryState initialState) : super(initialState) {
on<GlossaryInitialEvent>(_onInitialize);
on<ChangeProductGroup>(_changeProductGroup);
}
_onInitialize(
GlossaryInitialEvent event,
Emitter<GlossaryState> emit,
) async {
try {
final box = await Hive.openBox('token');
final token = box.get('token');
if (token == null) {
NavigatorService.popAndPushNamed(
AppRoutes.authScreen,
);
}
final responseProductGroups = await http.get(
Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/getProductGroups'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (responseProductGroups.statusCode == 200) {
final dataProductGroups = jsonDecode(responseProductGroups.body);
if (dataProductGroups is List<dynamic>) {
if (state is GlossaryLoadedState) {
var currentState = state as GlossaryLoadedState;
emit(currentState.copyWith(
glossaryModelObj: currentState.glossaryModelObj,
token: currentState.token,
productGroups: currentState.productGroups,
productList: currentState.productList));
} else {
List<SelectionPopupModel> productGroupsModel =
_fillProductGroups(dataProductGroups);
emit(GlossaryLoadedState(
token: token,
productGroups: productGroupsModel,
productList: []));
await _getProductListById(token, emit, productGroupsModel[0].id);
}
} else {
// Один или несколько ключевых полей отсутствуют, обрабатываем ошибку
emit(GlossaryErrorState('Список категорий некорректного формата'));
}
} else {
// Статус ответа не равен 200, обрабатываем ошибку
emit(GlossaryErrorState(
'Ошибка сервера: ${responseProductGroups.statusCode}'));
}
} catch (e) {
// Ошибка при декодировании JSON или другая ошибка выполнения
emit(GlossaryErrorState('Ошибка при получении данных о чатах: $e'));
}
}
void _changeProductGroup(
ChangeProductGroup event, Emitter<GlossaryState> emit) async {
if (state is GlossaryLoadedState) {
var currentState = state as GlossaryLoadedState;
await _getProductListById(currentState.token, emit, event.id);
}
}
Future<void> _getProductListById(
token, Emitter<GlossaryState> emit, int? productGroupID) async {
final responseProductList = await http.get(
Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/index/$productGroupID'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
);
final dataProductList = jsonDecode(responseProductList.body);
if (dataProductList is List<dynamic>) {
var currentState = state as GlossaryLoadedState;
emit(currentState.copyWith(
glossaryModelObj: currentState.glossaryModelObj,
productGroups: currentState.productGroups,
productList: dataProductList));
} else {
emit(GlossaryErrorState('Список продуктов некорректного формата'));
}
}
List<SelectionPopupModel> _fillProductGroups(List<dynamic> productGroups) {
List<SelectionPopupModel> result = [];
for (int i = 0; i < productGroups.length; i++) {
result.add(
SelectionPopupModel(
id: productGroups[i]['id'],
title: productGroups[i]['name'],
isSelected: i == 0,
),
);
}
return result;
}
}

View File

@@ -0,0 +1,30 @@
// ignore_for_file: must_be_immutable
part of 'glossary_screen_bloc.dart';
/// Abstract class for all events that can be dispatched from the
///Glossary widget.
///
/// Events must be immutable and implement the [Equatable] interface.
@immutable
abstract class GlossaryEvent extends Equatable {}
/// Event that is dispatched when the Glossary widget is first created.
class GlossaryInitialEvent extends GlossaryEvent {
@override
List<Object?> get props => [];
}
class DeleteEntry extends GlossaryEvent {
DeleteEntry(this.id);
final int id;
@override
List<Object?> get props => [id];
}
class ChangeProductGroup extends GlossaryEvent {
ChangeProductGroup(this.id);
final int id;
@override
List<Object?> get props => [id];
}

View File

@@ -0,0 +1,64 @@
// ignore_for_file: must_be_immutable
part of 'glossary_screen_bloc.dart';
/// Represents the state of Glossary in the application.
///
class GlossaryState extends Equatable {
GlossaryState({
this.glossaryModelObj,
});
GlossaryModel? glossaryModelObj;
@override
List<Object?> get props => [glossaryModelObj];
GlossaryState copyWith({GlossaryModel? glossaryModelObj}) {
return GlossaryState(
glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj,
);
}
}
class GlossaryLoadedState extends GlossaryState {
GlossaryLoadedState(
{this.glossaryModelObj,
this.productGroups,
this.productList,
this.token});
GlossaryModel? glossaryModelObj;
String? token;
List<SelectionPopupModel>? productGroups;
List<dynamic>? productList;
@override
List<Object?> get props =>
[glossaryModelObj, productGroups, productList, token];
GlossaryLoadedState copyWith(
{GlossaryModel? glossaryModelObj,
String? token,
List<SelectionPopupModel>? productGroups,
List<dynamic>? productList}) {
return GlossaryLoadedState(
glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj,
productGroups: productGroups ?? this.productGroups,
productList: productList ?? this.productList,
token: token ?? this.token,
);
}
}
class GlossaryErrorState extends GlossaryState {
final String errorMessage;
GlossaryErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}
class GlossaryInitialState extends GlossaryState {
GlossaryInitialState({required GlossaryModel glossaryModelObj})
: super(glossaryModelObj: glossaryModelObj);
}

View File

@@ -0,0 +1,333 @@
import 'bloc/glossary_screen_bloc.dart';
import 'models/glossary_screen_model.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:eatvio_app/widgets/custom_bottom_bar.dart';
import 'package:eatvio_app/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/widgets/custom_drop_down.dart';
import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart';
import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart';
class GlossaryScreen extends StatelessWidget {
const GlossaryScreen({Key? key}) : super(key: key);
static Widget builder(BuildContext context) {
return BlocProvider<GlossaryBloc>(
create: (context) =>
GlossaryBloc(GlossaryState(glossaryModelObj: GlossaryModel()))
..add(GlossaryInitialEvent()),
child: GlossaryScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<GlossaryBloc, GlossaryState>(
builder: (context, state) {
if (state is GlossaryInitialState) {
// Это начальное состояние, данные ещё не загружены.
return Container(
color: Colors.white,
);
} else if (state is GlossaryLoadedState) {
// Данные загружены, можно строить экран.
return _buildGlossary(context, state);
} else if (state is GlossaryErrorState) {
// Произошла ошибка при загрузке данных.
return Center(child: Text(state.errorMessage));
} else {
// Другие состояния, если они есть.
return Container(); // Пустой контейнер для необработанных состояний.
}
},
);
}
_buildGlossary(BuildContext context, GlossaryLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
backgroundColor: Colors.grey[200],
resizeToAvoidBottomInset: true,
body: SingleChildScrollView(
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(color: Colors.grey[200]),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 20),
_buildSearch(context),
SizedBox(height: 20),
_buildMenu(context),
SizedBox(height: 20),
_buildProducts(context, state)
],
),
),
),
),
bottomNavigationBar: _buildBottomBar(context),
),
);
}
_buildSearch(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: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
CustomSearchView(
hintText: "Поиск записей",
),
SizedBox(height: 10),
CustomOutlinedButton(
text: "Найти продукт",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
],
),
),
);
}
_buildMenu(BuildContext context) {
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: Column(children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Поиск по алфавиту",
style: CustomTextStyles.titleMediumSemiBold,
),
),
SizedBox(
height: 8,
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(33, _fillAlphabet),
),
),
]),
);
}
_buildProducts(BuildContext context, GlossaryLoadedState 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: Column(children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Группа продуктов",
style: CustomTextStyles.titleMediumSemiBold,
),
),
SizedBox(
height: 8,
),
CustomDropDown(
items: state.productGroups,
defaultValue: state.productGroups?[0],
fillColor: Colors.grey[200]!,
hintText: 'Выберите категорию',
borderDecoration: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Colors.grey[200]!, width: 1),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 9,
),
onChanged: _changeProductGroup(context, state),
width: MediaQuery.of(context)
.size
.width, // Set a percentage value as per your layout
),
SizedBox(
height: 8,
),
Align(
alignment: Alignment.centerLeft,
child: Text("*КБЖУ указано за 100гр")),
SizedBox(
height: 8,
),
_buildProductList(context, state)
]),
);
}
_buildProductList(BuildContext context, GlossaryLoadedState state) {
List<Widget> postWidgets = [];
if (state.productList != null) {
for (var post in state.productList!) {
postWidgets.add(_buildProductEntry(post, context));
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: postWidgets,
);
}
_buildProductEntry(product, 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: 1,
blurRadius: 3,
offset: Offset(0, 3),
),
],
),
child: Column(children: [
ListTile(
trailing: CachedNetworkImage(
imageUrl: dotenv.env['STORAGE_URL']! + (product['image'] ?? ''),
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(
product?['name'] ?? '',
style: CustomTextStyles.titleMediumSemiBold,
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(children: [
Column(children: [
Text("Ккал - ${product['energy_on_100gr'].toString()}"),
SizedBox(height: 8),
Text("Белки - ${product['proteins_on_100gr'].toString()}")
]),
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: Column(children: [
Text("Жиры - ${product['fats_on_100gr'].toString()}"),
SizedBox(height: 8),
Text(
"Углеводы - ${product['carbohydrates_on_100gr'].toString()}")
]),
)
])
],
),
),
]));
}
Widget _fillAlphabet(int index) {
return ScrollItem(title: String.fromCharCode(1040 + index));
}
List<SelectionPopupModel> fillProductGroups(List<dynamic> productGroups) {
List<SelectionPopupModel> result = [];
for (int i = 0; i < productGroups.length; i++) {
result.add(
SelectionPopupModel(
id: productGroups[i]['id'],
title: productGroups[i]['name'],
isSelected: true,
),
);
}
return result;
}
///Handling route based on bottom click actions
String getCurrentRoute(BottomBarEnum type) {
switch (type) {
case BottomBarEnum.tf:
return AppRoutes.glossaryScreen;
default:
return "/";
}
}
///Handling page based on route
Widget getCurrentPage(
BuildContext context,
String currentRoute,
) {
switch (currentRoute) {
case AppRoutes.glossaryScreen:
return GlossaryScreen.builder(context);
default:
return GlossaryScreen();
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}
_changeProductGroup(BuildContext context, GlossaryLoadedState state) {
return (value) {
context.read<GlossaryBloc>().add(ChangeProductGroup(value.id));
};
}
}

View File

@@ -0,0 +1,16 @@
// ignore_for_file: must_be_immutable
import 'package:equatable/equatable.dart';
/// This class defines the variables used in the [glossary_screen],
/// and is typically used to hold data that is passed between different parts of the application.
class GlossaryModel extends Equatable {
GlossaryModel();
GlossaryModel copyWith() {
return GlossaryModel();
}
@override
List<Object?> get props => [];
}

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

View File

@@ -38,6 +38,7 @@ import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/recipe_edit_sc
import 'package:eatvio_app/presentation/feed/blog_post_screen/blog_post_screen.dart';
import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart';
import 'package:eatvio_app/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart';
import 'package:eatvio_app/presentation/glossary/glossary_screen/glossary_screen.dart';
class AppRoutes {
// REGISTRATION
@@ -126,6 +127,8 @@ class AppRoutes {
static const String planCreateScreen = '/planCreate';
static const String glossaryScreen = '/glossary';
static Map<String, WidgetBuilder> get routes => {
dateOfBirthScreen: DateOfBirthScreen.builder,
cityOfLivingScreen: CityOfLivingScreen.builder,
@@ -167,5 +170,6 @@ class AppRoutes {
blogPostScreen: BlogPostScreen.builder,
blogPostEditScreen: BlogPostEditScreen.builder,
blogPostCreateScreen: BlogPostCreateScreen.builder,
glossaryScreen: GlossaryScreen.builder,
};
}

View File

@@ -23,6 +23,7 @@ class CustomDropDown extends StatelessWidget {
this.filled = true,
this.validator,
this.onChanged,
this.defaultValue,
}) : super(
key: key,
);
@@ -61,6 +62,8 @@ class CustomDropDown extends StatelessWidget {
final bool? filled;
final SelectionPopupModel? defaultValue;
final FormFieldValidator<SelectionPopupModel>? validator;
final Function(SelectionPopupModel)? onChanged;
@@ -77,18 +80,19 @@ class CustomDropDown extends StatelessWidget {
_buildDropDownWidget(BuildContext context) {
return SizedBox(
width: width ?? double.maxFinite,
width: width ?? MediaQuery.of(context).size.width - 20,
child: DropdownButtonFormField<SelectionPopupModel>(
focusNode: focusNode ?? FocusNode(),
icon: icon,
autofocus: autofocus!,
isExpanded: true,
style: textStyle ?? Theme.of(context).textTheme.titleMedium,
items: items?.map((SelectionPopupModel item) {
return DropdownMenuItem<SelectionPopupModel>(
value: item,
child: Text(
item.title,
overflow: TextOverflow.ellipsis,
overflow: TextOverflow.fade,
style: hintStyle ?? Theme.of(context).textTheme.titleMedium,
),
);

View File

@@ -41,10 +41,8 @@ class CustomNavigationMenu extends StatelessWidget {
),
MenuItem(
icon: ImageConstant.imgFinance,
text: 'Финансы',
onTap: () {
// Действие при нажатии на "Финансы"
},
text: 'Глоссарий',
onTap: () => Navigator.pushNamed(context, AppRoutes.glossaryScreen),
),
MenuItem(
icon: ImageConstant.imgSupport,