From 20126c912a3cd3b855f47df7ad8c9b0c94f654e9 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Thu, 23 May 2024 13:52:39 +0300 Subject: [PATCH] feat(glossary): Classes, state management and routing for glossary --- .../bloc/glossary_screen_bloc.dart | 88 ++++++++++ .../bloc/glossary_screen_event.dart | 23 +++ .../bloc/glossary_screen_state.dart | 52 ++++++ .../glossary_screen/glossary_screen.dart | 152 ++++++++++++++++++ .../models/glossary_screen_model.dart | 19 +++ lib/routes/app_routes.dart | 4 + lib/widgets/custom_navigation_menu.dart | 6 +- 7 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart create mode 100644 lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart create mode 100644 lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart create mode 100644 lib/presentation/glossary/glossary_screen/glossary_screen.dart create mode 100644 lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart new file mode 100644 index 00000000..067f6325 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart @@ -0,0 +1,88 @@ +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 { + GlossaryBloc(GlossaryState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + GlossaryInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? glossaryID = state.glossaryModelObj?.glossaryID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/show/$glossaryID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is GlossaryLoadedState) { + var currentState = state as GlossaryLoadedState; + emit(currentState.copyWith( + glossaryModelObj: currentState.glossaryModelObj, + glossary: currentState.glossary)); + } else { + emit(GlossaryLoadedState(glossary: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(GlossaryErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(GlossaryErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(GlossaryErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.glossaryScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(GlossaryErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart new file mode 100644 index 00000000..3bc7f131 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart @@ -0,0 +1,23 @@ +// 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 get props => []; +} + +class DeleteEntry extends GlossaryEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart new file mode 100644 index 00000000..cd23333a --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart @@ -0,0 +1,52 @@ +// 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 get props => [glossaryModelObj]; + GlossaryState copyWith({GlossaryModel? glossaryModelObj}) { + return GlossaryState( + glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, + ); + } +} + +class GlossaryLoadedState extends GlossaryState { + GlossaryLoadedState({this.glossaryModelObj, this.glossary}); + + GlossaryModel? glossaryModelObj; + Map? glossary; + @override + List get props => [glossaryModelObj, glossary]; + GlossaryLoadedState copyWith( + {GlossaryModel? glossaryModelObj, Map? glossary}) { + return GlossaryLoadedState( + glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, + glossary: glossary ?? this.glossary, + ); + } +} + +class GlossaryErrorState extends GlossaryState { + final String errorMessage; + + GlossaryErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class GlossaryInitialState extends GlossaryState { + GlossaryInitialState({required GlossaryModel glossaryModelObj}) + : super(glossaryModelObj: glossaryModelObj); +} diff --git a/lib/presentation/glossary/glossary_screen/glossary_screen.dart b/lib/presentation/glossary/glossary_screen/glossary_screen.dart new file mode 100644 index 00000000..d7b76a00 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/glossary_screen.dart @@ -0,0 +1,152 @@ +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_dotenv/flutter_dotenv.dart'; + +class GlossaryScreen extends StatelessWidget { + const GlossaryScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final glossaryID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => GlossaryBloc(GlossaryState( + glossaryModelObj: GlossaryModel(glossaryID: glossaryID.toString()))) + ..add(GlossaryInitialEvent()), + child: GlossaryScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + 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( + 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), + ), + ], + ), + 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() + .add(DeleteEntry(state.glossary?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + ///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) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart b/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart new file mode 100644 index 00000000..e7f4367b --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart @@ -0,0 +1,19 @@ +// 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 { + final String glossaryID; + + GlossaryModel({required String glossaryID}) : glossaryID = glossaryID; + + GlossaryModel copyWith(String glossaryID) { + return GlossaryModel(glossaryID: glossaryID); + } + + @override + List get props => [glossaryID]; +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 7163f5e5..6e29d6b0 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -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 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, }; } diff --git a/lib/widgets/custom_navigation_menu.dart b/lib/widgets/custom_navigation_menu.dart index 674dd0f2..4bbdc758 100644 --- a/lib/widgets/custom_navigation_menu.dart +++ b/lib/widgets/custom_navigation_menu.dart @@ -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,