Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Username1548
2025-05-19 21:38:20 +07:00
53 changed files with 534 additions and 216 deletions

View File

@@ -16,7 +16,7 @@ jobs:
run: |
security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db
env:
KEYCHAIN_PASSWORD: EeChahboo9Ei
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Flutter build
run: |
@@ -30,8 +30,7 @@ jobs:
env:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
MATCH_PASSWORD: EeChahboo9Ei
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_PASSWORD: EeChahboo9Ei
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: iqsm-pfxm-bpmg-nkry
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}

View File

@@ -16,6 +16,20 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Create key.properties file
run: |
cat > android/key.properties <<EOF
storePassword=${{ vars.SIGNING_STORE_PASSWORD }}
keyPassword=${{ vars.SIGNING_KEY_PASSWORD }}
keyAlias=${{ vars.SIGNING_KEY_ALIAS }}
storeFile=upload-keystore.jks
EOF
- name: Decode keystore from secret
run: |
echo "${{ vars.SIGNING_KEY_JKS }}"
printf "%s" "${{ vars.SIGNING_KEY_JKS }}" | base64 -d > android/app/upload-keystore.jks
- name: Cache Android SDK
id: cache-sdk
uses: "https://gitea.com/actions/cache@v4"
@@ -104,19 +118,6 @@ jobs:
- name: Test
run: flutter test
- name: Create key.properties file
run: |
cat > android/key.properties <<EOF
storePassword=${{ secrets.SIGNING_STORE_PASSWORD }}
keyPassword=${{ secrets.SIGNING_KEY_PASSWORD }}
keyAlias=${{ secrets.SIGNING_KEY_ALIAS }}
storeFile=my-release-key.jks
EOF
- name: Decode keystore from secret
run: |
printf "%s" "${{ secrets.SIGNING_KEY_JKS }}" | base64 -d > android/app/my-release-key.jks
- name: Build apk
run: flutter build apk --release

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 9;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 9;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 9;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>9</string>
<string>15</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -138,7 +138,7 @@ class AppRouter {
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
return '/my-profile-screen';
return '/feed-screen';
} else {
return null;
}
@@ -196,7 +196,13 @@ class AppRouter {
return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(path: RecipeCreateScreen.path, name: RecipeCreateScreen.name, builder: (context, state) => const RecipeCreateScreen()),
GoRoute(
path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(
recipe: state.extra != null ? state.extra as Recipe : null,
),
),
GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)),
GoRoute(
path: '/subs-screen/:slug',
@@ -224,7 +230,10 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
final chatId = data['chatId'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(chatId: chatId, sendInitialMessage: sendInitialMessage,);
return ChatScreen(
chatId: chatId,
sendInitialMessage: sendInitialMessage,
);
},
),
GoRoute(

View File

@@ -107,7 +107,7 @@ Future<void> init() async {
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));

View File

