forked from eatvio/eatvio_app
Compare commits
58 Commits
issue-701
...
personalDi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72c40d8ee | ||
|
|
6ed7faed15 | ||
|
|
16fccaea8e | ||
|
|
a723673d23 | ||
|
|
89f697f7a6 | ||
| d883c9e19a | |||
|
|
884830f232 | ||
|
|
7b297f954e | ||
|
|
dbd92a65ea | ||
|
|
f3af743487 | ||
|
|
c95f1988ae | ||
|
|
35a5e6d6be | ||
|
|
977b7150fe | ||
| 5b81014e72 | |||
|
|
563d84ca12 | ||
| 06cafb4257 | |||
|
|
a34cb01c1f | ||
|
|
8799375880 | ||
|
|
fb5a0d52bc | ||
|
|
2e3172bc45 | ||
|
|
41313f09af | ||
|
|
ce0099c385 | ||
|
|
14e4d7973e | ||
|
|
a138707111 | ||
|
|
4c7dd4a12a | ||
|
|
c2eda628dd | ||
|
|
53f2b78dfa | ||
|
|
ed612aa019 | ||
|
|
1f4c450ce7 | ||
|
|
1841199291 | ||
|
|
4c5532ff32 | ||
|
|
c1cbf1658b | ||
|
|
e7ab43c845 | ||
|
|
b9136a80de | ||
| bb68ea963c | |||
|
|
228ea7c902 | ||
| 8c85d1837a | |||
|
|
1d8f414672 | ||
| 49453c8b31 | |||
|
|
138e2c1073 | ||
| eb39ae5e7c | |||
|
|
46841fb502 | ||
|
|
8e72350152 | ||
|
|
ca3bd7cc0d | ||
|
|
517e493b5e | ||
|
|
1bc0ab3237 | ||
|
|
6b7b1f2d18 | ||
|
|
82bc51e45c | ||
|
|
77666aca0e | ||
|
|
16430af989 | ||
|
|
8c2cd5eedd | ||
|
|
996f2ecd70 | ||
|
|
19787f459e | ||
|
|
7e64b24a8c | ||
|
|
a07d085ba7 | ||
|
|
5616bedd25 | ||
|
|
0a3d9e577d | ||
|
|
55e3abec99 |
5
Makefile
5
Makefile
@@ -4,4 +4,7 @@ run-tests:
|
||||
run-analyse:
|
||||
flutter analyze
|
||||
|
||||
run-checks: run-analyse run-tests
|
||||
run-checks: run-analyse run-tests
|
||||
|
||||
build-generated-files:
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
@@ -63,6 +63,18 @@
|
||||
|
||||
<data android:pathPattern="/ru/plan/.*" />
|
||||
<data android:pathPattern="/en/plan/.*" />
|
||||
|
||||
<data android:pathPattern="/ru/chat/.*" />
|
||||
<data android:pathPattern="/en/chat/.*" />
|
||||
|
||||
<data android:pathPattern="/ru/chat" />
|
||||
<data android:pathPattern="/en/chat" />
|
||||
|
||||
<data android:pathPattern="/ru/profile/.*" />
|
||||
<data android:pathPattern="/en/profile/.*" />
|
||||
|
||||
<data android:pathPattern="/ru/diary/.*" />
|
||||
<data android:pathPattern="/en/diary/.*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
|
||||
9
assets/icons/logo_2.svg
Normal file
9
assets/icons/logo_2.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="89" height="90" viewBox="0 0 89 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.0119 0C65.9947 0 83 9.05026 83 29.6764C83 50.2792 65.9947 67 45.0119 67C24.0053 67 7 50.2792 7 29.6764C7 9.05026 24.0053 0 45.0119 0Z" fill="#43A6F2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.8844 43.5162C39.8585 43.5162 34.9274 42.5623 30.3282 40.7209C25.7291 38.8573 21.5803 36.1284 18.1428 32.7119C16.5544 31.1589 16.6493 28.6741 18.3087 27.2098C19.9919 25.7234 22.6234 25.8122 24.2118 27.3651C26.867 30.0052 30.0675 32.1129 33.6235 33.5327C37.1796 34.9748 40.9964 35.7069 44.8844 35.7069C48.7486 35.7069 52.5655 34.9748 56.1215 33.5327C59.6776 32.1129 62.878 30.0052 65.5332 27.3651C67.1216 25.8122 69.7531 25.7234 71.4363 27.2098C73.0958 28.6741 73.1906 31.1589 71.6022 32.7119C68.1647 36.1284 64.016 38.8573 59.4168 40.7209C54.8176 42.5623 49.8866 43.5162 44.8844 43.5162Z" fill="white"/>
|
||||
<path d="M9.69721 80.8957L3.51318 88.877C2.93793 89.6257 2.28049 90 1.54087 90C0.513623 90 0 89.3382 0 88.0147V77.3663C0 75.7888 0.56841 75 1.70523 75C2.84205 75 3.41046 75.7888 3.41046 77.3663V84.2246L9.71775 76.0829C10.2793 75.361 10.882 75 11.5257 75C12.5803 75 13.1077 75.7487 13.1077 77.246V87.6337C13.1077 89.2112 12.5393 90 11.4024 90C10.2656 90 9.69721 89.2112 9.69721 87.6337V80.8957Z" fill="#323F4B"/>
|
||||
<path d="M23.0017 78.2888C21.769 78.2888 21.0019 78.2553 20.7006 78.1885C19.7829 78.0013 19.3241 77.5201 19.3241 76.7447C19.3241 75.7019 20.1322 75.1805 21.7484 75.1805H27.6859C29.3021 75.1805 30.1102 75.7019 30.1102 76.7447C30.1102 77.5201 29.6514 78.0013 28.7337 78.1885C28.4324 78.2553 27.6654 78.2888 26.4327 78.2888V87.6337C26.4327 89.2112 25.8642 90 24.7274 90C23.5769 90 23.0017 89.2112 23.0017 87.6337V78.2888Z" fill="#323F4B"/>
|
||||
<path d="M38.6564 89.8195C37.7935 89.8195 37.1909 89.6791 36.8485 89.3984C36.506 89.1043 36.3348 88.5428 36.3348 87.7139V77.5267C36.3348 76.6578 36.506 76.0495 36.8485 75.7019C37.1909 75.3543 37.8072 75.1805 38.6975 75.1805H41.4916C42.8476 75.1805 43.9981 75.4545 44.9432 76.0027C46.0937 76.6578 46.6689 77.6003 46.6689 78.8302C46.6689 80.4211 46.08 81.4973 44.9021 82.0588V82.119C45.7787 82.3864 46.4087 82.7807 46.7922 83.3021C47.1757 83.8102 47.3675 84.512 47.3675 85.4078C47.3675 86.8917 46.8401 88.0214 45.7855 88.7968C44.8541 89.4786 43.6009 89.8195 42.0258 89.8195H38.6564ZM39.7453 83.5027V86.9519H41.4505C42.1764 86.9519 42.7175 86.8583 43.0736 86.6711C43.5393 86.4171 43.7721 85.9626 43.7721 85.3075C43.7721 84.5455 43.5461 84.0374 43.0941 83.7834C42.7654 83.5963 42.197 83.5027 41.3889 83.5027H39.7453ZM39.7453 78.0281V80.8957H41.2245C41.8957 80.8957 42.3682 80.8155 42.6421 80.6551C43.0256 80.4412 43.2174 80.0401 43.2174 79.4519C43.2174 78.5027 42.5531 78.0281 41.2245 78.0281H39.7453Z" fill="#323F4B"/>
|
||||
<path d="M64.1493 80.8957L57.9653 88.877C57.39 89.6257 56.7326 90 55.993 90C54.9657 90 54.4521 89.3382 54.4521 88.0147V77.3663C54.4521 75.7888 55.0205 75 56.1573 75C57.2941 75 57.8626 75.7888 57.8626 77.3663V84.2246L64.1698 76.0829C64.7314 75.361 65.3341 75 65.9778 75C67.0324 75 67.5598 75.7487 67.5598 77.246V87.6337C67.5598 89.2112 66.9913 90 65.8545 90C64.7177 90 64.1493 89.2112 64.1493 87.6337V80.8957Z" fill="#323F4B"/>
|
||||
<path d="M74.6391 82.5201C74.6391 80.3275 75.2691 78.5428 76.5292 77.1658C77.8304 75.7219 79.5904 75 81.8093 75C84.0418 75 85.8087 75.7152 87.1099 77.1457C88.37 78.5227 89 80.3142 89 82.5201C89 84.7126 88.37 86.4973 87.1099 87.8743C85.795 89.2914 84.0281 90 81.8093 90C79.5904 90 77.8304 89.2848 76.5292 87.8543C75.2691 86.4906 74.6391 84.7126 74.6391 82.5201ZM78.255 82.5602C78.255 83.7634 78.5358 84.7594 79.0973 85.5481C79.7274 86.4706 80.6314 86.9318 81.8093 86.9318C82.9872 86.9318 83.898 86.4773 84.5417 85.5682C85.1033 84.766 85.3841 83.7634 85.3841 82.5602C85.3841 81.3168 85.117 80.2941 84.5828 79.492C83.9391 78.5428 83.0146 78.0682 81.8093 78.0682C80.604 78.0682 79.6863 78.5428 79.0563 79.492C78.5221 80.2941 78.255 81.3168 78.255 82.5602Z" fill="#323F4B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/images/logo_bg.png
Normal file
BIN
assets/images/logo_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
28716
coverage/lcov.info
Normal file
28716
coverage/lcov.info
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/main.dart' as app;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -12,7 +14,7 @@ void main() {
|
||||
await AppInitializer.initialize(buildType: AppBuildType.tests);
|
||||
});
|
||||
|
||||
testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async {
|
||||
testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const app.MyApp());
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
await skipWelcomeScreen(tester);
|
||||
@@ -24,14 +26,14 @@ void main() {
|
||||
expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.');
|
||||
expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.');
|
||||
|
||||
await tester.enterText(emailFieldFinder, 'reshetnick.mike+user4@gmail.com');
|
||||
await tester.enterText(passwordFieldFinder, '1234567890');
|
||||
await tester.enterText(emailFieldFinder, EnvConfig.testEmail);
|
||||
await tester.enterText(passwordFieldFinder, EnvConfig.testPassword);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(signInButtonFinder);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
expect(signInButtonFinder, findsNothing, reason: 'Sign-in button should not be visible after successful login and navigation.');
|
||||
expect(FeedScreen, findsOne, reason: 'Feed screen should be visible after successful login and navigation.');
|
||||
});
|
||||
}
|
||||
21
integration_test/common_cases/skip_sign_in.dart
Normal file
21
integration_test/common_cases/skip_sign_in.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Future<void> skipSignIn(WidgetTester tester) async {
|
||||
final emailFieldFinder = find.byKey(AppKeys.emailTextFieldAuthScreen);
|
||||
final passwordFieldFinder = find.byKey(AppKeys.passwordTextFieldAuthScreen);
|
||||
final signInButtonFinder = find.byKey(AppKeys.signInButtonAuthScreen);
|
||||
|
||||
expect(emailFieldFinder, findsOneWidget, reason: 'Email text field should be found.');
|
||||
expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.');
|
||||
expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.');
|
||||
|
||||
await tester.enterText(emailFieldFinder, EnvConfig.testEmail);
|
||||
await tester.enterText(passwordFieldFinder, EnvConfig.testPassword);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(signInButtonFinder);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
}
|
||||
@@ -5,4 +5,4 @@ Future<void> skipWelcomeScreen(WidgetTester tester) async {
|
||||
final welcomeScreenButtonFinder = find.byKey(AppKeys.welcomeScreenButton);
|
||||
await tester.tap(welcomeScreenButtonFinder);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
147
integration_test/create_recipe/README.md
Normal file
147
integration_test/create_recipe/README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Интеграционные тесты создания рецепта
|
||||
|
||||
Этот набор интеграционных тестов покрывает полный пользовательский путь создания нового рецепта в приложении Eatvio.
|
||||
|
||||
## Структура файлов
|
||||
|
||||
- `create_recipe_test.dart` - Основные интеграционные тесты
|
||||
- `create_recipe_helpers.dart` - Helper функции для тестирования
|
||||
- `README.md` - Документация
|
||||
|
||||
## Тестовые сценарии
|
||||
|
||||
### 1. Полное создание рецепта
|
||||
**Файл:** `create_recipe_test.dart` - `should create and publish a new recipe successfully`
|
||||
|
||||
**Описание:** Тестирует полный пользовательский путь создания рецепта с заполнением всех полей.
|
||||
|
||||
**Шаги:**
|
||||
1. Запуск приложения и вход в систему
|
||||
2. Навигация к экрану создания рецепта
|
||||
3. Заполнение основной информации (название, тип, порции, калории, время)
|
||||
4. Добавление тегов
|
||||
5. Добавление ингредиентов
|
||||
6. Добавление шагов приготовления
|
||||
7. Симуляция добавления изображения
|
||||
8. Публикация рецепта
|
||||
9. Проверка успешного перехода
|
||||
|
||||
### 2. Валидация полей
|
||||
**Файл:** `create_recipe_test.dart` - `should validate required fields before publishing`
|
||||
|
||||
**Описание:** Проверяет, что валидация обязательных полей работает корректно.
|
||||
|
||||
**Шаги:**
|
||||
1. Запуск приложения и вход в систему
|
||||
2. Навигация к экрану создания рецепта
|
||||
3. Попытка публикации без заполнения полей
|
||||
4. Проверка, что остались на экране создания (валидация не прошла)
|
||||
|
||||
### 3. Минимальное создание рецепта
|
||||
**Файл:** `create_recipe_test.dart` - `should create recipe with minimal required fields`
|
||||
|
||||
**Описание:** Тестирует создание рецепта с минимально необходимыми полями.
|
||||
|
||||
**Шаги:**
|
||||
1. Запуск приложения и вход в систему
|
||||
2. Навигация к экрану создания рецепта
|
||||
3. Заполнение только обязательных полей
|
||||
4. Добавление минимума ингредиентов
|
||||
5. Симуляция добавления изображения
|
||||
6. Публикация рецепта
|
||||
7. Проверка успешного перехода
|
||||
|
||||
## Helper функции
|
||||
|
||||
### Навигация
|
||||
- `navigateToCreateRecipeScreen()` - Переход к экрану создания рецепта
|
||||
|
||||
### Заполнение формы
|
||||
- `fillRecipeBasicInfo()` - Заполнение основной информации о рецепте
|
||||
- `selectRecipeType()` - Выбор типа рецепта
|
||||
- `addTags()` - Добавление тегов
|
||||
- `addIngredients()` - Добавление ингредиентов
|
||||
- `addCookingSteps()` - Добавление шагов приготовления
|
||||
|
||||
### Публикация и проверки
|
||||
- `publishRecipe()` - Публикация рецепта
|
||||
- `verifyRecipePublished()` - Проверка успешной публикации
|
||||
- `verifyValidationErrors()` - Проверка ошибок валидации
|
||||
|
||||
### Симуляция изображений
|
||||
- `mockImageSelection()` - Симуляция выбора изображения для рецепта
|
||||
- `mockStepImageSelection()` - Симуляция выбора изображения для шага
|
||||
|
||||
### Тестовые данные
|
||||
- `createTestRecipeData()` - Создание набора тестовых данных
|
||||
|
||||
## Ключи для тестирования
|
||||
|
||||
В файле `lib/app_keys.dart` добавлены следующие ключи для стабильного тестирования:
|
||||
|
||||
### Основные поля
|
||||
- `recipeCreateScreenTitleField` - Поле названия
|
||||
- `recipeCreateScreenDescriptionField` - Поле описания
|
||||
- `recipeCreateScreenTypeDropdown` - Выпадающий список типа рецепта
|
||||
- `recipeCreateScreenPortionsField` - Поле количества порций
|
||||
- `recipeCreateScreenCaloriesField` - Поле калорийности
|
||||
|
||||
### Время приготовления
|
||||
- `recipeCreateScreenTotalHoursField` - Часы общего времени
|
||||
- `recipeCreateScreenTotalMinutesField` - Минуты общего времени
|
||||
- `recipeCreateScreenActiveHoursField` - Часы активного времени
|
||||
- `recipeCreateScreenActiveMinutesField` - Минуты активного времени
|
||||
|
||||
### Теги и ингредиенты
|
||||
- `recipeCreateScreenTagField` - Поле для ввода тега
|
||||
- `recipeCreateScreenAddTagButton` - Кнопка добавления тега
|
||||
- `recipeCreateScreenAddIngredientButton` - Кнопка добавления ингредиента
|
||||
|
||||
### Шаги приготовления
|
||||
- `recipeCreateScreenAddStepButton` - Кнопка добавления шага
|
||||
- `recipeCreateScreenStepDescriptionField` - Поле описания шага
|
||||
- `recipeCreateScreenStepImageWidget` - Виджет изображения шага
|
||||
|
||||
### Изображения и публикация
|
||||
- `recipeCreateScreenImageWidget` - Виджет основного изображения
|
||||
- `recipeCreateScreenPublishButton` - Кнопка публикации
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
```bash
|
||||
# Запуск всех интеграционных тестов
|
||||
flutter test integration_test/
|
||||
|
||||
# Запуск только тестов создания рецепта
|
||||
flutter test integration_test/create_recipe/
|
||||
|
||||
# Запуск конкретного теста
|
||||
flutter test integration_test/create_recipe/create_recipe_test.dart
|
||||
```
|
||||
|
||||
## Требования
|
||||
|
||||
- Flutter SDK
|
||||
- Настроенная тестовая среда (AppBuildType.tests)
|
||||
- Тестовые учетные данные в .env файле
|
||||
- Доступ к API бэкенда для тестирования
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Изображения**: В текущей реализации используется симуляция выбора изображений. Для полноценного тестирования рекомендуется добавить моки для `image_picker`.
|
||||
|
||||
2. **API взаимодействие**: Тесты взаимодействуют с реальным API бэкенда. Убедитесь, что тестовая среда настроена корректно.
|
||||
|
||||
3. **Время выполнения**: Тесты могут занимать значительное время из-за анимаций и API запросов. Используйте `pumpAndSettle()` с достаточными таймаутами.
|
||||
|
||||
4. **Стабильность**: Ключи для тестирования добавлены в UI компоненты для обеспечения стабильности тестов при изменениях в интерфейсе.
|
||||
|
||||
## Расширение тестов
|
||||
|
||||
Для добавления новых тестовых сценариев:
|
||||
|
||||
1. Создайте новый `testWidgets` в `create_recipe_test.dart`
|
||||
2. Используйте существующие helper функции или создайте новые в `create_recipe_helpers.dart`
|
||||
3. При необходимости добавьте новые ключи в `app_keys.dart`
|
||||
4. Обновите документацию в этом файле
|
||||
|
||||
237
integration_test/create_recipe/create_recipe_helpers.dart
Normal file
237
integration_test/create_recipe/create_recipe_helpers.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// Helper функции для интеграционных тестов создания рецепта
|
||||
|
||||
/// Навигация к экрану создания рецепта
|
||||
Future<void> navigateToCreateRecipeScreen(WidgetTester tester) async {
|
||||
// Переключаемся на вкладку "Рецепты" если она есть
|
||||
final recipesTab = find.text('Рецепты');
|
||||
if (recipesTab.evaluate().isNotEmpty) {
|
||||
await tester.tap(recipesTab);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Находим кнопку добавления рецепта (круглая кнопка с плюсом)
|
||||
final addRecipeButton = find.byType(InkWell).first;
|
||||
await tester.tap(addRecipeButton);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||
}
|
||||
|
||||
/// Заполняет основную информацию о рецепте
|
||||
Future<void> fillRecipeBasicInfo(
|
||||
WidgetTester tester, {
|
||||
String title = 'Интеграционный Тестовый Рецепт',
|
||||
TypeOfRecipe type = TypeOfRecipe.salads,
|
||||
int portions = 4,
|
||||
double calories = 250.0,
|
||||
int totalHours = 1,
|
||||
int totalMinutes = 30,
|
||||
int activeHours = 0,
|
||||
int activeMinutes = 45,
|
||||
String description = 'Этот рецепт создан для интеграционного тестирования. Он содержит все необходимые ингредиенты и пошаговую инструкцию приготовления.',
|
||||
}) async {
|
||||
// Заполняем название рецепта
|
||||
final titleField = find.byKey(AppKeys.recipeCreateScreenTitleField);
|
||||
await tester.enterText(titleField, title);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Выбираем тип рецепта
|
||||
await selectRecipeType(tester, type);
|
||||
|
||||
// Заполняем количество порций
|
||||
final portionsField = find.byKey(AppKeys.recipeCreateScreenPortionsField);
|
||||
await tester.enterText(portionsField, portions.toString());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Заполняем калорийность
|
||||
final caloriesField = find.byKey(AppKeys.recipeCreateScreenCaloriesField);
|
||||
await tester.enterText(caloriesField, calories.toString());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Заполняем общее время приготовления
|
||||
final totalHoursField = find.byKey(AppKeys.recipeCreateScreenTotalHoursField);
|
||||
await tester.enterText(totalHoursField, totalHours.toString());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final totalMinutesField = find.byKey(AppKeys.recipeCreateScreenTotalMinutesField);
|
||||
await tester.enterText(totalMinutesField, totalMinutes.toString());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Заполняем активное время приготовления
|
||||
final activeHoursField = find.byKey(AppKeys.recipeCreateScreenActiveHoursField);
|
||||
await tester.enterText(activeHoursField, activeHours.toString());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final activeMinutesField = find.byKey(AppKeys.recipeCreateScreenActiveMinutesField);
|
||||
await tester.enterText(activeMinutesField, activeMinutes.toString());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Заполняем описание
|
||||
final descriptionField = find.byKey(AppKeys.recipeCreateScreenDescriptionField);
|
||||
await tester.enterText(descriptionField, description);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Выбирает тип рецепта из выпадающего списка
|
||||
Future<void> selectRecipeType(WidgetTester tester, TypeOfRecipe type) async {
|
||||
final typeDropdown = find.byKey(AppKeys.recipeCreateScreenTypeDropdown);
|
||||
await tester.tap(typeDropdown);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Выбираем нужный тип из выпадающего списка
|
||||
final typeOption = find.text(type.toString());
|
||||
await tester.tap(typeOption);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Добавляет теги к рецепту
|
||||
Future<void> addTags(WidgetTester tester, List<String> tags) async {
|
||||
final tagField = find.byKey(AppKeys.recipeCreateScreenTagField);
|
||||
final addTagButton = find.byKey(AppKeys.recipeCreateScreenAddTagButton);
|
||||
|
||||
for (final tag in tags) {
|
||||
await tester.enterText(tagField, tag);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(addTagButton);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавляет ингредиенты к рецепту
|
||||
Future<void> addIngredients(WidgetTester tester, List<Map<String, String>> ingredients) async {
|
||||
final addIngredientButton = find.byKey(AppKeys.recipeCreateScreenAddIngredientButton);
|
||||
|
||||
for (final ingredient in ingredients) {
|
||||
await tester.ensureVisible(addIngredientButton);
|
||||
await tester.tap(addIngredientButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await addIngredientInModal(tester, ingredient['name']!, ingredient['quantity']!);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавляет ингредиент в модальном окне
|
||||
Future<void> addIngredientInModal(WidgetTester tester, String name, String quantity) async {
|
||||
// Ищем поля в модальном окне
|
||||
final nameFields = find.byType(TextFormField);
|
||||
if (nameFields.evaluate().isNotEmpty) {
|
||||
await tester.enterText(nameFields.first, name);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Ищем поле количества (второе поле)
|
||||
if (nameFields.evaluate().length > 1) {
|
||||
await tester.enterText(nameFields.at(1), quantity);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Ищем кнопку добавления в модальном окне
|
||||
final addButton = find.text('Добавить');
|
||||
if (addButton.evaluate().isNotEmpty) {
|
||||
await tester.tap(addButton);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавляет шаги приготовления
|
||||
Future<void> addCookingSteps(WidgetTester tester, List<Map<String, String>> steps) async {
|
||||
final addStepButton = find.byKey(AppKeys.recipeCreateScreenAddStepButton);
|
||||
|
||||
for (int i = 0; i < steps.length; i++) {
|
||||
final step = steps[i];
|
||||
|
||||
// Заполняем описание шага
|
||||
final stepDescription = find.byKey(Key('${AppKeys.recipeCreateScreenStepDescriptionField}_$i'));
|
||||
await tester.enterText(stepDescription, step['description']!);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Симулируем добавление изображения к шагу если указано
|
||||
if (step.containsKey('hasImage') && step['hasImage'] == 'true') {
|
||||
await mockStepImageSelection(tester, i);
|
||||
}
|
||||
|
||||
// Добавляем следующий шаг (кроме последнего)
|
||||
if (i < steps.length - 1) {
|
||||
await tester.ensureVisible(addStepButton);
|
||||
await tester.tap(addStepButton);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Публикует рецепт
|
||||
Future<void> publishRecipe(WidgetTester tester) async {
|
||||
final publishButton = find.byKey(AppKeys.recipeCreateScreenPublishButton);
|
||||
await tester.ensureVisible(publishButton);
|
||||
await tester.tap(publishButton);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5)); // Дать время на API-запрос
|
||||
}
|
||||
|
||||
/// Симулирует выбор изображения для основного рецепта
|
||||
Future<void> mockImageSelection(WidgetTester tester) async {
|
||||
final imageWidget = find.byKey(AppKeys.recipeCreateScreenImageWidget);
|
||||
if (imageWidget.evaluate().isNotEmpty) {
|
||||
await tester.tap(imageWidget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// В реальном интеграционном тесте здесь будет мок image_picker
|
||||
// который возвращает тестовое изображение
|
||||
}
|
||||
}
|
||||
|
||||
/// Симулирует выбор изображения для шага приготовления
|
||||
Future<void> mockStepImageSelection(WidgetTester tester, int stepIndex) async {
|
||||
final stepImageWidget = find.byKey(Key('${AppKeys.recipeCreateScreenStepImageWidget}_$stepIndex'));
|
||||
if (stepImageWidget.evaluate().isNotEmpty) {
|
||||
await tester.tap(stepImageWidget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// В реальном интеграционном тесте здесь будет мок image_picker
|
||||
// который возвращает тестовое изображение
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверяет, что рецепт был успешно опубликован
|
||||
void verifyRecipePublished(WidgetTester tester) {
|
||||
// После успешной публикации должен произойти переход обратно на экран ленты
|
||||
expect(find.byKey(AppKeys.recipeCreateScreenPublishButton), findsNothing);
|
||||
// Можно добавить дополнительные проверки в зависимости от того, куда происходит переход
|
||||
}
|
||||
|
||||
/// Проверяет, что валидация полей работает корректно
|
||||
void verifyValidationErrors(WidgetTester tester) {
|
||||
// Проверяем, что остались на экране создания рецепта (валидация не прошла)
|
||||
expect(find.byType(RecipeCreateScreen), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.recipeCreateScreenPublishButton), findsOneWidget);
|
||||
}
|
||||
|
||||
/// Создает тестовые данные для рецепта
|
||||
Map<String, dynamic> createTestRecipeData() {
|
||||
return {
|
||||
'title': 'Интеграционный Тестовый Рецепт',
|
||||
'type': TypeOfRecipe.salads,
|
||||
'portions': 4,
|
||||
'calories': 250.0,
|
||||
'totalHours': 1,
|
||||
'totalMinutes': 30,
|
||||
'activeHours': 0,
|
||||
'activeMinutes': 45,
|
||||
'description': 'Этот рецепт создан для интеграционного тестирования. Он содержит все необходимые ингредиенты и пошаговую инструкцию приготовления.',
|
||||
'tags': ['здоровое питание', 'быстро', 'легко'],
|
||||
'ingredients': [
|
||||
{'name': 'Помидоры', 'quantity': '200'},
|
||||
{'name': 'Огурцы', 'quantity': '150'},
|
||||
{'name': 'Лук', 'quantity': '50'},
|
||||
],
|
||||
'steps': [
|
||||
{'description': 'Помойте и нарежьте помидоры и огурцы кубиками среднего размера.', 'hasImage': 'true'},
|
||||
{'description': 'Нарежьте лук полукольцами и добавьте к овощам.', 'hasImage': 'false'},
|
||||
{'description': 'Смешайте все ингредиенты в большой миске, добавьте соль и перец по вкусу.', 'hasImage': 'true'},
|
||||
],
|
||||
};
|
||||
}
|
||||
128
integration_test/create_recipe/create_recipe_test.dart
Normal file
128
integration_test/create_recipe/create_recipe_test.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/main.dart' as app;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../common_cases/skip_welcome_screen.dart';
|
||||
import '../common_cases/skip_sign_in.dart';
|
||||
import 'create_recipe_helpers.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() async {
|
||||
await AppInitializer.initialize(buildType: AppBuildType.tests);
|
||||
});
|
||||
|
||||
group('Create Recipe Integration Tests', () {
|
||||
testWidgets('should create and publish a new recipe successfully', (tester) async {
|
||||
// 1. Запустить приложение
|
||||
await tester.pumpWidget(const app.MyApp());
|
||||
await tester.pumpAndSettle(const Duration(seconds: 20));
|
||||
|
||||
// 2. Пропустить онбординг и войти в систему
|
||||
await skipWelcomeScreen(tester);
|
||||
await skipSignIn(tester);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
// 3. Перейти на экран создания рецепта
|
||||
await navigateToCreateRecipeScreen(tester);
|
||||
|
||||
// Проверяем, что мы на экране создания рецепта
|
||||
expect(find.byType(RecipeCreateScreen), findsOneWidget);
|
||||
|
||||
// 4. Заполнить все поля формы
|
||||
final testData = createTestRecipeData();
|
||||
await fillRecipeBasicInfo(
|
||||
tester,
|
||||
title: testData['title'] as String,
|
||||
type: testData['type'] as TypeOfRecipe,
|
||||
portions: testData['portions'] as int,
|
||||
calories: testData['calories'] as double,
|
||||
totalHours: testData['totalHours'] as int,
|
||||
totalMinutes: testData['totalMinutes'] as int,
|
||||
activeHours: testData['activeHours'] as int,
|
||||
activeMinutes: testData['activeMinutes'] as int,
|
||||
description: testData['description'] as String,
|
||||
);
|
||||
|
||||
// 5. Добавить теги
|
||||
await addTags(tester, testData['tags'] as List<String>);
|
||||
|
||||
// 6. Добавить ингредиенты
|
||||
await addIngredients(tester, testData['ingredients'] as List<Map<String, String>>);
|
||||
|
||||
// 7. Добавить шаги приготовления
|
||||
await addCookingSteps(tester, testData['steps'] as List<Map<String, String>>);
|
||||
|
||||
// 8. Симулировать добавление изображения
|
||||
await mockImageSelection(tester);
|
||||
|
||||
// 9. Нажать "Опубликовать"
|
||||
await publishRecipe(tester);
|
||||
|
||||
// 10. Проверить результат
|
||||
verifyRecipePublished(tester);
|
||||
});
|
||||
|
||||
testWidgets('should validate required fields before publishing', (tester) async {
|
||||
// 1. Запустить приложение и войти в систему
|
||||
await tester.pumpWidget(const app.MyApp());
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
await skipWelcomeScreen(tester);
|
||||
await skipSignIn(tester);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
// 2. Перейти на экран создания рецепта
|
||||
await navigateToCreateRecipeScreen(tester);
|
||||
|
||||
// 3. Попытаться опубликовать без заполнения полей
|
||||
await publishRecipe(tester);
|
||||
|
||||
// 4. Проверить, что валидация сработала
|
||||
verifyValidationErrors(tester);
|
||||
});
|
||||
|
||||
testWidgets('should create recipe with minimal required fields', (tester) async {
|
||||
// 1. Запустить приложение и войти в систему
|
||||
await tester.pumpWidget(const app.MyApp());
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
await skipWelcomeScreen(tester);
|
||||
await skipSignIn(tester);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
// 2. Перейти на экран создания рецепта
|
||||
await navigateToCreateRecipeScreen(tester);
|
||||
|
||||
// 3. Заполнить только обязательные поля
|
||||
await fillRecipeBasicInfo(
|
||||
tester,
|
||||
title: 'Минимальный тестовый рецепт',
|
||||
type: TypeOfRecipe.firstMeals,
|
||||
portions: 2,
|
||||
calories: 300.0,
|
||||
totalHours: 0,
|
||||
totalMinutes: 30,
|
||||
activeHours: 0,
|
||||
activeMinutes: 15,
|
||||
description: 'Простой рецепт для тестирования.',
|
||||
);
|
||||
|
||||
// 4. Добавить минимум ингредиентов
|
||||
await addIngredients(tester, [
|
||||
{'name': 'Тестовый ингредиент', 'quantity': '100'},
|
||||
]);
|
||||
|
||||
// 5. Симулировать добавление изображения
|
||||
await mockImageSelection(tester);
|
||||
|
||||
// 6. Опубликовать рецепт
|
||||
await publishRecipe(tester);
|
||||
|
||||
// 7. Проверить результат
|
||||
verifyRecipePublished(tester);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/main.dart' as app;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../common_cases/skip_sign_in.dart';
|
||||
import '../common_cases/skip_welcome_screen.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() async {
|
||||
await AppInitializer.initialize(buildType: AppBuildType.tests);
|
||||
});
|
||||
|
||||
testWidgets('qwe', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const app.MyApp());
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
await skipWelcomeScreen(tester);
|
||||
await skipSignIn(tester);
|
||||
|
||||
final blogPostCreationFinder = find.byKey(AppKeys.feedScreenBlogPostCreationButton);
|
||||
|
||||
expect(blogPostCreationFinder, findsOneWidget, reason: 'Blog post creation button should be found.');
|
||||
|
||||
await tester.tap(blogPostCreationFinder);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
@@ -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 = 80;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -406,7 +406,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -539,7 +539,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -557,7 +557,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -578,7 +578,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -596,7 +596,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>80</string>
|
||||
<string>82</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppKeys {
|
||||
|
||||
class AppKeys {
|
||||
// Auth Screen
|
||||
static const hiddenLogoButtonAuthScreen = ValueKey('hiddenLogoButtonAuthScreen');
|
||||
static const emailTextFieldAuthScreen = ValueKey('emailTextFieldAuthScreen');
|
||||
static const passwordTextFieldAuthScreen = ValueKey('passwordTextFieldAuthScreen');
|
||||
static const signInButtonAuthScreen = ValueKey('signInButtonAuthScreen');
|
||||
|
||||
// Welcome Screen
|
||||
// Welcome Screen
|
||||
static const welcomeScreenButton = ValueKey('welcomeScreenButton');
|
||||
|
||||
// Feed Screen
|
||||
static const feedScreenBlogPostCreationButton = ValueKey('feedScreenBlogPostCreationButton');
|
||||
|
||||
// BlogPostCreateScreen
|
||||
static const blogPostCreateScreenTitleTextField = ValueKey('blogPostCreateScreenTitleTextField');
|
||||
static const blogPostCreateScreenContentTextField = ValueKey('blogPostCreateScreenContentTextField');
|
||||
static const blogPostCreateScreenVideoLinkTextField = ValueKey('blogPostCreateScreenVideoLinkTextField');
|
||||
static const blogPostCreateScreenCreateButton = ValueKey('blogPostCreateScreenCreateButton');
|
||||
static const imageWidget = ValueKey('imageWidget');
|
||||
|
||||
// Recipe Create Screen
|
||||
static const recipeCreateScreenTitleField = ValueKey('recipeCreateScreenTitleField');
|
||||
static const recipeCreateScreenDescriptionField = ValueKey('recipeCreateScreenDescriptionField');
|
||||
static const recipeCreateScreenTypeDropdown = ValueKey('recipeCreateScreenTypeDropdown');
|
||||
static const recipeCreateScreenPortionsField = ValueKey('recipeCreateScreenPortionsField');
|
||||
static const recipeCreateScreenCaloriesField = ValueKey('recipeCreateScreenCaloriesField');
|
||||
static const recipeCreateScreenTotalHoursField = ValueKey('recipeCreateScreenTotalHoursField');
|
||||
static const recipeCreateScreenTotalMinutesField = ValueKey('recipeCreateScreenTotalMinutesField');
|
||||
static const recipeCreateScreenActiveHoursField = ValueKey('recipeCreateScreenActiveHoursField');
|
||||
static const recipeCreateScreenActiveMinutesField = ValueKey('recipeCreateScreenActiveMinutesField');
|
||||
static const recipeCreateScreenTagField = ValueKey('recipeCreateScreenTagField');
|
||||
static const recipeCreateScreenAddTagButton = ValueKey('recipeCreateScreenAddTagButton');
|
||||
static const recipeCreateScreenAddIngredientButton = ValueKey('recipeCreateScreenAddIngredientButton');
|
||||
static const recipeCreateScreenAddStepButton = ValueKey('recipeCreateScreenAddStepButton');
|
||||
static const recipeCreateScreenPublishButton = ValueKey('recipeCreateScreenPublishButton');
|
||||
static const recipeCreateScreenImageWidget = ValueKey('recipeCreateScreenImageWidget');
|
||||
static const recipeCreateScreenStepDescriptionField = ValueKey('recipeCreateScreenStepDescriptionField');
|
||||
static const recipeCreateScreenStepImageWidget = ValueKey('recipeCreateScreenStepImageWidget');
|
||||
static const recipeCreateScreenIngredientNameField = ValueKey('recipeCreateScreenIngredientNameField');
|
||||
static const recipeCreateScreenIngredientQuantityField = ValueKey('recipeCreateScreenIngredientQuantityField');
|
||||
static const recipeCreateScreenIngredientAddButton = ValueKey('recipeCreateScreenIngredientAddButton');
|
||||
}
|
||||
|
||||
502
lib/common/widgets/markdown_editor.dart
Normal file
502
lib/common/widgets/markdown_editor.dart
Normal file
@@ -0,0 +1,502 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
|
||||
class MarkdownEditor extends StatefulWidget {
|
||||
const MarkdownEditor({
|
||||
super.key,
|
||||
this.initialText,
|
||||
this.hintText,
|
||||
this.hasError = false,
|
||||
this.minLines = 4,
|
||||
this.maxLines = 6,
|
||||
this.heightPreview = 120,
|
||||
this.onChanged,
|
||||
this.onToolbarVisibilityChanged,
|
||||
this.showInlineToolbar = true,
|
||||
});
|
||||
|
||||
final String? initialText;
|
||||
final String? hintText;
|
||||
final bool hasError;
|
||||
final int minLines;
|
||||
final int maxLines;
|
||||
final double heightPreview;
|
||||
final void Function(String value)? onChanged;
|
||||
final void Function(bool visible)? onToolbarVisibilityChanged;
|
||||
final bool showInlineToolbar;
|
||||
|
||||
@override
|
||||
State<MarkdownEditor> createState() => MarkdownEditorState();
|
||||
}
|
||||
|
||||
class MarkdownEditorState extends State<MarkdownEditor> {
|
||||
late final TextEditingController _controller;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool _isPreview = false;
|
||||
bool _enablePanel = false;
|
||||
late final KeyboardVisibilityController _keyboardVisibilityController;
|
||||
late final StreamSubscription<bool> _keyboardSubscription;
|
||||
|
||||
bool get isToolbarVisible => _enablePanel || _isPreview;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialText ?? '');
|
||||
_keyboardVisibilityController = KeyboardVisibilityController();
|
||||
_keyboardSubscription = _keyboardVisibilityController.onChange.listen((visible) {
|
||||
setState(() {
|
||||
_enablePanel = visible;
|
||||
});
|
||||
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyboardSubscription.cancel();
|
||||
_focusNode.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _hasFormatting(String prefix, String suffix) {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
|
||||
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
|
||||
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
|
||||
return beforeCursor == prefix && afterCursor == suffix;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _removeFormatting(String prefix, String suffix) {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
|
||||
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
|
||||
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
|
||||
final afterCursor = text.substring(cursorPosition + suffix.length);
|
||||
final newText = beforeCursor + afterCursor;
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPosition - prefix.length);
|
||||
}
|
||||
}
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _applyMarkdownFormat(String prefix, String suffix) {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final formattedText = '$prefix$selectedText$suffix';
|
||||
final newText = text.replaceRange(selection.start, selection.end, formattedText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPosition + prefix.length);
|
||||
}
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _applyHeading() {
|
||||
if (_hasFormatting('# ', '')) {
|
||||
_removeFormatting('# ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('# ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBold() {
|
||||
if (_hasFormatting('**', '**')) {
|
||||
_removeFormatting('**', '**');
|
||||
} else {
|
||||
_applyMarkdownFormat('**', '**');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyItalic() {
|
||||
if (_hasFormatting('*', '*')) {
|
||||
_removeFormatting('*', '*');
|
||||
} else {
|
||||
_applyMarkdownFormat('*', '*');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyNumberedList() {
|
||||
if (_hasFormatting('1. ', '')) {
|
||||
_removeFormatting('1. ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('1. ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBulletedList() {
|
||||
if (_hasFormatting('- ', '')) {
|
||||
_removeFormatting('- ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('- ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyLink() {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
|
||||
if (linkRegex.hasMatch(selectedText)) {
|
||||
final match = linkRegex.firstMatch(selectedText);
|
||||
final linkText = match?.group(1) ?? selectedText;
|
||||
final newText = text.replaceRange(selection.start, selection.end, linkText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
|
||||
} else {
|
||||
final formattedText = '[$selectedText](url)';
|
||||
final newText = text.replaceRange(selection.start, selection.end, formattedText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
|
||||
}
|
||||
} else {
|
||||
_applyMarkdownFormat('[', '](url)');
|
||||
}
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _applyHorizontalRule() {
|
||||
final cursorPosition = _controller.selection.baseOffset;
|
||||
final text = _controller.text;
|
||||
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPosition + 5);
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _togglePreview() {
|
||||
setState(() {
|
||||
_isPreview = !_isPreview;
|
||||
});
|
||||
if (!_isPreview) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
} else {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
|
||||
}
|
||||
|
||||
Widget _mdButton(IconData icon, VoidCallback onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mdTextButton(String text, VoidCallback onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMarkdownPreview(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.heightPreview,
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF6F6F6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFE0E0E0)),
|
||||
),
|
||||
child: SingleChildScrollView(child: _parseMarkdown(_controller.text)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildToolbar(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width - 76,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
_mdTextButton('H', _applyHeading),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_bold, _applyBold),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_italic, _applyItalic),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_numbered, _applyNumberedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.link, _applyLink),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
|
||||
),
|
||||
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _parseMarkdown(String text) {
|
||||
if (text.isEmpty) {
|
||||
return Text(widget.hintText ?? '');
|
||||
}
|
||||
final lines = text.split('\n');
|
||||
final widgets = <Widget>[];
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final line = lines[i];
|
||||
if (line.trim().isEmpty) {
|
||||
widgets.add(const SizedBox(height: 8));
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('# ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(2),
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('## ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(3),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('### ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(4),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(child: _parseInlineMarkdown(line.substring(2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
|
||||
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
|
||||
final number = match?.group(1) ?? '1';
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$number. ', style: const TextStyle(fontSize: 16)),
|
||||
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (line.trim() == '---') {
|
||||
widgets.add(const Divider(color: Colors.grey, thickness: 1));
|
||||
} else {
|
||||
widgets.add(_parseInlineMarkdown(line));
|
||||
}
|
||||
if (i < lines.length - 1) {
|
||||
widgets.add(const SizedBox(height: 4));
|
||||
}
|
||||
}
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
|
||||
}
|
||||
|
||||
Widget _parseInlineMarkdown(String text) {
|
||||
final spans = <TextSpan>[];
|
||||
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
|
||||
int lastEnd = 0;
|
||||
for (final match in regex.allMatches(text)) {
|
||||
if (match.start > lastEnd) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastEnd, match.start),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}
|
||||
final matchedText = match.group(0)!;
|
||||
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: matchedText.substring(2, matchedText.length - 2),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: matchedText.substring(1, matchedText.length - 1),
|
||||
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
|
||||
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
|
||||
if (linkMatch != null) {
|
||||
final linkText = linkMatch.group(1)!;
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: linkText,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
lastEnd = match.end;
|
||||
}
|
||||
if (lastEnd < text.length) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastEnd),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}
|
||||
return RichText(text: TextSpan(children: spans));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_isPreview)
|
||||
_buildMarkdownPreview(context)
|
||||
else
|
||||
CustomTextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
hintText: widget.hintText,
|
||||
hasError: widget.hasError,
|
||||
onChanged: (value) => widget.onChanged?.call(value),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.showInlineToolbar && (isToolbarVisible || !_isPreview))
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
_mdTextButton('H', _applyHeading),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_bold, _applyBold),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_italic, _applyItalic),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_numbered, _applyNumberedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.link, _applyLink),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
|
||||
],
|
||||
),
|
||||
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -40,6 +41,7 @@ class AppConfig {
|
||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
|
||||
];
|
||||
|
||||
/// Делегаты локализации
|
||||
|
||||
@@ -29,23 +29,45 @@ class AppRouter {
|
||||
/// Обработка редиректов для deep links
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
final segments = state.uri.pathSegments;
|
||||
if (segments.length >= 3) {
|
||||
if (segments.length == 4) {
|
||||
final type = segments[1];
|
||||
switch (type) {
|
||||
case 'diary':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/diary-screen';
|
||||
}
|
||||
}
|
||||
if (segments.length == 3) {
|
||||
final type = segments[1];
|
||||
final contentIdentifier = segments[2];
|
||||
|
||||
switch (type) {
|
||||
case 'recipe':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier';
|
||||
return '/about-recipes-screen?slug=$contentIdentifier';
|
||||
case 'blog_post':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.blogPostScreen}?id=$contentIdentifier';
|
||||
return '/blog-post-screen?id=$contentIdentifier';
|
||||
case 'plan':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier';
|
||||
return '/about-plan-screen?slug=$contentIdentifier';
|
||||
case 'diet':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier';
|
||||
return '/about-diet-screen?slug=$contentIdentifier';
|
||||
case 'chat':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/chat-select-screen';
|
||||
case 'profile':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/profile-screen?slug=$contentIdentifier';
|
||||
}
|
||||
}
|
||||
if (segments.length == 2) {
|
||||
final type = segments[1];
|
||||
switch (type) {
|
||||
case 'chat':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/chat-select-screen';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
lib/core/cubit/marketing/marketing_cubit.dart
Normal file
29
lib/core/cubit/marketing/marketing_cubit.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MarketingCubit extends Cubit<MarketingCubitState> {
|
||||
final Prefs prefs;
|
||||
MarketingCubit(this.prefs) : super(const MarketingCubitState());
|
||||
|
||||
Future<bool> checkRegistrationDate() async {
|
||||
try {
|
||||
final milliseconds = prefs.getInt(PrefsKeys.registrationDate);
|
||||
final marketingBottomSheetHasBeenShown = prefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown);
|
||||
if (milliseconds == null || marketingBottomSheetHasBeenShown == true) {
|
||||
return false;
|
||||
}
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(milliseconds);
|
||||
final today = DateTime.now();
|
||||
final daysCount = today.difference(date).inDays;
|
||||
final needToShowMarketingBottomSheet = daysCount >= 14;
|
||||
if (needToShowMarketingBottomSheet) {
|
||||
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
|
||||
}
|
||||
return needToShowMarketingBottomSheet;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
lib/core/cubit/marketing/marketing_cubit_state.dart
Normal file
3
lib/core/cubit/marketing/marketing_cubit_state.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class MarketingCubitState {
|
||||
const MarketingCubitState();
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
@@ -38,6 +39,7 @@ import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cub
|
||||
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
@@ -74,10 +76,10 @@ class CubitRegistrations {
|
||||
static void _registerFactoryCubits() {
|
||||
// Auth related cubits
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
||||
// Feed related cubits
|
||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
||||
@@ -130,7 +132,7 @@ class CubitRegistrations {
|
||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
||||
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
|
||||
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl()));
|
||||
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
||||
@@ -143,5 +145,7 @@ class CubitRegistrations {
|
||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
||||
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
||||
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
||||
class ExternalRegistrations {
|
||||
@@ -11,23 +13,20 @@ class ExternalRegistrations {
|
||||
/// Регистрация всех внешних зависимостей
|
||||
static void registerAll() {
|
||||
_registerDio();
|
||||
_registerPrefs();
|
||||
}
|
||||
|
||||
/// Регистрация Dio HTTP клиента
|
||||
static void _registerDio() {
|
||||
sl.registerLazySingleton(
|
||||
() => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: sl<AppConfig>().baseUrl,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
),
|
||||
)..interceptors.addAll([
|
||||
TokenInterceptor(),
|
||||
PhoneVerifyErrorInterceptor(),
|
||||
]),
|
||||
() =>
|
||||
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Регистрация Prefs
|
||||
static void _registerPrefs() {
|
||||
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.da
|
||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
@@ -68,6 +69,7 @@ class RepositoryRegistrations {
|
||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
|
||||
|
||||
// Content repositories
|
||||
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
||||
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
|
||||
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
|
||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
||||
import 'package:eatvio_app/features/profile/services/profile_service.dart';
|
||||
@@ -76,6 +77,7 @@ class ServiceRegistrations {
|
||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
|
||||
|
||||
// Content services
|
||||
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
||||
|
||||
@@ -29,6 +29,22 @@ class FirebaseApi {
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<void> registerAnonymousPushToken() async {
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
await sl<Dio>().post(
|
||||
'/api/anonymous_token',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushToken() async {
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
try {
|
||||
|
||||
11
lib/core/mixins/registration_date_cache_mixin.dart
Normal file
11
lib/core/mixins/registration_date_cache_mixin.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
|
||||
mixin RegistrationDateCacheMixin {
|
||||
late final Prefs prefs;
|
||||
|
||||
|
||||
Future<void> cacheRegistrationDate(DateTime date) async {
|
||||
await prefs.setInt(PrefsKeys.registrationDate, date.millisecondsSinceEpoch);
|
||||
}
|
||||
}
|
||||
43
lib/core/prefs.dart
Normal file
43
lib/core/prefs.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
class Prefs {
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
Prefs(this._prefs);
|
||||
|
||||
String _key(PrefsKeys key) => key.name;
|
||||
|
||||
Future<void> setString(PrefsKeys key, String value) async {
|
||||
await _prefs.setString(_key(key), value);
|
||||
}
|
||||
|
||||
String? getString(PrefsKeys key) {
|
||||
return _prefs.getString(_key(key));
|
||||
}
|
||||
|
||||
Future<void> setInt(PrefsKeys key, int value) async {
|
||||
await _prefs.setInt(_key(key), value);
|
||||
}
|
||||
|
||||
int? getInt(PrefsKeys key) {
|
||||
return _prefs.getInt(_key(key));
|
||||
}
|
||||
|
||||
Future<void> setBool(PrefsKeys key, bool value) async {
|
||||
await _prefs.setBool(_key(key), value);
|
||||
}
|
||||
|
||||
bool? getBool(PrefsKeys key) {
|
||||
return _prefs.getBool(_key(key));
|
||||
}
|
||||
|
||||
Future<void> remove(PrefsKeys key) async {
|
||||
await _prefs.remove(_key(key));
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await _prefs.clear();
|
||||
}
|
||||
}
|
||||
1
lib/core/prefs_keys.dart
Normal file
1
lib/core/prefs_keys.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
@@ -18,6 +20,10 @@ class AuthRoutes {
|
||||
path: RouteConstants.authScreen,
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
redirect: (context, state) {
|
||||
sl<FirebaseApi>().registerAnonymousPushToken();
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||
GoRoute(
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
||||
@@ -27,6 +29,8 @@ import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
@@ -40,7 +44,6 @@ import 'package:eatvio_app/features/finances_and_consultations_archive/presentat
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
/// Остальные маршруты приложения
|
||||
@@ -55,18 +58,24 @@ class OtherRoutes {
|
||||
builder: (context, state) => const WelcomeScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
if (welcomeScreenWasShowed == false) {
|
||||
final prefs = sl<Prefs>();
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||
await firebaseApi.registerPushToken();
|
||||
if (!welcomeScreenWasShowed) {
|
||||
return null;
|
||||
} else if (await TokenStorage.haveTokenInStorage()) {
|
||||
return RouteConstants.feedScreen;
|
||||
return '/feed-screen';
|
||||
} else {
|
||||
return RouteConstants.authScreen;
|
||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||
if (!hasAnonymousTokenBeenSent) {
|
||||
await firebaseApi.registerAnonymousPushToken();
|
||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||
}
|
||||
return '/auth-screen';
|
||||
}
|
||||
} catch (e) {
|
||||
return RouteConstants.feedScreen;
|
||||
return '/feed-screen';
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -139,7 +148,13 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.profileScreen,
|
||||
name: 'profileScreen',
|
||||
builder: (context, state) => ProfileScreen(author: state.extra as Author),
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
return ProfileScreen(slug: slug);
|
||||
}
|
||||
return ProfileScreen(slug: state.extra as String);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.subsScreen,
|
||||
@@ -161,7 +176,7 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.emptyChatScreen,
|
||||
name: 'emptyChatScreen',
|
||||
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
|
||||
builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel),
|
||||
),
|
||||
|
||||
// Consultant routes
|
||||
@@ -201,7 +216,8 @@ class OtherRoutes {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final planId = data['planId'] as int;
|
||||
final successfulPayment = data['successfulPayment'] as bool?;
|
||||
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
|
||||
final customerSlug = data['customerSlug'] as String?;
|
||||
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment, customerSlug: customerSlug);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -224,6 +240,11 @@ class OtherRoutes {
|
||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: PersonalDietSuggestionScreen.path,
|
||||
name: PersonalDietSuggestionScreen.name,
|
||||
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,4 +249,10 @@ abstract class TextStyles {
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.255,
|
||||
);
|
||||
|
||||
static const textFS24FW700 = TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.255,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class PrimaryColors {
|
||||
// Blue
|
||||
Color get blue400 => const Color(0XFF43A6F2);
|
||||
|
||||
Color get blueA400 => const Color(0XFF2F80ED);
|
||||
Color get blueA400 => const Color(0XFF2F80ED);
|
||||
// BlueGray
|
||||
Color get blueGray400 => const Color(0XFF7B8794);
|
||||
|
||||
@@ -141,6 +141,8 @@ class PrimaryColors {
|
||||
|
||||
Color get gray60003 => const Color(0XFF707070);
|
||||
|
||||
Color get gray60005 => const Color(0xFF6C7C93);
|
||||
|
||||
// Green
|
||||
Color get greenA700 => const Color(0XFF019A3E);
|
||||
|
||||
@@ -148,6 +150,8 @@ class PrimaryColors {
|
||||
|
||||
Color get green => const Color(0XFF0BB450);
|
||||
|
||||
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
|
||||
|
||||
// LightGreen
|
||||
Color get lightGreen300 => const Color(0XFFB2DF8A);
|
||||
|
||||
|
||||
124
lib/core/widgets/marketing_bottom_sheet.dart
Normal file
124
lib/core/widgets/marketing_bottom_sheet.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
void showMarketingBottomSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
|
||||
child: const MarketingBottomSheet(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MarketingBottomSheet extends StatelessWidget {
|
||||
const MarketingBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsetsGeometry.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(image: AssetImage('assets/images/logo_bg.png'), fit: BoxFit.cover),
|
||||
),
|
||||
child: Center(child: SvgPicture.asset('assets/icons/logo_2.svg', height: 90)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(s.congratulations, style: TextStyles.textFS24FW700.copyWith(color: appTheme.blueA400)),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: appTheme.lightGray0, borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: appTheme.blueA400),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text('14', style: TextStyles.textFS24FW700.copyWith(color: appTheme.whiteA700)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: s.youWithUs,
|
||||
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.gray60005),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: s.fourteenDays,
|
||||
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
s.weAreLookingFor,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.letsInvite,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.invite,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
leftIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Transform.rotate(
|
||||
angle: 3.14,
|
||||
child: SvgPicture.asset('assets/icons/share.svg', width: 14, height: 14, colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn)),
|
||||
),
|
||||
),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
SharePlus.instance.share(ShareParams(uri: Uri.parse('https://promo.eatvio.ru/')));
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.eatvioIsAHelthyHabbit,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,10 @@ import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionWidget extends StatelessWidget {
|
||||
const OptionWidget({super.key, required this.option, this.disableDuration = false});
|
||||
const OptionWidget({super.key, required this.option, this.disableDuration = false, required this.duration});
|
||||
final PacketOptions? option;
|
||||
final bool disableDuration;
|
||||
final int duration;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (option == null) {
|
||||
@@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (!disableDuration) ...[
|
||||
const _OptionDurationWidget(duration: 7),
|
||||
const SizedBox(height: 16),
|
||||
_OptionDurationWidget(duration: duration),
|
||||
SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12),
|
||||
],
|
||||
if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[
|
||||
const _OptionHasPersonalDietWidget(),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 16),
|
||||
@@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _OptionDurationWidget extends StatelessWidget {
|
||||
const _OptionDurationWidget({required this.duration});
|
||||
final int duration;
|
||||
@@ -92,3 +96,43 @@ class _OptionDurationWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionHasPersonalDietWidget extends StatelessWidget {
|
||||
const _OptionHasPersonalDietWidget();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: appTheme.transparentGreen),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Container(
|
||||
width: 4,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.green,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(100),
|
||||
bottomRight: Radius.circular(100),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(
|
||||
s.personalDietCreating,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ abstract class YandexProfileModel with _$YandexProfileModel {
|
||||
@JsonKey(name: 'real_name') required String realName,
|
||||
@JsonKey(name: 'first_name') required String firstName,
|
||||
@JsonKey(name: 'last_name') required String lastName,
|
||||
@JsonKey(name: 'sex') required String sex,
|
||||
@JsonKey(name: 'sex') String? sex,
|
||||
@JsonKey(name: 'default_email') required String defaultEmail,
|
||||
@JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone,
|
||||
}) = _YandexProfileModel;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import 'dart:developer';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class OAuthCubit extends Cubit<OAuthState> {
|
||||
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
||||
final AuthRepository authRepository;
|
||||
|
||||
OAuthCubit({required this.authRepository}) : super(const OAuthState());
|
||||
OAuthCubit({required this.authRepository, required Prefs prefs}) : super(const OAuthState()) {
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
|
||||
try {
|
||||
@@ -63,6 +67,7 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
clientId: clientId,
|
||||
);
|
||||
TokenStorage.saveTokenInStorage(token);
|
||||
cacheRegistrationDate(DateTime.now());
|
||||
emit(OAuthState.loaded(token: token));
|
||||
} on DioException catch (e) {
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
|
||||
@@ -11,7 +11,9 @@ class BlogPostCubit extends Cubit<BlogPostState> {
|
||||
Future<void> getBlogPost(int id) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
final postModel = await feedRepository.getBlogPost(id: id);
|
||||
|
||||
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class BlogPostScreen extends StatefulWidget {
|
||||
final int id;
|
||||
@@ -198,7 +199,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: blogPostState.blogPostModel?.author),
|
||||
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: blogPostState.blogPostModel?.author.authorSlug),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
@@ -231,10 +232,35 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
),
|
||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Html(
|
||||
data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'),
|
||||
style: {"*": Style(color: Colors.black87)},
|
||||
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
|
||||
style: {
|
||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -16,7 +16,7 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
|
||||
return true;
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
emit(const BlogPostCreateState.loaded());
|
||||
emit(BlogPostCreateState.error(errorMessage: error.toString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
|
||||
return true;
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
emit(const BlogPostCreateState.loaded());
|
||||
emit(BlogPostCreateState.error(errorMessage: error.toString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
@@ -24,10 +25,7 @@ import 'package:permission_handler/permission_handler.dart';
|
||||
class BlogPostCreateScreen extends StatefulWidget {
|
||||
final FeedBlogPostModel? feedBlogPostModel;
|
||||
|
||||
const BlogPostCreateScreen({
|
||||
super.key,
|
||||
this.feedBlogPostModel,
|
||||
});
|
||||
const BlogPostCreateScreen({super.key, this.feedBlogPostModel});
|
||||
|
||||
@override
|
||||
State<BlogPostCreateScreen> createState() => _BlogPostCreateScreenState();
|
||||
@@ -42,6 +40,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
final descriptionController = TextEditingController();
|
||||
final videoUrlController = TextEditingController();
|
||||
bool isLoading = false;
|
||||
bool enableMdPanel = false;
|
||||
bool isPreviewMode = false;
|
||||
final FocusNode descriptionFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -50,6 +51,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
|
||||
setState(() {
|
||||
shrinkImage = visible;
|
||||
enableMdPanel = visible;
|
||||
});
|
||||
});
|
||||
if (widget.feedBlogPostModel != null) {
|
||||
@@ -62,9 +64,326 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
keyboardSubscription.cancel();
|
||||
descriptionFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _hasFormatting(String prefix, String suffix) {
|
||||
final text = descriptionController.text;
|
||||
final selection = descriptionController.selection;
|
||||
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
|
||||
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
|
||||
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
|
||||
return beforeCursor == prefix && afterCursor == suffix;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _removeFormatting(String prefix, String suffix) {
|
||||
final text = descriptionController.text;
|
||||
final selection = descriptionController.selection;
|
||||
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
|
||||
|
||||
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
|
||||
descriptionController.selection = newSelection;
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
|
||||
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
|
||||
final afterCursor = text.substring(cursorPosition + suffix.length);
|
||||
final newText = beforeCursor + afterCursor;
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newCursorPosition = cursorPosition - prefix.length;
|
||||
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _applyMarkdownFormat(String prefix, String suffix) {
|
||||
final text = descriptionController.text;
|
||||
final selection = descriptionController.selection;
|
||||
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final formattedText = '$prefix$selectedText$suffix';
|
||||
|
||||
final newText = text.replaceRange(selection.start, selection.end, formattedText);
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
|
||||
descriptionController.selection = newSelection;
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newCursorPosition = cursorPosition + prefix.length;
|
||||
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyHeading() {
|
||||
if (_hasFormatting('# ', '')) {
|
||||
_removeFormatting('# ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('# ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBold() {
|
||||
if (_hasFormatting('**', '**')) {
|
||||
_removeFormatting('**', '**');
|
||||
} else {
|
||||
_applyMarkdownFormat('**', '**');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyItalic() {
|
||||
if (_hasFormatting('*', '*')) {
|
||||
_removeFormatting('*', '*');
|
||||
} else {
|
||||
_applyMarkdownFormat('*', '*');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyNumberedList() {
|
||||
if (_hasFormatting('1. ', '')) {
|
||||
_removeFormatting('1. ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('1. ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBulletedList() {
|
||||
if (_hasFormatting('- ', '')) {
|
||||
_removeFormatting('- ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('- ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyLink() {
|
||||
final text = descriptionController.text;
|
||||
final selection = descriptionController.selection;
|
||||
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
|
||||
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
|
||||
if (linkRegex.hasMatch(selectedText)) {
|
||||
final match = linkRegex.firstMatch(selectedText);
|
||||
final linkText = match?.group(1) ?? selectedText;
|
||||
final newText = text.replaceRange(selection.start, selection.end, linkText);
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
|
||||
descriptionController.selection = newSelection;
|
||||
} else {
|
||||
final formattedText = '[$selectedText](url)';
|
||||
final newText = text.replaceRange(selection.start, selection.end, formattedText);
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newSelection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
|
||||
descriptionController.selection = newSelection;
|
||||
}
|
||||
} else {
|
||||
_applyMarkdownFormat('[', '](url)');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyHorizontalRule() {
|
||||
final cursorPosition = descriptionController.selection.baseOffset;
|
||||
final text = descriptionController.text;
|
||||
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
|
||||
descriptionController.text = newText;
|
||||
|
||||
final newCursorPosition = cursorPosition + 5;
|
||||
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
|
||||
}
|
||||
|
||||
void _togglePreview() {
|
||||
setState(() {
|
||||
isPreviewMode = !isPreviewMode;
|
||||
});
|
||||
|
||||
if (!isPreviewMode) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
descriptionFocusNode.requestFocus();
|
||||
});
|
||||
} else {
|
||||
descriptionFocusNode.unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMarkdownPreview() {
|
||||
return Container(
|
||||
height: 120,
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF6F6F6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFE0E0E0)),
|
||||
),
|
||||
child: SingleChildScrollView(child: _parseMarkdown(descriptionController.text)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _parseMarkdown(String text) {
|
||||
if (text.isEmpty) {
|
||||
return const Text(
|
||||
'Описание',
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF323F4B), fontWeight: FontWeight.w500),
|
||||
);
|
||||
}
|
||||
|
||||
final lines = text.split('\n');
|
||||
final widgets = <Widget>[];
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final line = lines[i];
|
||||
|
||||
if (line.trim().isEmpty) {
|
||||
widgets.add(const SizedBox(height: 8));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('# ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(2),
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('## ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(3),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('### ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(4),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(child: _parseInlineMarkdown(line.substring(2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
|
||||
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
|
||||
final number = match?.group(1) ?? '1';
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$number. ', style: const TextStyle(fontSize: 16)),
|
||||
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (line.trim() == '---') {
|
||||
widgets.add(const Divider(color: Colors.grey, thickness: 1));
|
||||
} else {
|
||||
widgets.add(_parseInlineMarkdown(line));
|
||||
}
|
||||
|
||||
if (i < lines.length - 1) {
|
||||
widgets.add(const SizedBox(height: 4));
|
||||
}
|
||||
}
|
||||
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
|
||||
}
|
||||
|
||||
Widget _parseInlineMarkdown(String text) {
|
||||
final spans = <TextSpan>[];
|
||||
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
|
||||
int lastEnd = 0;
|
||||
|
||||
for (final match in regex.allMatches(text)) {
|
||||
if (match.start > lastEnd) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastEnd, match.start),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final matchedText = match.group(0)!;
|
||||
|
||||
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: matchedText.substring(2, matchedText.length - 2),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: matchedText.substring(1, matchedText.length - 1),
|
||||
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
|
||||
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
|
||||
if (linkMatch != null) {
|
||||
final linkText = linkMatch.group(1)!;
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: linkText,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lastEnd = match.end;
|
||||
}
|
||||
|
||||
if (lastEnd < text.length) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastEnd),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RichText(text: TextSpan(children: spans));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<BlogPostCreateCubit>(
|
||||
@@ -102,13 +421,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
@@ -126,96 +439,102 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mdButton(IconData icon, Function() onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mdTextButton(String text, Function() onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Scaffold _buildBlogPostCreate(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: SizedBox(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'createBlogPost',
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
backgroundColor: appTheme.blueA400,
|
||||
label: isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text(
|
||||
'Создать',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
floatingActionButton: enableMdPanel || isPreviewMode
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!isPreviewMode) ...[
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width - 76,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
_mdTextButton('H', _applyHeading),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_bold, _applyBold),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_italic, _applyItalic),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_numbered, _applyNumberedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.link, _applyLink),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
|
||||
),
|
||||
child: Row(children: [_mdButton(isPreviewMode ? Icons.edit : Icons.visibility, _togglePreview)]),
|
||||
),
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
if (widget.feedBlogPostModel != null) {
|
||||
final result = await blogPostCubit.updateBlogPost(
|
||||
id: widget.feedBlogPostModel!.id,
|
||||
title: titleController.text,
|
||||
description: descriptionController.text,
|
||||
videoUrl: videoUrlController.text,
|
||||
image: image,
|
||||
oldImageUrl: widget.feedBlogPostModel!.image,
|
||||
);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
if (!result) {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: S.current.could_not_update_blog_post);
|
||||
} else {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
}
|
||||
} else {
|
||||
final result = await blogPostCubit.createBlogPost(
|
||||
title: titleController.text,
|
||||
description: descriptionController.text,
|
||||
videoUrl: videoUrlController.text,
|
||||
image: image,
|
||||
);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
if (!result) {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
|
||||
} else {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
appBar: AppBar(),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -224,17 +543,19 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
const SizedBox(height: 16),
|
||||
const Text('Заголовок'),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
controller: titleController,
|
||||
),
|
||||
CustomTextFormField(key: AppKeys.blogPostCreateScreenTitleTextField, controller: titleController),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Описание'),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
isPreviewMode
|
||||
? _buildMarkdownPreview()
|
||||
: CustomTextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
controller: descriptionController,
|
||||
focusNode: descriptionFocusNode,
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (videoUrlController.text.isEmpty) const Text('Изображение поста'),
|
||||
if (videoUrlController.text.isEmpty) const SizedBox(height: 8),
|
||||
@@ -243,16 +564,75 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
if (image == null && widget.feedBlogPostModel?.image == null) const Text('Ссылка на видео'),
|
||||
if (image == null && widget.feedBlogPostModel?.image == null) const SizedBox(height: 8),
|
||||
if (image == null && widget.feedBlogPostModel?.image == null)
|
||||
CustomTextFormField(
|
||||
controller: videoUrlController,
|
||||
),
|
||||
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
|
||||
const SizedBox(height: 80),
|
||||
_createBlogPostButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createBlogPostButton() {
|
||||
return InkWell(
|
||||
onTap: isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
if (widget.feedBlogPostModel != null) {
|
||||
final result = await blogPostCubit.updateBlogPost(
|
||||
id: widget.feedBlogPostModel!.id,
|
||||
title: titleController.text,
|
||||
description: descriptionController.text,
|
||||
videoUrl: videoUrlController.text,
|
||||
image: image,
|
||||
oldImageUrl: widget.feedBlogPostModel!.image,
|
||||
);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
if (!result) {
|
||||
if (!mounted) return;
|
||||
showErrorDialog(context, title: S.current.could_not_update_blog_post);
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
context.pop();
|
||||
}
|
||||
} else {
|
||||
final result = await blogPostCubit.createBlogPost(
|
||||
title: titleController.text,
|
||||
description: descriptionController.text,
|
||||
videoUrl: videoUrlController.text,
|
||||
image: image,
|
||||
);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
if (!result) {
|
||||
if (!mounted) return;
|
||||
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: const Center(
|
||||
child: Text('Создать', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildAddPhoto() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -265,22 +645,14 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
height: shrinkImage ? 52 : 128,
|
||||
curve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: AppDecoration.fillGray.copyWith(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: AppDecoration.fillGray.copyWith(borderRadius: BorderRadius.circular(16)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.photo_library,
|
||||
color: appTheme.blueGray300,
|
||||
),
|
||||
Icon(Icons.photo_library, color: appTheme.blueGray300),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Добавить фото",
|
||||
selectionColor: appTheme.blueA400,
|
||||
),
|
||||
Text("Добавить фото", selectionColor: appTheme.blueA400),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -289,13 +661,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image(
|
||||
image: FileImage(
|
||||
File(
|
||||
image!.path,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Image(image: FileImage(File(image!.path))),
|
||||
),
|
||||
),
|
||||
if (widget.feedBlogPostModel?.image != null && image == null)
|
||||
@@ -303,9 +669,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image(
|
||||
image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}'),
|
||||
),
|
||||
child: Image(image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}')),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -18,7 +18,6 @@ import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
@@ -115,25 +114,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
ChosenPlanStateEnum.statePendingPayment,
|
||||
ChosenPlanStateEnum.stateInCoordination,
|
||||
].contains(chosenPlan?.chosenPlanState);
|
||||
|
||||
final showAboutOrderButton =
|
||||
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
|
||||
BannerType? bannerType;
|
||||
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
bannerType = null;
|
||||
} else if (amIConsultant && !state.hasCard) {
|
||||
bannerType = BannerType.card;
|
||||
} else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.date;
|
||||
} else if (!amIConsultant &&
|
||||
(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) {
|
||||
bannerType = BannerType.payment;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) {
|
||||
bannerType = BannerType.collectCharge;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
|
||||
bannerType = BannerType.finishPlan;
|
||||
}
|
||||
|
||||
PacketOptions? chosenOption;
|
||||
if (chosenPlan?.optionType == 'options1') {
|
||||
chosenOption = chosenPlan?.plan.packet1Options;
|
||||
@@ -142,6 +122,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
} else if (chosenPlan?.optionType == 'options3') {
|
||||
chosenOption = chosenPlan?.plan.packet3Options;
|
||||
}
|
||||
|
||||
final showAboutOrderButton =
|
||||
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
|
||||
BannerType? bannerType;
|
||||
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
bannerType = null;
|
||||
} else if (amIConsultant && !state.hasCard && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.card;
|
||||
} else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.date;
|
||||
} else if (!amIConsultant &&
|
||||
(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) {
|
||||
bannerType = BannerType.payment;
|
||||
} else if (chosenOption?.hasPersonalDiet == true && amIConsultant) {
|
||||
bannerType = BannerType.setPersonalDiet;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) {
|
||||
bannerType = BannerType.collectCharge;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
|
||||
bannerType = BannerType.finishPlan;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
@@ -150,15 +151,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
centerTitle: false,
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: state.chatRoom.users.receiver.slug,
|
||||
authorName: state.chatRoom.users.receiver.name,
|
||||
authorProfileIcon: state.chatRoom.users.receiver.profilePhotoUrl ?? '',
|
||||
authorIsMe: false,
|
||||
),
|
||||
);
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -326,6 +319,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
chosenPlanId: chosenPlan?.id,
|
||||
planName: chosenPlan?.plan.name,
|
||||
planPrice: chosenOption?.price.toDouble(),
|
||||
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -453,7 +447,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
|
||||
if (isBlocked) {
|
||||
@@ -468,7 +462,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
||||
chatRoomCubit.sendMessage(
|
||||
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
||||
@@ -503,6 +497,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
left: 16,
|
||||
child: AboutOrderButton(
|
||||
onTap: () async {
|
||||
if (amIConsultant) {
|
||||
GoRouter.of(
|
||||
context,
|
||||
).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id, 'customerSlug': state.chatRoom.users.receiver.slug});
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
|
||||
},
|
||||
),
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -16,12 +16,9 @@ import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
||||
|
||||
class EmptyChatScreen extends StatefulWidget {
|
||||
final Author author;
|
||||
final ProfileModel user;
|
||||
|
||||
const EmptyChatScreen({
|
||||
super.key,
|
||||
required this.author,
|
||||
});
|
||||
const EmptyChatScreen({super.key, required this.user});
|
||||
|
||||
@override
|
||||
State<EmptyChatScreen> createState() => _EmptyChatScreenState();
|
||||
@@ -40,34 +37,19 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
centerTitle: false,
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: widget.author.authorSlug,
|
||||
authorName: widget.author.authorName,
|
||||
authorProfileIcon: widget.author.authorProfileIcon,
|
||||
authorIsMe: false,
|
||||
),
|
||||
);
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: widget.user.slug);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
foregroundImage: NetworkImage(widget.author.authorProfileIcon),
|
||||
foregroundImage: NetworkImage(widget.user.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.author.authorName,
|
||||
style: CustomTextStyles.titleSmallBold.copyWith(
|
||||
color: PrimaryColors().black,
|
||||
),
|
||||
),
|
||||
],
|
||||
children: [Text(widget.user.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black))],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -84,18 +66,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () => showImagePicker(widget.author.authorSlug),
|
||||
onTap: () => showImagePicker(widget.user.slug),
|
||||
customBorder: const CircleBorder(),
|
||||
child: Ink(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Transform.rotate(
|
||||
angle: pi / 4,
|
||||
child: const Icon(
|
||||
Icons.attach_file_rounded,
|
||||
size: 30,
|
||||
color: Color(0xFF858E99),
|
||||
),
|
||||
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -117,8 +95,10 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
hintText: 'Сообщение',
|
||||
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
||||
filled: true,
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
@@ -139,40 +119,29 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.user.slug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
const SnackBar(content: Text('Вы не можете отправить сообщение заблокированному пользователю'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (textEditingController.text.isNotEmpty) {
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
data: textEditingController.text.trim(),
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
await sl<ChatRepository>().sendMessage(data: textEditingController.text.trim(), slug: widget.user.slug);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
}
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||
if (context.mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -201,53 +170,36 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
|
||||
// Проверяем, заблокирован ли пользователь
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.user.slug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
const SnackBar(content: Text('Вы не можете отправить сообщение заблокированному пользователю'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final picker = ImagePicker();
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
List<File> files = images.map((image) => File(image.path)).toList();
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
files: files,
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
await sl<ChatRepository>().sendMessage(files: files, slug: widget.user.slug);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_library_rounded),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
@@ -256,28 +208,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
List<File> files = images.map((image) => File(image.path)).toList();
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
files: files,
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
await sl<ChatRepository>().sendMessage(files: files, slug: widget.user.slug);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
@@ -308,35 +246,17 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
);
|
||||
if (result != null) {
|
||||
List<File> files = result.paths.map((path) => File(path!)).toList();
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
files: files,
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
await sl<ChatRepository>().sendMessage(files: files, slug: widget.user.slug);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.file_present_rounded),
|
||||
SizedBox(width: 8),
|
||||
Text('Файлы'),
|
||||
],
|
||||
),
|
||||
child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,17 +9,28 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class ChosePlanDateWidget extends StatefulWidget {
|
||||
const ChosePlanDateWidget({super.key, required this.chatCubit});
|
||||
const ChosePlanDateWidget({super.key, required this.chatCubit, required this.hasPersonalDiet});
|
||||
final ChatRoomCubit chatCubit;
|
||||
|
||||
final bool hasPersonalDiet;
|
||||
@override
|
||||
State<ChosePlanDateWidget> createState() => _ChosePlanDateWidget();
|
||||
}
|
||||
|
||||
class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
||||
final startDate = [DateTime.now()];
|
||||
late final List<DateTime> startDate;
|
||||
bool visible = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.hasPersonalDiet) {
|
||||
startDate = [DateTime.now().add(const Duration(days: 2))];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -27,64 +38,129 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
||||
if (widget.chatCubit.state is ChatRoomStateLoaded) {
|
||||
planName = (widget.chatCubit.state as ChatRoomStateLoaded).chatRoom.chosenPlan?.plan.name;
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
planName ?? '',
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
planName ?? '',
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
if (widget.hasPersonalDiet) ...[
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
PortalTarget(
|
||||
closeDuration: const Duration(milliseconds: 20),
|
||||
anchor: const Aligned(
|
||||
shiftToWithinBound: AxisFlag(x: true, y: true),
|
||||
follower: Alignment.topRight,
|
||||
target: Alignment.bottomLeft,
|
||||
),
|
||||
portalFollower: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: visible ? appTheme.whiteA700 : Colors.transparent,
|
||||
boxShadow: visible
|
||||
? [
|
||||
BoxShadow(
|
||||
color: appTheme.black900.withValues(alpha: 0.15),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: TextStyles.textMMediumAuto.copyWith(
|
||||
color: visible ? appTheme.black : Colors.transparent,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
s.chosenPlanStartDateDescr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
visible = !visible;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
width: 22,
|
||||
height: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25.0),
|
||||
child: Text(
|
||||
s.chose_plan_start_date_2,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25.0),
|
||||
child: Text(
|
||||
s.chose_plan_start_date_2,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
CalendarWidget(
|
||||
startDate: startDate,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
|
||||
if (result) {
|
||||
showSuccessToast('Дата установлена');
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
showErrorToast('Не удалось установить дату');
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
height: 60,
|
||||
text: s.confirm,
|
||||
buttonTextStyle: TextStyles.textLSemibold.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
),
|
||||
buttonStyle: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
const SizedBox(height: 12),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
CalendarWidget(
|
||||
startDate: startDate,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
|
||||
if (result) {
|
||||
showSuccessToast('Дата установлена');
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
showErrorToast('Не удалось установить дату');
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
height: 60,
|
||||
text: s.confirm,
|
||||
buttonTextStyle: TextStyles.textLSemibold.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
),
|
||||
buttonStyle: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -17,12 +18,12 @@ import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class NotificationBanner extends StatefulWidget {
|
||||
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId});
|
||||
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId, this.hasPersonalDiet = false});
|
||||
final BannerType bannerType;
|
||||
final double? planPrice;
|
||||
final String? planName;
|
||||
final int? chosenPlanId;
|
||||
|
||||
final bool hasPersonalDiet;
|
||||
@override
|
||||
State<NotificationBanner> createState() => _NotificationBannerState();
|
||||
}
|
||||
@@ -68,7 +69,8 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
||||
BannerType.date => [
|
||||
SizedBox(
|
||||
width: size.width * 0.6,
|
||||
child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
child: Text(widget.hasPersonalDiet == true ? s.choose_plan_start_date_with_personal_diet : s.choose_plan_start_date,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
@@ -78,6 +80,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
||||
context: context,
|
||||
builder: (context) => ChosePlanDateWidget(
|
||||
chatCubit: cubit,
|
||||
hasPersonalDiet: widget.hasPersonalDiet,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -155,6 +158,20 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
BannerType.setPersonalDiet => [
|
||||
SizedBox(
|
||||
width: size.width * 0.6,
|
||||
child: Text(s.createPersonalDiet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(MyDietsScreen.name);
|
||||
},
|
||||
child: Text(s.goToMyDiets, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -242,4 +259,4 @@ class _StarIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
enum BannerType { date, card, payment, collectCharge, finishPlan }
|
||||
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet }
|
||||
|
||||
@@ -22,6 +22,7 @@ abstract class ConsultationModel with _$ConsultationModel {
|
||||
@JsonKey(name: 'end_date', fromJson: parseDate) required DateTime endDate,
|
||||
@JsonKey(name: 'consultant_id') required int consultantId,
|
||||
required List<DiaryModel> diaries,
|
||||
@JsonKey(name: 'personal_diet_id') required int? personalDietId,
|
||||
}) = _ConsultationModel;
|
||||
|
||||
factory ConsultationModel.fromJson(Map<String, dynamic> json) => _$ConsultationModelFromJson(json);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -54,6 +55,8 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
final nameController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||
bool _mdToolbarVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -76,9 +79,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(
|
||||
title: state.dietData.name ?? 'Диета',
|
||||
),
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
|
||||
);
|
||||
});
|
||||
} else if (state.drafted) {
|
||||
@@ -87,15 +88,15 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(
|
||||
title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
|
||||
),
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
@@ -124,34 +125,23 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
context.read<CreateDietCubit>().setName(value);
|
||||
},
|
||||
),
|
||||
if (state.dietNameError != null) ...[
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ValidationErrorWidget(error: state.dietNameError ?? ''),
|
||||
],
|
||||
if (state.dietNameError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietNameError ?? '')],
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 132,
|
||||
child: CustomTextFormField(
|
||||
hasError: state.dietDescriptionError != null,
|
||||
controller: descriptionController,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
maxLines: null,
|
||||
minLines: null,
|
||||
expands: true,
|
||||
hintText: s.enter_diet_description,
|
||||
onChanged: (value) {
|
||||
context.read<CreateDietCubit>().setDetails(value);
|
||||
},
|
||||
),
|
||||
MarkdownEditor(
|
||||
key: _mdKey,
|
||||
initialText: descriptionController.text,
|
||||
hintText: s.enter_diet_description,
|
||||
hasError: state.dietDescriptionError != null,
|
||||
minLines: 6,
|
||||
maxLines: 8,
|
||||
showInlineToolbar: false,
|
||||
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
|
||||
onChanged: (value) {
|
||||
descriptionController.text = value;
|
||||
context.read<CreateDietCubit>().setDetails(value);
|
||||
},
|
||||
),
|
||||
if (state.dietDescriptionError != null) ...[
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ValidationErrorWidget(error: state.dietDescriptionError ?? ''),
|
||||
],
|
||||
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
|
||||
const SizedBox(height: 12),
|
||||
const DietDurationDropDownWidget(),
|
||||
const SizedBox(height: 12),
|
||||
@@ -171,9 +161,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${s.diet_plan} (${state.dietData.duration ?? 0} ${getDayForm(state.dietData.duration ?? 0, s)})',
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -196,9 +184,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
onPressed: () async {
|
||||
context.read<CreateDietCubit>().setDietState(DietState.draft);
|
||||
if (widget.diet != null) {
|
||||
final result = await context.read<CreateDietCubit>().updateDiet(
|
||||
widget.diet!.id,
|
||||
);
|
||||
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
|
||||
if (result == 'error') {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'dart:developer';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -47,6 +48,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
type['veg'] = diet.type.veg ?? false;
|
||||
type['carb'] = diet.type.carb ?? false;
|
||||
type['light'] = diet.type.light ?? false;
|
||||
final appConfig = sl<AppConfig>();
|
||||
emit(
|
||||
state.copyWith.dietData(
|
||||
dailySchedule: dailySchedule,
|
||||
@@ -55,7 +57,8 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
name: diet.name,
|
||||
duration: diet.duration,
|
||||
type: type,
|
||||
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${diet.image}',
|
||||
// Avoid network image URLs during tests to prevent HTTP fetch attempts
|
||||
imageUrl: appConfig.buildType == AppBuildType.tests ? null : '${appConfig.baseUrl}/storage/${diet.image}',
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
|
||||
@@ -76,11 +79,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
|
||||
if (currentDaysValue != null && dailySchedule != null) {
|
||||
final deepCopyOfDailySchedule = dailySchedule
|
||||
.map(
|
||||
(day) => day.map((meal) => [...meal]).toList(),
|
||||
)
|
||||
.toList();
|
||||
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||
|
||||
if (currentDaysValue > selectedDaysValue) {
|
||||
final difference = currentDaysValue - selectedDaysValue;
|
||||
@@ -93,28 +92,16 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
deepCopyOfDailySchedule.add([[], [], [], [], []]);
|
||||
}
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
dietData: state.dietData.copyWith(
|
||||
dailySchedule: deepCopyOfDailySchedule,
|
||||
),
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule)));
|
||||
} else {
|
||||
final List<List<List<DailySchedule>>> dailySchedule = List.generate(selectedDaysValue, (index) => [[], [], [], [], []]);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
dietData: state.dietData.copyWith(dailySchedule: dailySchedule),
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule)));
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
durationIndex: index,
|
||||
dietData: state.dietData.copyWith(
|
||||
duration: durationValues[index],
|
||||
),
|
||||
dietData: state.dietData.copyWith(duration: durationValues[index]),
|
||||
dietDurationError: null,
|
||||
),
|
||||
);
|
||||
@@ -125,7 +112,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
}
|
||||
|
||||
void setImage(XFile? image, {String? imageUrl}) {
|
||||
emit(state.copyWith(dietData: state.dietData.copyWith(image: image, imageUrl: imageUrl), dietImageError: null));
|
||||
emit(
|
||||
state.copyWith(
|
||||
dietData: state.dietData.copyWith(image: image, imageUrl: imageUrl),
|
||||
dietImageError: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setDailySchedule(List<List<List<DailySchedule>>> dailySchedule) {
|
||||
@@ -133,27 +125,25 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
}
|
||||
|
||||
void setDietState(DietState dietState) {
|
||||
emit(state.copyWith(dietData: state.dietData.copyWith(state: dietState), dietState: dietState, dietStateError: null));
|
||||
emit(
|
||||
state.copyWith(
|
||||
dietData: state.dietData.copyWith(state: dietState),
|
||||
dietState: dietState,
|
||||
dietStateError: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setDietType({required String key, required bool value}) {
|
||||
final type = Map<String, bool>.from(state.dietData.type);
|
||||
type[key] = value;
|
||||
emit(
|
||||
state.copyWith(
|
||||
dietData: state.dietData.copyWith(type: type),
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(dietData: state.dietData.copyWith(type: type)));
|
||||
}
|
||||
|
||||
void addPortion({required int dayIndex, required int mealIndex, required DailySchedule portion}) {
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
if (dailySchedule != null) {
|
||||
final deepCopyOfDailySchedule = dailySchedule
|
||||
.map(
|
||||
(day) => day.map((meal) => [...meal]).toList(),
|
||||
)
|
||||
.toList();
|
||||
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||
deepCopyOfDailySchedule[dayIndex][mealIndex].add(portion);
|
||||
|
||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||
@@ -162,18 +152,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
}
|
||||
}
|
||||
|
||||
void removePortion({
|
||||
required int day,
|
||||
required int meal,
|
||||
required int portionIndex,
|
||||
}) {
|
||||
void removePortion({required int day, required int meal, required int portionIndex}) {
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
if (dailySchedule != null) {
|
||||
final deepCopyOfDailySchedule = dailySchedule
|
||||
.map(
|
||||
(day) => day.map((meal) => [...meal]).toList(),
|
||||
)
|
||||
.toList();
|
||||
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||
deepCopyOfDailySchedule[day][meal].removeAt(portionIndex);
|
||||
|
||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||
@@ -184,11 +166,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
void updatePortion({required int dayIndex, required int mealIndex, required int portionIndex, required DailySchedule portion}) {
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
if (dailySchedule != null) {
|
||||
final deepCopyOfDailySchedule = dailySchedule
|
||||
.map(
|
||||
(day) => day.map((meal) => [...meal]).toList(),
|
||||
)
|
||||
.toList();
|
||||
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||
deepCopyOfDailySchedule[dayIndex][mealIndex][portionIndex] = portion;
|
||||
|
||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||
@@ -199,11 +177,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
void copyDay(int dayIndex) {
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
if (dailySchedule != null) {
|
||||
final deepCopyOfDailySchedule = dailySchedule
|
||||
.map(
|
||||
(day) => day.map((meal) => [...meal]).toList(),
|
||||
)
|
||||
.toList();
|
||||
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||
final copiedDay = deepCopyOfDailySchedule[dayIndex];
|
||||
emit(state.copyWith(copiedDay: copiedDay));
|
||||
}
|
||||
@@ -213,11 +187,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
final copiedDay = state.copiedDay;
|
||||
if (dailySchedule != null && copiedDay != null) {
|
||||
final deepCopyOfDailySchedule = dailySchedule
|
||||
.map(
|
||||
(day) => day.map((meal) => [...meal]).toList(),
|
||||
)
|
||||
.toList();
|
||||
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||
deepCopyOfDailySchedule[dayIndex] = copiedDay;
|
||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||
emit(state.copyWith(dietData: newDietData));
|
||||
@@ -268,14 +238,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
final dailyNutrition = <DailyNutrition>[];
|
||||
if (carbs != null && calories != null && fats != null && proteins != null) {
|
||||
for (int i = 0; i < carbs.length; i++) {
|
||||
dailyNutrition.add(
|
||||
DailyNutrition(
|
||||
carbohydrates: carbs[i],
|
||||
energy: calories[i],
|
||||
fats: fats[i],
|
||||
proteins: proteins[i],
|
||||
),
|
||||
);
|
||||
dailyNutrition.add(DailyNutrition(carbohydrates: carbs[i], energy: calories[i], fats: fats[i], proteins: proteins[i]));
|
||||
}
|
||||
}
|
||||
final dailySchedule = <int, Map<int, Map<int, DailySchedule>>>{};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -28,16 +29,21 @@ class CreatePlanScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<CreatePlanCubit>(),
|
||||
child: const CreatePlanScreenContent(),
|
||||
);
|
||||
return BlocProvider(create: (context) => sl<CreatePlanCubit>(), child: const CreatePlanScreenContent());
|
||||
}
|
||||
}
|
||||
|
||||
class CreatePlanScreenContent extends StatelessWidget {
|
||||
class CreatePlanScreenContent extends StatefulWidget {
|
||||
const CreatePlanScreenContent({super.key});
|
||||
|
||||
@override
|
||||
State<CreatePlanScreenContent> createState() => _CreatePlanScreenContentState();
|
||||
}
|
||||
|
||||
class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||
bool _mdToolbarVisible = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -51,15 +57,14 @@ class CreatePlanScreenContent extends StatelessWidget {
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(
|
||||
title: state.planData.name ?? 'План',
|
||||
isPlan: true,
|
||||
),
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.planData.name ?? 'План', isPlan: true),
|
||||
);
|
||||
});
|
||||
}
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
@@ -104,10 +109,7 @@ class CreatePlanScreenContent extends StatelessWidget {
|
||||
},
|
||||
maxLength: 150,
|
||||
),
|
||||
if (state.validationState.titleHasError) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
if (state.validationState.titleHasError) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.atLeast3Symbols)],
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -140,20 +142,17 @@ class CreatePlanScreenContent extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 132,
|
||||
child: CustomTextFormField(
|
||||
hasError: state.validationState.descriptionHasError,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
maxLength: 1000,
|
||||
maxLines: null,
|
||||
minLines: null,
|
||||
expands: true,
|
||||
hintText: s.plan_desc_example,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setDetails(value);
|
||||
},
|
||||
),
|
||||
MarkdownEditor(
|
||||
key: _mdKey,
|
||||
hintText: s.plan_desc_example,
|
||||
hasError: state.validationState.descriptionHasError,
|
||||
minLines: 6,
|
||||
maxLines: 8,
|
||||
showInlineToolbar: false,
|
||||
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setDetails(value);
|
||||
},
|
||||
),
|
||||
if (state.validationState.descriptionHasError) ...[
|
||||
const SizedBox(height: 4),
|
||||
@@ -253,10 +252,7 @@ class CreatePlanScreenContent extends StatelessWidget {
|
||||
}
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 24.0),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SelectExactOptionWidget(),
|
||||
),
|
||||
child: Align(alignment: Alignment.bottomCenter, child: SelectExactOptionWidget()),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -67,6 +67,13 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
validateSelectedOptionItems();
|
||||
}
|
||||
|
||||
void togglePersonalizedDietOptionState() {
|
||||
final options = [...state.planData.options];
|
||||
options[state.selectedOptionIndex ?? 0] =
|
||||
options[state.selectedOptionIndex ?? 0].copyWith(hasPersonalDiet: !options[state.selectedOptionIndex ?? 0].hasPersonalDiet);
|
||||
emit(state.copyWith(planData: state.planData.copyWith(options: options)));
|
||||
}
|
||||
|
||||
void setDuration(int index) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -418,18 +425,21 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
||||
'description_md': state.planData.options[0].description,
|
||||
'items': Map.fromIterable(state.planData.options[0].items),
|
||||
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
'packet2_options': {
|
||||
'short': state.planData.options[1].short,
|
||||
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
|
||||
'description_md': state.planData.options[1].description,
|
||||
'items': Map.fromIterable(state.planData.options[1].items),
|
||||
'has_personal_diet': state.planData.options[1].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
'packet3_options': {
|
||||
'short': state.planData.options[2].short,
|
||||
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
|
||||
'description_md': state.planData.options[2].description,
|
||||
'items': Map.fromIterable(state.planData.options[2].items),
|
||||
'has_personal_diet': state.planData.options[2].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -440,6 +450,7 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
||||
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
||||
'description_md': state.planData.options[0].description,
|
||||
'items': Map.fromIterable(state.planData.options[0].items),
|
||||
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
2 => thirdOptionCustomerPriceController,
|
||||
_ => secondOptionCustomerPriceController,
|
||||
};
|
||||
|
||||
|
||||
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
|
||||
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||
@@ -255,33 +255,31 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const CustomDivider(),
|
||||
// const SizedBox(height: 16),
|
||||
// Row(
|
||||
// children: [
|
||||
// Checkbox(
|
||||
// value: isChecked,
|
||||
// activeColor: appTheme.gray,
|
||||
// checkColor: appTheme.gray,
|
||||
// fillColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
// side: WidgetStateBorderSide.resolveWith(
|
||||
// (Set<WidgetState> states) {
|
||||
// return BorderSide(width: 1, color: appTheme.gray);
|
||||
// },
|
||||
// ),
|
||||
// onChanged: (bool? value) {
|
||||
// setState(() {
|
||||
// isChecked = value ?? false;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// s.option_personal_diet_desc,
|
||||
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: state.planData.options[selectedOptionIndex].hasPersonalDiet,
|
||||
activeColor: appTheme.gray,
|
||||
checkColor: appTheme.gray,
|
||||
fillColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(Set<WidgetState> states) {
|
||||
return BorderSide(width: 1, color: appTheme.gray);
|
||||
},
|
||||
),
|
||||
onChanged: (bool? value) {
|
||||
context.read<CreatePlanCubit>().togglePersonalizedDietOptionState();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
s.option_personal_diet_desc,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
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';
|
||||
@@ -7,6 +8,7 @@ import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.da
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart';
|
||||
import 'package:eatvio_app/common/widgets/markdown_editor.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';
|
||||
@@ -18,10 +20,7 @@ import 'package:go_router/go_router.dart';
|
||||
class RecipeCreateScreen extends StatefulWidget {
|
||||
final Recipe? recipe;
|
||||
|
||||
const RecipeCreateScreen({
|
||||
super.key,
|
||||
this.recipe,
|
||||
});
|
||||
const RecipeCreateScreen({super.key, this.recipe});
|
||||
|
||||
static const name = 'recipeCreateScreen';
|
||||
static const path = '/recipe-create-screen';
|
||||
@@ -32,6 +31,8 @@ class RecipeCreateScreen extends StatefulWidget {
|
||||
|
||||
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||
bool _mdToolbarVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -52,20 +53,18 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
child: BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
builder: (BuildContext context, CreateRecipeState state) {
|
||||
if (state.loading) {
|
||||
return const Scaffold(
|
||||
body: Loader(),
|
||||
);
|
||||
return const Scaffold(body: Loader());
|
||||
} else {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null
|
||||
? _mdKey.currentState!.buildToolbar(context)
|
||||
: null,
|
||||
appBar: AppBar(
|
||||
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
|
||||
centerTitle: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
@@ -75,22 +74,23 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
children: [
|
||||
InfoAboutRecipeWidget(
|
||||
createRecipeCubit: createRecipeCubit,
|
||||
markdownEditorKey: _mdKey,
|
||||
onMarkdownToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
|
||||
),
|
||||
const IngredientsWidget(),
|
||||
StepsWidget(
|
||||
createRecipeCubit: createRecipeCubit,
|
||||
),
|
||||
StepsWidget(createRecipeCubit: createRecipeCubit),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
builder: (context, state) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return InkWell(
|
||||
key: AppKeys.recipeCreateScreenPublishButton,
|
||||
onTap: () async {
|
||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
||||
widget.recipe == null ? false : true,
|
||||
widget.recipe?.slug,
|
||||
);
|
||||
widget.recipe == null ? false : true,
|
||||
widget.recipe?.slug,
|
||||
);
|
||||
if (isSuccess) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!context.mounted) return;
|
||||
@@ -98,9 +98,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedRecipeBottomSheet(
|
||||
title: state.entity.title ?? 'Рецепт',
|
||||
),
|
||||
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -112,20 +110,13 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
@@ -12,13 +13,7 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AddPortionWidget extends StatefulWidget {
|
||||
const AddPortionWidget({
|
||||
super.key,
|
||||
required this.createRecipeCubit,
|
||||
this.portionIndex,
|
||||
this.productNameControllerText,
|
||||
this.quantityControllerText,
|
||||
});
|
||||
const AddPortionWidget({super.key, required this.createRecipeCubit, this.portionIndex, this.productNameControllerText, this.quantityControllerText});
|
||||
final CreateRecipeCubit createRecipeCubit;
|
||||
final String? productNameControllerText;
|
||||
final String? quantityControllerText;
|
||||
@@ -43,76 +38,65 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16) +
|
||||
EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Добавление блюда',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 32,
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: CustomTextFormField(
|
||||
key: AppKeys.recipeCreateScreenIngredientNameField,
|
||||
controller: productNameController,
|
||||
autofocus: true,
|
||||
hintText: 'Введите название продукта/блюда',
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'glossary-search',
|
||||
const Duration(milliseconds: 250),
|
||||
() async {
|
||||
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
selectedGlossary = null;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.createRecipeCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.createRecipeCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.2 - 4,
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: CustomTextFormField(
|
||||
key: AppKeys.recipeCreateScreenIngredientQuantityField,
|
||||
controller: quantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
hintText: selectedGlossary == null
|
||||
? '100'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? '100'
|
||||
: '1',
|
||||
? '100'
|
||||
: '1',
|
||||
suffixText: selectedGlossary == null
|
||||
? 'гр'
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 256,
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
@@ -127,28 +111,12 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
setState(() {});
|
||||
FocusManager.instance.primaryFocus?.nextFocus();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
title: Text(
|
||||
glossariesList[i].name,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
|
||||
subtitle: glossariesList[i].grInPortion == 0
|
||||
? null
|
||||
: Text(
|
||||
'${glossariesList[i].grInPortion} г',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.add_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
|
||||
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
);
|
||||
},
|
||||
@@ -170,34 +138,23 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
child: Ink(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0xFFF6F6F6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
|
||||
),
|
||||
),
|
||||
if (widget.portionIndex != null) const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () => addPortion(context),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.portionIndex == null ? 'Добавить' : 'Сохранить',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
key: AppKeys.recipeCreateScreenIngredientAddButton,
|
||||
onTap: () => addPortion(context),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.portionIndex == null ? 'Добавить' : 'Сохранить',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -215,8 +172,8 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
var measurementUnits = selectedGlossary?.grInPortion == null
|
||||
? Portion.pcs
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? Portion.grams
|
||||
: Portion.pcs;
|
||||
? Portion.grams
|
||||
: Portion.pcs;
|
||||
final glossary = selectedGlossary;
|
||||
if (glossary != null) {
|
||||
final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits);
|
||||
@@ -229,12 +186,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
widget.createRecipeCubit.addIngredient(
|
||||
Ingredient(
|
||||
name: portion.name,
|
||||
unit: portion.portion.getValue(),
|
||||
portion: portion.quantity.toString(),
|
||||
productId: glossary.id,
|
||||
),
|
||||
Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: glossary.id),
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -247,13 +199,8 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: GlossaryModerationRequestModalWidget(
|
||||
productName: productNameController.text,
|
||||
diaryCubit: sl<DiaryCubit>(),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: sl<DiaryCubit>()),
|
||||
),
|
||||
);
|
||||
if (newGlossary != null) {
|
||||
@@ -268,12 +215,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
widget.createRecipeCubit.addIngredient(
|
||||
Ingredient(
|
||||
name: portion.name,
|
||||
unit: portion.portion.getValue(),
|
||||
portion: portion.quantity.toString(),
|
||||
productId: newGlossary.id,
|
||||
),
|
||||
Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: newGlossary.id),
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -281,12 +223,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
} else {
|
||||
widget.createRecipeCubit.updateIngredient(
|
||||
index: portionIndex,
|
||||
ingredient: Ingredient(
|
||||
name: portion.name,
|
||||
unit: portion.portion.getValue(),
|
||||
portion: portion.quantity.toString(),
|
||||
productId: newGlossary.id,
|
||||
),
|
||||
ingredient: Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: newGlossary.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_drop_down.dart';
|
||||
@@ -15,8 +17,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class InfoAboutRecipeWidget extends StatefulWidget {
|
||||
final CreateRecipeCubit createRecipeCubit;
|
||||
final GlobalKey<MarkdownEditorState>? markdownEditorKey;
|
||||
final void Function(bool visible)? onMarkdownToolbarVisibilityChanged;
|
||||
|
||||
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit});
|
||||
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit, this.markdownEditorKey, this.onMarkdownToolbarVisibilityChanged});
|
||||
|
||||
@override
|
||||
State<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
|
||||
@@ -46,125 +50,126 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Название'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
bloc: widget.createRecipeCubit,
|
||||
buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
hintText: 'Например: тар-тар из говядины',
|
||||
hasError: state.titleValidationError != null,
|
||||
initialValue: state.entity.title,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceTitle(value);
|
||||
if (state.titleValidationError != null) {
|
||||
widget.createRecipeCubit.validateTitle();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (state.titleValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.titleValidationError!,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Название'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
bloc: widget.createRecipeCubit,
|
||||
buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
key: AppKeys.recipeCreateScreenTitleField,
|
||||
hintText: 'Например: тар-тар из говядины',
|
||||
hasError: state.titleValidationError != null,
|
||||
initialValue: state.entity.title,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceTitle(value);
|
||||
if (state.titleValidationError != null) {
|
||||
widget.createRecipeCubit.validateTitle();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (state.titleValidationError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.titleValidationError!)],
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Тип блюда'),
|
||||
const SizedBox(height: 4),
|
||||
CustomDropDown(
|
||||
items: items,
|
||||
value: selectedType,
|
||||
onChanged: (newSelectedType) {
|
||||
setState(() {
|
||||
selectedType = newSelectedType;
|
||||
});
|
||||
widget.createRecipeCubit.changeType(
|
||||
TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TagsWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const PortionWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const TimeOfCookWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Описание'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
buildWhen: (previous, current) => previous.descriptionValidationError != current.descriptionValidationError,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
|
||||
minLines: 5,
|
||||
initialValue: state.entity.description,
|
||||
maxLines: 5,
|
||||
hasError: state.descriptionValidationError != null,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceDescription(value);
|
||||
if (state.descriptionValidationError != null) {
|
||||
widget.createRecipeCubit.validateDescription();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (state.descriptionValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.descriptionValidationError!,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Тип блюда'),
|
||||
const SizedBox(height: 4),
|
||||
CustomDropDown(
|
||||
key: AppKeys.recipeCreateScreenTypeDropdown,
|
||||
items: items,
|
||||
value: selectedType,
|
||||
onChanged: (newSelectedType) {
|
||||
setState(() {
|
||||
selectedType = newSelectedType;
|
||||
});
|
||||
widget.createRecipeCubit.changeType(TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TagsWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const PortionWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const TimeOfCookWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Описание'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
buildWhen: (previous, current) => previous.descriptionValidationError != current.descriptionValidationError,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
KeyedSubtree(
|
||||
key: AppKeys.recipeCreateScreenDescriptionField,
|
||||
child: MarkdownEditor(
|
||||
key: widget.markdownEditorKey,
|
||||
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
|
||||
minLines: 5,
|
||||
maxLines: 5,
|
||||
initialText: state.entity.description,
|
||||
hasError: state.descriptionValidationError != null,
|
||||
showInlineToolbar: false,
|
||||
onToolbarVisibilityChanged: widget.onMarkdownToolbarVisibilityChanged,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceDescription(value);
|
||||
if (state.descriptionValidationError != null) {
|
||||
widget.createRecipeCubit.validateDescription();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.descriptionValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: state.descriptionValidationError!),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Фото обложка'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
buildWhen: (previous, current) => previous.imageValidationError != current.imageValidationError || previous.entity.image != current.entity.image,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: state.imageValidationError != null
|
||||
? BoxDecoration(border: Border.all(width: 1, color: appTheme.red), borderRadius: BorderRadius.circular(8))
|
||||
: null,
|
||||
child: ImageWidget(image: state.entity.image),
|
||||
),
|
||||
if (state.imageValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.imageValidationError!,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Фото обложка'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.imageValidationError != current.imageValidationError || previous.entity.image != current.entity.image,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: state.imageValidationError != null
|
||||
? BoxDecoration(
|
||||
border: Border.all(width: 1, color: appTheme.red),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)
|
||||
: null,
|
||||
child: ImageWidget(key: AppKeys.recipeCreateScreenImageWidget, image: state.entity.image),
|
||||
),
|
||||
if (state.imageValidationError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.imageValidationError!)],
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Ссылка на видео (если есть)'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Прикрепите ссылку на видео',
|
||||
initialValue: widget.createRecipeCubit.state.entity.videoUrl,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceVideoUrl(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Ссылка на видео (если есть)'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Прикрепите ссылку на видео',
|
||||
initialValue: widget.createRecipeCubit.state.entity.videoUrl,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceVideoUrl(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
|
||||
@@ -40,11 +41,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Ингредиенты ',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
),
|
||||
ReorderableListView(
|
||||
@@ -63,46 +60,29 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
key: Key('$index'),
|
||||
leading: Icon(
|
||||
Icons.drag_handle_rounded,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
leading: Icon(Icons.drag_handle_rounded, color: Colors.grey.shade600),
|
||||
title: Text(state.entity.ingredients[index].name),
|
||||
subtitle: Text('${state.entity.ingredients[index].portion} ${state.entity.ingredients[index].unit == 'gr' ? 'гр' : 'шт'}'),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
createRecipeCubit.removeIngredient(index);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
icon: Icon(Icons.delete_outline_rounded, color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
key: AppKeys.recipeCreateScreenAddIngredientButton,
|
||||
onTap: () {
|
||||
nameController.clear();
|
||||
gramsController.clear();
|
||||
pcsController.clear();
|
||||
_modalBottomSheet(
|
||||
createRecipeCubit: createRecipeCubit,
|
||||
);
|
||||
_modalBottomSheet(createRecipeCubit: createRecipeCubit);
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
title: Text(
|
||||
'Добавить ингредиент',
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.add_rounded,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text('Добавить ингредиент', style: TextStyle(color: appTheme.blueA400)),
|
||||
leading: Icon(Icons.add_rounded, color: appTheme.blueA400),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -121,9 +101,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
||||
return showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))),
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return AddPortionWidget(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
@@ -39,12 +40,13 @@ class _PortionWidgetState extends State<PortionWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Кол-во порций'),
|
||||
const Text('Количество порций'),
|
||||
const SizedBox(height: 4),
|
||||
Column(
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
hintText: '1',
|
||||
key: AppKeys.recipeCreateScreenPortionsField,
|
||||
hintText: 'Например: 1',
|
||||
hasError: state.servingsValidationError != null,
|
||||
initialValue: state.entity.countOfPortions?.toString(),
|
||||
textInputType: TextInputType.number,
|
||||
@@ -57,9 +59,7 @@ class _PortionWidgetState extends State<PortionWidget> {
|
||||
),
|
||||
if (state.servingsValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.servingsValidationError!,
|
||||
),
|
||||
ValidationErrorWidget(error: state.servingsValidationError!),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -72,11 +72,12 @@ class _PortionWidgetState extends State<PortionWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Ккал в одной порции'),
|
||||
const Text('Калорийность'),
|
||||
const SizedBox(height: 4),
|
||||
Column(
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
key: AppKeys.recipeCreateScreenCaloriesField,
|
||||
hintText: '100',
|
||||
hasError: state.energyPerServingValidationError != null,
|
||||
initialValue: state.entity.energyInOnePortion?.toString(),
|
||||
@@ -91,9 +92,7 @@ class _PortionWidgetState extends State<PortionWidget> {
|
||||
),
|
||||
if (state.energyPerServingValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.energyPerServingValidationError!,
|
||||
),
|
||||
ValidationErrorWidget(error: state.energyPerServingValidationError!),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
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';
|
||||
@@ -22,107 +23,92 @@ class _StepsWidgetState extends State<StepsWidget> {
|
||||
bloc: widget.createRecipeCubit,
|
||||
builder: (BuildContext context, CreateRecipeState state) {
|
||||
return Material(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Пошаговая инструкция',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Пошаговая инструкция',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Шаг ${index + 1}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreateRecipeCubit>().removeStep(state.entity.steps[index]);
|
||||
},
|
||||
child: const Icon(Icons.delete_outline_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Описание'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Например: подготовьте ингредиенты',
|
||||
initialValue: widget.createRecipeCubit.state.entity.steps[index].description,
|
||||
maxLines: 5,
|
||||
minLines: 3,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceDescriptionInStep(value, index);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Фото шага'),
|
||||
const SizedBox(height: 4),
|
||||
StepImageWidget(
|
||||
index: index,
|
||||
createRecipeCubit: widget.createRecipeCubit,
|
||||
),
|
||||
],
|
||||
),
|
||||
itemCount: state.entity.steps.length,
|
||||
separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 24),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<CreateRecipeCubit>().addStep();
|
||||
},
|
||||
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,
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
key: AppKeys.recipeCreateScreenAddStepButton,
|
||||
onTap: () {
|
||||
context.read<CreateRecipeCubit>().addStep();
|
||||
},
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(
|
||||
'Шаг ${index + 1}',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreateRecipeCubit>().removeStep(state.entity.steps[index]);
|
||||
},
|
||||
child: const Icon(Icons.delete_outline_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Описание'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
key: AppKeys.recipeCreateScreenStepDescriptionField,
|
||||
hintText: 'Например: подготовьте ингредиенты',
|
||||
initialValue: widget.createRecipeCubit.state.entity.steps[index].description,
|
||||
maxLines: 5,
|
||||
minLines: 3,
|
||||
onChanged: (value) {
|
||||
widget.createRecipeCubit.replaceDescriptionInStep(value, index);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Фото шага'),
|
||||
const SizedBox(height: 4),
|
||||
StepImageWidget(
|
||||
key: Key('${AppKeys.recipeCreateScreenStepImageWidget}_$index'),
|
||||
index: index,
|
||||
createRecipeCubit: widget.createRecipeCubit,
|
||||
),
|
||||
],
|
||||
),
|
||||
itemCount: state.entity.steps.length,
|
||||
separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
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';
|
||||
@@ -26,9 +27,11 @@ class _TagsWidgetState extends State<TagsWidget> {
|
||||
const Text('Теги'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
key: AppKeys.recipeCreateScreenTagField,
|
||||
hintText: 'Например: без майонеза',
|
||||
controller: tagController,
|
||||
suffix: IconButton(
|
||||
key: AppKeys.recipeCreateScreenAddTagButton,
|
||||
onPressed: () {
|
||||
if (tagController.text.isNotEmpty) {
|
||||
context.read<CreateRecipeCubit>().addTag(tagController.text);
|
||||
@@ -37,10 +40,7 @@ class _TagsWidgetState extends State<TagsWidget> {
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.done,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
icon: Icon(Icons.done, color: appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -56,22 +56,13 @@ class _TagsWidgetState extends State<TagsWidget> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: Colors.grey.shade200),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
state.entity.tags[i],
|
||||
),
|
||||
Text(state.entity.tags[i]),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.grey.shade600,
|
||||
size: 20,
|
||||
),
|
||||
Icon(Icons.delete_outline_rounded, color: Colors.grey.shade600, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
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_diet/presentation/widgets/error_widget.dart';
|
||||
@@ -43,20 +44,12 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
builder: (context) => const AlertDialog(
|
||||
content: Text(
|
||||
'Общее время приготовления - это период времени от начала приготовления блюда до подачи его на стол.',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1.2,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: Color(0xFF323F4B), height: 1.2),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.help_outline_rounded,
|
||||
color: appTheme.blueA400,
|
||||
size: 20,
|
||||
),
|
||||
child: Icon(Icons.help_outline_rounded, color: appTheme.blueA400, size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -66,9 +59,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Часы'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Часов',
|
||||
key: AppKeys.recipeCreateScreenTotalHoursField,
|
||||
hintText: '',
|
||||
hasError: state.cookingTimeValidationError != null,
|
||||
textInputType: TextInputType.number,
|
||||
initialValue: state.entity.totalCookHours?.toString(),
|
||||
@@ -83,9 +80,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
),
|
||||
if (state.cookingTimeValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.cookingTimeValidationError!,
|
||||
),
|
||||
ValidationErrorWidget(error: state.cookingTimeValidationError!),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -93,9 +88,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Минуты'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Минут',
|
||||
key: AppKeys.recipeCreateScreenTotalMinutesField,
|
||||
hintText: '',
|
||||
hasError: state.cookingTimeValidationError != null,
|
||||
textInputType: TextInputType.number,
|
||||
initialValue: state.entity.totalCookMinutes?.toString(),
|
||||
@@ -110,9 +109,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
),
|
||||
if (state.cookingTimeValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.cookingTimeValidationError!,
|
||||
),
|
||||
ValidationErrorWidget(error: state.cookingTimeValidationError!),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -131,20 +128,12 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
builder: (context) => const AlertDialog(
|
||||
content: Text(
|
||||
'Активное время приготовления - это период времени, требующий активного участия в приготовлении блюда. Оно включает мытье и очистку продуктов, нарезку, шинковку, взбивание, перемешивание, размешивание, перекладывание и т.п.',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1.2,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: Color(0xFF323F4B), height: 1.2),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.help_outline_rounded,
|
||||
color: appTheme.blueA400,
|
||||
size: 20,
|
||||
),
|
||||
child: Icon(Icons.help_outline_rounded, color: appTheme.blueA400, size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -154,9 +143,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Часы'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Часов',
|
||||
key: AppKeys.recipeCreateScreenActiveHoursField,
|
||||
hintText: '',
|
||||
hasError: state.activeTimeValidationError != null,
|
||||
initialValue: state.entity.activeCookHours?.toString(),
|
||||
textInputType: TextInputType.number,
|
||||
@@ -172,9 +165,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
),
|
||||
if (state.activeTimeValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.activeTimeValidationError!,
|
||||
),
|
||||
ValidationErrorWidget(error: state.activeTimeValidationError!),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -182,9 +173,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Минуты'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Минут',
|
||||
key: AppKeys.recipeCreateScreenActiveMinutesField,
|
||||
hintText: '',
|
||||
hasError: state.activeTimeValidationError != null,
|
||||
initialValue: state.entity.activeCookMinutes?.toString(),
|
||||
textInputType: TextInputType.number,
|
||||
@@ -200,9 +195,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
),
|
||||
if (state.activeTimeValidationError != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(
|
||||
error: state.activeTimeValidationError!,
|
||||
),
|
||||
ValidationErrorWidget(error: state.activeTimeValidationError!),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,22 +4,24 @@ import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class AuthorCard extends StatelessWidget {
|
||||
const AuthorCard({super.key, required this.author, required this.title, this.backgroundColor, this.titleStyle});
|
||||
const AuthorCard({super.key, required this.author, required this.title, this.backgroundColor, this.titleStyle, this.customerProfile});
|
||||
final Author author;
|
||||
final String title;
|
||||
final Color? backgroundColor;
|
||||
final TextStyle? titleStyle;
|
||||
|
||||
final ProfileModel? customerProfile;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: author);
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: customerProfile != null ? customerProfile!.slug : author.authorSlug);
|
||||
},
|
||||
child: Container(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
@@ -39,24 +41,48 @@ class AuthorCard extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(
|
||||
author.authorProfileIcon.contains('https') ? author.authorProfileIcon : sl<AppConfig>().baseUrl + author.authorProfileIcon,
|
||||
customerProfile != null
|
||||
? customerProfile!.profilePhotoUrl.contains('https')
|
||||
? customerProfile!.profilePhotoUrl
|
||||
: sl<AppConfig>().baseUrl + customerProfile!.profilePhotoUrl
|
||||
: author.authorProfileIcon.contains('https')
|
||||
? author.authorProfileIcon
|
||||
: sl<AppConfig>().baseUrl + author.authorProfileIcon,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text(author.authorName, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
subtitle: Text(
|
||||
customerProfile != null ? customerProfile!.name : author.authorName,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
const SizedBox(width: 10),
|
||||
if (author.authorPlaceInRating != null)
|
||||
InfoBouble(
|
||||
radius: 12,
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
|
||||
if (customerProfile == null) ...[
|
||||
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
if (author.authorPlaceInRating != null)
|
||||
InfoBouble(
|
||||
radius: 12,
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [
|
||||
Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400)),
|
||||
],
|
||||
),
|
||||
] else
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(UserDetailsScreen.name, extra: customerProfile!.slug);
|
||||
},
|
||||
child: InfoBouble(
|
||||
radius: 12,
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [Text('Анкета пользователя', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -13,8 +13,15 @@ class CalendarWidget extends StatefulWidget {
|
||||
|
||||
class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay = DateTime.now();
|
||||
late DateTime _focusedDay;
|
||||
DateTime? _selectedDay;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
|
||||
_focusedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -23,7 +30,7 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: TableCalendar(
|
||||
firstDay: DateTime.now(),
|
||||
firstDay: widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0],
|
||||
lastDay: DateTime.utc(2030, 3, 14),
|
||||
locale: 'ru_RU',
|
||||
startingDayOfWeek: StartingDayOfWeek.monday,
|
||||
|
||||
@@ -67,11 +67,19 @@ abstract class PacketOptions with _$PacketOptions {
|
||||
@JsonKey(name: "price") required dynamic price,
|
||||
@JsonKey(name: "short") required String short,
|
||||
@JsonKey(name: "description_md") required String description,
|
||||
@JsonKey(name: "has_personal_diet", fromJson: _parseHasPersonalDietField) required bool hasPersonalDiet,
|
||||
}) = _PacketOptions;
|
||||
|
||||
factory PacketOptions.fromJson(Map<String, dynamic> json) => _$PacketOptionsFromJson(json);
|
||||
|
||||
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '');
|
||||
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '', hasPersonalDiet: false);
|
||||
}
|
||||
|
||||
bool _parseHasPersonalDietField(dynamic json) {
|
||||
if (json == null) return false;
|
||||
if (json == '1') return true;
|
||||
if (json == '0') return false;
|
||||
return json as bool;
|
||||
}
|
||||
|
||||
List<String> _parseItems(dynamic json) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/core/widgets/add_item_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/marketing_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
|
||||
@@ -70,6 +73,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
void initState() {
|
||||
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
_checkRegistrationDate(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -77,6 +81,15 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
|
||||
}
|
||||
|
||||
void _checkRegistrationDate(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final needToShowMarketingBottomSheet = await context.read<MarketingCubit>().checkRegistrationDate();
|
||||
if (needToShowMarketingBottomSheet && context.mounted) {
|
||||
showMarketingBottomSheet(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
blogPostsScrollController.dispose();
|
||||
@@ -215,7 +228,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
3 when !hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
|
||||
),
|
||||
_ => const AddItemButton(),
|
||||
_ => const AddItemButton(key: AppKeys.feedScreenBlogPostCreationButton),
|
||||
},
|
||||
],
|
||||
),
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart';
|
||||
@@ -18,6 +17,7 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class BlogPostWidget extends StatefulWidget {
|
||||
final int index;
|
||||
@@ -58,17 +58,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
authorName: state.feedBlogPostList[widget.index].author.authorName,
|
||||
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
|
||||
? state.feedBlogPostList[widget.index].author.authorProfileIcon
|
||||
: sl<AppConfig>().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
),
|
||||
);
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: state.feedBlogPostList[widget.index].author.authorSlug);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
radius: 20,
|
||||
@@ -134,14 +124,39 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
const SizedBox(height: 8),
|
||||
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Html(
|
||||
data:
|
||||
(state.feedBlogPostList[widget.index].description.length > 200
|
||||
? state.feedBlogPostList[widget.index].description.substring(0, 200)
|
||||
: state.feedBlogPostList[widget.index].description)
|
||||
.replaceAll('\n', '<br>'),
|
||||
style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
|
||||
data: md.markdownToHtml(
|
||||
(state.feedBlogPostList[widget.index].description.length > 200
|
||||
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
|
||||
),
|
||||
style: {
|
||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.feedBlogPostList[widget.index].description.length > 200)
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/prof
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/feed/models/comment_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -96,12 +95,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: comment.authorSlug,
|
||||
authorName: comment.authorName,
|
||||
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl<AppConfig>().baseUrl + comment.authorProfileIcon,
|
||||
authorIsMe: isMe,
|
||||
),
|
||||
extra: comment.authorSlug,
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
|
||||
@@ -2,9 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/badge_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -22,12 +19,7 @@ class ConsultantWidget extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: consultant.slug,
|
||||
authorName: consultant.name,
|
||||
authorProfileIcon: consultant.photo,
|
||||
authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == consultant.slug ? true : false,
|
||||
),
|
||||
extra: consultant.slug,
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
|
||||
@@ -17,15 +17,12 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class DietsWidget extends StatefulWidget {
|
||||
class DietsWidget extends StatefulWidget {
|
||||
final Diet diet;
|
||||
final int index;
|
||||
const DietsWidget({
|
||||
super.key,
|
||||
required this.diet,
|
||||
required this.index,
|
||||
});
|
||||
const DietsWidget({super.key, required this.diet, required this.index});
|
||||
|
||||
@override
|
||||
State<DietsWidget> createState() => _DietsWidgetState();
|
||||
@@ -33,18 +30,11 @@ class DietsWidget extends StatefulWidget {
|
||||
|
||||
class _DietsWidgetState extends State<DietsWidget> {
|
||||
bool isMoreOptionsVisible = false;
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'aboutDietScreen',
|
||||
extra: {
|
||||
'slug': widget.diet.slug,
|
||||
'index': widget.index,
|
||||
'navigateToComments': false,
|
||||
},
|
||||
),
|
||||
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': false}),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
@@ -104,12 +94,7 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
bottom: 16,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
child: InfoRow(
|
||||
diet: widget.diet,
|
||||
itemsBackgroundColor: appTheme.whiteA700,
|
||||
disableDescriptionOfTypes: true,
|
||||
type: ProductType.diet,
|
||||
),
|
||||
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -120,26 +105,48 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.diet.name,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
if (widget.diet.details.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
widget.diet.details.length > 200
|
||||
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.diet.details.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
"body": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
padding: HtmlPaddings.zero,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
lineHeight: const LineHeight(1.5),
|
||||
),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -149,12 +156,7 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
padding: EdgeInsets.only(left: 16, top: 8),
|
||||
child: Text(
|
||||
'Показать ещё',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF8B8B8B),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -172,18 +174,14 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
await context.read<DietsCubit>().likeDiet(widget.diet.id, widget.index);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FeedIconButton(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
text: widget.diet.totalComments.toString(),
|
||||
onTap: () =>
|
||||
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': true}),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FeedIconButton(
|
||||
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
|
||||
onTap: () async {
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||
import 'package:eatvio_app/core/widgets/info_row.dart';
|
||||
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -20,16 +19,13 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class PlanWidget extends StatefulWidget {
|
||||
final PlanModel plan;
|
||||
final int index;
|
||||
|
||||
const PlanWidget({
|
||||
super.key,
|
||||
required this.plan,
|
||||
required this.index,
|
||||
});
|
||||
const PlanWidget({super.key, required this.plan, required this.index});
|
||||
|
||||
@override
|
||||
State<PlanWidget> createState() => _PlanWidgetState();
|
||||
@@ -42,14 +38,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'aboutPlanScreen',
|
||||
extra: {
|
||||
'slug': widget.plan.slug,
|
||||
'index': widget.index,
|
||||
'navigateToComments': false,
|
||||
},
|
||||
),
|
||||
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': false}),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
@@ -110,12 +99,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
bottom: 16,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
child: InfoRow(
|
||||
plan: widget.plan,
|
||||
itemsBackgroundColor: appTheme.whiteA700,
|
||||
disableDescriptionOfTypes: true,
|
||||
type: ProductType.plan,
|
||||
),
|
||||
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -126,26 +110,43 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.plan.name,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18),
|
||||
),
|
||||
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18)),
|
||||
),
|
||||
if (widget.plan.details.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.plan.details.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
widget.plan.details.length > 200
|
||||
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.plan.details.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
fontSize: FontSize(16),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -154,12 +155,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
padding: EdgeInsets.only(left: 16, top: 8),
|
||||
child: Text(
|
||||
'Показать ещё',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF8B8B8B),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -168,15 +164,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: widget.plan.author.authorSlug,
|
||||
authorName: widget.plan.author.authorName,
|
||||
authorProfileIcon: widget.plan.author.authorProfileIcon,
|
||||
authorIsMe: widget.plan.author.authorIsMe,
|
||||
),
|
||||
);
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: widget.plan.author.authorSlug);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
@@ -187,10 +175,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
image: DecorationImage(image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
subtitle: Row(
|
||||
@@ -200,12 +185,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
backgroundColor: appTheme.gray50,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
|
||||
children: [
|
||||
Text(
|
||||
'${widget.plan.author.authorPlaceInRating}-й в рейтинге',
|
||||
style: TextStyles.textXSSemibold.copyWith(
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
Text('${widget.plan.author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textXSSemibold.copyWith(color: appTheme.blueA400)),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -218,21 +198,14 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
),
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Text(
|
||||
widget.plan.author.authorName,
|
||||
style: TextStyles.textSSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
child: Text(widget.plan.author.authorName, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
FeedIconButton(
|
||||
@@ -244,20 +217,17 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
await context.read<PlansCubit>().likePlan(widget.plan.slug, widget.index);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FeedIconButton(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
text: widget.plan.totalComments.toString(),
|
||||
onTap: () {
|
||||
return GoRouter.of(context)
|
||||
.pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
|
||||
onTap: () {
|
||||
return GoRouter.of(
|
||||
context,
|
||||
).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FeedIconButton(
|
||||
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
|
||||
onTap: () async {
|
||||
|
||||
@@ -20,16 +20,13 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class RecipesWidget extends StatefulWidget {
|
||||
final int index;
|
||||
final Recipe recipe;
|
||||
|
||||
const RecipesWidget({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
required this.index,
|
||||
});
|
||||
const RecipesWidget({super.key, required this.recipe, required this.index});
|
||||
|
||||
@override
|
||||
State<RecipesWidget> createState() => _RecipesWidgetState();
|
||||
@@ -56,14 +53,8 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
}
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'aboutRecipesScreen',
|
||||
extra: {
|
||||
'recipe': widget.recipe.slug,
|
||||
'index': widget.index,
|
||||
'navigateToComments': false,
|
||||
},
|
||||
),
|
||||
onTap: () =>
|
||||
GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': false}),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
@@ -139,10 +130,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.recipe.title ?? '',
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(widget.recipe.title ?? '', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
if (widget.recipe.videoUrl != null && widget.recipe.videoUrl!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
@@ -152,10 +140,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
s.there_is_a_video,
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(s.there_is_a_video, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -165,17 +150,41 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.recipe.description.length > 200
|
||||
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: widget.recipe.description.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
widget.recipe.description.length > 200
|
||||
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: widget.recipe.description.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
color: appTheme.black,
|
||||
margin: Margins.all(0),
|
||||
fontFamily: TextStyles.textSMedium.fontFamily,
|
||||
"body": Style(
|
||||
margin: Margins.zero,
|
||||
padding: HtmlPaddings.zero,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight,
|
||||
lineHeight: const LineHeight(1.5),
|
||||
),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -184,12 +193,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
padding: EdgeInsets.only(left: 16, top: 8),
|
||||
child: Text(
|
||||
'Показать ещё',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF8B8B8B),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -211,14 +215,9 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
FeedIconButton(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
text: widget.recipe.commentsCount.toString(),
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'aboutRecipesScreen',
|
||||
extra: {
|
||||
'recipe': widget.recipe.slug,
|
||||
'index': widget.index,
|
||||
'navigateToComments': true,
|
||||
},
|
||||
),
|
||||
onTap: () => GoRouter.of(
|
||||
context,
|
||||
).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': true}),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FeedIconButton(
|
||||
@@ -239,19 +238,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.visibility_rounded,
|
||||
size: 16,
|
||||
color: appTheme.gray500,
|
||||
),
|
||||
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.recipe.viewsCount.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.gray500,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -268,26 +259,17 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
Container _iconButton(String iconPath, [String? text]) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
|
||||
height: 32,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
iconPath,
|
||||
),
|
||||
SvgPicture.asset(iconPath),
|
||||
if (text != null) const SizedBox(width: 8),
|
||||
if (text != null)
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -21,13 +20,7 @@ class _BlockedUserWidgetState extends State<BlockedUserWidget> {
|
||||
return InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: widget.profile.slug,
|
||||
authorName: widget.profile.name,
|
||||
authorProfileIcon: widget.profile.profilePhotoUrl,
|
||||
authorIsMe: false,
|
||||
isEntitySubscribedByMe: false,
|
||||
),
|
||||
extra: widget.profile.slug,
|
||||
),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.d
|
||||
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -24,14 +25,12 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class MyDietWidget extends StatefulWidget {
|
||||
final Diet diet;
|
||||
|
||||
const MyDietWidget({
|
||||
super.key,
|
||||
required this.diet,
|
||||
});
|
||||
const MyDietWidget({super.key, required this.diet});
|
||||
|
||||
@override
|
||||
State<MyDietWidget> createState() => _MyDietWidgetState();
|
||||
@@ -42,10 +41,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -78,26 +74,48 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.diet.name,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
if (widget.diet.details.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
widget.diet.details.length > 200
|
||||
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.diet.details.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
"body": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
padding: HtmlPaddings.zero,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
lineHeight: const LineHeight(1.5),
|
||||
),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -129,6 +147,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
}
|
||||
} else if (state.selectedStateIndex == 2) {
|
||||
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
|
||||
} else {
|
||||
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
|
||||
}
|
||||
@@ -230,11 +249,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
leftIcon: SvgPicture.asset(
|
||||
'assets/icons/trash_can.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
// Removed SVG icon usage after switching to text buttons
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class MyPlanWidget extends StatefulWidget {
|
||||
final PlanModel plan;
|
||||
@@ -43,7 +44,11 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
||||
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -68,16 +73,41 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.plan.details.length > 200
|
||||
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.plan.details.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
widget.plan.details.length > 200
|
||||
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.plan.details.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
"body": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
padding: HtmlPaddings.zero,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
lineHeight: const LineHeight(1.5),
|
||||
),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class OrderInfoCubit extends Cubit<OrderInfoState> {
|
||||
OrderInfoCubit({required this.chatRepository}) : super(OrderInfoState());
|
||||
OrderInfoCubit({required this.chatRepository, required this.profileRepository}) : super(OrderInfoState());
|
||||
final ChatRepository chatRepository;
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
Future<String?> getPaymentLink() async {
|
||||
try {
|
||||
@@ -20,10 +21,14 @@ class OrderInfoCubit extends Cubit<OrderInfoState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getChosenPlan(int id) async {
|
||||
Future<void> getChosenPlan({required int id, String? customerSlug}) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final chosenPlan = await chatRepository.getChosenPlan(id: id);
|
||||
if (customerSlug != null) {
|
||||
final profile = await profileRepository.getProfileBySlug(customerSlug);
|
||||
emit(state.copyWith(customerProfile: profile));
|
||||
}
|
||||
emit(state.copyWith(isLoading: false, chosenPlan: chosenPlan));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'order_info_state.freezed.dart';
|
||||
@@ -9,6 +10,7 @@ abstract class OrderInfoState with _$OrderInfoState {
|
||||
factory OrderInfoState({
|
||||
@Default(true) bool isLoading,
|
||||
@Default(null) ChosenPlanModel? chosenPlan,
|
||||
@Default(null) ProfileModel? customerProfile,
|
||||
}) = _OrderInfoState;
|
||||
|
||||
}
|
||||
|
||||
@@ -24,31 +24,24 @@ import 'package:go_router/go_router.dart';
|
||||
class InfoByOrderScreen extends StatelessWidget {
|
||||
static const name = 'infoByOrderScreen';
|
||||
static const path = '/info-by-order-screen';
|
||||
const InfoByOrderScreen({super.key, this.successfulPayment, required this.planId});
|
||||
const InfoByOrderScreen({super.key, this.successfulPayment, required this.planId, this.customerSlug});
|
||||
final bool? successfulPayment;
|
||||
final int planId;
|
||||
final String? customerSlug;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<OrderInfoCubit>()..getChosenPlan(planId),
|
||||
child: InfoByOrderScreenContent(
|
||||
successfulPayment: successfulPayment,
|
||||
planId: planId,
|
||||
),
|
||||
create: (context) => sl<OrderInfoCubit>()..getChosenPlan(id: planId, customerSlug: customerSlug),
|
||||
child: InfoByOrderScreenContent(successfulPayment: successfulPayment, planId: planId, customerSlug: customerSlug),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InfoByOrderScreenContent extends StatefulWidget {
|
||||
static const name = 'infoByOrderScreen';
|
||||
static const path = '/info-by-order-screen';
|
||||
const InfoByOrderScreenContent({
|
||||
super.key,
|
||||
required this.planId,
|
||||
required this.successfulPayment,
|
||||
});
|
||||
class InfoByOrderScreenContent extends StatefulWidget {
|
||||
const InfoByOrderScreenContent({super.key, required this.planId, required this.successfulPayment, this.customerSlug});
|
||||
final int planId;
|
||||
final bool? successfulPayment;
|
||||
final String? customerSlug;
|
||||
@override
|
||||
State<InfoByOrderScreenContent> createState() => _InfoByOrderScreenContentState();
|
||||
}
|
||||
@@ -64,39 +57,30 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
||||
if (state.isLoading) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
s.infoAboutOrder,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
}),
|
||||
),
|
||||
body: const Center(
|
||||
child: Loader(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: const Center(child: Loader()),
|
||||
);
|
||||
}
|
||||
if (plan == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
s.infoAboutOrder,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
}),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(
|
||||
s.sorry_we_did_not_find_the_plan,
|
||||
style: TextStyles.textSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(s.sorry_we_did_not_find_the_plan, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -122,58 +106,35 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
s.infoAboutOrder,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
}),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
|
||||
CollectChargeBanner(
|
||||
chosenPlan: plan,
|
||||
price: chosenOption?.price ?? 0,
|
||||
)
|
||||
],
|
||||
if (widget.successfulPayment != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
PaymentBanner(
|
||||
successfulPayment: widget.successfulPayment ?? false,
|
||||
),
|
||||
],
|
||||
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[CollectChargeBanner(chosenPlan: plan, price: chosenOption?.price ?? 0)],
|
||||
if (widget.successfulPayment != null) ...[const SizedBox(height: 8), PaymentBanner(successfulPayment: widget.successfulPayment ?? false)],
|
||||
const SizedBox(height: 8),
|
||||
AuthorCard(
|
||||
author: plan.plan.author,
|
||||
title: s.plan_seller,
|
||||
title: state.customerProfile != null ? s.customer : s.plan_seller,
|
||||
titleStyle: TextStyles.textSSemibold.copyWith(color: appTheme.gray),
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
customerProfile: state.customerProfile,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PlanProps(
|
||||
planDuration: plan.plan.duration,
|
||||
planPrice: chosenOption?.price.toDouble() ?? 0,
|
||||
planStatus: planStatus),
|
||||
PlanProps(planDuration: plan.plan.duration, planPrice: chosenOption?.price.toDouble() ?? 0, planStatus: planStatus),
|
||||
const SizedBox(height: 8),
|
||||
PlanInfo(
|
||||
title: plan.plan.name,
|
||||
description: plan.plan.details,
|
||||
imagePath: plan.plan.image,
|
||||
),
|
||||
PlanInfo(title: plan.plan.name, description: plan.plan.details, imagePath: plan.plan.image),
|
||||
const SizedBox(height: 8),
|
||||
OptionWidget(
|
||||
option: chosenOption,
|
||||
disableDuration: true,
|
||||
),
|
||||
OptionWidget(option: chosenOption, disableDuration: true, duration: plan.plan.duration),
|
||||
const SizedBox(height: 8),
|
||||
PlanState(
|
||||
state: plan.chosenPlanState,
|
||||
),
|
||||
PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,),
|
||||
const SizedBox(height: 20),
|
||||
if (widget.successfulPayment == null)
|
||||
CustomOutlinedButton(
|
||||
@@ -184,13 +145,10 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
||||
onPressed: () async {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => CancelPlanBottomSheet(
|
||||
enableForm: plan.chosenPlanState.isStateAfterPaidState(),
|
||||
planId: plan.id,
|
||||
),
|
||||
builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isStateAfterPaidState(), planId: plan.id),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,42 +5,65 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PlanState extends StatelessWidget {
|
||||
PlanState({super.key, required this.state});
|
||||
class PlanState extends StatefulWidget {
|
||||
const PlanState({super.key, required this.state, required this.hasPersonalDiet});
|
||||
final ChosenPlanStateEnum state;
|
||||
final List<List<ChosenPlanStateEnum>> states = [
|
||||
[
|
||||
ChosenPlanStateEnum.stateInited,
|
||||
ChosenPlanStateEnum.stateInCoordination,
|
||||
],
|
||||
[
|
||||
ChosenPlanStateEnum.stateStartDateHasBeenSet,
|
||||
ChosenPlanStateEnum.statePendingPayment,
|
||||
],
|
||||
[
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
],
|
||||
[
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
],
|
||||
[
|
||||
ChosenPlanStateEnum.stateComplained,
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
ChosenPlanStateEnum.stateChargeWasCollected,
|
||||
]
|
||||
];
|
||||
final bool hasPersonalDiet;
|
||||
|
||||
@override
|
||||
State<PlanState> createState() => _PlanStateState();
|
||||
}
|
||||
|
||||
class _PlanStateState extends State<PlanState> {
|
||||
late final List<List<ChosenPlanStateEnum>> states;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.hasPersonalDiet) {
|
||||
states = [
|
||||
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
|
||||
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
|
||||
[ChosenPlanStateEnum.statePaid],
|
||||
[ChosenPlanStateEnum.personalDietSet],
|
||||
[
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
],
|
||||
[ChosenPlanStateEnum.stateComplained, ChosenPlanStateEnum.stateDone, ChosenPlanStateEnum.stateChargeWasCollected],
|
||||
];
|
||||
} else {
|
||||
states = [
|
||||
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
|
||||
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
|
||||
[ChosenPlanStateEnum.statePaid],
|
||||
[
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
],
|
||||
[ChosenPlanStateEnum.stateComplained, ChosenPlanStateEnum.stateDone, ChosenPlanStateEnum.stateChargeWasCollected],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
final titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.in_progress, s.done];
|
||||
final List<String> titles;
|
||||
if (widget.hasPersonalDiet) {
|
||||
titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.personal_diet_set, s.in_progress, s.done];
|
||||
} else {
|
||||
titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.in_progress, s.done];
|
||||
}
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: List.generate(states.length, (index) {
|
||||
final color = index <= states.indexWhere((element) => element.contains(state)) ? appTheme.greenA700 : appTheme.lightGray00;
|
||||
final color = index <= states.indexWhere((element) => element.contains(widget.state)) ? appTheme.greenA700 : appTheme.lightGray00;
|
||||
final text = titles[index];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -51,29 +74,16 @@ class PlanState extends StatelessWidget {
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: appTheme.whiteA700,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
child: Center(child: Icon(Icons.check, color: appTheme.whiteA700, size: 14)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(text, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Container(
|
||||
height: 12,
|
||||
width: 1,
|
||||
color: color,
|
||||
),
|
||||
)
|
||||
child: Container(height: 12, width: 1, color: color),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
|
||||
final DietRepository dietRepository;
|
||||
final PersonalDietSuggestionRepository personalDietSuggestionService;
|
||||
late final int dietId;
|
||||
PersonalDietSuggestionCubit({required this.dietRepository, required this.personalDietSuggestionService}) : super(PersonalDietSuggestionState());
|
||||
|
||||
Future<void> setDietName(int dietId) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
final diet = await dietRepository.getDiet(dietId);
|
||||
emit(state.copyWith(isLoading: false, dietName: diet.name));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getConsultationWithPersonalDiets(int dietId) async {
|
||||
try {
|
||||
this.dietId = dietId;
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
setDietName(dietId);
|
||||
final consultations = await personalDietSuggestionService.getConsultationWithPersonalDiets();
|
||||
final consultationWithDiet = consultations.where((consultation) => consultation.personalDietId != null).toList();
|
||||
final consultationWithoutDiet = consultations.where((consultation) => consultation.personalDietId == null).toList();
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
consultationsWithDiet: consultationWithDiet,
|
||||
consultationsWithoutDiet: consultationWithoutDiet,
|
||||
));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> attachDietToConsultation({
|
||||
required int consultationId,
|
||||
}) async {
|
||||
try {
|
||||
await personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> reattachDietToConsultation({
|
||||
required int consultationId,
|
||||
}) async {
|
||||
try {
|
||||
await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'personal_diet_suggestion_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PersonalDietSuggestionState with _$PersonalDietSuggestionState {
|
||||
factory PersonalDietSuggestionState({
|
||||
@Default([]) List<ConsultationModel> consultationsWithDiet,
|
||||
@Default([]) List<ConsultationModel> consultationsWithoutDiet,
|
||||
@Default('') String dietName,
|
||||
@Default(false) bool isLoading,
|
||||
@Default(null) String? error,
|
||||
}) = _PersonalDietSuggestionState;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.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 ConsultationWidget extends StatelessWidget {
|
||||
const ConsultationWidget({super.key, required this.consultationModel, required this.dietName});
|
||||
final ConsultationModel consultationModel;
|
||||
final String dietName;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: appTheme.whiteA700,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (consultationModel.personalDietId == null)
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultationModel.userProfilePhotoUrl),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
consultationModel.userName,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(
|
||||
consultationModel.planTitle,
|
||||
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
|
||||
),
|
||||
Text(
|
||||
'Старт: ${consultationModel.userName}',
|
||||
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultationModel.userProfilePhotoUrl),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
consultationModel.userName,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(
|
||||
consultationModel.planTitle,
|
||||
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (consultationModel.personalDietId != null)
|
||||
CustomOutlinedButton(
|
||||
text: s.changeDiet,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.dividerColor)),
|
||||
onPressed: () async {
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestDietBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
onTap: () async {
|
||||
final result = await context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionSuccessBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionFailureBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
CustomOutlinedButton(
|
||||
text: s.suggestDiet,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestDietBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
onTap: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
final result = await context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionSuccessBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: null,
|
||||
child: SuggestionFailureBottomSheet(
|
||||
userName: consultationModel.userName,
|
||||
dietName: dietName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestDietBottomSheet extends StatefulWidget {
|
||||
const SuggestDietBottomSheet({
|
||||
super.key,
|
||||
required this.userName,
|
||||
required this.dietName,
|
||||
required this.onTap,
|
||||
});
|
||||
final String userName;
|
||||
final String dietName;
|
||||
final Future<void> Function() onTap;
|
||||
@override
|
||||
State<SuggestDietBottomSheet> createState() => _SuggestDietBottomSheet();
|
||||
}
|
||||
|
||||
class _SuggestDietBottomSheet extends State<SuggestDietBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
'Предложить пользователю ${widget.userName} «${widget.dietName}»',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
CustomOutlinedButton(
|
||||
text: s.toOffer,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: widget.onTap,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestionSuccessBottomSheet extends StatefulWidget {
|
||||
const SuggestionSuccessBottomSheet({
|
||||
super.key,
|
||||
required this.userName,
|
||||
required this.dietName,
|
||||
});
|
||||
final String userName;
|
||||
final String dietName;
|
||||
|
||||
@override
|
||||
State<SuggestionSuccessBottomSheet> createState() => _SuggestionSuccessBottomSheet();
|
||||
}
|
||||
|
||||
class _SuggestionSuccessBottomSheet extends State<SuggestionSuccessBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
'«${widget.dietName}» предложена пользователю ${widget.userName}',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
CustomOutlinedButton(
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestionFailureBottomSheet extends StatefulWidget {
|
||||
const SuggestionFailureBottomSheet({
|
||||
super.key,
|
||||
required this.userName,
|
||||
required this.dietName,
|
||||
});
|
||||
final String userName;
|
||||
final String dietName;
|
||||
|
||||
@override
|
||||
State<SuggestionFailureBottomSheet> createState() => _SuggestionFailureBottomSheet();
|
||||
}
|
||||
|
||||
class _SuggestionFailureBottomSheet extends State<SuggestionFailureBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
'Не удалось предложить диету ${widget.dietName} пользователю ${widget.userName}',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
CustomOutlinedButton(
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/consultation_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PersonalDietSuggestionScreen extends StatelessWidget {
|
||||
final int dietId;
|
||||
const PersonalDietSuggestionScreen({super.key, required this.dietId});
|
||||
static const path = '/personal-diet-suggestion';
|
||||
static const name = 'personalDietSuggestion';
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<PersonalDietSuggestionCubit>()..getConsultationWithPersonalDiets(dietId),
|
||||
child: const PersonalDietSuggestionScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PersonalDietSuggestionScreenContent extends StatelessWidget {
|
||||
const PersonalDietSuggestionScreenContent({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<PersonalDietSuggestionCubit, PersonalDietSuggestionState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'${s.dietSuggestion}\n«${state.dietName}»',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(child: Loader())
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...List.generate(state.consultationsWithDiet.length, (index) {
|
||||
return ConsultationWidget(
|
||||
consultationModel: state.consultationsWithDiet[index],
|
||||
dietName: state.dietName,
|
||||
);
|
||||
}),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
...List.generate(state.consultationsWithoutDiet.length, (index) {
|
||||
return ConsultationWidget(
|
||||
consultationModel: state.consultationsWithoutDiet[index],
|
||||
dietName: state.dietName,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
|
||||
|
||||
class PersonalDietSuggestionRepository {
|
||||
final PersonalDietSuggestionService personalDietSuggestionService;
|
||||
|
||||
PersonalDietSuggestionRepository({required this.personalDietSuggestionService});
|
||||
|
||||
Future<List<ConsultationModel>> getConsultationWithPersonalDiets() async{
|
||||
try {
|
||||
return (await personalDietSuggestionService.getConsultationWithPersonalDiets()).data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) {
|
||||
try {
|
||||
return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) {
|
||||
try {
|
||||
return personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
|
||||
class PersonalDietSuggestionService {
|
||||
final Dio dio;
|
||||
PersonalDietSuggestionService(this.dio);
|
||||
|
||||
Future<ConsultationPaginationModel> getConsultationWithPersonalDiets() async {
|
||||
try {
|
||||
final response = await dio.get('/api/consultations', queryParameters: {
|
||||
'filters[plan][has_personal_diet][\$personal_diet]': '1',
|
||||
'filters[last_state][\$notIn][]': ['done', 'charge_was_collected', 'complained', 'last_day_finished', 'canceled_by_the_initiator', 'inited'],
|
||||
'sort[]': 'start_date:desc',
|
||||
});
|
||||
return ConsultationPaginationModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) async {
|
||||
try {
|
||||
await dio.post('/api/chosen_plans/$consultationId/attach_diet', data: {'diet_id': dietId});
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) async {
|
||||
try {
|
||||
await dio.post('/api/chosen_plans/$consultationId/re_attach_diet', data: {'diet_id': dietId});
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ enum ChosenPlanStateEnum {
|
||||
stateComplained,
|
||||
@JsonValue('done')
|
||||
stateDone,
|
||||
@JsonValue('personal_diet_setted')
|
||||
personalDietSet,
|
||||
@JsonValue('charge_was_collected')
|
||||
stateChargeWasCollected;
|
||||
|
||||
@@ -40,6 +42,7 @@ enum ChosenPlanStateEnum {
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.personalDietSet,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -64,7 +67,8 @@ enum ChosenPlanStateEnum {
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
ChosenPlanStateEnum.stateComplained,
|
||||
ChosenPlanStateEnum.stateDone
|
||||
ChosenPlanStateEnum.stateDone,
|
||||
ChosenPlanStateEnum.personalDietSet
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,12 +505,15 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
switch (state.chosenOptionIndex) {
|
||||
1 => OptionWidget(
|
||||
option: state.planModel?.packet2Options,
|
||||
duration: state.planModel?.duration ?? 0,
|
||||
),
|
||||
2 => OptionWidget(
|
||||
option: state.planModel?.packet3Options,
|
||||
duration: state.planModel?.duration ?? 0,
|
||||
),
|
||||
_ => OptionWidget(
|
||||
option: state.planModel?.packet1Options,
|
||||
duration: state.planModel?.duration ?? 0,
|
||||
),
|
||||
},
|
||||
const SizedBox(
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.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/diets_pagination_model.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';
|
||||
@@ -24,9 +23,11 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
emit(const ProfileState.loading());
|
||||
try {
|
||||
final profile = await profileRepository.getProfileBySlug(slug);
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
emit(
|
||||
ProfileState.loaded(
|
||||
profile: profile,
|
||||
isMyProfile: myProfile.slug == profile.slug,
|
||||
profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
|
||||
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
|
||||
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null,
|
||||
@@ -46,6 +47,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
emit(
|
||||
ProfileState.loaded(
|
||||
profile: profile,
|
||||
isMyProfile: true,
|
||||
profileDetails: await profileRepository.getMyProfileDetails(),
|
||||
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug),
|
||||
weightPoints: await profileRepository.getWeight(period: period),
|
||||
@@ -74,6 +76,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
weightPoints: (state as ProfileStateLoaded).weightPoints,
|
||||
circumferencePoints: (state as ProfileStateLoaded).circumferencePoints,
|
||||
consultantInfo: (state as ProfileStateLoaded).consultantInfo,
|
||||
isMyProfile: (state as ProfileStateLoaded).isMyProfile,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -100,11 +103,12 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getChatRoomIdBySlug({required Author author}) async {
|
||||
Future<String?> getChatRoomIdBySlug({required String slug}) async {
|
||||
try {
|
||||
final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug);
|
||||
final chatIdOrError = await chatRepository.getChatByUserSlug(slug);
|
||||
final userProfile = await profileRepository.getProfileBySlug(slug);
|
||||
if (chatIdOrError == '404') {
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: author);
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: userProfile);
|
||||
} else if (chatIdOrError == 'error') {
|
||||
showErrorToast('Ошибка создания чата');
|
||||
} else {
|
||||
|
||||
@@ -14,9 +14,10 @@ abstract class ProfileState with _$ProfileState {
|
||||
const factory ProfileState.loading() = ProfileStateLoading;
|
||||
|
||||
const factory ProfileState.loaded({
|
||||
required ProfileModel profile,
|
||||
required ProfileModel profile,
|
||||
required ProfileDetailsModel profileDetails,
|
||||
required List<FeedBlogPostModel> blogPosts,
|
||||
required bool isMyProfile,
|
||||
List<WeightModel>? weightPoints,
|
||||
List<CircumferenceModel>? circumferencePoints,
|
||||
ConsultationRequestModel? consultantInfo,
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
|
||||
@@ -44,9 +43,9 @@ import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
final Author? author;
|
||||
final String? slug;
|
||||
|
||||
const ProfileScreen({super.key, this.author});
|
||||
const ProfileScreen({super.key, this.slug});
|
||||
|
||||
@override
|
||||
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||
@@ -71,7 +70,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
|
||||
widget.slug == null ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.slug!);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -110,7 +109,6 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
Text(
|
||||
'Пользователь ограничил доступ к своей странице',
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
@@ -120,15 +118,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
|
||||
return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state);
|
||||
} else if (state is ProfileStateError) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: DataErrorWidget(
|
||||
onReload: () async {
|
||||
widget.author == null || widget.author!.authorIsMe
|
||||
? await profileCubit.getMyProfile()
|
||||
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
|
||||
widget.slug == null ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.slug!);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -143,7 +139,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
Scaffold _profileScreen(ProfileStateLoaded state) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -246,7 +252,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
Scaffold _myProfileScreen(ProfileStateLoaded state) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(toolbarHeight: widget.author == null ? 0 : null),
|
||||
appBar: AppBar(
|
||||
toolbarHeight: widget.slug == null ? 0 : null,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -440,7 +457,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
},
|
||||
child: Text(
|
||||
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
|
||||
state.isMyProfile ? 'Редактировать' : '',
|
||||
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
@@ -518,7 +535,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
},
|
||||
child: Text(
|
||||
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
|
||||
state.isMyProfile ? 'Редактировать' : '',
|
||||
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
@@ -673,24 +690,47 @@ 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,
|
||||
preferBelow: true,
|
||||
showDuration: const Duration(seconds: 5),
|
||||
richMessage: WidgetSpan(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(state.profile.badges[i].title, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 4),
|
||||
Text(state.profile.badges[i].description, style: TextStyles.textSMedium.copyWith(color: appTheme.gray600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)],
|
||||
),
|
||||
child: SvgPicture.network('${sl<AppConfig>().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40),
|
||||
)
|
||||
: Tooltip(
|
||||
message: state.profile.badges[i].description,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 32),
|
||||
preferBelow: true,
|
||||
showDuration: const Duration(seconds: 5),
|
||||
richMessage: WidgetSpan(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(state.profile.badges[i].title, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 4),
|
||||
Text(state.profile.badges[i].description, style: TextStyles.textSMedium.copyWith(color: appTheme.gray600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)],
|
||||
),
|
||||
child: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40),
|
||||
@@ -829,10 +869,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
icon: 'assets/icons/eye.svg',
|
||||
text: 'Посмотреть',
|
||||
onTap: () async {
|
||||
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [sl<AppConfig>().baseUrl + state.profile.profilePhotoUrl], index: 0));
|
||||
context.pushTransparentRoute(
|
||||
rootNavigator: true,
|
||||
PhotoViewScreen(imageUrls: [sl<AppConfig>().baseUrl + state.profile.profilePhotoUrl], index: 0),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.author == null || widget.author!.authorIsMe) ...[
|
||||
if (state.isMyProfile) ...[
|
||||
_buildDivider(),
|
||||
_buildCompactItem(
|
||||
icon: 'assets/icons/edit_paint.svg',
|
||||
@@ -986,11 +1029,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
onTap: () {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
SharePlus.instance.share(
|
||||
ShareParams(uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
|
||||
ShareParams(
|
||||
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/profile/${state.profile.slug}'),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.author != null) ...[
|
||||
if (!state.isMyProfile) ...[
|
||||
_buildDivider(),
|
||||
_buildCompactItem(
|
||||
icon: 'assets/icons/complain.svg',
|
||||
@@ -999,12 +1045,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug),
|
||||
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.slug),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (widget.author != null && !state.profile.isBlockedByMe) ...[
|
||||
if (!state.isMyProfile && !state.profile.isBlockedByMe) ...[
|
||||
_buildDivider(),
|
||||
_buildCompactItem(
|
||||
icon: 'assets/icons/complain.svg',
|
||||
@@ -1012,7 +1058,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
onTap: () {
|
||||
isFirstFetch = true;
|
||||
sl<BlockedUsersCubit>().blockUser(state.profile.slug);
|
||||
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
|
||||
state.isMyProfile ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(state.profile.slug);
|
||||
if (state.profile.roles.contains('consultant')) {
|
||||
plansCubit.getPlans(state.profile.slug);
|
||||
}
|
||||
@@ -1020,7 +1066,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
},
|
||||
),
|
||||
],
|
||||
if (widget.author != null && state.profile.isBlockedByMe) ...[
|
||||
if (!state.isMyProfile && state.profile.isBlockedByMe) ...[
|
||||
_buildDivider(),
|
||||
_buildCompactItem(
|
||||
icon: 'assets/icons/complain.svg',
|
||||
@@ -1028,7 +1074,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
onTap: () {
|
||||
isFirstFetch = true;
|
||||
sl<BlockedUsersCubit>().unblockUser(state.profile.slug);
|
||||
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
|
||||
state.isMyProfile ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(state.profile.slug);
|
||||
if (state.profile.roles.contains('consultant')) {
|
||||
plansCubit.getPlans(state.profile.slug);
|
||||
}
|
||||
@@ -1091,7 +1137,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
GestureDetector(
|
||||
onTap: () async => await profileCubit.getChatRoomIdBySlug(author: widget.author!),
|
||||
onTap: () async => await profileCubit.getChatRoomIdBySlug(slug: widget.slug!),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
||||
@@ -16,17 +16,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class ProfileBlogPostWidget extends StatefulWidget {
|
||||
final int index;
|
||||
final ProfileCubit profileCubit;
|
||||
|
||||
const ProfileBlogPostWidget({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.profileCubit,
|
||||
});
|
||||
const ProfileBlogPostWidget({super.key, required this.index, required this.profileCubit});
|
||||
|
||||
@override
|
||||
State<ProfileBlogPostWidget> createState() => _ProfileBlogPostWidgetState();
|
||||
@@ -46,15 +43,9 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
totalLikes = state.blogPosts[widget.index].totalLikes;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'blogPostScreen',
|
||||
extra: state.blogPosts[widget.index].id,
|
||||
),
|
||||
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.blogPosts[widget.index].id),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -80,19 +71,12 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
children: [
|
||||
Text(
|
||||
state.blogPosts[widget.index].author.authorName,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${calculateDifference(state.blogPosts[widget.index].createdAt)} в ${state.blogPosts[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.blogPosts[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black45,
|
||||
),
|
||||
style: const TextStyle(height: 1, color: Colors.black45),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -114,11 +98,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
state.blogPosts[widget.index].title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -128,14 +108,36 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Html(
|
||||
data: state.blogPosts[widget.index].description.length > 200
|
||||
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
state.blogPosts[widget.index].description.length > 200
|
||||
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
fontSize: FontSize(16),
|
||||
color: Colors.black87,
|
||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -144,12 +146,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
'Показать ещё',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF8B8B8B),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (state.blogPosts[widget.index].title.isNotEmpty || state.blogPosts[widget.index].description.isNotEmpty) const SizedBox(height: 8),
|
||||
@@ -157,10 +154,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
rootNavigator: true,
|
||||
PhotoViewScreen(
|
||||
imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'],
|
||||
index: 0,
|
||||
),
|
||||
PhotoViewScreen(imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'], index: 0),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
@@ -192,10 +186,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
isLiked
|
||||
? await context.read<FeedCubit>().likePost(state.blogPosts[widget.index].id)
|
||||
: await context.read<FeedCubit>().unlikedPost(state.blogPosts[widget.index].id);
|
||||
widget.profileCubit.updateFeedBlogPostList(
|
||||
state.blogPosts[widget.index],
|
||||
isLiked ? false : true,
|
||||
);
|
||||
widget.profileCubit.updateFeedBlogPostList(state.blogPosts[widget.index], isLiked ? false : true);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -204,16 +195,10 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
'assets/icons/comment.svg',
|
||||
height: 20,
|
||||
width: 20,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.gray600,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
|
||||
),
|
||||
text: state.blogPosts[widget.index].totalComments.toString(),
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'blogPostScreen',
|
||||
extra: state.blogPosts[widget.index].id,
|
||||
),
|
||||
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.blogPosts[widget.index].id),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FeedIconButton(
|
||||
@@ -234,19 +219,11 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.visibility_rounded,
|
||||
size: 16,
|
||||
color: appTheme.gray500,
|
||||
),
|
||||
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
state.blogPosts[widget.index].totalViews.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.gray500,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -15,13 +14,7 @@ class SubActionButtonWidget extends StatefulWidget {
|
||||
final String name;
|
||||
final String porfileIcon;
|
||||
|
||||
const SubActionButtonWidget({
|
||||
super.key,
|
||||
required this.subActionType,
|
||||
required this.slug,
|
||||
required this.name,
|
||||
required this.porfileIcon,
|
||||
});
|
||||
const SubActionButtonWidget({super.key, required this.subActionType, required this.slug, required this.name, required this.porfileIcon});
|
||||
|
||||
@override
|
||||
State<SubActionButtonWidget> createState() => _SubActionButtonWidgetState();
|
||||
@@ -49,51 +42,25 @@ class _SubActionButtonWidgetState extends State<SubActionButtonWidget> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: appTheme.blueA400),
|
||||
child: const Text(
|
||||
'Подписаться',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': await sl<ProfileCubit>().getChatRoomIdBySlug(
|
||||
author: Author(
|
||||
authorSlug: widget.slug,
|
||||
authorName: widget.name,
|
||||
authorProfileIcon: widget.porfileIcon,
|
||||
authorIsMe: false,
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
GoRouter.of(context).pushNamed('chatScreen', extra: {'uuid': await sl<ProfileCubit>().getChatRoomIdBySlug(slug: widget.slug)});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: const Color(0xFFF1F2F6),
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: const Color(0xFFF1F2F6)),
|
||||
child: const Text(
|
||||
'Сообщение',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF6D6F71),
|
||||
),
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF6D6F71)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/sub_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -24,13 +23,7 @@ class _SubWidgetState extends State<SubWidget> {
|
||||
return InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: widget.subModel.slug,
|
||||
authorName: widget.subModel.name,
|
||||
authorProfileIcon: widget.subModel.profilePhotoUrl,
|
||||
authorIsMe: false,
|
||||
isEntitySubscribedByMe: false,
|
||||
),
|
||||
extra: widget.subModel.slug,
|
||||
),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
|
||||
@@ -15,15 +15,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class MyRecipeWidget extends StatefulWidget {
|
||||
final MyRecipeModel recipe;
|
||||
|
||||
const MyRecipeWidget({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
});
|
||||
const MyRecipeWidget({super.key, required this.recipe});
|
||||
|
||||
@override
|
||||
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
|
||||
@@ -50,10 +48,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -61,13 +56,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () async => GoRouter.of(context).pushNamed(
|
||||
'aboutRecipesScreen',
|
||||
extra: {
|
||||
'recipe': widget.recipe.slug,
|
||||
'navigateToComments': false,
|
||||
},
|
||||
),
|
||||
onTap: () async => GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'navigateToComments': false}),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
@@ -99,28 +88,48 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.recipe.title!,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(widget.recipe.title!, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
if (widget.recipe.description.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.recipe.description.length > 200
|
||||
? '${widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.recipe.description.replaceAll('\n', '<br>'),
|
||||
data: md.markdownToHtml(
|
||||
widget.recipe.description.length > 200
|
||||
? '${widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||
: widget.recipe.description.replaceAll('\n', '<br>'),
|
||||
),
|
||||
style: {
|
||||
"*": Style(
|
||||
"body": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
padding: HtmlPaddings.zero,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
lineHeight: const LineHeight(1.5),
|
||||
),
|
||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||
"h1": Style(fontSize: FontSize(24)),
|
||||
"h2": Style(fontSize: FontSize(20)),
|
||||
"h3": Style(fontSize: FontSize(18)),
|
||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
||||
"em, i": Style(fontStyle: FontStyle.italic),
|
||||
"code": Style(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
||||
"blockquote": Style(
|
||||
padding: HtmlPaddings.only(left: 16),
|
||||
margin: Margins.only(left: 8, bottom: 12),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -192,26 +201,17 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
Container _iconButton(String iconPath, [String? text]) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
|
||||
height: 32,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
iconPath,
|
||||
),
|
||||
SvgPicture.asset(iconPath),
|
||||
if (text != null) const SizedBox(width: 8),
|
||||
if (text != null)
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'dart:developer';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
|
||||
@@ -12,10 +14,9 @@ import 'package:eatvio_app/features/registration/services/registration_analytics
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
class RegistrationCubit extends Cubit<RegistrationState> with RegistrationDateCacheMixin {
|
||||
final RegistrationRepository registrationRepository;
|
||||
|
||||
RegistrationCubit({required this.registrationRepository})
|
||||
RegistrationCubit({required this.registrationRepository, required Prefs prefs})
|
||||
: super(
|
||||
const RegistrationState(
|
||||
sheet: UserRegistrationSheet(
|
||||
@@ -47,7 +48,9 @@ class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
phone: '',
|
||||
),
|
||||
),
|
||||
);
|
||||
) {
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
|
||||
|
||||
@@ -59,6 +62,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
RegistrationAnalyticsService.reportRegistrationComplete();
|
||||
cacheRegistrationDate(DateTime.now());
|
||||
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
|
||||
class WelcomeScreen extends StatefulWidget {
|
||||
@@ -82,8 +84,8 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
|
||||
InkWell(
|
||||
key: AppKeys.welcomeScreenButton,
|
||||
onTap: () async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setBool('welcomeScreenWasShowed', true);
|
||||
final sharedPreferences = sl<Prefs>();
|
||||
await sharedPreferences.setBool(PrefsKeys.welcomeScreenWasShowed, true);
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
goRouter.go('/feed-screen');
|
||||
} else {
|
||||
|
||||
@@ -138,6 +138,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
|
||||
),
|
||||
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
|
||||
"changeDiet": MessageLookupByLibrary.simpleMessage("Заменить диету"),
|
||||
"chargeApproved": MessageLookupByLibrary.simpleMessage("Выплата одобрена"),
|
||||
"chargeCollectionFailed": MessageLookupByLibrary.simpleMessage(
|
||||
"Не удалось оформить выплату",
|
||||
@@ -158,6 +159,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"choose_plan_start_date": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старта плана питания",
|
||||
),
|
||||
"choose_plan_start_date_with_personal_diet":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старта плана питания с учетом составления персональной диеты",
|
||||
),
|
||||
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
|
||||
),
|
||||
@@ -165,6 +170,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"chose_plan_start_date_2": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старты плана. Она отобразиться в дневнике пользователя",
|
||||
),
|
||||
"chosenPlanStartDateDescr": MessageLookupByLibrary.simpleMessage(
|
||||
"Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты",
|
||||
),
|
||||
"circumference_empty": MessageLookupByLibrary.simpleMessage(
|
||||
"Все замеры обязательны к заполнению",
|
||||
),
|
||||
@@ -199,6 +207,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"congratsYouHaveFinishedPlan": MessageLookupByLibrary.simpleMessage(
|
||||
"Поздравляем, Вы закончили план",
|
||||
),
|
||||
"congratulations": MessageLookupByLibrary.simpleMessage("Поздравляем!"),
|
||||
"consultantLeftComment": m0,
|
||||
"consultations_count": MessageLookupByLibrary.simpleMessage(
|
||||
"Кол-во проведенных консулдьтаций",
|
||||
@@ -245,10 +254,14 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Не удалось обновить диету",
|
||||
),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Страна"),
|
||||
"createPersonalDiet": MessageLookupByLibrary.simpleMessage(
|
||||
"Составьте персональную диету",
|
||||
),
|
||||
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
|
||||
"cunsutation_was_paid_successfully": MessageLookupByLibrary.simpleMessage(
|
||||
"Поздравляем, консультация успешно оплачена",
|
||||
),
|
||||
"customer": MessageLookupByLibrary.simpleMessage("Покупатель"),
|
||||
"customer_price": MessageLookupByLibrary.simpleMessage(
|
||||
"Цена для покупателя",
|
||||
),
|
||||
@@ -272,6 +285,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
|
||||
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
|
||||
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
|
||||
"dietSuggestion": MessageLookupByLibrary.simpleMessage("Предложение диеты"),
|
||||
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
|
||||
"diet_has_been_sent_to_draft": MessageLookupByLibrary.simpleMessage(
|
||||
"снята с публикации и перенесена в черновик",
|
||||
@@ -305,6 +319,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",
|
||||
),
|
||||
"eatvioIsAHelthyHabbit": MessageLookupByLibrary.simpleMessage(
|
||||
"Итвио — это полезная привычка",
|
||||
),
|
||||
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
|
||||
"email": MessageLookupByLibrary.simpleMessage("Электронная почта"),
|
||||
@@ -378,6 +395,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"for_whole_period_of_time": MessageLookupByLibrary.simpleMessage(
|
||||
"За всё время",
|
||||
),
|
||||
"fourteenDays": MessageLookupByLibrary.simpleMessage("14 дней"),
|
||||
"fraud": MessageLookupByLibrary.simpleMessage("Мошенничество"),
|
||||
"fraud_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Мошенничество с целью наживы. Если вас обманули, пожалуйста, сообщите об этом в правоохранительные органы.",
|
||||
@@ -387,6 +405,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
|
||||
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
|
||||
"general_info": MessageLookupByLibrary.simpleMessage("Общая информация"),
|
||||
"goToMyDiets": MessageLookupByLibrary.simpleMessage(
|
||||
"Перейти к моим диетам",
|
||||
),
|
||||
"go_to_diary": MessageLookupByLibrary.simpleMessage("Перейти в дневник"),
|
||||
"gr": MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"grams": MessageLookupByLibrary.simpleMessage("гр"),
|
||||
@@ -418,6 +439,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"initial_measurements": MessageLookupByLibrary.simpleMessage(
|
||||
"Первичные замеры",
|
||||
),
|
||||
"invite": MessageLookupByLibrary.simpleMessage("Пригласить"),
|
||||
"invited_people_count": MessageLookupByLibrary.simpleMessage(
|
||||
"Количество приглашенных",
|
||||
),
|
||||
@@ -440,6 +462,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"leaveComplainRequest": MessageLookupByLibrary.simpleMessage(
|
||||
"Подать жалобу",
|
||||
),
|
||||
"letsInvite": MessageLookupByLibrary.simpleMessage(
|
||||
"Давайте пригласим близких в итвио",
|
||||
),
|
||||
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
|
||||
"light_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",
|
||||
@@ -560,6 +585,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
|
||||
"pending_payment": MessageLookupByLibrary.simpleMessage("Ожидается оплата"),
|
||||
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
|
||||
"personalDietCreating": MessageLookupByLibrary.simpleMessage(
|
||||
"Составление индивидуальной диеты",
|
||||
),
|
||||
"personal_diet_set": MessageLookupByLibrary.simpleMessage(
|
||||
"Персональная диета установлена",
|
||||
),
|
||||
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
|
||||
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
|
||||
"physical_fitness": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -689,6 +720,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"successfully_published": MessageLookupByLibrary.simpleMessage(
|
||||
"успешно опубликован",
|
||||
),
|
||||
"suggestDiet": MessageLookupByLibrary.simpleMessage("Предложить диету"),
|
||||
"suicide": MessageLookupByLibrary.simpleMessage("Склонение к самоубийству"),
|
||||
"takes_pills": MessageLookupByLibrary.simpleMessage("Принимает лекарства"),
|
||||
"tap_on_link_to_pay": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -773,6 +805,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"want_to_see_tomorrow": MessageLookupByLibrary.simpleMessage(
|
||||
"Хотите заглянуть в будущее?",
|
||||
),
|
||||
"weAreLookingFor": MessageLookupByLibrary.simpleMessage(
|
||||
"Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
|
||||
),
|
||||
"weapon": MessageLookupByLibrary.simpleMessage("Оружие"),
|
||||
"weight": MessageLookupByLibrary.simpleMessage("Вес"),
|
||||
"weight_chart": MessageLookupByLibrary.simpleMessage("График веса"),
|
||||
@@ -801,6 +836,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage(
|
||||
"Вы уверены, что хотите удалить аккаунт?",
|
||||
),
|
||||
"youWithUs": MessageLookupByLibrary.simpleMessage("Вы с итвио уже\n"),
|
||||
"you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage(
|
||||
"Вы не можете перейти в этот день",
|
||||
),
|
||||
|
||||
@@ -28,10 +28,9 @@ class S {
|
||||
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
|
||||
|
||||
static Future<S> load(Locale locale) {
|
||||
final name =
|
||||
(locale.countryCode?.isEmpty ?? false)
|
||||
? locale.languageCode
|
||||
: locale.toString();
|
||||
final name = (locale.countryCode?.isEmpty ?? false)
|
||||
? locale.languageCode
|
||||
: locale.toString();
|
||||
final localeName = Intl.canonicalizedLocale(name);
|
||||
return initializeMessages(localeName).then((_) {
|
||||
Intl.defaultLocale = localeName;
|
||||
@@ -3435,6 +3434,86 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Составление индивидуальной диеты`
|
||||
String get personalDietCreating {
|
||||
return Intl.message(
|
||||
'Составление индивидуальной диеты',
|
||||
name: 'personalDietCreating',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Предложение диеты`
|
||||
String get dietSuggestion {
|
||||
return Intl.message(
|
||||
'Предложение диеты',
|
||||
name: 'dietSuggestion',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Предложить диету`
|
||||
String get suggestDiet {
|
||||
return Intl.message(
|
||||
'Предложить диету',
|
||||
name: 'suggestDiet',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Заменить диету`
|
||||
String get changeDiet {
|
||||
return Intl.message(
|
||||
'Заменить диету',
|
||||
name: 'changeDiet',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Выберите дату старта плана питания с учетом составления персональной диеты`
|
||||
String get choose_plan_start_date_with_personal_diet {
|
||||
return Intl.message(
|
||||
'Выберите дату старта плана питания с учетом составления персональной диеты',
|
||||
name: 'choose_plan_start_date_with_personal_diet',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты`
|
||||
String get chosenPlanStartDateDescr {
|
||||
return Intl.message(
|
||||
'Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты',
|
||||
name: 'chosenPlanStartDateDescr',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Составьте персональную диету`
|
||||
String get createPersonalDiet {
|
||||
return Intl.message(
|
||||
'Составьте персональную диету',
|
||||
name: 'createPersonalDiet',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Перейти к моим диетам`
|
||||
String get goToMyDiets {
|
||||
return Intl.message(
|
||||
'Перейти к моим диетам',
|
||||
name: 'goToMyDiets',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Удаление аккаунта`
|
||||
String get accountDeleting {
|
||||
return Intl.message(
|
||||
@@ -3499,6 +3578,81 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Покупатель`
|
||||
String get customer {
|
||||
return Intl.message('Покупатель', name: 'customer', desc: '', args: []);
|
||||
}
|
||||
|
||||
/// `Персональная диета установлена`
|
||||
String get personal_diet_set {
|
||||
return Intl.message(
|
||||
'Персональная диета установлена',
|
||||
name: 'personal_diet_set',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Поздравляем!`
|
||||
String get congratulations {
|
||||
return Intl.message(
|
||||
'Поздравляем!',
|
||||
name: 'congratulations',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Вы с итвио уже\n`
|
||||
String get youWithUs {
|
||||
return Intl.message(
|
||||
'Вы с итвио уже\n',
|
||||
name: 'youWithUs',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `14 дней`
|
||||
String get fourteenDays {
|
||||
return Intl.message('14 дней', name: 'fourteenDays', desc: '', args: []);
|
||||
}
|
||||
|
||||
/// `Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!`
|
||||
String get weAreLookingFor {
|
||||
return Intl.message(
|
||||
'Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!',
|
||||
name: 'weAreLookingFor',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Давайте пригласим близких в итвио`
|
||||
String get letsInvite {
|
||||
return Intl.message(
|
||||
'Давайте пригласим близких в итвио',
|
||||
name: 'letsInvite',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Итвио — это полезная привычка`
|
||||
String get eatvioIsAHelthyHabbit {
|
||||
return Intl.message(
|
||||
'Итвио — это полезная привычка',
|
||||
name: 'eatvioIsAHelthyHabbit',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Пригласить`
|
||||
String get invite {
|
||||
return Intl.message('Пригласить', name: 'invite', desc: '', args: []);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -408,12 +408,29 @@
|
||||
"copy": "Скопировать",
|
||||
"reply": "Ответить",
|
||||
"deleteComment": "Удалить комментарий",
|
||||
"personalDietCreating": "Составление индивидуальной диеты",
|
||||
"dietSuggestion": "Предложение диеты",
|
||||
"suggestDiet": "Предложить диету",
|
||||
"changeDiet": "Заменить диету",
|
||||
"choose_plan_start_date_with_personal_diet": "Выберите дату старта плана питания с учетом составления персональной диеты",
|
||||
"chosenPlanStartDateDescr": "Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты",
|
||||
"createPersonalDiet": "Составьте персональную диету",
|
||||
"goToMyDiets": "Перейти к моим диетам",
|
||||
"accountDeleting": "Удаление аккаунта",
|
||||
"youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?",
|
||||
"itCantBeRecovered": "Его нельзя будет восстановить",
|
||||
"delete": "Удалить",
|
||||
"cannotDeleteAccount": "Не получилось удалить ваш аккаунт",
|
||||
"resend": "Отправить повторно",
|
||||
"notifications": "Уведомления"
|
||||
"notifications": "Уведомления",
|
||||
"customer": "Покупатель",
|
||||
"personal_diet_set": "Персональная диета установлена",
|
||||
"congratulations": "Поздравляем!",
|
||||
"youWithUs": "Вы с итвио уже\n",
|
||||
"fourteenDays": "14 дней",
|
||||
"weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
|
||||
"letsInvite": "Давайте пригласим близких в итвио",
|
||||
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
|
||||
"invite": "Пригласить"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.1+80
|
||||
version: 1.3.3+82
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
@@ -76,6 +76,7 @@ dependencies:
|
||||
appmetrica_plugin: ^3.3.0
|
||||
vk_id: ^1.0.3
|
||||
flutter_login_yandex: ^0.0.5
|
||||
markdown: ^7.3.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.7.0
|
||||
|
||||
133
test/features/blog_post_create/README.md
Normal file
133
test/features/blog_post_create/README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Blog Post Creation Tests
|
||||
|
||||
Этот набор тестов покрывает функциональность создания, обновления и удаления блог постов в приложении EatVio.
|
||||
|
||||
## Структура тестов
|
||||
|
||||
### 1. Basic Widget Tests (`basic_widget_test.dart`)
|
||||
|
||||
Базовые виджет тесты для проверки основных компонентов создания блог постов:
|
||||
|
||||
- **Создание BlogPostCreateCubit** - проверка инициализации cubit
|
||||
- **Отображение полей формы** - проверка наличия всех необходимых полей
|
||||
- **Обновление полей** - проверка ввода текста в поля заголовка, описания и видео
|
||||
- **Отображение лейблов** - проверка корректности отображения подписей полей
|
||||
- **Placeholder текст** - проверка подсказок в полях ввода
|
||||
- **Кнопка создания** - проверка отображения и функциональности кнопки
|
||||
- **Обработка нажатий** - проверка реакции на нажатия кнопок
|
||||
- **Состояния загрузки** - проверка отображения индикатора загрузки
|
||||
- **Сообщения об ошибках** - проверка отображения ошибок
|
||||
- **Сообщения об успехе** - проверка отображения успешных операций
|
||||
- **Валидация полей** - проверка валидации обязательных полей
|
||||
- **Полная форма** - проверка отображения всех полей формы
|
||||
- **Загрузка изображений** - проверка функциональности загрузки изображений
|
||||
|
||||
### 2. Advanced Widget Tests (`advanced_widget_test.dart`)
|
||||
|
||||
Продвинутые тесты для проверки сложных сценариев:
|
||||
|
||||
- **Создание с всеми полями** - тест создания блог поста со всеми заполненными полями
|
||||
- **Создание с изображением** - тест создания блог поста с загруженным изображением
|
||||
- **Обновление блог поста** - тест обновления существующего блог поста
|
||||
- **Удаление блог поста** - тест удаления блог поста
|
||||
- **Состояния загрузки** - проверка состояний загрузки во время операций
|
||||
- **Обработка ошибок** - проверка обработки ошибок при создании
|
||||
- **Успешное создание** - проверка успешного завершения операций
|
||||
- **Валидация формы** - проверка валидации полей перед отправкой
|
||||
- **Взаимодействие с загрузкой изображений** - проверка функциональности выбора изображений
|
||||
- **Валидация URL видео** - проверка валидации ссылок на видео
|
||||
- **Отправка с пустыми полями** - проверка обработки пустых полей
|
||||
- **Длинный контент** - проверка обработки длинного текста
|
||||
|
||||
### 3. Integration Widget Tests (`integration_widget_test.dart`)
|
||||
|
||||
Интеграционные тесты для проверки полного потока работы:
|
||||
|
||||
- **Полный поток создания** - тест полного процесса создания блог поста
|
||||
- **Поток обновления** - тест полного процесса обновления блог поста
|
||||
- **Поток удаления** - тест полного процесса удаления блог поста
|
||||
- **Обработка ошибок** - тест обработки ошибок в полном потоке
|
||||
- **Состояния загрузки** - тест состояний загрузки в полном потоке
|
||||
- **Успешное завершение** - тест успешного завершения операций
|
||||
- **Валидация формы** - тест валидации в полном потоке
|
||||
- **Загрузка изображений** - тест загрузки изображений в полном потоке
|
||||
- **Полный функционал** - тест всех функций создания блог постов
|
||||
|
||||
## Ключи приложения (AppKeys)
|
||||
|
||||
Тесты используют следующие ключи из `app_keys.dart`:
|
||||
|
||||
- `blogPostCreateScreenTitleTextField` - поле заголовка
|
||||
- `blogPostCreateScreenContentTextField` - поле описания
|
||||
- `blogPostCreateScreenVideoLinkTextField` - поле ссылки на видео
|
||||
- `blogPostCreateScreenCreateButton` - кнопка создания/обновления
|
||||
- `imageWidget` - виджет загрузки изображения
|
||||
|
||||
## Моки (Mocks)
|
||||
|
||||
Тесты используют моки для следующих компонентов:
|
||||
|
||||
- `MockBlogPostRepository` - репозиторий для работы с блог постами
|
||||
- `MockFeedCubit` - cubit для управления лентой постов
|
||||
|
||||
## Состояния BlogPostCreateState
|
||||
|
||||
Тесты проверяют следующие состояния:
|
||||
|
||||
- `BlogPostCreateState` - начальное состояние
|
||||
- `BlogPostCreateStateLoading` - состояние загрузки
|
||||
- `BlogPostCreateStateLoaded` - состояние успешного завершения
|
||||
- `BlogPostCreateStateError` - состояние ошибки
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
Для запуска всех тестов блог постов:
|
||||
|
||||
```bash
|
||||
flutter test test/features/blog_post_create/
|
||||
```
|
||||
|
||||
Для запуска конкретного теста:
|
||||
|
||||
```bash
|
||||
flutter test test/features/blog_post_create/basic_widget_test.dart
|
||||
flutter test test/features/blog_post_create/advanced_widget_test.dart
|
||||
flutter test test/features/blog_post_create/integration_widget_test.dart
|
||||
```
|
||||
|
||||
## Покрытие функциональности
|
||||
|
||||
Тесты покрывают:
|
||||
|
||||
- ✅ Создание блог постов
|
||||
- ✅ Обновление блог постов
|
||||
- ✅ Удаление блог постов
|
||||
- ✅ Загрузка изображений
|
||||
- ✅ Валидация полей
|
||||
- ✅ Обработка ошибок
|
||||
- ✅ Состояния загрузки
|
||||
- ✅ Интеграция с лентой постов
|
||||
- ✅ Пользовательский интерфейс
|
||||
- ✅ Взаимодействие с формами
|
||||
|
||||
## Зависимости
|
||||
|
||||
Тесты используют следующие пакеты:
|
||||
|
||||
- `flutter_test` - базовые тесты Flutter
|
||||
- `mockito` - создание моков
|
||||
- `flutter_bloc` - тестирование BLoC
|
||||
- `image_picker` - работа с изображениями
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
test/features/blog_post_create/
|
||||
├── basic_widget_test.dart
|
||||
├── basic_widget_test.mocks.dart
|
||||
├── advanced_widget_test.dart
|
||||
├── advanced_widget_test.mocks.dart
|
||||
├── integration_widget_test.dart
|
||||
├── integration_widget_test.mocks.dart
|
||||
└── README.md
|
||||
```
|
||||
524
test/features/blog_post_create/advanced_widget_test.dart
Normal file
524
test/features/blog_post_create/advanced_widget_test.dart
Normal file
@@ -0,0 +1,524 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'advanced_widget_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([BlogPostRepository])
|
||||
void main() {
|
||||
group('Blog Post Creation Advanced Widget Tests', () {
|
||||
late MockBlogPostRepository mockBlogPostRepository;
|
||||
late BlogPostCreateCubit blogPostCreateCubit;
|
||||
|
||||
setUp(() {
|
||||
mockBlogPostRepository = MockBlogPostRepository();
|
||||
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
blogPostCreateCubit.close();
|
||||
});
|
||||
|
||||
Widget createTestWidget(Widget child) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
home: BlocProvider<BlogPostCreateCubit>(
|
||||
create: (context) => blogPostCreateCubit,
|
||||
child: Scaffold(body: SingleChildScrollView(child: child)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should handle blog post creation with all fields', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
|
||||
|
||||
Widget formWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.createBlogPost(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
videoUrl: 'https://youtube.com/watch?v=test',
|
||||
image: null,
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=test');
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
verify(mockBlogPostRepository.createBlogPost('Test Title', 'https://youtube.com/watch?v=test', 'Test Description', null)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post creation with image', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
|
||||
|
||||
Widget formWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
key: AppKeys.imageWidget,
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.createBlogPost(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
videoUrl: null,
|
||||
image: null, // В реальном тесте здесь был бы XFile
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
verify(mockBlogPostRepository.createBlogPost('Test Title', null, 'Test Description', null)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post update', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {});
|
||||
|
||||
final testBlogPost = FeedBlogPostModel(
|
||||
id: 1,
|
||||
state: 'published',
|
||||
image: 'original_image.jpg',
|
||||
description: 'Original Description',
|
||||
author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true),
|
||||
videoUrl: 'https://youtube.com/watch?v=original',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
title: 'Original Title',
|
||||
totalLikes: 0,
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
Widget formWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок'),
|
||||
controller: TextEditingController(text: testBlogPost.title),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
controller: TextEditingController(text: testBlogPost.description),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
controller: TextEditingController(text: testBlogPost.videoUrl),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.updateBlogPost(
|
||||
id: testBlogPost.id,
|
||||
title: 'Updated Title',
|
||||
description: 'Updated Description',
|
||||
videoUrl: 'https://youtube.com/watch?v=updated',
|
||||
image: null,
|
||||
oldImageUrl: testBlogPost.image,
|
||||
);
|
||||
},
|
||||
child: const Text('Обновить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Updated Title');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Updated Description');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=updated');
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
verify(
|
||||
mockBlogPostRepository.updateBlogPost(1, 'Updated Title', 'https://youtube.com/watch?v=updated', 'Updated Description', null, 'original_image.jpg'),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post deletion', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.deleteBlogPost(any)).thenAnswer((_) async {});
|
||||
|
||||
Widget deleteWidget = ElevatedButton(
|
||||
key: const Key('deleteBlogPostButton'),
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.deleteBlogPost(1);
|
||||
},
|
||||
child: const Text('Удалить пост'),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(deleteWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('deleteBlogPostButton')));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
verify(mockBlogPostRepository.deleteBlogPost(1)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
});
|
||||
|
||||
Widget loadingWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
builder: (context, state) {
|
||||
if (state is BlogPostCreateStateLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return const Text('Not Loading');
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(loadingWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Trigger loading state
|
||||
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
|
||||
await tester.pump();
|
||||
await tester.pump(); // Need to pump twice to get the loading state
|
||||
|
||||
// Assert
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
// Wait for the async operation to complete
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('should handle error state during blog post creation', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenThrow(Exception('Network error'));
|
||||
|
||||
Widget errorWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
builder: (context, state) {
|
||||
if (state is BlogPostCreateStateError) {
|
||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||
}
|
||||
return const Text('No Error');
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(errorWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Trigger error state
|
||||
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
|
||||
await tester.pump();
|
||||
await tester.pump(); // Need to pump twice to get the error state
|
||||
|
||||
// Assert
|
||||
expect(find.textContaining('Error:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle success state after blog post creation', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
|
||||
|
||||
Widget successWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
builder: (context, state) {
|
||||
if (state is BlogPostCreateStateLoaded) {
|
||||
return const Center(child: Text('Blog post created successfully!'));
|
||||
}
|
||||
return const Text('Not Loaded');
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(successWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Trigger success state
|
||||
blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
|
||||
await tester.pump();
|
||||
await tester.pump(); // Need to pump twice to get the success state
|
||||
|
||||
// Assert
|
||||
expect(find.text('Blog post created successfully!'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should validate form fields before submission', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget validationWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', errorText: 'Заголовок не может быть пустым'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание не может быть пустым'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(validationWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Заголовок не может быть пустым'), findsOneWidget);
|
||||
expect(find.text('Описание не может быть пустым'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle image picker interaction', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
bool imagePickerCalled = false;
|
||||
Widget imagePickerWidget = GestureDetector(
|
||||
key: AppKeys.imageWidget,
|
||||
onTap: () {
|
||||
imagePickerCalled = true;
|
||||
},
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(imagePickerWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(AppKeys.imageWidget));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(imagePickerCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should handle video URL validation', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget videoValidationWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео', errorText: 'Введите корректную ссылку на видео'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(videoValidationWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Введите корректную ссылку на видео'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle form submission with empty fields', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget emptyFormWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.createBlogPost(title: '', videoUrl: '', description: '', image: null);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(emptyFormWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
verify(mockBlogPostRepository.createBlogPost('', '', '', null)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post creation with long content', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
const longContent =
|
||||
'Это очень длинное описание блог поста, которое содержит много текста и должно быть правильно обработано системой. '
|
||||
'Оно может содержать несколько абзацев и различные форматирования. '
|
||||
'Пользователи должны иметь возможность создавать подробные и информативные посты.';
|
||||
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
|
||||
|
||||
Widget longContentWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: longContent, image: null);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(longContentWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), longContent);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
verify(mockBlogPostRepository.createBlogPost('Test Title', null, longContent, null)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
443
test/features/blog_post_create/basic_widget_test.dart
Normal file
443
test/features/blog_post_create/basic_widget_test.dart
Normal file
@@ -0,0 +1,443 @@
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
|
||||
import 'basic_widget_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([BlogPostRepository])
|
||||
void main() {
|
||||
group('Blog Post Creation Basic Widget Tests', () {
|
||||
late MockBlogPostRepository mockBlogPostRepository;
|
||||
late BlogPostCreateCubit blogPostCreateCubit;
|
||||
|
||||
setUp(() {
|
||||
mockBlogPostRepository = MockBlogPostRepository();
|
||||
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
blogPostCreateCubit.close();
|
||||
});
|
||||
|
||||
Widget createTestWidget(Widget child) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
home: BlocProvider<BlogPostCreateCubit>(
|
||||
create: (context) => blogPostCreateCubit,
|
||||
child: Scaffold(body: SingleChildScrollView(child: child)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should create BlogPostCreateCubit successfully', (WidgetTester tester) async {
|
||||
// Arrange & Act
|
||||
final cubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
|
||||
|
||||
// Assert
|
||||
expect(cubit, isA<BlogPostCreateCubit>());
|
||||
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
testWidgets('should display basic blog post form fields', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should update title when user types in title field', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const testTitle = 'Тестовый заголовок поста';
|
||||
|
||||
// Act
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), testTitle);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.text(testTitle), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should update description when user types in description field', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const testDescription = 'Это тестовое описание блог поста';
|
||||
|
||||
// Act
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), testDescription);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.text(testDescription), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should update video URL when user types in video field', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const testVideoUrl = 'https://youtube.com/watch?v=example';
|
||||
|
||||
// Act
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), testVideoUrl);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.text(testVideoUrl), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display form labels correctly', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = const Column(children: [Text('Заголовок'), Text('Описание'), Text('Ссылка на видео')]);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Заголовок'), findsOneWidget);
|
||||
expect(find.text('Описание'), findsOneWidget);
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display placeholder text in title field', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Введите заголовок поста'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display create button', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать'));
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
expect(find.text('Опубликовать'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle button tap', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
bool buttonPressed = false;
|
||||
Widget formWidget = ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {
|
||||
buttonPressed = true;
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Act
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(buttonPressed, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should display loading state', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget loadingWidget = const Center(child: CircularProgressIndicator());
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(loadingWidget));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display error message', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget errorWidget = const Center(child: Text('Произошла ошибка при создании блог поста'));
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(errorWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Произошла ошибка при создании блог поста'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display success message', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget successWidget = const Center(child: Text('Блог пост успешно создан!'));
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(successWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Блог пост успешно создан!'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should validate required fields', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget validationWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', errorText: 'Заголовок обязателен'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание обязательно'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(validationWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Заголовок обязателен'), findsOneWidget);
|
||||
expect(find.text('Описание обязательно'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display form with all required fields', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget completeForm = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(completeForm));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display image upload widget', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget imageWidget = GestureDetector(
|
||||
key: AppKeys.imageWidget,
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(imageWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.imageWidget), findsOneWidget);
|
||||
expect(find.text('Добавить изображение'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle image upload tap', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
bool imageUploadTapped = false;
|
||||
Widget imageWidget = GestureDetector(
|
||||
key: AppKeys.imageWidget,
|
||||
onTap: () {
|
||||
imageUploadTapped = true;
|
||||
},
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(imageWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Act
|
||||
await tester.tap(find.byKey(AppKeys.imageWidget));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(imageUploadTapped, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should display blog post creation form with image and video options', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget completeForm = Column(
|
||||
children: [
|
||||
const Text('Заголовок'),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(hintText: 'Введите заголовок поста'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Описание'),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(), maxLines: 6, minLines: 4),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Изображение поста'),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
key: AppKeys.imageWidget,
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Ссылка на видео'),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, decoration: const InputDecoration()),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(completeForm));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Заголовок'), findsOneWidget);
|
||||
expect(find.text('Описание'), findsOneWidget);
|
||||
expect(find.text('Изображение поста'), findsOneWidget);
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.imageWidget), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user