feat(glossary): Glossary layouts and catergory load and change API

This commit is contained in:
2024-05-24 15:01:59 +03:00
parent 20126c912a
commit 1c17a93428
7 changed files with 514 additions and 108 deletions

View File

@@ -15,7 +15,7 @@ part 'glossary_screen_state.dart';
class GlossaryBloc extends Bloc<GlossaryEvent, GlossaryState> {
GlossaryBloc(GlossaryState initialState) : super(initialState) {
on<GlossaryInitialEvent>(_onInitialize);
on<DeleteEntry>(_deleteEntry);
on<ChangeProductGroup>(_changeProductGroup);
}
_onInitialize(
@@ -30,31 +30,39 @@ class GlossaryBloc extends Bloc<GlossaryEvent, GlossaryState> {
AppRoutes.authScreen,
);
}
final String? glossaryID = state.glossaryModelObj?.glossaryID;
final response = await http.get(
Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/show/$glossaryID'),
final responseProductGroups = await http.get(
Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/getProductGroups'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data is Map<String, dynamic>) {
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,
glossary: currentState.glossary));
token: currentState.token,
productGroups: currentState.productGroups,
productList: currentState.productList));
} else {
emit(GlossaryLoadedState(glossary: data));
List<SelectionPopupModel> productGroupsModel =
_fillProductGroups(dataProductGroups);
emit(GlossaryLoadedState(
token: token,
productGroups: productGroupsModel,
productList: []));
await _getProductListById(token, emit, productGroupsModel[0].id);
}
} else {
// Один или несколько ключевых полей отсутствуют, обрабатываем ошибку
emit(GlossaryErrorState('Некоторые необходимые данные отсутствуют.'));
emit(GlossaryErrorState('Список категорий некорректного формата'));
}
} else {
// Статус ответа не равен 200, обрабатываем ошибку
emit(GlossaryErrorState('Ошибка сервера: ${response.statusCode}'));
emit(GlossaryErrorState(
'Ошибка сервера: ${responseProductGroups.statusCode}'));
}
} catch (e) {
// Ошибка при декодировании JSON или другая ошибка выполнения
@@ -62,27 +70,46 @@ class GlossaryBloc extends Bloc<GlossaryEvent, GlossaryState> {
}
}
FutureOr<void> _deleteEntry(
DeleteEntry event, Emitter<GlossaryState> emit) async {
final box = await Hive.openBox('token');
final token = box.get('token');
if (token == null) {
NavigatorService.popAndPushNamed(
AppRoutes.authScreen,
);
void _changeProductGroup(
ChangeProductGroup event, Emitter<GlossaryState> emit) async {
if (state is GlossaryLoadedState) {
var currentState = state as GlossaryLoadedState;
await _getProductListById(currentState.token, emit, event.id);
}
final int id = event.id;
final response = await http.post(
Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/delete/$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',
},
);
if (response.statusCode == 200) {
NavigatorService.pushNamed(AppRoutes.glossaryScreen);
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 {
// Статус ответа не равен 200, обрабатываем ошибку
emit(GlossaryErrorState('Ошибка: ${response.statusCode}'));
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

@@ -21,3 +21,10 @@ class DeleteEntry extends GlossaryEvent {
@override
List<Object?> get props => [id];
}
class ChangeProductGroup extends GlossaryEvent {
ChangeProductGroup(this.id);
final int id;
@override
List<Object?> get props => [id];
}

View File

@@ -22,17 +22,29 @@ class GlossaryState extends Equatable {
}
class GlossaryLoadedState extends GlossaryState {
GlossaryLoadedState({this.glossaryModelObj, this.glossary});
GlossaryLoadedState(
{this.glossaryModelObj,
this.productGroups,
this.productList,
this.token});
GlossaryModel? glossaryModelObj;
Map<String, dynamic>? glossary;
String? token;
List<SelectionPopupModel>? productGroups;
List<dynamic>? productList;
@override
List<Object?> get props => [glossaryModelObj, glossary];
List<Object?> get props =>
[glossaryModelObj, productGroups, productList, token];
GlossaryLoadedState copyWith(
{GlossaryModel? glossaryModelObj, Map<String, dynamic>? glossary}) {
{GlossaryModel? glossaryModelObj,
String? token,
List<SelectionPopupModel>? productGroups,
List<dynamic>? productList}) {
return GlossaryLoadedState(
glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj,
glossary: glossary ?? this.glossary,
productGroups: productGroups ?? this.productGroups,
productList: productList ?? this.productList,
token: token ?? this.token,
);
}
}

View File

@@ -1,10 +1,14 @@
import 'bloc/glossary_screen_bloc.dart';
import 'models/glossary_screen_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: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);
@@ -42,80 +46,257 @@ class GlossaryScreen extends StatelessWidget {
_buildGlossary(BuildContext context, GlossaryLoadedState state) {
mediaQueryData = MediaQuery.of(context);
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
width: double.maxFinite,
decoration: AppDecoration.fillGray,
child: ListView(
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: 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),
),
],
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
CachedNetworkImage(
imageUrl: dotenv.env['STORAGE_URL']! +
(state.glossary?['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,
),
),
SizedBox(height: 16.0),
Text(state.glossary?['title'],
style: CustomTextStyles.titleMediumBlack),
SizedBox(height: 8.0),
Text(
state.glossary?['description'],
),
SizedBox(height: 16.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () => Navigator.pushNamed(
context, AppRoutes.glossaryScreen,
arguments: state.glossary?['id'].toString()),
child: Text('Редактировать'),
),
ElevatedButton(
onPressed: () => context
.read<GlossaryBloc>()
.add(DeleteEntry(state.glossary?['id'])),
child: Text('Удалить'),
),
],
)
]),
)
SizedBox(height: 20),
_buildSearch(context),
SizedBox(height: 20),
_buildMenu(context),
SizedBox(height: 20),
_buildProducts(context, state)
],
),
),
bottomNavigationBar: _buildBottomBar(context)));
),
),
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
@@ -145,8 +326,9 @@ class GlossaryScreen extends StatelessWidget {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}
/// Navigates to the previous screen.
onTapArrowleftone(BuildContext context) {
NavigatorService.goBack();
_changeProductGroup(BuildContext context, GlossaryLoadedState state) {
return (value) {
context.read<GlossaryBloc>().add(ChangeProductGroup(value.id));
};
}
}

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

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