diff --git a/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart b/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart index 29c6c673..eab8dd94 100644 --- a/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart +++ b/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart @@ -1,4 +1,6 @@ //import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '/core/app_export.dart'; @@ -7,6 +9,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:hive/hive.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; part 'my_profile_event.dart'; part 'my_profile_state.dart'; @@ -15,6 +18,7 @@ part 'my_profile_state.dart'; class MyProfileBloc extends Bloc { MyProfileBloc(MyProfileState initialState) : super(initialState) { on(_onInitialize); + on(_uploadAvatar); } Future _onInitialize( @@ -242,4 +246,35 @@ class MyProfileBloc extends Bloc { return "экстремальный (несколько интенсивных тренировок в день)"; } } // колхоз, но свитчкейсы в дарте странно ведут себя при сравнении double + + Future _uploadAvatar( + UploadAvatar event, Emitter emit) async { + if (state is MyProfileLoadedState) { + var currentState = state as MyProfileLoadedState; + + await uploadImageToServer(event.image.path, currentState.token); + } + } + + Future uploadImageToServer(String imagePath, String? token) async { + String url = '${dotenv.env['SERVER_ROOT']!}/profile/photo/upload'; + print(url); + var request = http.MultipartRequest('POST', Uri.parse(url)); + request.headers['Authorization'] = 'Bearer $token'; + + request.files.add(await http.MultipartFile.fromPath('photo', imagePath)); + + var client = http.Client(); + var streamedResponse = await client.send(request); + + if (streamedResponse.statusCode == 200) { + var response = await http.Response.fromStream(streamedResponse); + var imageUrl = response.body; + print('Image successfully uploaded'); + return imageUrl; + } else { + throw Exception( + 'Failed to upload image. Status code: ${streamedResponse.statusCode} ${streamedResponse}}'); + } + } } diff --git a/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart b/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart index 99c025a4..ca76f246 100644 --- a/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart +++ b/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart @@ -14,3 +14,10 @@ class MyProfileInitialEvent extends MyProfileEvent { @override List get props => []; } + +class UploadAvatar extends MyProfileEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart index 7ac120a4..d02f26d2 100644 --- a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart @@ -2,15 +2,12 @@ import 'bloc/my_profile_bloc.dart'; import 'models/my_profile_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/custom_app_bar.dart'; import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; -import 'package:eatvio_app/widgets/app_bar/exit_button.dart'; -import 'package:eatvio_app/widgets/next_step_button.dart'; import 'package:eatvio_app/widgets/custom_text_form_field.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:hive/hive.dart'; import 'package:flutter/services.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:http/http.dart' as http; @@ -55,122 +52,129 @@ class MyProfileScreen extends StatelessWidget { mediaQueryData = MediaQuery.of(context); return SafeArea( child: Scaffold( - appBar: _buildAppBar(context, state), - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 18.v), - child: Column(children: [ - _buildMyBlog(context), - SizedBox(height: 15.v), - _buildSettings(context), - SizedBox(height: 30.v), - _buildCarousel(context, state), - SizedBox(height: 15.v), - _buildPlans( - context, - state.plans?.map((plan) => plan['name']).toList(), - state.plans?.map((plan) => plan['image']).toList(), - state.plans?.map((plan) => plan['state']).toList(), - ), - SizedBox(height: 5.v), - ])), + 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(children: [ + SizedBox(height: 30.v), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: _buildInfo(context, state)), + SizedBox(height: 15.v), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: _buildCarousel(context, state)), + SizedBox(height: 15.v), + if (!state.plans!.isEmpty) + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: _buildPlans( + context, + state.plans + ?.map((plan) => plan['name']) + .toList(), + state.plans + ?.map((plan) => plan['image']) + .toList(), + state.plans + ?.map((plan) => plan['state']) + .toList(), + )), + SizedBox(height: 5.v), + ])))), bottomNavigationBar: _buildBottomBar(context))); } /// Section Widget - _buildAppBar(BuildContext context, MyProfileLoadedState state) { - print(state.user?['profile_photo_url']); - return CustomAppBar( - height: 54.v, - leadingWidth: 28.h, - title: Padding( - padding: EdgeInsets.only(left: 20.h), - child: Row(children: [ - CachedNetworkImage( - imageUrl: state.user?['profile_photo_url'], - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 20, - ), - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.error), - backgroundColor: Colors.red, - radius: 20, - ), - ), - AppbarSubtitle( - text: state.user?['name'], - margin: EdgeInsets.only(left: 35.h, top: 12.v, bottom: 9.v)) - ])), - actions: [ - ExitButton( - text: "exit".tr, - margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 17.v), - onTap: _onTapLogout(context), - ) - ]); + _buildInfo(BuildContext context, MyProfileLoadedState state) { + return Column(children: [ + GestureDetector( + onTap: addImage(context), + child: CachedNetworkImage( + imageUrl: + dotenv.env['STORAGE_URL']! + state.user?['profile_photo_path'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 60, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.grey[200], + radius: 60, + ), + )), + AppbarSubtitle(text: state.user?['name']), + SizedBox(height: 20.v), + Text(state.userDetails?['additional_info']), + ]); } - Function _onTapLogout(BuildContext context) { + addImage(BuildContext context) { return () async { - final box = await Hive.openBox('token'); - final token = box.get('token'); - final response = await http.post( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/logout'), - headers: { - 'Authorization': 'Bearer $token', - }, - ); - - if (response.statusCode == 204) { - print(response.statusCode); - box.delete('token'); // Удаляем токен из Hive - Navigator.pushNamed( - context, - AppRoutes.authScreen, - ); + final picker = ImagePicker(); + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + for (var image in images) { + context.read().add(UploadAvatar(image)); + } } }; } - /// Section Widget - _buildMyBlog(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 7.h), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - CustomImageView( - imagePath: ImageConstant.imgGrid, - height: 24.adaptSize, - width: 24.adaptSize, - margin: EdgeInsets.symmetric(vertical: 3.v)), - Expanded( - child: NextStepButton( - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - text: "my_blog".tr.toUpperCase(), - margin: EdgeInsets.only(left: 10.h))) - ])); - } - - /// Section Widget - _buildSettings(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 7.h), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - CustomImageView( - imagePath: ImageConstant.imgSearch, - height: 24.adaptSize, - width: 24.adaptSize, - margin: EdgeInsets.symmetric(vertical: 3.v)), - Expanded( - child: NextStepButton( - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: onTapSettings(context), - text: "my_settings".tr.toUpperCase(), - margin: EdgeInsets.only(left: 10.h))), - ])); - } - onTapSettings(BuildContext context) { return () { Navigator.pushNamed(context, AppRoutes.mySettings); @@ -183,7 +187,7 @@ class MyProfileScreen extends StatelessWidget { _buildCarousel(BuildContext contex, MyProfileLoadedState state) { return CarouselSlider( - options: CarouselOptions(height: 340.0), + options: CarouselOptions(height: 265.0), items: [1, 2, 3].map((chartType) { return Builder( builder: (BuildContext context) { diff --git a/lib/presentation/profile/my_profile_screen/my_settings_screen.dart b/lib/presentation/profile/my_profile_screen/my_settings_screen.dart index b514939b..82ddc56b 100644 --- a/lib/presentation/profile/my_profile_screen/my_settings_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_settings_screen.dart @@ -45,42 +45,134 @@ class MySettingsScreen extends StatelessWidget { mediaQueryData = MediaQuery.of(context); return SafeArea( - child: Scaffold( - appBar: _buildAppBar(context, "my_settings".tr), - body: Padding( - padding: const EdgeInsets.all(16.0), - 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: [ - SectionTitle('Язык'), - LanguageDropdown(), - SectionTitle('Уведомления'), - NotificationSwitch('Уведомление об обновлениях'), - NotificationSwitch('Напоминания о заполнении'), - SectionTitle('Дополнительная информация'), - ExpandableList('Персональные данные'), - AboutList('О нас', ['ООО Итвио', '2023-н.в.']), + SizedBox(height: 30), + 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: state.user?['name'], + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v)) + ])), + )), + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: [ + SectionTitle('Язык'), + LanguageDropdown(), + ], + ), + ), + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: [ + SectionTitle('Уведомления'), + NotificationSwitch('Уведомление об обновлениях'), + NotificationSwitch('Напоминания о заполнении'), + ], + ), + ), + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: [ + SectionTitle('Дополнительная информация'), + ExpandableList('Персональные данные'), + AboutList('О нас', ['ООО Итвио', '2023-н.в.']), + ], + ), + ), ], ), ), - bottomNavigationBar: _buildBottomBar(context))); - } - - /// Section Widget - _buildAppBar(BuildContext context, String username) { - return 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: username, - margin: EdgeInsets.only(left: 80.h, top: 12.v, bottom: 9.v)) - ])), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), ); } diff --git a/lib/widgets/custom_navigation_menu.dart b/lib/widgets/custom_navigation_menu.dart index d817a8f6..fbe6cbce 100644 --- a/lib/widgets/custom_navigation_menu.dart +++ b/lib/widgets/custom_navigation_menu.dart @@ -1,6 +1,9 @@ import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/widgets/custom_navigation_menu_item.dart'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; class CustomNavigationMenu extends StatelessWidget { @override @@ -53,12 +56,33 @@ class CustomNavigationMenu extends StatelessWidget { MenuItem( icon: ImageConstant.imgExit, text: 'Выход', - onTap: () { - // Действие при нажатии на "Выход" - }, + onTap: _onTapLogout(context), + // Действие при нажатии на "Выход" ), ], ), ); } } + +_onTapLogout(BuildContext context) { + return () async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/logout'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + + if (response.statusCode == 204) { + print(response.statusCode); + box.delete('token'); // Удаляем токен из Hive + Navigator.pushNamed( + context, + AppRoutes.authScreen, + ); + } + }; +}