@@ -6,9 +6,10 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
@@ -26,6 +27,7 @@ class CustomPopup extends StatelessWidget {
final String? slug;
final int? id;
final FeedBlogPostModel? feedBlogPostModel;
final Recipe? recipe;
const CustomPopup({
super.key,
@@ -35,6 +37,7 @@ class CustomPopup extends StatelessWidget {
this.id,
this.slug,
this.feedBlogPostModel,
this.recipe,
});
@override
@@ -94,6 +97,8 @@ class CustomPopup extends StatelessWidget {
addItem('assets/icons/edit_paint.svg', 'Редактировать', () {
if (complainedContentType == ComplainedContent.posts) {
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel);
} else if (complainedContentType == ComplainedContent.recipe) {
GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe);
} else {
showInfoToast('В разработке');
return;
@@ -103,8 +108,6 @@ class CustomPopup extends StatelessWidget {
addItem('assets/icons/remove_from_public.svg', 'Снять с публикации', () async {
if (complainedContentType == ComplainedContent.diet) {
final result = await sl<MyDietsCubit>().unpublishDiet(id!);
if (!context.mounted) return;
context.read<FeedCubit>().loadFeedBlogPostList();
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
@@ -112,8 +115,6 @@ class CustomPopup extends StatelessWidget {
showSuccessToast('Диета перенесена в архив, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().unpublish(slug!);
if (!context.mounted) return;
context.read<FeedCubit>().loadFeedBlogPostList();
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
@@ -125,8 +126,6 @@ class CustomPopup extends StatelessWidget {
addItem('assets/icons/red_trash.svg', 'Удалить', () async {
if (complainedContentType == ComplainedContent.posts) {
final result = await sl<BlogPostCreateCubit>().deleteBlogPost(id!);
if (!context.mounted) return;
context.read<FeedCubit>().loadFeedBlogPostList();
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
@@ -134,8 +133,6 @@ class CustomPopup extends StatelessWidget {
showSuccessToast('Пост удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.recipe) {
final result = await sl<RecipesCubit>().deleteRecipe(slug!);
if (!context.mounted) return;
context.read<FeedCubit>().loadFeedBlogPostList();
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
@@ -143,8 +140,6 @@ class CustomPopup extends StatelessWidget {
showSuccessToast('Рецепт удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.diet) {
final result = await sl<DietsCubit>().deleteDiet(id!);
if (!context.mounted) return;
context.read<FeedCubit>().loadFeedBlogPostList();
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
@@ -152,8 +147,6 @@ class CustomPopup extends StatelessWidget {
showSuccessToast('Диета удалена, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().deletePlan(slug!);
if (!context.mounted) return;
context.read<FeedCubit>().loadFeedBlogPostList();
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;

View File

@@ -30,7 +30,7 @@ class AuthCubit extends Cubit<AuthState> {
);
showSuccessToast('Успешный вход');
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.go('/my-profile-screen');
AppRouter.navigatorKey.currentContext?.go('/feed-screen');
return true;
} catch (e) {
log(e.toString());

View File

@@ -27,7 +27,7 @@ class BottomNavigationBarWidget extends StatefulWidget {
}
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 0;
int currentBottomNavBarIndex = 1;
late final List<String> pages;
@override

View File

@@ -0,0 +1,26 @@
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'consultation_request_model.freezed.dart';
part 'consultation_request_model.g.dart';
@freezed
class ConsultationRequestModel with _$ConsultationRequestModel {
const factory ConsultationRequestModel({
@JsonKey(name: 'rating_place') required int ratingPlace,
@JsonKey(name: 'education_resource') required EducationModel educationModel,
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
}) = _ConsultationRequestModel;
factory ConsultationRequestModel.fromJson(Map<String, dynamic> json) => _$ConsultationRequestModelFromJson(json);
}
@freezed
class CertPhotosModel with _$CertPhotosModel {
const factory CertPhotosModel({
required String url,
}) = _CertPhotosModel;
factory CertPhotosModel.fromJson(Map<String, dynamic> json) => _$CertPhotosModelFromJson(json);
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:http_parser/http_parser.dart';
import 'package:dio/dio.dart';
@@ -33,7 +34,9 @@ class ConsultantRepository {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(
fileBytes,
filename: file.path.split('/').last,
filename: file.path
.split('/')
.last,
contentType: MediaType('application', 'octet-stream'),
);
multipartFiles.add(multipartFile);
@@ -46,4 +49,12 @@ class ConsultantRepository {
rethrow;
}
}
Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async {
try {
return await consultantService.getConsultantRequestStatus(slug: slug);
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:retrofit/retrofit.dart';
part 'consultant_service.g.dart';
@@ -18,4 +19,9 @@ abstract class ConsultantService {
Future<List<String>> uploadPhotos({
@Part(name: 'photos[]', contentType: 'multipart/form-data') List<MultipartFile>? photos,
});
@GET('/api/consultant_request/{slug}/status')
Future<ConsultationRequestModel> getConsultantRequestStatus({
@Path() required String slug,
});
}

View File

@@ -55,6 +55,27 @@ enum TypeOfRecipe {
}
}
static TypeOfRecipe? fromApiType(String value) {
switch (value) {
case 'first_meals':
return TypeOfRecipe.firstMeals;
case 'second_meals':
return TypeOfRecipe.secondMeals;
case 'salads':
return TypeOfRecipe.salads;
case 'snacks':
return TypeOfRecipe.snacks;
case 'sweets':
return TypeOfRecipe.sweets;
case 'drinks':
return TypeOfRecipe.drinks;
case 'other':
return TypeOfRecipe.other;
default:
return null;
}
}
static TypeOfRecipe? fromString(String value) {
switch (value) {
case 'Первые блюда':
@@ -80,6 +101,7 @@ enum TypeOfRecipe {
@freezed
class RecipeCreateEntity with _$RecipeCreateEntity {
const RecipeCreateEntity._();
const factory RecipeCreateEntity({
String? title,
TypeOfRecipe? typeOfRecipe,
@@ -94,7 +116,7 @@ class RecipeCreateEntity with _$RecipeCreateEntity {
String? videoUrl,
required List<Ingredient> ingredients,
required List<StepEntity> steps,
required List<String> tags, // tags for search
required List<String> tags,
}) = _RecipeCreateEntity;
RecipeModel toModel() => RecipeModel(
@@ -118,6 +140,7 @@ class RecipeCreateEntity with _$RecipeCreateEntity {
@freezed
class StepEntity with _$StepEntity {
const StepEntity._();
const factory StepEntity({
String? description,
File? image,

View File

@@ -1,13 +1,17 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart';
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart';
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i;
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
class CreateRecipeCubit extends Cubit<CreateRecipeState> {
final RecipeRepository recipeRepository;
@@ -17,6 +21,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
required this.glossaryRepository,
}) : super(
const CreateRecipeState(
loading: true,
entity: RecipeCreateEntity(
typeOfRecipe: TypeOfRecipe.firstMeals,
ingredients: [],
@@ -43,7 +48,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(steps: steps));
}
addIngredient(Ingredient entity) {
addIngredient(i.Ingredient entity) {
emit(
state.copyWith.entity(
ingredients: [
@@ -55,20 +60,20 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
removeIngredient(int index) {
List<Ingredient> ingredients = [...state.entity.ingredients];
List<i.Ingredient> ingredients = [...state.entity.ingredients];
ingredients.removeAt(index);
emit(state.copyWith.entity(ingredients: ingredients));
}
void updateIngredient({required int index, required Ingredient ingredient}) {
void updateIngredient({required int index, required i.Ingredient ingredient}) {
final ingredients = [...state.entity.ingredients];
ingredients[index] = ingredient;
emit(state.copyWith.entity(ingredients: ingredients));
}
replaceIngredientPosition(int oldIndex, int newIndex) {
List<Ingredient> ingredients = [...state.entity.ingredients];
List<i.Ingredient> ingredients = [...state.entity.ingredients];
final item = ingredients.removeAt(oldIndex);
ingredients.insert(newIndex, item);
emit(state.copyWith.entity(ingredients: ingredients));
@@ -152,10 +157,10 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(steps: steps));
}
Future<bool> publishRecipe() async {
Future<bool> publishRecipe([bool update = false, String? slug]) async {
try {
final model = state.entity.toModel();
await recipeRepository.publishRecipe(model);
await recipeRepository.publishRecipe(model, update, slug);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
@@ -163,6 +168,64 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
}
Future<File> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes);
} else {
throw Exception('Не удалось загрузить изображение');
}
}
disableLoading() {
emit(state.copyWith(loading: false));
}
Future<void> recipeToEntity(Recipe recipe) async {
final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}');
final List<StepEntity> steps = await Future.wait(
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: await downloadImageAsFile('$baseUrl/storage/${e.image}'),
),
),
);
emit(
CreateRecipeState(
entity: RecipeCreateEntity(
title: recipe.title,
typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!),
countOfPortions: recipe.servings,
energyInOnePortion: recipe.calories,
totalCookHours: recipe.cookingTime ~/ 60,
totalCookMinutes: recipe.cookingTime % 60,
activeCookHours: recipe.activeCookingTime ~/ 60,
activeCookMinutes: recipe.activeCookingTime % 60,
description: recipe.description,
image: file,
videoUrl: recipe.videoUrl,
ingredients: recipe.ingredients!
.map((e) => i.Ingredient(
name: e.name,
unit: e.unit!,
portion: e.portion,
productId: e.productId,
))
.toList(),
steps: steps,
tags: recipe.tags.map((e) => (e.name)).toList(),
),
loading: false,
),
);
}
bool validateAll() {
final isTitleValid = validateTitle();
final isDescValid = validateDescription();
@@ -251,6 +314,15 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
}
Future<bool> deleteRecipe(String slug) async {
try {
await recipeRepository.deleteRecipe(slug);
return true;
} catch (e) {
return false;
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
}

View File

@@ -7,6 +7,7 @@ part 'create_recipe_state.freezed.dart';
class CreateRecipeState with _$CreateRecipeState {
const factory CreateRecipeState({
required RecipeCreateEntity entity,
required bool loading,
String? titleValidationError,
String? descriptionValidationError,
String? cookingTimeValidationError,
@@ -15,5 +16,5 @@ class CreateRecipeState with _$CreateRecipeState {
String? energyPerServingValidationError,
String? imageValidationError,
}) = _CreateRecipeState;
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
@@ -8,13 +9,19 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip
import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class RecipeCreateScreen extends StatefulWidget {
const RecipeCreateScreen({super.key});
final Recipe? recipe;
const RecipeCreateScreen({
super.key,
this.recipe,
});
static const name = 'recipeCreateScreen';
static const path = '/recipe-create-screen';
@@ -24,85 +31,115 @@ class RecipeCreateScreen extends StatefulWidget {
}
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
late final CreateRecipeCubit createRecipeCubit;
@override
void initState() {
super.initState();
createRecipeCubit = sl<CreateRecipeCubit>();
if (widget.recipe != null) {
createRecipeCubit.recipeToEntity(widget.recipe!);
} else {
createRecipeCubit.disableLoading();
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocProvider<CreateRecipeCubit>(
create: (BuildContext context) => sl<CreateRecipeCubit>(),
child: Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
title: const Text('Добавление рецепта'),
centerTitle: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(12),
),
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 8, bottom: 32),
child: Column(
spacing: 16,
children: [
const InfoAboutRecipeWidget(),
const IngredientsWidget(),
const StepsWidget(),
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
builder: (context, state) {
return Builder(
builder: (context) {
return InkWell(
onTap: () async {
if (context.read<CreateRecipeCubit>().validateAll()) {
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe();
if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedRecipeBottomSheet(
title: state.entity.title ?? 'Рецепт',
),
);
});
} else {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
}
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: const Text(
'Опубликовать',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
);
},
);
},
create: (BuildContext context) => createRecipeCubit,
child: BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
builder: (BuildContext context, CreateRecipeState state) {
if (state.loading) {
return const Scaffold(
body: Loader(),
);
} else {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
centerTitle: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(12),
),
),
),
],
),
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 8, bottom: 32),
child: Column(
spacing: 16,
children: [
InfoAboutRecipeWidget(
createRecipeCubit: createRecipeCubit,
),
const IngredientsWidget(),
StepsWidget(
createRecipeCubit: createRecipeCubit,
),
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
builder: (context, state) {
return Builder(
builder: (context) {
return InkWell(
onTap: () async {
if (context.read<CreateRecipeCubit>().validateAll()) {
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
widget.recipe == null ? false : true,
widget.recipe?.slug,
);
if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedRecipeBottomSheet(
title: state.entity.title ?? 'Рецепт',
),
);
});
} else {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
}
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: Text(
widget.recipe != null ? 'Обновить' : 'Опубликовать',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
);
},
);
},
),
],
),
),
);
}
},
),
);
}

View File

@@ -14,7 +14,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class InfoAboutRecipeWidget extends StatefulWidget {
const InfoAboutRecipeWidget({super.key});
final CreateRecipeCubit createRecipeCubit;
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit});
@override
State<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
@@ -23,13 +25,19 @@ class InfoAboutRecipeWidget extends StatefulWidget {
class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
final List<SelectionPopupModel> items = TypeOfRecipe.values.map((e) => SelectionPopupModel(title: e.toString())).toList();
late SelectionPopupModel selectedType;
late final CreateRecipeCubit createRecipeCubit;
@override
void initState() {
super.initState();
selectedType = items.first;
createRecipeCubit = context.read<CreateRecipeCubit>();
if (widget.createRecipeCubit.state.entity.typeOfRecipe != null) {
for (int i = 0; i < items.length; i++) {
if (items[i].title == widget.createRecipeCubit.state.entity.typeOfRecipe!.toString()) {
selectedType = items[i];
}
}
} else {
selectedType = items.first;
}
}
@override
@@ -44,6 +52,7 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
const Text('Название'),
const SizedBox(height: 4),
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
bloc: widget.createRecipeCubit,
buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError,
builder: (context, state) {
return Column(
@@ -51,10 +60,11 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
CustomTextFormField(
hintText: 'Например: тар-тар из говядины',
hasError: state.titleValidationError != null,
initialValue: state.entity.title,
onChanged: (value) {
createRecipeCubit.replaceTitle(value);
widget.createRecipeCubit.replaceTitle(value);
if (state.titleValidationError != null) {
createRecipeCubit.validateTitle();
widget.createRecipeCubit.validateTitle();
}
},
),
@@ -78,7 +88,7 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
setState(() {
selectedType = newSelectedType;
});
createRecipeCubit.changeType(
widget.createRecipeCubit.changeType(
TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other,
);
},
@@ -100,12 +110,13 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
CustomTextFormField(
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
minLines: 5,
initialValue: state.entity.description,
maxLines: 5,
hasError: state.descriptionValidationError != null,
onChanged: (value) {
createRecipeCubit.replaceDescription(value);
widget.createRecipeCubit.replaceDescription(value);
if (state.descriptionValidationError != null) {
createRecipeCubit.validateDescription();
widget.createRecipeCubit.validateDescription();
}
},
),
@@ -148,8 +159,9 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
const SizedBox(height: 4),
CustomTextFormField(
hintText: 'Прикрепите ссылку на видео',
initialValue: widget.createRecipeCubit.state.entity.videoUrl,
onChanged: (value) {
createRecipeCubit.replaceVideoUrl(value);
widget.createRecipeCubit.replaceVideoUrl(value);
},
),
],

View File

@@ -14,6 +14,7 @@ class PortionWidget extends StatefulWidget {
class _PortionWidgetState extends State<PortionWidget> {
late final CreateRecipeCubit createRecipeCubit;
@override
void initState() {
super.initState();
@@ -45,6 +46,7 @@ class _PortionWidgetState extends State<PortionWidget> {
CustomTextFormField(
hintText: '1',
hasError: state.servingsValidationError != null,
initialValue: state.entity.countOfPortions?.toString(),
textInputType: TextInputType.number,
onChanged: (value) {
createRecipeCubit.replacePortionsCount(int.parse(value));
@@ -77,6 +79,7 @@ class _PortionWidgetState extends State<PortionWidget> {
CustomTextFormField(
hintText: '100',
hasError: state.energyPerServingValidationError != null,
initialValue: state.entity.energyInOnePortion?.toString(),
suffixText: 'Ккал',
textInputType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (value) {

View File

@@ -3,16 +3,16 @@ import 'dart:io';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
class StepImageWidget extends StatefulWidget {
final CreateRecipeCubit createRecipeCubit;
final int index;
const StepImageWidget({
super.key,
required this.index,
required this.index, required this.createRecipeCubit,
});
@override
@@ -21,12 +21,15 @@ class StepImageWidget extends StatefulWidget {
class _StepImageWidgetState extends State<StepImageWidget> {
XFile? image;
late final CreateRecipeCubit createRecipeCubit;
@override
void initState() {
super.initState();
createRecipeCubit = context.read<CreateRecipeCubit>();
if (widget.createRecipeCubit.state.entity.steps.length > widget.index) {
if (widget.createRecipeCubit.state.entity.steps[widget.index].image != null) {
image = XFile(widget.createRecipeCubit.state.entity.steps[widget.index].image!.path);
}
}
}
@override
@@ -56,7 +59,7 @@ class _StepImageWidgetState extends State<StepImageWidget> {
setState(() {
image = null;
});
createRecipeCubit.replaceImageInStep(null, widget.index);
widget.createRecipeCubit.replaceImageInStep(null, widget.index);
},
child: Container(
padding: const EdgeInsets.all(4),
@@ -84,7 +87,7 @@ class _StepImageWidgetState extends State<StepImageWidget> {
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (image != null) {
createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
}
if (!context.mounted) return;
Navigator.of(context).pop();
@@ -103,7 +106,7 @@ class _StepImageWidgetState extends State<StepImageWidget> {
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (image != null) {
createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
}
if (!context.mounted) return;
Navigator.of(context).pop();

View File

@@ -7,24 +7,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class StepsWidget extends StatefulWidget {
const StepsWidget({super.key});
final CreateRecipeCubit createRecipeCubit;
const StepsWidget({super.key, required this.createRecipeCubit});
@override
State<StepsWidget> createState() => _StepsWidgetState();
}
class _StepsWidgetState extends State<StepsWidget> {
late final CreateRecipeCubit createRecipeCubit;
@override
void initState() {
super.initState();
createRecipeCubit = context.read<CreateRecipeCubit>();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
bloc: widget.createRecipeCubit,
builder: (BuildContext context, CreateRecipeState state) {
return Material(
shape: RoundedRectangleBorder(
@@ -83,10 +78,11 @@ class _StepsWidgetState extends State<StepsWidget> {
const SizedBox(height: 4),
CustomTextFormField(
hintText: 'Например: подготовьте ингредиенты',
initialValue: widget.createRecipeCubit.state.entity.steps[index].description,
maxLines: 5,
minLines: 3,
onChanged: (value) {
createRecipeCubit.replaceDescriptionInStep(value, index);
widget.createRecipeCubit.replaceDescriptionInStep(value, index);
},
),
const SizedBox(height: 12),
@@ -94,6 +90,7 @@ class _StepsWidgetState extends State<StepsWidget> {
const SizedBox(height: 4),
StepImageWidget(
index: index,
createRecipeCubit: widget.createRecipeCubit,
),
],
),

View File

@@ -1,6 +1,8 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TagsWidget extends StatefulWidget {
const TagsWidget({super.key});
@@ -10,8 +12,16 @@ class TagsWidget extends StatefulWidget {
}
class _TagsWidgetState extends State<TagsWidget> {
late final CreateRecipeCubit createRecipeCubit;
final tagController = TextEditingController();
final List<String> tags = [];
late final List<String> tags;
@override
void initState() {
super.initState();
createRecipeCubit = context.read<CreateRecipeCubit>();
tags = createRecipeCubit.state.entity.tags;
}
@override
Widget build(BuildContext context) {

View File

@@ -71,6 +71,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
hintText: 'Часов',
hasError: state.cookingTimeValidationError != null,
textInputType: TextInputType.number,
initialValue: state.entity.totalCookHours?.toString(),
onChanged: (value) {
final hours = int.tryParse(value);
if (hours == null) return;
@@ -97,6 +98,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
hintText: 'Минут',
hasError: state.cookingTimeValidationError != null,
textInputType: TextInputType.number,
initialValue: state.entity.totalCookMinutes?.toString(),
onChanged: (value) {
final minutes = int.tryParse(value);
if (minutes == null) return;
@@ -156,6 +158,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
CustomTextFormField(
hintText: 'Часов',
hasError: state.activeTimeValidationError != null,
initialValue: state.entity.activeCookHours?.toString(),
textInputType: TextInputType.number,
onChanged: (value) {
final hours = int.tryParse(value);
@@ -183,6 +186,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
CustomTextFormField(
hintText: 'Минут',
hasError: state.activeTimeValidationError != null,
initialValue: state.entity.activeCookMinutes?.toString(),
textInputType: TextInputType.number,
onChanged: (value) {
final minutes = int.tryParse(value);

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
@@ -11,7 +10,6 @@ import 'package:eatvio_app/features/create_recipe/models/product.dart';
class RecipeRepository {
final CreateRecipeService recipeService;
RecipeRepository({required this.recipeService});
@@ -23,7 +21,7 @@ class RecipeRepository {
}
}
Future<void> publishRecipe(RecipeModel recipe) async {
Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async {
try {
if (recipe.image == null || (recipe.image?.isEmpty ?? true)) {
return;
@@ -72,15 +70,11 @@ class RecipeRepository {
};
formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty));
final formData = FormData.fromMap(formMap);
await recipeService.publishRecipe(formData: formData);
} catch (e) {
rethrow;
}
}
Future<RecipeModel> editRecipe(RecipeModel recipe, String id) async {
try {
return await recipeService.editRecipe(jsonDecode(jsonEncode(recipe)), id);
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.publishRecipe(formData: formData);
}
} catch (e) {
rethrow;
}
@@ -100,5 +94,13 @@ class RecipeRepository {
} catch (e) {
rethrow;
}
}
}
Future<void> deleteRecipe(String slug) async {
try {
return await recipeService.deleteRecipe(slug: slug);
} catch (e) {
rethrow;
}
}
}

View File

@@ -37,11 +37,18 @@ abstract class CreateRecipeService {
@Body() required FormData formData,
});
@POST('/api/recipes/update/{id}')
Future<RecipeModel> editRecipe(@Part() Map<String, Object?> recipe, String id);
@GET('/api/recipes/tags')
Future<RecipeTagModel> getTags();
@DELETE('/api/recipes/delete/{slug}')
Future<void> deleteRecipe({
@Path() required String slug,
});
}
@POST('/api/recipes/update/{slug}')
@MultiPart()
Future<void> updateRecipe({
@Path() required String slug,
@Body() required FormData formData,
});
}

View File

@@ -25,21 +25,40 @@ class Recipe with _$Recipe {
@JsonKey(name: 'was_liked_by_me') required bool wasLikedByMe,
@JsonKey(name: 'is_favorite') required bool isFavorite,
@JsonKey(name: 'energy_per_serving') required double calories,
int? servings,
@JsonKey(name: 'total_likes') required int likesCount,
@JsonKey(name: 'total_views') required int viewsCount,
required List<Tag> tags,
required Author author,
}) = _Recipes;
factory Recipe.fromJson(Map<String, Object?> json) => _$RecipeFromJson(json);
}
@freezed
class Tag with _$Tag {
const factory Tag({required String name}) = _Tag;
factory Tag.fromJson(Map<String, Object?> json) => _$TagFromJson(json);
}
@freezed
class Ingredient with _$Ingredient {
const factory Ingredient({required String name, String? unit, required String portion}) = _Ingredient;
const factory Ingredient({
required String name,
String? unit,
required String portion,
@JsonKey(name: 'product_nutritional_id', fromJson: _parseProductId) required int productId,
}) = _Ingredient;
factory Ingredient.fromJson(Map<String, Object?> json) => _$IngredientFromJson(json);
}
int _parseProductId(json) {
if (json is String) {
return int.parse(json);
}
return json as int;
}
@freezed
class Step with _$Step {
const factory Step({required String image, @JsonKey(name: 'description_md') required String description}) = _Step;

View File

@@ -127,6 +127,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
path: 'recipe/${widget.recipe.slug}',
slug: widget.recipe.slug,
complainedContentType: ComplainedContent.recipe,
recipe: widget.recipe,
),
),
),

View File

@@ -26,7 +26,6 @@ class SubModel with _$SubModel {
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: 'profile_photo_url') required String profilePhotoUrl,
required List<String> roles,
@JsonKey(name: 'is_public_profile') required bool isPublicProfile,
}) = _SubModel;
factory SubModel.fromJson(Map<String, Object?> json) => _$SubModelFromJson(json);

View File

@@ -2,6 +2,7 @@ import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
@@ -10,20 +11,24 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class ProfileCubit extends Cubit<ProfileState> {
final ProfileRepository profileRepository;
final ChatRepository chatRepository;
final ConsultantRepository consultantRepository;
String _currentPeriod = '1';
ProfileCubit({
required this.profileRepository,
required this.chatRepository,
required this.consultantRepository,
}) : super(const ProfileState.loading());
Future<void> getProfileBySlug(String slug) async {
try {
final profile = await profileRepository.getProfileBySlug(slug);
emit(
ProfileState.loaded(
profile: await profileRepository.getProfileBySlug(slug),
profile: profile,
profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null,
),
);
} catch (e) {
@@ -44,6 +49,7 @@ class ProfileCubit extends Cubit<ProfileState> {
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug),
weightPoints: await profileRepository.getWeight(period: period),
circumferencePoints: await profileRepository.getCircumferences(period: period),
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null,
),
);
} catch (e, stackTrace) {

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -18,6 +19,7 @@ abstract class ProfileState with _$ProfileState {
required List<FeedBlogPostModel> blogPosts,
List<WeightModel>? weightPoints,
List<CircumferenceModel>? circumferencePoints,
ConsultationRequestModel? consultantInfo,
}) = ProfileStateLoaded;
const factory ProfileState.error({

View File

@@ -93,7 +93,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
} else if (state is ProfileStateError) {
return DataErrorWidget(
onReload: () async {
widget.author == null || widget.author!.authorIsMe ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.author!.authorSlug);
widget.author == null || widget.author!.authorIsMe
? await profileCubit.getMyProfile()
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
},
);
} else {
@@ -144,25 +146,24 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
),
const SizedBox(width: 12),
if (state.profile.ratingPlace != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.profile.ratingPlace!}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.consultantInfo!.ratingPlace}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
),
),
],
),
if (state.profile.educationResource?.hasMedicalEducation == 1)
if (state.consultantInfo!.educationModel.hasMedicalEducation == 1)
const Row(
children: [
SizedBox(width: 16),
@@ -221,6 +222,61 @@ class _ProfileScreenState extends State<ProfileScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_header(state),
if (state.profile.roles.contains('consultant'))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const SizedBox(width: 16),
const Text(
'Консультант',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(
0xFF323F4B,
),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.consultantInfo!.ratingPlace}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
),
),
],
),
if (state.consultantInfo!.educationModel.hasMedicalEducation == 1)
const Row(
children: [
SizedBox(width: 16),
Icon(
Icons.done_rounded,
color: Colors.green,
),
Text(
'Медицинское образование',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state),
],
),
@@ -371,7 +427,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
_diplomasAndCertificates(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant') || state.profile.diplomasAndCertificates == null || state.profile.diplomasAndCertificates!.isEmpty) {
if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
@@ -416,8 +472,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
if (state.profile.diplomasAndCertificates != null)
...state.profile.diplomasAndCertificates!.map((d) => _diplomaImage('$baseUrl/storage/${d.path}')),
if (state.consultantInfo!.photos.isNotEmpty)
...List.generate(
state.consultantInfo!.photos.length * 2 - 1,
(index) {
if (index.isEven) {
final photo = state.consultantInfo!.photos[index ~/ 2];
return _diplomaImage(photo.url);
} else {
return const SizedBox(width: 8);
}
},
),
],
),
),
@@ -456,8 +522,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
_education(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant') ||
state.profile.educationResource?.completedCourses == null ||
(state.profile.educationResource?.completedCourses?.isEmpty ?? false)) {
state.consultantInfo!.educationModel.completedCourses == null ||
(state.consultantInfo!.educationModel.completedCourses?.isEmpty ?? false)) {
return const SizedBox.shrink();
}
return Padding(
@@ -495,11 +561,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
child: Column(
children: [
if (state.profile.educationResource?.completedCourses != null)
...state.profile.educationResource!.completedCourses!.map((c) => _educationBlock(c.values.first.title, c.values.first.date)),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _educationBlock(state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
state.consultantInfo!.educationModel.completedCourses![index].values.first.date),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.consultantInfo!.educationModel.completedCourses!.length,
),
],
),
),
@@ -564,39 +636,39 @@ class _ProfileScreenState extends State<ProfileScreen> {
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.vkLink ?? '',
state.consultantInfo!.educationModel.vkLink ?? '',
),
mode: LaunchMode.externalApplication,
);
},
'assets/icons/vk.svg',
state.profile.educationResource?.vkLink == null ? true : false,
state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false,
),
const SizedBox(width: 12),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.vkLink ?? '',
state.consultantInfo!.educationModel.tgLink ?? '',
),
mode: LaunchMode.externalApplication,
);
},
'assets/icons/telegram.svg',
state.profile.educationResource?.vkLink == null ? true : false,
state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false,
),
const SizedBox(width: 12),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.profile.educationResource?.personalSiteLink ?? '',
state.consultantInfo!.educationModel.personalSiteLink ?? '',
),
mode: LaunchMode.externalApplication,
);
},
'assets/icons/web.svg',
state.profile.educationResource?.personalSiteLink == null ? true : false,
state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false,
),
],
),
@@ -620,7 +692,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: SvgPicture.asset(
icon,
width: 24,
colorFilter: const ColorFilter.mode(Colors.black12, BlendMode.srcIn),
colorFilter: disabled ? const ColorFilter.mode(Colors.black12, BlendMode.srcIn) : const ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
),
),
@@ -659,7 +731,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
for (int i = 0; i < state.profile.badges.length; i++) ...[
state.profile.badges[i].image.contains('svg')
? Tooltip(
message: state.profile.badges[i].description,
message: state.profile.badges[i].description,
triggerMode: TooltipTriggerMode.tap,
textStyle: const TextStyle(
color: Colors.black,

View File

@@ -218,6 +218,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
path: 'recipe/${widget.recipe.slug}',
complainedContentType: ComplainedContent.recipe,
slug: widget.recipe.slug,
recipe: widget.recipe,
),
const SizedBox(width: 16),
],
@@ -401,6 +402,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
path: 'recipe/${widget.recipe.slug}',
complainedContentType: ComplainedContent.recipe,
slug: widget.recipe.slug,
recipe: widget.recipe,
),
const SizedBox(width: 16),
],
@@ -425,6 +427,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
path: 'recipe/${widget.recipe.slug}',
complainedContentType: ComplainedContent.recipe,
slug: widget.recipe.slug,
recipe: widget.recipe,
),
const SizedBox(width: 16),
],

View File

@@ -57,7 +57,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
),
);
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.go('/my-profile-screen');
AppRouter.navigatorKey.currentContext?.go('/feed-screen');
} catch (e) {
log(e.toString());
}

View File

@@ -25,8 +25,8 @@ class UpdateScreen extends StatelessWidget {
fit: BoxFit.cover,
),
),
SafeArea(
child: Positioned.fill(
Positioned.fill(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(

View File

@@ -37,8 +37,8 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
fit: BoxFit.cover,
),
),
SafeArea(
child: Positioned.fill(
Positioned.fill(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
@@ -80,7 +80,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
onTap: () async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setBool('welcomeScreenWasShowed', true);
goRouter.go('/my-profile-screen');
goRouter.go('/feed-screen');
},
borderRadius: BorderRadius.circular(8),
child: Ink(

View File

@@ -35,7 +35,7 @@ void main() {
test('getProfileDetailsBySlug', () async {
when(mockProfileService.getProfileDetailsBySlug(slug: 'slug')).thenAnswer(
(_) async => await Future.value(
(_) async => await Future.value(
const ProfileDetailsModel(
firstName: 'Alexnader',
lastName: 'Avanesyan',
@@ -70,21 +70,21 @@ void main() {
test('getBlogPostByAuthorSlug', () async {
when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer(
(_) async => [],
(_) async => [],
);
expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA<List<FeedBlogPostModel>>());
});
test('getWeight', () async {
when(mockProfileService.getWeight(period: '')).thenAnswer(
(_) async => [],
(_) async => [],
);
expect(await mockProfileService.getWeight(period: ''), isA<List<WeightModel>>());
});
test('getCircumferences', () async {
when(mockProfileService.getCircumferences(period: '')).thenAnswer(
(_) async => [],
(_) async => [],
);
expect(await mockProfileService.getCircumferences(period: ''), isA<List<CircumferenceModel>>());
});