forked from eatvio/eatvio_app
Compare commits
67 Commits
issue-710
...
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 | |||
|
|
250d9614e3 | ||
| b9b06f11a6 | |||
|
|
f35cdd26a3 | ||
|
|
5ee86ddc92 | ||
|
|
fceaf3d162 | ||
|
|
46841fb502 | ||
|
|
8e72350152 | ||
|
|
ca3bd7cc0d | ||
|
|
782f75d26b | ||
|
|
79dc590beb | ||
|
|
517e493b5e | ||
|
|
1bc0ab3237 | ||
|
|
a8994eecf4 | ||
|
|
8d3ae15112 | ||
|
|
6b7b1f2d18 | ||
|
|
82bc51e45c | ||
|
|
77666aca0e | ||
|
|
16430af989 | ||
|
|
8c2cd5eedd | ||
|
|
996f2ecd70 | ||
|
|
19787f459e | ||
|
|
7e64b24a8c | ||
|
|
a07d085ba7 | ||
|
|
5616bedd25 | ||
|
|
0a3d9e577d | ||
|
|
55e3abec99 |
5
Makefile
5
Makefile
@@ -4,4 +4,7 @@ run-tests:
|
|||||||
run-analyse:
|
run-analyse:
|
||||||
flutter analyze
|
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="/ru/plan/.*" />
|
||||||
<data android:pathPattern="/en/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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- 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/app_keys.dart';
|
||||||
import 'package:eatvio_app/core/app_initializer.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:eatvio_app/main.dart' as app;
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
@@ -12,7 +14,7 @@ void main() {
|
|||||||
await AppInitializer.initialize(buildType: AppBuildType.tests);
|
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.pumpWidget(const app.MyApp());
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||||
await skipWelcomeScreen(tester);
|
await skipWelcomeScreen(tester);
|
||||||
@@ -24,14 +26,14 @@ void main() {
|
|||||||
expect(passwordFieldFinder, findsOneWidget, reason: 'Password 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.');
|
expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.');
|
||||||
|
|
||||||
await tester.enterText(emailFieldFinder, 'reshetnick.mike+user4@gmail.com');
|
await tester.enterText(emailFieldFinder, EnvConfig.testEmail);
|
||||||
await tester.enterText(passwordFieldFinder, '1234567890');
|
await tester.enterText(passwordFieldFinder, EnvConfig.testPassword);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(signInButtonFinder);
|
await tester.tap(signInButtonFinder);
|
||||||
|
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
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);
|
final welcomeScreenButtonFinder = find.byKey(AppKeys.welcomeScreenButton);
|
||||||
await tester.tap(welcomeScreenButtonFinder);
|
await tester.tap(welcomeScreenButtonFinder);
|
||||||
await tester.pumpAndSettle();
|
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_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 80;
|
CURRENT_PROJECT_VERSION = 81;
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.3.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 80;
|
CURRENT_PROJECT_VERSION = 81;
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -557,7 +557,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.3.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 80;
|
CURRENT_PROJECT_VERSION = 81;
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -596,7 +596,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.3.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>80</string>
|
<string>82</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -1,13 +1,44 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AppKeys {
|
class AppKeys {
|
||||||
|
|
||||||
// Auth Screen
|
// Auth Screen
|
||||||
static const hiddenLogoButtonAuthScreen = ValueKey('hiddenLogoButtonAuthScreen');
|
static const hiddenLogoButtonAuthScreen = ValueKey('hiddenLogoButtonAuthScreen');
|
||||||
static const emailTextFieldAuthScreen = ValueKey('emailTextFieldAuthScreen');
|
static const emailTextFieldAuthScreen = ValueKey('emailTextFieldAuthScreen');
|
||||||
static const passwordTextFieldAuthScreen = ValueKey('passwordTextFieldAuthScreen');
|
static const passwordTextFieldAuthScreen = ValueKey('passwordTextFieldAuthScreen');
|
||||||
static const signInButtonAuthScreen = ValueKey('signInButtonAuthScreen');
|
static const signInButtonAuthScreen = ValueKey('signInButtonAuthScreen');
|
||||||
|
|
||||||
// Welcome Screen
|
// Welcome Screen
|
||||||
static const welcomeScreenButton = ValueKey('welcomeScreenButton');
|
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/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/env_config.dart';
|
||||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
@@ -40,6 +41,7 @@ class AppConfig {
|
|||||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
||||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
||||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||||
|
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Делегаты локализации
|
/// Делегаты локализации
|
||||||
|
|||||||
@@ -29,23 +29,45 @@ class AppRouter {
|
|||||||
/// Обработка редиректов для deep links
|
/// Обработка редиректов для deep links
|
||||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||||
final segments = state.uri.pathSegments;
|
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 type = segments[1];
|
||||||
final contentIdentifier = segments[2];
|
final contentIdentifier = segments[2];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'recipe':
|
case 'recipe':
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier';
|
return '/about-recipes-screen?slug=$contentIdentifier';
|
||||||
case 'blog_post':
|
case 'blog_post':
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '${RouteConstants.blogPostScreen}?id=$contentIdentifier';
|
return '/blog-post-screen?id=$contentIdentifier';
|
||||||
case 'plan':
|
case 'plan':
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier';
|
return '/about-plan-screen?slug=$contentIdentifier';
|
||||||
case 'diet':
|
case 'diet':
|
||||||
FlutterNativeSplash.remove();
|
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/internet_connection_cubit/internet_connection_cubit.dart';
|
||||||
import 'package:eatvio_app/core/views/cubit/views_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';
|
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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
import 'package:eatvio_app/features/order_info/cubit/order_info_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/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/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||||
@@ -74,10 +76,10 @@ class CubitRegistrations {
|
|||||||
static void _registerFactoryCubits() {
|
static void _registerFactoryCubits() {
|
||||||
// Auth related cubits
|
// Auth related cubits
|
||||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
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<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl()));
|
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
||||||
// Feed related cubits
|
// Feed related cubits
|
||||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: 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<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||||
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
||||||
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: 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<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
||||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
||||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
||||||
@@ -143,5 +145,7 @@ class CubitRegistrations {
|
|||||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||||
sl.registerFactory<MenuCubit>(() => MenuCubit(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/app_config.dart';
|
||||||
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.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/interceptors/token_interceptor.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
||||||
class ExternalRegistrations {
|
class ExternalRegistrations {
|
||||||
@@ -11,23 +13,20 @@ class ExternalRegistrations {
|
|||||||
/// Регистрация всех внешних зависимостей
|
/// Регистрация всех внешних зависимостей
|
||||||
static void registerAll() {
|
static void registerAll() {
|
||||||
_registerDio();
|
_registerDio();
|
||||||
|
_registerPrefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Регистрация Dio HTTP клиента
|
/// Регистрация Dio HTTP клиента
|
||||||
static void _registerDio() {
|
static void _registerDio() {
|
||||||
sl.registerLazySingleton(
|
sl.registerLazySingleton(
|
||||||
() => Dio(
|
() =>
|
||||||
BaseOptions(
|
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
||||||
baseUrl: sl<AppConfig>().baseUrl,
|
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
||||||
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_finance/repositories/cards_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/repositories/my_diets_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/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/plan_repository.dart';
|
||||||
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
||||||
import 'package:eatvio_app/features/profile/repositories/profile_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<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||||
|
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
|
||||||
|
|
||||||
// Content repositories
|
// Content repositories
|
||||||
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
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_finance/services/cards_service.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/services/my_plans_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/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/plan_service.dart';
|
||||||
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
||||||
import 'package:eatvio_app/features/profile/services/profile_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<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||||
|
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
|
||||||
|
|
||||||
// Content services
|
// Content services
|
||||||
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
||||||
|
|||||||
@@ -29,6 +29,22 @@ class FirebaseApi {
|
|||||||
return token;
|
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 {
|
Future<void> registerPushToken() async {
|
||||||
if (await TokenStorage.haveTokenInStorage()) {
|
if (await TokenStorage.haveTokenInStorage()) {
|
||||||
try {
|
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/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||||
@@ -18,6 +20,10 @@ class AuthRoutes {
|
|||||||
path: RouteConstants.authScreen,
|
path: RouteConstants.authScreen,
|
||||||
name: 'authScreen',
|
name: 'authScreen',
|
||||||
builder: (context, state) => const AuthScreen(),
|
builder: (context, state) => const AuthScreen(),
|
||||||
|
redirect: (context, state) {
|
||||||
|
sl<FirebaseApi>().registerAnonymousPushToken();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.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/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.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_finance/presentation/pages/user_finance_screen.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_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/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/profile_screen.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_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';
|
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:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
/// Остальные маршруты приложения
|
/// Остальные маршруты приложения
|
||||||
@@ -55,18 +58,24 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const WelcomeScreen(),
|
builder: (context, state) => const WelcomeScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final prefs = sl<Prefs>();
|
||||||
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
|
final firebaseApi = sl<FirebaseApi>();
|
||||||
await sl<FirebaseApi>().registerPushToken();
|
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||||
if (welcomeScreenWasShowed == false) {
|
await firebaseApi.registerPushToken();
|
||||||
|
if (!welcomeScreenWasShowed) {
|
||||||
return null;
|
return null;
|
||||||
} else if (await TokenStorage.haveTokenInStorage()) {
|
} else if (await TokenStorage.haveTokenInStorage()) {
|
||||||
return RouteConstants.feedScreen;
|
return '/feed-screen';
|
||||||
} else {
|
} 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) {
|
} catch (e) {
|
||||||
return RouteConstants.feedScreen;
|
return '/feed-screen';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -139,7 +148,13 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.profileScreen,
|
path: RouteConstants.profileScreen,
|
||||||
name: '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(
|
GoRoute(
|
||||||
path: RouteConstants.subsScreen,
|
path: RouteConstants.subsScreen,
|
||||||
@@ -161,7 +176,7 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.emptyChatScreen,
|
path: RouteConstants.emptyChatScreen,
|
||||||
name: 'emptyChatScreen',
|
name: 'emptyChatScreen',
|
||||||
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
|
builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Consultant routes
|
// Consultant routes
|
||||||
@@ -201,7 +216,8 @@ class OtherRoutes {
|
|||||||
final data = state.extra as Map<String, dynamic>;
|
final data = state.extra as Map<String, dynamic>;
|
||||||
final planId = data['planId'] as int;
|
final planId = data['planId'] as int;
|
||||||
final successfulPayment = data['successfulPayment'] as bool?;
|
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(
|
GoRoute(
|
||||||
@@ -224,6 +240,11 @@ class OtherRoutes {
|
|||||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
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,
|
fontWeight: FontWeight.w500,
|
||||||
height: 1.255,
|
height: 1.255,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static const textFS24FW700 = TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.255,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class PrimaryColors {
|
|||||||
// Blue
|
// Blue
|
||||||
Color get blue400 => const Color(0XFF43A6F2);
|
Color get blue400 => const Color(0XFF43A6F2);
|
||||||
|
|
||||||
Color get blueA400 => const Color(0XFF2F80ED);
|
Color get blueA400 => const Color(0XFF2F80ED);
|
||||||
// BlueGray
|
// BlueGray
|
||||||
Color get blueGray400 => const Color(0XFF7B8794);
|
Color get blueGray400 => const Color(0XFF7B8794);
|
||||||
|
|
||||||
@@ -141,6 +141,8 @@ class PrimaryColors {
|
|||||||
|
|
||||||
Color get gray60003 => const Color(0XFF707070);
|
Color get gray60003 => const Color(0XFF707070);
|
||||||
|
|
||||||
|
Color get gray60005 => const Color(0xFF6C7C93);
|
||||||
|
|
||||||
// Green
|
// Green
|
||||||
Color get greenA700 => const Color(0XFF019A3E);
|
Color get greenA700 => const Color(0XFF019A3E);
|
||||||
|
|
||||||
@@ -148,6 +150,8 @@ class PrimaryColors {
|
|||||||
|
|
||||||
Color get green => const Color(0XFF0BB450);
|
Color get green => const Color(0XFF0BB450);
|
||||||
|
|
||||||
|
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
|
||||||
|
|
||||||
// LightGreen
|
// LightGreen
|
||||||
Color get lightGreen300 => const Color(0XFFB2DF8A);
|
Color get lightGreen300 => const Color(0XFFB2DF8A);
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ extension DateTimeExtension on DateTime {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day;
|
return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String toISO8601WithTimeZoneOffset() {
|
||||||
|
final formatter = DateFormat("yyyy-MM-ddTHH:mm:ss");
|
||||||
|
final formatted = formatter.format(this);
|
||||||
|
final offset = timeZoneOffset;
|
||||||
|
final hours = offset.inHours.abs().toString().padLeft(2, '0');
|
||||||
|
final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0');
|
||||||
|
final sign = offset.isNegative ? '-' : '+';
|
||||||
|
|
||||||
|
return "$formatted$sign$hours:$minutes";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String calculateDifference(DateTime dateTime, [String? format]) {
|
String calculateDifference(DateTime dateTime, [String? format]) {
|
||||||
|
|||||||
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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class OptionWidget extends StatelessWidget {
|
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 PacketOptions? option;
|
||||||
final bool disableDuration;
|
final bool disableDuration;
|
||||||
|
final int duration;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (option == null) {
|
if (option == null) {
|
||||||
@@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (!disableDuration) ...[
|
if (!disableDuration) ...[
|
||||||
const _OptionDurationWidget(duration: 7),
|
_OptionDurationWidget(duration: duration),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12),
|
||||||
|
],
|
||||||
|
if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[
|
||||||
|
const _OptionHasPersonalDietWidget(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
const CustomDivider(),
|
const CustomDivider(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _OptionDurationWidget extends StatelessWidget {
|
class _OptionDurationWidget extends StatelessWidget {
|
||||||
const _OptionDurationWidget({required this.duration});
|
const _OptionDurationWidget({required this.duration});
|
||||||
final int 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: 'real_name') required String realName,
|
||||||
@JsonKey(name: 'first_name') required String firstName,
|
@JsonKey(name: 'first_name') required String firstName,
|
||||||
@JsonKey(name: 'last_name') required String lastName,
|
@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_email') required String defaultEmail,
|
||||||
@JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone,
|
@JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone,
|
||||||
}) = _YandexProfileModel;
|
}) = _YandexProfileModel;
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:dio/dio.dart';
|
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/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.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:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class OAuthCubit extends Cubit<OAuthState> {
|
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
||||||
final AuthRepository authRepository;
|
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 {
|
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
|
||||||
try {
|
try {
|
||||||
@@ -63,6 +67,7 @@ class OAuthCubit extends Cubit<OAuthState> {
|
|||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
);
|
);
|
||||||
TokenStorage.saveTokenInStorage(token);
|
TokenStorage.saveTokenInStorage(token);
|
||||||
|
cacheRegistrationDate(DateTime.now());
|
||||||
emit(OAuthState.loaded(token: token));
|
emit(OAuthState.loaded(token: token));
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
emit(OAuthState.error(errorMessage: e.toString()));
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ class BlogPostCubit extends Cubit<BlogPostState> {
|
|||||||
Future<void> getBlogPost(int id) async {
|
Future<void> getBlogPost(int id) async {
|
||||||
try {
|
try {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
final postModel = await feedRepository.getBlogPost(id: id);
|
final postModel = await feedRepository.getBlogPost(id: id);
|
||||||
|
|
||||||
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
|
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: 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:flutter_html/flutter_html.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class BlogPostScreen extends StatefulWidget {
|
class BlogPostScreen extends StatefulWidget {
|
||||||
final int id;
|
final int id;
|
||||||
@@ -198,7 +199,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: blogPostState.blogPostModel?.author),
|
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: blogPostState.blogPostModel?.author.authorSlug),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -231,10 +232,35 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
),
|
),
|
||||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Html(
|
child: Html(
|
||||||
data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'),
|
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
|
||||||
style: {"*": Style(color: Colors.black87)},
|
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),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
handleError(error, stackTrace);
|
handleError(error, stackTrace);
|
||||||
emit(const BlogPostCreateState.loaded());
|
emit(BlogPostCreateState.error(errorMessage: error.toString()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
handleError(error, stackTrace);
|
handleError(error, stackTrace);
|
||||||
emit(const BlogPostCreateState.loaded());
|
emit(BlogPostCreateState.error(errorMessage: error.toString()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:eatvio_app/app_keys.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/permission_handler/permission_handler.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 {
|
class BlogPostCreateScreen extends StatefulWidget {
|
||||||
final FeedBlogPostModel? feedBlogPostModel;
|
final FeedBlogPostModel? feedBlogPostModel;
|
||||||
|
|
||||||
const BlogPostCreateScreen({
|
const BlogPostCreateScreen({super.key, this.feedBlogPostModel});
|
||||||
super.key,
|
|
||||||
this.feedBlogPostModel,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BlogPostCreateScreen> createState() => _BlogPostCreateScreenState();
|
State<BlogPostCreateScreen> createState() => _BlogPostCreateScreenState();
|
||||||
@@ -42,6 +40,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
final videoUrlController = TextEditingController();
|
final videoUrlController = TextEditingController();
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
bool enableMdPanel = false;
|
||||||
|
bool isPreviewMode = false;
|
||||||
|
final FocusNode descriptionFocusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -50,6 +51,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
|
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
|
||||||
setState(() {
|
setState(() {
|
||||||
shrinkImage = visible;
|
shrinkImage = visible;
|
||||||
|
enableMdPanel = visible;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (widget.feedBlogPostModel != null) {
|
if (widget.feedBlogPostModel != null) {
|
||||||
@@ -62,9 +64,326 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
keyboardSubscription.cancel();
|
keyboardSubscription.cancel();
|
||||||
|
descriptionFocusNode.dispose();
|
||||||
super.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<BlogPostCreateCubit>(
|
return BlocProvider<BlogPostCreateCubit>(
|
||||||
@@ -102,13 +421,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Row(
|
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
|
||||||
children: [
|
|
||||||
Icon(Icons.photo_camera),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Фото'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
CupertinoActionSheetAction(
|
CupertinoActionSheetAction(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -126,96 +439,102 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Row(
|
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
|
||||||
children: [
|
|
||||||
Icon(Icons.photo),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Галерея'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
cancelButton: CupertinoActionSheetAction(
|
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
|
||||||
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) {
|
Scaffold _buildBlogPostCreate(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(),
|
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||||
floatingActionButton: SizedBox(
|
floatingActionButton: enableMdPanel || isPreviewMode
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
? Row(
|
||||||
child: FloatingActionButton.extended(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
heroTag: 'createBlogPost',
|
children: [
|
||||||
highlightElevation: 0,
|
if (!isPreviewMode) ...[
|
||||||
elevation: 0,
|
Container(
|
||||||
focusElevation: 0,
|
width: MediaQuery.of(context).size.width - 76,
|
||||||
shape: RoundedRectangleBorder(
|
height: 52,
|
||||||
borderRadius: BorderRadius.circular(16),
|
decoration: BoxDecoration(
|
||||||
),
|
color: Colors.white,
|
||||||
backgroundColor: appTheme.blueA400,
|
borderRadius: BorderRadius.circular(20),
|
||||||
label: isLoading
|
boxShadow: [
|
||||||
? const CircularProgressIndicator()
|
BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
|
||||||
: const Text(
|
],
|
||||||
'Создать',
|
),
|
||||||
style: TextStyle(
|
child: Row(
|
||||||
color: Colors.white,
|
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 {
|
: null,
|
||||||
setState(() {
|
appBar: AppBar(),
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -224,17 +543,19 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text('Заголовок'),
|
const Text('Заголовок'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
CustomTextFormField(
|
CustomTextFormField(key: AppKeys.blogPostCreateScreenTitleTextField, controller: titleController),
|
||||||
controller: titleController,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text('Описание'),
|
const Text('Описание'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
CustomTextFormField(
|
isPreviewMode
|
||||||
controller: descriptionController,
|
? _buildMarkdownPreview()
|
||||||
maxLines: 6,
|
: CustomTextFormField(
|
||||||
minLines: 4,
|
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||||
),
|
controller: descriptionController,
|
||||||
|
focusNode: descriptionFocusNode,
|
||||||
|
maxLines: 6,
|
||||||
|
minLines: 4,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (videoUrlController.text.isEmpty) const Text('Изображение поста'),
|
if (videoUrlController.text.isEmpty) const Text('Изображение поста'),
|
||||||
if (videoUrlController.text.isEmpty) const SizedBox(height: 8),
|
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 Text('Ссылка на видео'),
|
||||||
if (image == null && widget.feedBlogPostModel?.image == null) const SizedBox(height: 8),
|
if (image == null && widget.feedBlogPostModel?.image == null) const SizedBox(height: 8),
|
||||||
if (image == null && widget.feedBlogPostModel?.image == null)
|
if (image == null && widget.feedBlogPostModel?.image == null)
|
||||||
CustomTextFormField(
|
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
|
||||||
controller: videoUrlController,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 80),
|
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() {
|
GestureDetector _buildAddPhoto() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -265,22 +645,14 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
height: shrinkImage ? 52 : 128,
|
height: shrinkImage ? 52 : 128,
|
||||||
curve: Curves.easeIn,
|
curve: Curves.easeIn,
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
decoration: AppDecoration.fillGray.copyWith(
|
decoration: AppDecoration.fillGray.copyWith(borderRadius: BorderRadius.circular(16)),
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.photo_library, color: appTheme.blueGray300),
|
||||||
Icons.photo_library,
|
|
||||||
color: appTheme.blueGray300,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text("Добавить фото", selectionColor: appTheme.blueA400),
|
||||||
"Добавить фото",
|
|
||||||
selectionColor: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -289,13 +661,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
|
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Image(
|
child: Image(image: FileImage(File(image!.path))),
|
||||||
image: FileImage(
|
|
||||||
File(
|
|
||||||
image!.path,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.feedBlogPostModel?.image != null && image == null)
|
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,
|
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Image(
|
child: Image(image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.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/custom_text_style.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.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/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/feed/models/plan_pagination_model.dart';
|
||||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.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';
|
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||||
@@ -115,25 +114,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
ChosenPlanStateEnum.statePendingPayment,
|
ChosenPlanStateEnum.statePendingPayment,
|
||||||
ChosenPlanStateEnum.stateInCoordination,
|
ChosenPlanStateEnum.stateInCoordination,
|
||||||
].contains(chosenPlan?.chosenPlanState);
|
].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;
|
PacketOptions? chosenOption;
|
||||||
if (chosenPlan?.optionType == 'options1') {
|
if (chosenPlan?.optionType == 'options1') {
|
||||||
chosenOption = chosenPlan?.plan.packet1Options;
|
chosenOption = chosenPlan?.plan.packet1Options;
|
||||||
@@ -142,6 +122,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
} else if (chosenPlan?.optionType == 'options3') {
|
} else if (chosenPlan?.optionType == 'options3') {
|
||||||
chosenOption = chosenPlan?.plan.packet3Options;
|
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(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -150,15 +151,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
|
||||||
'profileScreen',
|
|
||||||
extra: Author(
|
|
||||||
authorSlug: state.chatRoom.users.receiver.slug,
|
|
||||||
authorName: state.chatRoom.users.receiver.name,
|
|
||||||
authorProfileIcon: state.chatRoom.users.receiver.profilePhotoUrl ?? '',
|
|
||||||
authorIsMe: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -326,6 +319,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
chosenPlanId: chosenPlan?.id,
|
chosenPlanId: chosenPlan?.id,
|
||||||
planName: chosenPlan?.plan.name,
|
planName: chosenPlan?.plan.name,
|
||||||
planPrice: chosenOption?.price.toDouble(),
|
planPrice: chosenOption?.price.toDouble(),
|
||||||
|
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -453,7 +447,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||||
await blockedUsersCubit.getBlockedUsers();
|
await blockedUsersCubit.getBlockedUsers();
|
||||||
final blockedState = blockedUsersCubit.state;
|
final blockedState = blockedUsersCubit.state;
|
||||||
|
|
||||||
if (blockedState is BlockedUsersStateLoaded) {
|
if (blockedState is BlockedUsersStateLoaded) {
|
||||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
|
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
@@ -468,7 +462,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
||||||
chatRoomCubit.sendMessage(
|
chatRoomCubit.sendMessage(
|
||||||
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
||||||
@@ -503,6 +497,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
left: 16,
|
left: 16,
|
||||||
child: AboutOrderButton(
|
child: AboutOrderButton(
|
||||||
onTap: () async {
|
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});
|
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/custom_text_style.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.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/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:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.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';
|
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
||||||
|
|
||||||
class EmptyChatScreen extends StatefulWidget {
|
class EmptyChatScreen extends StatefulWidget {
|
||||||
final Author author;
|
final ProfileModel user;
|
||||||
|
|
||||||
const EmptyChatScreen({
|
const EmptyChatScreen({super.key, required this.user});
|
||||||
super.key,
|
|
||||||
required this.author,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EmptyChatScreen> createState() => _EmptyChatScreenState();
|
State<EmptyChatScreen> createState() => _EmptyChatScreenState();
|
||||||
@@ -40,34 +37,19 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed('profileScreen', extra: widget.user.slug);
|
||||||
'profileScreen',
|
|
||||||
extra: Author(
|
|
||||||
authorSlug: widget.author.authorSlug,
|
|
||||||
authorName: widget.author.authorName,
|
|
||||||
authorProfileIcon: widget.author.authorProfileIcon,
|
|
||||||
authorIsMe: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
foregroundImage: NetworkImage(widget.author.authorProfileIcon),
|
foregroundImage: NetworkImage(widget.user.profilePhotoUrl),
|
||||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [Text(widget.user.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black))],
|
||||||
Text(
|
|
||||||
widget.author.authorName,
|
|
||||||
style: CustomTextStyles.titleSmallBold.copyWith(
|
|
||||||
color: PrimaryColors().black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -84,18 +66,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => showImagePicker(widget.author.authorSlug),
|
onTap: () => showImagePicker(widget.user.slug),
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: pi / 4,
|
angle: pi / 4,
|
||||||
child: const Icon(
|
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
|
||||||
Icons.attach_file_rounded,
|
|
||||||
size: 30,
|
|
||||||
color: Color(0xFF858E99),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -117,8 +95,10 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
hintText: 'Сообщение',
|
hintText: 'Сообщение',
|
||||||
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
||||||
filled: true,
|
filled: true,
|
||||||
border:
|
border: OutlineInputBorder(
|
||||||
OutlineInputBorder(borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
|
borderRadius: BorderRadius.circular(100),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(100),
|
borderRadius: BorderRadius.circular(100),
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
@@ -139,40 +119,29 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||||
await blockedUsersCubit.getBlockedUsers();
|
await blockedUsersCubit.getBlockedUsers();
|
||||||
final blockedState = blockedUsersCubit.state;
|
final blockedState = blockedUsersCubit.state;
|
||||||
|
|
||||||
if (blockedState is BlockedUsersStateLoaded) {
|
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 (isBlocked) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(content: Text('Вы не можете отправить сообщение заблокированному пользователю'), backgroundColor: Colors.red),
|
||||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textEditingController.text.isNotEmpty) {
|
if (textEditingController.text.isNotEmpty) {
|
||||||
await sl<ChatRepository>().sendMessage(
|
await sl<ChatRepository>().sendMessage(data: textEditingController.text.trim(), slug: widget.user.slug);
|
||||||
data: textEditingController.text.trim(),
|
|
||||||
slug: widget.author.authorSlug,
|
|
||||||
);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
textEditingController.clear();
|
textEditingController.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||||
'chatScreen',
|
|
||||||
extra: {
|
|
||||||
'uuid': chatIdOrError,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -201,53 +170,36 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
CupertinoActionSheetAction(
|
CupertinoActionSheetAction(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
// Проверяем, заблокирован ли пользователь
|
// Проверяем, заблокирован ли пользователь
|
||||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||||
await blockedUsersCubit.getBlockedUsers();
|
await blockedUsersCubit.getBlockedUsers();
|
||||||
final blockedState = blockedUsersCubit.state;
|
final blockedState = blockedUsersCubit.state;
|
||||||
|
|
||||||
if (blockedState is BlockedUsersStateLoaded) {
|
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 (isBlocked) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(content: Text('Вы не можете отправить сообщение заблокированному пользователю'), backgroundColor: Colors.red),
|
||||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
List<XFile>? images = await picker.pickMultiImage();
|
List<XFile>? images = await picker.pickMultiImage();
|
||||||
if (images.isNotEmpty) {
|
if (images.isNotEmpty) {
|
||||||
List<File> files = images.map((image) => File(image.path)).toList();
|
List<File> files = images.map((image) => File(image.path)).toList();
|
||||||
await sl<ChatRepository>().sendMessage(
|
await sl<ChatRepository>().sendMessage(files: files, slug: widget.user.slug);
|
||||||
files: files,
|
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||||
slug: widget.author.authorSlug,
|
|
||||||
);
|
|
||||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||||
'chatScreen',
|
|
||||||
extra: {
|
|
||||||
'uuid': chatIdOrError,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Row(
|
child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]),
|
||||||
children: [
|
|
||||||
Icon(Icons.photo_library_rounded),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Галерея'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
CupertinoActionSheetAction(
|
CupertinoActionSheetAction(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -256,28 +208,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
List<XFile>? images = await picker.pickMultiImage();
|
List<XFile>? images = await picker.pickMultiImage();
|
||||||
if (images.isNotEmpty) {
|
if (images.isNotEmpty) {
|
||||||
List<File> files = images.map((image) => File(image.path)).toList();
|
List<File> files = images.map((image) => File(image.path)).toList();
|
||||||
await sl<ChatRepository>().sendMessage(
|
await sl<ChatRepository>().sendMessage(files: files, slug: widget.user.slug);
|
||||||
files: files,
|
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||||
slug: widget.author.authorSlug,
|
|
||||||
);
|
|
||||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||||
'chatScreen',
|
|
||||||
extra: {
|
|
||||||
'uuid': chatIdOrError,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Row(
|
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
|
||||||
children: [
|
|
||||||
Icon(Icons.photo_camera),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Фото'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
CupertinoActionSheetAction(
|
CupertinoActionSheetAction(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -308,35 +246,17 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
List<File> files = result.paths.map((path) => File(path!)).toList();
|
List<File> files = result.paths.map((path) => File(path!)).toList();
|
||||||
await sl<ChatRepository>().sendMessage(
|
await sl<ChatRepository>().sendMessage(files: files, slug: widget.user.slug);
|
||||||
files: files,
|
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.user.slug);
|
||||||
slug: widget.author.authorSlug,
|
|
||||||
);
|
|
||||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
AppRouter.navigatorKey.currentContext?.pushReplacementNamed('chatScreen', extra: {'uuid': chatIdOrError});
|
||||||
'chatScreen',
|
|
||||||
extra: {
|
|
||||||
'uuid': chatIdOrError,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Row(
|
child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]),
|
||||||
children: [
|
|
||||||
Icon(Icons.file_present_rounded),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Файлы'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
cancelButton: CupertinoActionSheetAction(
|
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
|
||||||
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/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
class ChosePlanDateWidget extends StatefulWidget {
|
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 ChatRoomCubit chatCubit;
|
||||||
|
final bool hasPersonalDiet;
|
||||||
@override
|
@override
|
||||||
State<ChosePlanDateWidget> createState() => _ChosePlanDateWidget();
|
State<ChosePlanDateWidget> createState() => _ChosePlanDateWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChosePlanDateWidget extends State<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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
@@ -27,64 +38,129 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
if (widget.chatCubit.state is ChatRoomStateLoaded) {
|
if (widget.chatCubit.state is ChatRoomStateLoaded) {
|
||||||
planName = (widget.chatCubit.state as ChatRoomStateLoaded).chatRoom.chosenPlan?.plan.name;
|
planName = (widget.chatCubit.state as ChatRoomStateLoaded).chatRoom.chosenPlan?.plan.name;
|
||||||
}
|
}
|
||||||
return Column(
|
return GestureDetector(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(height: 4),
|
children: [
|
||||||
const BottomSheetHandlerBar(),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(height: 12),
|
const BottomSheetHandlerBar(),
|
||||||
Text(
|
const SizedBox(height: 12),
|
||||||
planName ?? '',
|
Row(
|
||||||
style: TextStyles.headingXLSemibold.copyWith(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: appTheme.black,
|
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),
|
||||||
const SizedBox(height: 12),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 25.0),
|
child: Text(
|
||||||
child: Text(
|
s.chose_plan_start_date_2,
|
||||||
s.chose_plan_start_date_2,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
style: TextStyles.textMMedium.copyWith(
|
||||||
style: TextStyles.textMMedium.copyWith(
|
color: appTheme.black,
|
||||||
color: appTheme.black,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
const Padding(
|
||||||
const Padding(
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
child: CustomDivider(),
|
||||||
child: CustomDivider(),
|
),
|
||||||
),
|
CalendarWidget(
|
||||||
CalendarWidget(
|
startDate: startDate,
|
||||||
startDate: startDate,
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
|
child: CustomOutlinedButton(
|
||||||
child: CustomOutlinedButton(
|
onPressed: () async {
|
||||||
onPressed: () async {
|
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
|
||||||
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
|
if (result) {
|
||||||
if (result) {
|
showSuccessToast('Дата установлена');
|
||||||
showSuccessToast('Дата установлена');
|
if (!context.mounted) return;
|
||||||
if (!context.mounted) return;
|
Navigator.pop(context);
|
||||||
Navigator.pop(context);
|
} else {
|
||||||
} else {
|
showErrorToast('Не удалось установить дату');
|
||||||
showErrorToast('Не удалось установить дату');
|
if (!context.mounted) return;
|
||||||
if (!context.mounted) return;
|
Navigator.pop(context);
|
||||||
Navigator.pop(context);
|
}
|
||||||
}
|
},
|
||||||
},
|
height: 60,
|
||||||
height: 60,
|
text: s.confirm,
|
||||||
text: s.confirm,
|
buttonTextStyle: TextStyles.textLSemibold.copyWith(
|
||||||
buttonTextStyle: TextStyles.textLSemibold.copyWith(
|
color: appTheme.whiteA700,
|
||||||
color: appTheme.whiteA700,
|
),
|
||||||
),
|
buttonStyle: ButtonStyle(
|
||||||
buttonStyle: ButtonStyle(
|
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||||
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/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/complete_plan_bottom_sheet.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_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/features/my_finance/presentation/pages/card_settings.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -17,12 +18,12 @@ import 'package:flutter_svg/svg.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class NotificationBanner extends StatefulWidget {
|
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 BannerType bannerType;
|
||||||
final double? planPrice;
|
final double? planPrice;
|
||||||
final String? planName;
|
final String? planName;
|
||||||
final int? chosenPlanId;
|
final int? chosenPlanId;
|
||||||
|
final bool hasPersonalDiet;
|
||||||
@override
|
@override
|
||||||
State<NotificationBanner> createState() => _NotificationBannerState();
|
State<NotificationBanner> createState() => _NotificationBannerState();
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,8 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
|||||||
BannerType.date => [
|
BannerType.date => [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: size.width * 0.6,
|
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),
|
const SizedBox(height: 12),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@@ -78,6 +80,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ChosePlanDateWidget(
|
builder: (context) => ChosePlanDateWidget(
|
||||||
chatCubit: cubit,
|
chatCubit: cubit,
|
||||||
|
hasPersonalDiet: widget.hasPersonalDiet,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -155,6 +158,20 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
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: 'end_date', fromJson: parseDate) required DateTime endDate,
|
||||||
@JsonKey(name: 'consultant_id') required int consultantId,
|
@JsonKey(name: 'consultant_id') required int consultantId,
|
||||||
required List<DiaryModel> diaries,
|
required List<DiaryModel> diaries,
|
||||||
|
@JsonKey(name: 'personal_diet_id') required int? personalDietId,
|
||||||
}) = _ConsultationModel;
|
}) = _ConsultationModel;
|
||||||
|
|
||||||
factory ConsultationModel.fromJson(Map<String, dynamic> json) => _$ConsultationModelFromJson(json);
|
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/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
|
||||||
@@ -54,6 +55,8 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
|||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||||
|
bool _mdToolbarVisible = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -76,9 +79,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
|||||||
showBottomSheetWithBlur(
|
showBottomSheetWithBlur(
|
||||||
userRootNavigator: true,
|
userRootNavigator: true,
|
||||||
context: context,
|
context: context,
|
||||||
child: SuccessPublishedDietOrPlanBottomSheet(
|
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
|
||||||
title: state.dietData.name ?? 'Диета',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (state.drafted) {
|
} else if (state.drafted) {
|
||||||
@@ -87,15 +88,15 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
|||||||
context.pop();
|
context.pop();
|
||||||
showBottomSheetWithBlur(
|
showBottomSheetWithBlur(
|
||||||
context: context,
|
context: context,
|
||||||
child: SuccessPublishedDietOrPlanBottomSheet(
|
child: SuccessPublishedDietOrPlanBottomSheet(title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках'),
|
||||||
title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: appTheme.gray100,
|
backgroundColor: appTheme.gray100,
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||||
|
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -124,34 +125,23 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
|||||||
context.read<CreateDietCubit>().setName(value);
|
context.read<CreateDietCubit>().setName(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (state.dietNameError != null) ...[
|
if (state.dietNameError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietNameError ?? '')],
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
ValidationErrorWidget(error: state.dietNameError ?? ''),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SizedBox(
|
MarkdownEditor(
|
||||||
height: 132,
|
key: _mdKey,
|
||||||
child: CustomTextFormField(
|
initialText: descriptionController.text,
|
||||||
hasError: state.dietDescriptionError != null,
|
hintText: s.enter_diet_description,
|
||||||
controller: descriptionController,
|
hasError: state.dietDescriptionError != null,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
minLines: 6,
|
||||||
maxLines: null,
|
maxLines: 8,
|
||||||
minLines: null,
|
showInlineToolbar: false,
|
||||||
expands: true,
|
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
|
||||||
hintText: s.enter_diet_description,
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
descriptionController.text = value;
|
||||||
context.read<CreateDietCubit>().setDetails(value);
|
context.read<CreateDietCubit>().setDetails(value);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (state.dietDescriptionError != null) ...[
|
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
ValidationErrorWidget(error: state.dietDescriptionError ?? ''),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const DietDurationDropDownWidget(),
|
const DietDurationDropDownWidget(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -171,9 +161,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${s.diet_plan} (${state.dietData.duration ?? 0} ${getDayForm(state.dietData.duration ?? 0, s)})',
|
'${s.diet_plan} (${state.dietData.duration ?? 0} ${getDayForm(state.dietData.duration ?? 0, s)})',
|
||||||
style: TextStyles.headingXLSemibold.copyWith(
|
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||||
color: appTheme.black,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -196,9 +184,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
context.read<CreateDietCubit>().setDietState(DietState.draft);
|
context.read<CreateDietCubit>().setDietState(DietState.draft);
|
||||||
if (widget.diet != null) {
|
if (widget.diet != null) {
|
||||||
final result = await context.read<CreateDietCubit>().updateDiet(
|
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
|
||||||
widget.diet!.id,
|
|
||||||
);
|
|
||||||
if (result == 'error') {
|
if (result == 'error') {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
|
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_config.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/const.dart';
|
||||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.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['veg'] = diet.type.veg ?? false;
|
||||||
type['carb'] = diet.type.carb ?? false;
|
type['carb'] = diet.type.carb ?? false;
|
||||||
type['light'] = diet.type.light ?? false;
|
type['light'] = diet.type.light ?? false;
|
||||||
|
final appConfig = sl<AppConfig>();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith.dietData(
|
state.copyWith.dietData(
|
||||||
dailySchedule: dailySchedule,
|
dailySchedule: dailySchedule,
|
||||||
@@ -55,7 +57,8 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
name: diet.name,
|
name: diet.name,
|
||||||
duration: diet.duration,
|
duration: diet.duration,
|
||||||
type: type,
|
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)));
|
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;
|
final dailySchedule = state.dietData.dailySchedule;
|
||||||
|
|
||||||
if (currentDaysValue != null && dailySchedule != null) {
|
if (currentDaysValue != null && dailySchedule != null) {
|
||||||
final deepCopyOfDailySchedule = dailySchedule
|
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||||
.map(
|
|
||||||
(day) => day.map((meal) => [...meal]).toList(),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (currentDaysValue > selectedDaysValue) {
|
if (currentDaysValue > selectedDaysValue) {
|
||||||
final difference = currentDaysValue - selectedDaysValue;
|
final difference = currentDaysValue - selectedDaysValue;
|
||||||
@@ -93,28 +92,16 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
deepCopyOfDailySchedule.add([[], [], [], [], []]);
|
deepCopyOfDailySchedule.add([[], [], [], [], []]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit(
|
emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule)));
|
||||||
state.copyWith(
|
|
||||||
dietData: state.dietData.copyWith(
|
|
||||||
dailySchedule: deepCopyOfDailySchedule,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
final List<List<List<DailySchedule>>> dailySchedule = List.generate(selectedDaysValue, (index) => [[], [], [], [], []]);
|
final List<List<List<DailySchedule>>> dailySchedule = List.generate(selectedDaysValue, (index) => [[], [], [], [], []]);
|
||||||
|
|
||||||
emit(
|
emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule)));
|
||||||
state.copyWith(
|
|
||||||
dietData: state.dietData.copyWith(dailySchedule: dailySchedule),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
durationIndex: index,
|
durationIndex: index,
|
||||||
dietData: state.dietData.copyWith(
|
dietData: state.dietData.copyWith(duration: durationValues[index]),
|
||||||
duration: durationValues[index],
|
|
||||||
),
|
|
||||||
dietDurationError: null,
|
dietDurationError: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -125,7 +112,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setImage(XFile? image, {String? imageUrl}) {
|
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) {
|
void setDailySchedule(List<List<List<DailySchedule>>> dailySchedule) {
|
||||||
@@ -133,27 +125,25 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setDietState(DietState dietState) {
|
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}) {
|
void setDietType({required String key, required bool value}) {
|
||||||
final type = Map<String, bool>.from(state.dietData.type);
|
final type = Map<String, bool>.from(state.dietData.type);
|
||||||
type[key] = value;
|
type[key] = value;
|
||||||
emit(
|
emit(state.copyWith(dietData: state.dietData.copyWith(type: type)));
|
||||||
state.copyWith(
|
|
||||||
dietData: state.dietData.copyWith(type: type),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPortion({required int dayIndex, required int mealIndex, required DailySchedule portion}) {
|
void addPortion({required int dayIndex, required int mealIndex, required DailySchedule portion}) {
|
||||||
final dailySchedule = state.dietData.dailySchedule;
|
final dailySchedule = state.dietData.dailySchedule;
|
||||||
if (dailySchedule != null) {
|
if (dailySchedule != null) {
|
||||||
final deepCopyOfDailySchedule = dailySchedule
|
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||||
.map(
|
|
||||||
(day) => day.map((meal) => [...meal]).toList(),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
deepCopyOfDailySchedule[dayIndex][mealIndex].add(portion);
|
deepCopyOfDailySchedule[dayIndex][mealIndex].add(portion);
|
||||||
|
|
||||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||||
@@ -162,18 +152,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removePortion({
|
void removePortion({required int day, required int meal, required int portionIndex}) {
|
||||||
required int day,
|
|
||||||
required int meal,
|
|
||||||
required int portionIndex,
|
|
||||||
}) {
|
|
||||||
final dailySchedule = state.dietData.dailySchedule;
|
final dailySchedule = state.dietData.dailySchedule;
|
||||||
if (dailySchedule != null) {
|
if (dailySchedule != null) {
|
||||||
final deepCopyOfDailySchedule = dailySchedule
|
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||||
.map(
|
|
||||||
(day) => day.map((meal) => [...meal]).toList(),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
deepCopyOfDailySchedule[day][meal].removeAt(portionIndex);
|
deepCopyOfDailySchedule[day][meal].removeAt(portionIndex);
|
||||||
|
|
||||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
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}) {
|
void updatePortion({required int dayIndex, required int mealIndex, required int portionIndex, required DailySchedule portion}) {
|
||||||
final dailySchedule = state.dietData.dailySchedule;
|
final dailySchedule = state.dietData.dailySchedule;
|
||||||
if (dailySchedule != null) {
|
if (dailySchedule != null) {
|
||||||
final deepCopyOfDailySchedule = dailySchedule
|
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||||
.map(
|
|
||||||
(day) => day.map((meal) => [...meal]).toList(),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
deepCopyOfDailySchedule[dayIndex][mealIndex][portionIndex] = portion;
|
deepCopyOfDailySchedule[dayIndex][mealIndex][portionIndex] = portion;
|
||||||
|
|
||||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||||
@@ -199,11 +177,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
void copyDay(int dayIndex) {
|
void copyDay(int dayIndex) {
|
||||||
final dailySchedule = state.dietData.dailySchedule;
|
final dailySchedule = state.dietData.dailySchedule;
|
||||||
if (dailySchedule != null) {
|
if (dailySchedule != null) {
|
||||||
final deepCopyOfDailySchedule = dailySchedule
|
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||||
.map(
|
|
||||||
(day) => day.map((meal) => [...meal]).toList(),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final copiedDay = deepCopyOfDailySchedule[dayIndex];
|
final copiedDay = deepCopyOfDailySchedule[dayIndex];
|
||||||
emit(state.copyWith(copiedDay: copiedDay));
|
emit(state.copyWith(copiedDay: copiedDay));
|
||||||
}
|
}
|
||||||
@@ -213,11 +187,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
final dailySchedule = state.dietData.dailySchedule;
|
final dailySchedule = state.dietData.dailySchedule;
|
||||||
final copiedDay = state.copiedDay;
|
final copiedDay = state.copiedDay;
|
||||||
if (dailySchedule != null && copiedDay != null) {
|
if (dailySchedule != null && copiedDay != null) {
|
||||||
final deepCopyOfDailySchedule = dailySchedule
|
final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
|
||||||
.map(
|
|
||||||
(day) => day.map((meal) => [...meal]).toList(),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
deepCopyOfDailySchedule[dayIndex] = copiedDay;
|
deepCopyOfDailySchedule[dayIndex] = copiedDay;
|
||||||
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
|
||||||
emit(state.copyWith(dietData: newDietData));
|
emit(state.copyWith(dietData: newDietData));
|
||||||
@@ -268,14 +238,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
|||||||
final dailyNutrition = <DailyNutrition>[];
|
final dailyNutrition = <DailyNutrition>[];
|
||||||
if (carbs != null && calories != null && fats != null && proteins != null) {
|
if (carbs != null && calories != null && fats != null && proteins != null) {
|
||||||
for (int i = 0; i < carbs.length; i++) {
|
for (int i = 0; i < carbs.length; i++) {
|
||||||
dailyNutrition.add(
|
dailyNutrition.add(DailyNutrition(carbohydrates: carbs[i], energy: calories[i], fats: fats[i], proteins: proteins[i]));
|
||||||
DailyNutrition(
|
|
||||||
carbohydrates: carbs[i],
|
|
||||||
energy: calories[i],
|
|
||||||
fats: fats[i],
|
|
||||||
proteins: proteins[i],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final dailySchedule = <int, Map<int, Map<int, DailySchedule>>>{};
|
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/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
|
||||||
@@ -28,16 +29,21 @@ class CreatePlanScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(create: (context) => sl<CreatePlanCubit>(), child: const CreatePlanScreenContent());
|
||||||
create: (context) => sl<CreatePlanCubit>(),
|
|
||||||
child: const CreatePlanScreenContent(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreatePlanScreenContent extends StatelessWidget {
|
class CreatePlanScreenContent extends StatefulWidget {
|
||||||
const CreatePlanScreenContent({super.key});
|
const CreatePlanScreenContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreatePlanScreenContent> createState() => _CreatePlanScreenContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||||
|
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||||
|
bool _mdToolbarVisible = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
@@ -51,15 +57,14 @@ class CreatePlanScreenContent extends StatelessWidget {
|
|||||||
showBottomSheetWithBlur(
|
showBottomSheetWithBlur(
|
||||||
userRootNavigator: true,
|
userRootNavigator: true,
|
||||||
context: context,
|
context: context,
|
||||||
child: SuccessPublishedDietOrPlanBottomSheet(
|
child: SuccessPublishedDietOrPlanBottomSheet(title: state.planData.name ?? 'План', isPlan: true),
|
||||||
title: state.planData.name ?? 'План',
|
|
||||||
isPlan: true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: appTheme.gray100,
|
backgroundColor: appTheme.gray100,
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||||
|
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -104,10 +109,7 @@ class CreatePlanScreenContent extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
maxLength: 150,
|
maxLength: 150,
|
||||||
),
|
),
|
||||||
if (state.validationState.titleHasError) ...[
|
if (state.validationState.titleHasError) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.atLeast3Symbols)],
|
||||||
const SizedBox(height: 4),
|
|
||||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -140,20 +142,17 @@ class CreatePlanScreenContent extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
MarkdownEditor(
|
||||||
height: 132,
|
key: _mdKey,
|
||||||
child: CustomTextFormField(
|
hintText: s.plan_desc_example,
|
||||||
hasError: state.validationState.descriptionHasError,
|
hasError: state.validationState.descriptionHasError,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
minLines: 6,
|
||||||
maxLength: 1000,
|
maxLines: 8,
|
||||||
maxLines: null,
|
showInlineToolbar: false,
|
||||||
minLines: null,
|
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
|
||||||
expands: true,
|
onChanged: (value) {
|
||||||
hintText: s.plan_desc_example,
|
context.read<CreatePlanCubit>().setDetails(value);
|
||||||
onChanged: (value) {
|
},
|
||||||
context.read<CreatePlanCubit>().setDetails(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (state.validationState.descriptionHasError) ...[
|
if (state.validationState.descriptionHasError) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -253,10 +252,7 @@ class CreatePlanScreenContent extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return const Padding(
|
return const Padding(
|
||||||
padding: EdgeInsets.only(bottom: 24.0),
|
padding: EdgeInsets.only(bottom: 24.0),
|
||||||
child: Align(
|
child: Align(alignment: Alignment.bottomCenter, child: SelectExactOptionWidget()),
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: SelectExactOptionWidget(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
|||||||
validateSelectedOptionItems();
|
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) {
|
void setDuration(int index) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@@ -418,18 +425,21 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
|||||||
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
||||||
'description_md': state.planData.options[0].description,
|
'description_md': state.planData.options[0].description,
|
||||||
'items': Map.fromIterable(state.planData.options[0].items),
|
'items': Map.fromIterable(state.planData.options[0].items),
|
||||||
|
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
|
||||||
},
|
},
|
||||||
'packet2_options': {
|
'packet2_options': {
|
||||||
'short': state.planData.options[1].short,
|
'short': state.planData.options[1].short,
|
||||||
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
|
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
|
||||||
'description_md': state.planData.options[1].description,
|
'description_md': state.planData.options[1].description,
|
||||||
'items': Map.fromIterable(state.planData.options[1].items),
|
'items': Map.fromIterable(state.planData.options[1].items),
|
||||||
|
'has_personal_diet': state.planData.options[1].hasPersonalDiet ? '1' : '0',
|
||||||
},
|
},
|
||||||
'packet3_options': {
|
'packet3_options': {
|
||||||
'short': state.planData.options[2].short,
|
'short': state.planData.options[2].short,
|
||||||
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
|
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
|
||||||
'description_md': state.planData.options[2].description,
|
'description_md': state.planData.options[2].description,
|
||||||
'items': Map.fromIterable(state.planData.options[2].items),
|
'items': Map.fromIterable(state.planData.options[2].items),
|
||||||
|
'has_personal_diet': state.planData.options[2].hasPersonalDiet ? '1' : '0',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -440,6 +450,7 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
|
|||||||
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
|
||||||
'description_md': state.planData.options[0].description,
|
'description_md': state.planData.options[0].description,
|
||||||
'items': Map.fromIterable(state.planData.options[0].items),
|
'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,
|
2 => thirdOptionCustomerPriceController,
|
||||||
_ => secondOptionCustomerPriceController,
|
_ => secondOptionCustomerPriceController,
|
||||||
};
|
};
|
||||||
|
|
||||||
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
|
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
|
||||||
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||||
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||||
@@ -255,33 +255,31 @@ class _OptionWidgetState extends State<OptionWidget> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const CustomDivider(),
|
const CustomDivider(),
|
||||||
// const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Row(
|
Row(
|
||||||
// children: [
|
children: [
|
||||||
// Checkbox(
|
Checkbox(
|
||||||
// value: isChecked,
|
value: state.planData.options[selectedOptionIndex].hasPersonalDiet,
|
||||||
// activeColor: appTheme.gray,
|
activeColor: appTheme.gray,
|
||||||
// checkColor: appTheme.gray,
|
checkColor: appTheme.gray,
|
||||||
// fillColor: const WidgetStatePropertyAll(Colors.transparent),
|
fillColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||||
// side: WidgetStateBorderSide.resolveWith(
|
side: WidgetStateBorderSide.resolveWith(
|
||||||
// (Set<WidgetState> states) {
|
(Set<WidgetState> states) {
|
||||||
// return BorderSide(width: 1, color: appTheme.gray);
|
return BorderSide(width: 1, color: appTheme.gray);
|
||||||
// },
|
},
|
||||||
// ),
|
),
|
||||||
// onChanged: (bool? value) {
|
onChanged: (bool? value) {
|
||||||
// setState(() {
|
context.read<CreatePlanCubit>().togglePersonalizedDietOptionState();
|
||||||
// isChecked = value ?? false;
|
},
|
||||||
// });
|
),
|
||||||
// },
|
Expanded(
|
||||||
// ),
|
child: Text(
|
||||||
// Expanded(
|
s.option_personal_diet_desc,
|
||||||
// child: Text(
|
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||||
// 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/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
import 'package:eatvio_app/core/widgets/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_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/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/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/ingredients_widget.dart';
|
||||||
import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart';
|
import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||||
@@ -18,10 +20,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
class RecipeCreateScreen extends StatefulWidget {
|
class RecipeCreateScreen extends StatefulWidget {
|
||||||
final Recipe? recipe;
|
final Recipe? recipe;
|
||||||
|
|
||||||
const RecipeCreateScreen({
|
const RecipeCreateScreen({super.key, this.recipe});
|
||||||
super.key,
|
|
||||||
this.recipe,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const name = 'recipeCreateScreen';
|
static const name = 'recipeCreateScreen';
|
||||||
static const path = '/recipe-create-screen';
|
static const path = '/recipe-create-screen';
|
||||||
@@ -32,6 +31,8 @@ class RecipeCreateScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||||
late final CreateRecipeCubit createRecipeCubit;
|
late final CreateRecipeCubit createRecipeCubit;
|
||||||
|
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||||
|
bool _mdToolbarVisible = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -52,20 +53,18 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
|||||||
child: BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
child: BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||||
builder: (BuildContext context, CreateRecipeState state) {
|
builder: (BuildContext context, CreateRecipeState state) {
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
return const Scaffold(
|
return const Scaffold(body: Loader());
|
||||||
body: Loader(),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF0F2F5),
|
backgroundColor: const Color(0xFFF0F2F5),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||||
|
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null
|
||||||
|
? _mdKey.currentState!.buildToolbar(context)
|
||||||
|
: null,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
|
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
|
||||||
borderRadius: BorderRadius.vertical(
|
|
||||||
bottom: Radius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
@@ -75,22 +74,23 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
|||||||
children: [
|
children: [
|
||||||
InfoAboutRecipeWidget(
|
InfoAboutRecipeWidget(
|
||||||
createRecipeCubit: createRecipeCubit,
|
createRecipeCubit: createRecipeCubit,
|
||||||
|
markdownEditorKey: _mdKey,
|
||||||
|
onMarkdownToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
|
||||||
),
|
),
|
||||||
const IngredientsWidget(),
|
const IngredientsWidget(),
|
||||||
StepsWidget(
|
StepsWidget(createRecipeCubit: createRecipeCubit),
|
||||||
createRecipeCubit: createRecipeCubit,
|
|
||||||
),
|
|
||||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Builder(
|
return Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
key: AppKeys.recipeCreateScreenPublishButton,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||||
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
||||||
widget.recipe == null ? false : true,
|
widget.recipe == null ? false : true,
|
||||||
widget.recipe?.slug,
|
widget.recipe?.slug,
|
||||||
);
|
);
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -98,9 +98,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
|||||||
showBottomSheetWithBlur(
|
showBottomSheetWithBlur(
|
||||||
userRootNavigator: true,
|
userRootNavigator: true,
|
||||||
context: context,
|
context: context,
|
||||||
child: SuccessPublishedRecipeBottomSheet(
|
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
|
||||||
title: state.entity.title ?? 'Рецепт',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -112,20 +110,13 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
color: appTheme.blueA400,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.recipe != null ? 'Обновить' : 'Опубликовать',
|
widget.recipe != null ? 'Обновить' : 'Опубликовать',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:easy_debounce/easy_debounce.dart';
|
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/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_divider.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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AddPortionWidget extends StatefulWidget {
|
class AddPortionWidget extends StatefulWidget {
|
||||||
const AddPortionWidget({
|
const AddPortionWidget({super.key, required this.createRecipeCubit, this.portionIndex, this.productNameControllerText, this.quantityControllerText});
|
||||||
super.key,
|
|
||||||
required this.createRecipeCubit,
|
|
||||||
this.portionIndex,
|
|
||||||
this.productNameControllerText,
|
|
||||||
this.quantityControllerText,
|
|
||||||
});
|
|
||||||
final CreateRecipeCubit createRecipeCubit;
|
final CreateRecipeCubit createRecipeCubit;
|
||||||
final String? productNameControllerText;
|
final String? productNameControllerText;
|
||||||
final String? quantityControllerText;
|
final String? quantityControllerText;
|
||||||
@@ -43,76 +38,65 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16) +
|
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||||
EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Добавление блюда',
|
'Добавление блюда',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Expanded(
|
||||||
width: MediaQuery.of(context).size.width * 0.8 - 32,
|
flex: 4,
|
||||||
child: CustomTextFormField(
|
child: CustomTextFormField(
|
||||||
|
key: AppKeys.recipeCreateScreenIngredientNameField,
|
||||||
controller: productNameController,
|
controller: productNameController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hintText: 'Введите название продукта/блюда',
|
hintText: 'Введите название продукта/блюда',
|
||||||
onChanged: (query) {
|
onChanged: (query) {
|
||||||
EasyDebounce.debounce(
|
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||||
'glossary-search',
|
selectedGlossary = null;
|
||||||
const Duration(milliseconds: 250),
|
if (query.isEmpty) {
|
||||||
() async {
|
glossariesList = [];
|
||||||
selectedGlossary = null;
|
selectedGlossary = null;
|
||||||
if (query.isEmpty) {
|
|
||||||
glossariesList = [];
|
|
||||||
selectedGlossary = null;
|
|
||||||
setState(() {});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
glossariesList = await widget.createRecipeCubit.getGlossaryListByQuery(query);
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
return;
|
||||||
);
|
}
|
||||||
|
glossariesList = await widget.createRecipeCubit.getGlossaryListByQuery(query);
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
SizedBox(
|
Expanded(
|
||||||
width: MediaQuery.of(context).size.width * 0.2 - 4,
|
flex: 1,
|
||||||
child: CustomTextFormField(
|
child: CustomTextFormField(
|
||||||
|
key: AppKeys.recipeCreateScreenIngredientQuantityField,
|
||||||
controller: quantityController,
|
controller: quantityController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
hintText: selectedGlossary == null
|
hintText: selectedGlossary == null
|
||||||
? '100'
|
? '100'
|
||||||
: selectedGlossary!.grInPortion == 0
|
: selectedGlossary!.grInPortion == 0
|
||||||
? '100'
|
? '100'
|
||||||
: '1',
|
: '1',
|
||||||
suffixText: selectedGlossary == null
|
suffixText: selectedGlossary == null
|
||||||
? 'гр'
|
? 'гр'
|
||||||
: selectedGlossary?.grInPortion == 0
|
: selectedGlossary?.grInPortion == 0
|
||||||
? 'гр'
|
? 'гр'
|
||||||
: 'шт',
|
: 'шт',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(maxHeight: 256),
|
||||||
maxHeight: 256,
|
|
||||||
),
|
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -127,28 +111,12 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
FocusManager.instance.primaryFocus?.nextFocus();
|
FocusManager.instance.primaryFocus?.nextFocus();
|
||||||
},
|
},
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
borderRadius: BorderRadius.circular(20),
|
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
glossariesList[i].name,
|
|
||||||
style: const TextStyle(
|
|
||||||
height: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: glossariesList[i].grInPortion == 0
|
subtitle: glossariesList[i].grInPortion == 0
|
||||||
? null
|
? null
|
||||||
: Text(
|
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
|
||||||
'${glossariesList[i].grInPortion} г',
|
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
|
||||||
style: TextStyle(
|
|
||||||
height: 1,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Icon(
|
|
||||||
Icons.add_rounded,
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.only(left: 12),
|
contentPadding: const EdgeInsets.only(left: 12),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -170,34 +138,23 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
child: Ink(
|
child: Ink(
|
||||||
width: 52,
|
width: 52,
|
||||||
height: 52,
|
height: 52,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||||
borderRadius: BorderRadius.circular(20),
|
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
|
||||||
color: const Color(0xFFF6F6F6),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.delete_outline_rounded,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.portionIndex != null) const SizedBox(width: 8),
|
if (widget.portionIndex != null) const SizedBox(width: 8),
|
||||||
InkWell(
|
Expanded(
|
||||||
onTap: () => addPortion(context),
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(20),
|
key: AppKeys.recipeCreateScreenIngredientAddButton,
|
||||||
child: Ink(
|
onTap: () => addPortion(context),
|
||||||
width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92),
|
borderRadius: BorderRadius.circular(20),
|
||||||
height: 52,
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
height: 52,
|
||||||
borderRadius: BorderRadius.circular(20),
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||||
color: appTheme.blueA400,
|
child: Center(
|
||||||
),
|
child: Text(
|
||||||
child: Center(
|
widget.portionIndex == null ? 'Добавить' : 'Сохранить',
|
||||||
child: Text(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
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
|
var measurementUnits = selectedGlossary?.grInPortion == null
|
||||||
? Portion.pcs
|
? Portion.pcs
|
||||||
: selectedGlossary?.grInPortion == 0
|
: selectedGlossary?.grInPortion == 0
|
||||||
? Portion.grams
|
? Portion.grams
|
||||||
: Portion.pcs;
|
: Portion.pcs;
|
||||||
final glossary = selectedGlossary;
|
final glossary = selectedGlossary;
|
||||||
if (glossary != null) {
|
if (glossary != null) {
|
||||||
final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits);
|
final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits);
|
||||||
@@ -229,12 +186,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
);
|
);
|
||||||
if (portionIndex == null) {
|
if (portionIndex == null) {
|
||||||
widget.createRecipeCubit.addIngredient(
|
widget.createRecipeCubit.addIngredient(
|
||||||
Ingredient(
|
Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: glossary.id),
|
||||||
name: portion.name,
|
|
||||||
unit: portion.portion.getValue(),
|
|
||||||
portion: portion.quantity.toString(),
|
|
||||||
productId: glossary.id,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -247,13 +199,8 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Padding(
|
builder: (context) => Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: sl<DiaryCubit>()),
|
||||||
),
|
|
||||||
child: GlossaryModerationRequestModalWidget(
|
|
||||||
productName: productNameController.text,
|
|
||||||
diaryCubit: sl<DiaryCubit>(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (newGlossary != null) {
|
if (newGlossary != null) {
|
||||||
@@ -268,12 +215,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
);
|
);
|
||||||
if (portionIndex == null) {
|
if (portionIndex == null) {
|
||||||
widget.createRecipeCubit.addIngredient(
|
widget.createRecipeCubit.addIngredient(
|
||||||
Ingredient(
|
Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: newGlossary.id),
|
||||||
name: portion.name,
|
|
||||||
unit: portion.portion.getValue(),
|
|
||||||
portion: portion.quantity.toString(),
|
|
||||||
productId: newGlossary.id,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -281,12 +223,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
|||||||
} else {
|
} else {
|
||||||
widget.createRecipeCubit.updateIngredient(
|
widget.createRecipeCubit.updateIngredient(
|
||||||
index: portionIndex,
|
index: portionIndex,
|
||||||
ingredient: Ingredient(
|
ingredient: Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: newGlossary.id),
|
||||||
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/data/selectionPopupModel/selection_popup_model.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_drop_down.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 {
|
class InfoAboutRecipeWidget extends StatefulWidget {
|
||||||
final CreateRecipeCubit createRecipeCubit;
|
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
|
@override
|
||||||
State<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
|
State<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
|
||||||
@@ -46,125 +50,126 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
physics: const BouncingScrollPhysics(),
|
||||||
children: [
|
child: Column(
|
||||||
const Text('Название'),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 4),
|
children: [
|
||||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
const Text('Название'),
|
||||||
bloc: widget.createRecipeCubit,
|
const SizedBox(height: 4),
|
||||||
buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError,
|
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||||
builder: (context, state) {
|
bloc: widget.createRecipeCubit,
|
||||||
return Column(
|
buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError,
|
||||||
children: [
|
builder: (context, state) {
|
||||||
CustomTextFormField(
|
return Column(
|
||||||
hintText: 'Например: тар-тар из говядины',
|
children: [
|
||||||
hasError: state.titleValidationError != null,
|
CustomTextFormField(
|
||||||
initialValue: state.entity.title,
|
key: AppKeys.recipeCreateScreenTitleField,
|
||||||
onChanged: (value) {
|
hintText: 'Например: тар-тар из говядины',
|
||||||
widget.createRecipeCubit.replaceTitle(value);
|
hasError: state.titleValidationError != null,
|
||||||
if (state.titleValidationError != null) {
|
initialValue: state.entity.title,
|
||||||
widget.createRecipeCubit.validateTitle();
|
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!,
|
|
||||||
),
|
),
|
||||||
|
if (state.titleValidationError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.titleValidationError!)],
|
||||||
],
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
const Text('Тип блюда'),
|
||||||
const Text('Тип блюда'),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(height: 4),
|
CustomDropDown(
|
||||||
CustomDropDown(
|
key: AppKeys.recipeCreateScreenTypeDropdown,
|
||||||
items: items,
|
items: items,
|
||||||
value: selectedType,
|
value: selectedType,
|
||||||
onChanged: (newSelectedType) {
|
onChanged: (newSelectedType) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedType = newSelectedType;
|
selectedType = newSelectedType;
|
||||||
});
|
});
|
||||||
widget.createRecipeCubit.changeType(
|
widget.createRecipeCubit.changeType(TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other);
|
||||||
TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other,
|
},
|
||||||
);
|
),
|
||||||
},
|
const SizedBox(height: 12),
|
||||||
),
|
const TagsWidget(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const TagsWidget(),
|
const PortionWidget(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const PortionWidget(),
|
const TimeOfCookWidget(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const TimeOfCookWidget(),
|
const Text('Описание'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 4),
|
||||||
const Text('Описание'),
|
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||||
const SizedBox(height: 4),
|
buildWhen: (previous, current) => previous.descriptionValidationError != current.descriptionValidationError,
|
||||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
builder: (context, state) {
|
||||||
buildWhen: (previous, current) => previous.descriptionValidationError != current.descriptionValidationError,
|
return Column(
|
||||||
builder: (context, state) {
|
children: [
|
||||||
return Column(
|
KeyedSubtree(
|
||||||
children: [
|
key: AppKeys.recipeCreateScreenDescriptionField,
|
||||||
CustomTextFormField(
|
child: MarkdownEditor(
|
||||||
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
|
key: widget.markdownEditorKey,
|
||||||
minLines: 5,
|
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
|
||||||
initialValue: state.entity.description,
|
minLines: 5,
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
hasError: state.descriptionValidationError != null,
|
initialText: state.entity.description,
|
||||||
onChanged: (value) {
|
hasError: state.descriptionValidationError != null,
|
||||||
widget.createRecipeCubit.replaceDescription(value);
|
showInlineToolbar: false,
|
||||||
if (state.descriptionValidationError != null) {
|
onToolbarVisibilityChanged: widget.onMarkdownToolbarVisibilityChanged,
|
||||||
widget.createRecipeCubit.validateDescription();
|
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!,
|
|
||||||
),
|
),
|
||||||
|
if (state.descriptionValidationError != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
ValidationErrorWidget(error: state.descriptionValidationError!),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
const Text('Фото обложка'),
|
||||||
const Text('Фото обложка'),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(height: 4),
|
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
buildWhen: (previous, current) =>
|
||||||
buildWhen: (previous, current) => previous.imageValidationError != current.imageValidationError || previous.entity.image != current.entity.image,
|
previous.imageValidationError != current.imageValidationError || previous.entity.image != current.entity.image,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: state.imageValidationError != null
|
decoration: state.imageValidationError != null
|
||||||
? BoxDecoration(border: Border.all(width: 1, color: appTheme.red), borderRadius: BorderRadius.circular(8))
|
? BoxDecoration(
|
||||||
: null,
|
border: Border.all(width: 1, color: appTheme.red),
|
||||||
child: ImageWidget(image: state.entity.image),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
)
|
||||||
if (state.imageValidationError != null) ...[
|
: null,
|
||||||
const SizedBox(height: 4),
|
child: ImageWidget(key: AppKeys.recipeCreateScreenImageWidget, image: state.entity.image),
|
||||||
ValidationErrorWidget(
|
|
||||||
error: state.imageValidationError!,
|
|
||||||
),
|
),
|
||||||
|
if (state.imageValidationError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.imageValidationError!)],
|
||||||
],
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
const Text('Ссылка на видео (если есть)'),
|
||||||
const Text('Ссылка на видео (если есть)'),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(height: 4),
|
CustomTextFormField(
|
||||||
CustomTextFormField(
|
hintText: 'Прикрепите ссылку на видео',
|
||||||
hintText: 'Прикрепите ссылку на видео',
|
initialValue: widget.createRecipeCubit.state.entity.videoUrl,
|
||||||
initialValue: widget.createRecipeCubit.state.entity.videoUrl,
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
widget.createRecipeCubit.replaceVideoUrl(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/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_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/bloc/create_recipe_cubit/create_recipe_state.dart';
|
||||||
@@ -40,11 +41,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Ингредиенты ',
|
'Ингредиенты ',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ReorderableListView(
|
ReorderableListView(
|
||||||
@@ -63,46 +60,29 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
key: Key('$index'),
|
key: Key('$index'),
|
||||||
leading: Icon(
|
leading: Icon(Icons.drag_handle_rounded, color: Colors.grey.shade600),
|
||||||
Icons.drag_handle_rounded,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
title: Text(state.entity.ingredients[index].name),
|
title: Text(state.entity.ingredients[index].name),
|
||||||
subtitle: Text('${state.entity.ingredients[index].portion} ${state.entity.ingredients[index].unit == 'gr' ? 'гр' : 'шт'}'),
|
subtitle: Text('${state.entity.ingredients[index].portion} ${state.entity.ingredients[index].unit == 'gr' ? 'гр' : 'шт'}'),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
createRecipeCubit.removeIngredient(index);
|
createRecipeCubit.removeIngredient(index);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(Icons.delete_outline_rounded, color: Colors.grey.shade600),
|
||||||
Icons.delete_outline_rounded,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
key: AppKeys.recipeCreateScreenAddIngredientButton,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
nameController.clear();
|
nameController.clear();
|
||||||
gramsController.clear();
|
gramsController.clear();
|
||||||
pcsController.clear();
|
pcsController.clear();
|
||||||
_modalBottomSheet(
|
_modalBottomSheet(createRecipeCubit: createRecipeCubit);
|
||||||
createRecipeCubit: createRecipeCubit,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
borderRadius: BorderRadius.circular(12),
|
title: Text('Добавить ингредиент', style: TextStyle(color: appTheme.blueA400)),
|
||||||
),
|
leading: Icon(Icons.add_rounded, color: appTheme.blueA400),
|
||||||
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(
|
return showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
context: context,
|
context: context,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))),
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
|
||||||
),
|
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AddPortionWidget(
|
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/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_diet/presentation/widgets/error_widget.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_cubit.dart';
|
||||||
@@ -39,12 +40,13 @@ class _PortionWidgetState extends State<PortionWidget> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Кол-во порций'),
|
const Text('Количество порций'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
hintText: '1',
|
key: AppKeys.recipeCreateScreenPortionsField,
|
||||||
|
hintText: 'Например: 1',
|
||||||
hasError: state.servingsValidationError != null,
|
hasError: state.servingsValidationError != null,
|
||||||
initialValue: state.entity.countOfPortions?.toString(),
|
initialValue: state.entity.countOfPortions?.toString(),
|
||||||
textInputType: TextInputType.number,
|
textInputType: TextInputType.number,
|
||||||
@@ -57,9 +59,7 @@ class _PortionWidgetState extends State<PortionWidget> {
|
|||||||
),
|
),
|
||||||
if (state.servingsValidationError != null) ...[
|
if (state.servingsValidationError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ValidationErrorWidget(
|
ValidationErrorWidget(error: state.servingsValidationError!),
|
||||||
error: state.servingsValidationError!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -72,11 +72,12 @@ class _PortionWidgetState extends State<PortionWidget> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Ккал в одной порции'),
|
const Text('Калорийность'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
|
key: AppKeys.recipeCreateScreenCaloriesField,
|
||||||
hintText: '100',
|
hintText: '100',
|
||||||
hasError: state.energyPerServingValidationError != null,
|
hasError: state.energyPerServingValidationError != null,
|
||||||
initialValue: state.entity.energyInOnePortion?.toString(),
|
initialValue: state.entity.energyInOnePortion?.toString(),
|
||||||
@@ -91,9 +92,7 @@ class _PortionWidgetState extends State<PortionWidget> {
|
|||||||
),
|
),
|
||||||
if (state.energyPerServingValidationError != null) ...[
|
if (state.energyPerServingValidationError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ValidationErrorWidget(
|
ValidationErrorWidget(error: state.energyPerServingValidationError!),
|
||||||
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/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
import 'package: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,
|
bloc: widget.createRecipeCubit,
|
||||||
builder: (BuildContext context, CreateRecipeState state) {
|
builder: (BuildContext context, CreateRecipeState state) {
|
||||||
return Material(
|
return Material(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
physics: const BouncingScrollPhysics(),
|
||||||
children: [
|
child: Column(
|
||||||
const Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'Пошаговая инструкция',
|
children: [
|
||||||
style: TextStyle(
|
const Text(
|
||||||
fontSize: 20,
|
'Пошаговая инструкция',
|
||||||
fontWeight: FontWeight.w600,
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
InkWell(
|
||||||
ListView.separated(
|
key: AppKeys.recipeCreateScreenAddStepButton,
|
||||||
shrinkWrap: true,
|
onTap: () {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
context.read<CreateRecipeCubit>().addStep();
|
||||||
itemBuilder: (context, index) => Column(
|
},
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
borderRadius: BorderRadius.circular(20),
|
||||||
children: [
|
child: Ink(
|
||||||
Row(
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
children: [
|
child: Container(
|
||||||
Container(
|
padding: const EdgeInsets.all(16),
|
||||||
padding: const EdgeInsets.symmetric(
|
alignment: Alignment.center,
|
||||||
horizontal: 12,
|
child: const Text(
|
||||||
vertical: 4,
|
'Добавить шаг',
|
||||||
),
|
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
||||||
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),
|
||||||
],
|
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/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
import 'package: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 Text('Теги'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
|
key: AppKeys.recipeCreateScreenTagField,
|
||||||
hintText: 'Например: без майонеза',
|
hintText: 'Например: без майонеза',
|
||||||
controller: tagController,
|
controller: tagController,
|
||||||
suffix: IconButton(
|
suffix: IconButton(
|
||||||
|
key: AppKeys.recipeCreateScreenAddTagButton,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (tagController.text.isNotEmpty) {
|
if (tagController.text.isNotEmpty) {
|
||||||
context.read<CreateRecipeCubit>().addTag(tagController.text);
|
context.read<CreateRecipeCubit>().addTag(tagController.text);
|
||||||
@@ -37,10 +40,7 @@ class _TagsWidgetState extends State<TagsWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(Icons.done, color: appTheme.blueA400),
|
||||||
Icons.done,
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -56,22 +56,13 @@ class _TagsWidgetState extends State<TagsWidget> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: Colors.grey.shade200),
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(state.entity.tags[i]),
|
||||||
state.entity.tags[i],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Icon(
|
Icon(Icons.delete_outline_rounded, color: Colors.grey.shade600, size: 20),
|
||||||
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/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.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_diet/presentation/widgets/error_widget.dart';
|
||||||
@@ -43,20 +44,12 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
builder: (context) => const AlertDialog(
|
builder: (context) => const AlertDialog(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Общее время приготовления - это период времени от начала приготовления блюда до подачи его на стол.',
|
'Общее время приготовления - это период времени от начала приготовления блюда до подачи его на стол.',
|
||||||
style: TextStyle(
|
style: TextStyle(fontWeight: FontWeight.w500, color: Color(0xFF323F4B), height: 1.2),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
height: 1.2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(Icons.help_outline_rounded, color: appTheme.blueA400, size: 20),
|
||||||
Icons.help_outline_rounded,
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -66,9 +59,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const Text('Часы'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
hintText: 'Часов',
|
key: AppKeys.recipeCreateScreenTotalHoursField,
|
||||||
|
hintText: '',
|
||||||
hasError: state.cookingTimeValidationError != null,
|
hasError: state.cookingTimeValidationError != null,
|
||||||
textInputType: TextInputType.number,
|
textInputType: TextInputType.number,
|
||||||
initialValue: state.entity.totalCookHours?.toString(),
|
initialValue: state.entity.totalCookHours?.toString(),
|
||||||
@@ -83,9 +80,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
),
|
),
|
||||||
if (state.cookingTimeValidationError != null) ...[
|
if (state.cookingTimeValidationError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ValidationErrorWidget(
|
ValidationErrorWidget(error: state.cookingTimeValidationError!),
|
||||||
error: state.cookingTimeValidationError!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -93,9 +88,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const Text('Минуты'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
hintText: 'Минут',
|
key: AppKeys.recipeCreateScreenTotalMinutesField,
|
||||||
|
hintText: '',
|
||||||
hasError: state.cookingTimeValidationError != null,
|
hasError: state.cookingTimeValidationError != null,
|
||||||
textInputType: TextInputType.number,
|
textInputType: TextInputType.number,
|
||||||
initialValue: state.entity.totalCookMinutes?.toString(),
|
initialValue: state.entity.totalCookMinutes?.toString(),
|
||||||
@@ -110,9 +109,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
),
|
),
|
||||||
if (state.cookingTimeValidationError != null) ...[
|
if (state.cookingTimeValidationError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ValidationErrorWidget(
|
ValidationErrorWidget(error: state.cookingTimeValidationError!),
|
||||||
error: state.cookingTimeValidationError!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -131,20 +128,12 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
builder: (context) => const AlertDialog(
|
builder: (context) => const AlertDialog(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Активное время приготовления - это период времени, требующий активного участия в приготовлении блюда. Оно включает мытье и очистку продуктов, нарезку, шинковку, взбивание, перемешивание, размешивание, перекладывание и т.п.',
|
'Активное время приготовления - это период времени, требующий активного участия в приготовлении блюда. Оно включает мытье и очистку продуктов, нарезку, шинковку, взбивание, перемешивание, размешивание, перекладывание и т.п.',
|
||||||
style: TextStyle(
|
style: TextStyle(fontWeight: FontWeight.w500, color: Color(0xFF323F4B), height: 1.2),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
height: 1.2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(Icons.help_outline_rounded, color: appTheme.blueA400, size: 20),
|
||||||
Icons.help_outline_rounded,
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -154,9 +143,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const Text('Часы'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
hintText: 'Часов',
|
key: AppKeys.recipeCreateScreenActiveHoursField,
|
||||||
|
hintText: '',
|
||||||
hasError: state.activeTimeValidationError != null,
|
hasError: state.activeTimeValidationError != null,
|
||||||
initialValue: state.entity.activeCookHours?.toString(),
|
initialValue: state.entity.activeCookHours?.toString(),
|
||||||
textInputType: TextInputType.number,
|
textInputType: TextInputType.number,
|
||||||
@@ -172,9 +165,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
),
|
),
|
||||||
if (state.activeTimeValidationError != null) ...[
|
if (state.activeTimeValidationError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ValidationErrorWidget(
|
ValidationErrorWidget(error: state.activeTimeValidationError!),
|
||||||
error: state.activeTimeValidationError!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -182,9 +173,13 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const Text('Минуты'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
hintText: 'Минут',
|
key: AppKeys.recipeCreateScreenActiveMinutesField,
|
||||||
|
hintText: '',
|
||||||
hasError: state.activeTimeValidationError != null,
|
hasError: state.activeTimeValidationError != null,
|
||||||
initialValue: state.entity.activeCookMinutes?.toString(),
|
initialValue: state.entity.activeCookMinutes?.toString(),
|
||||||
textInputType: TextInputType.number,
|
textInputType: TextInputType.number,
|
||||||
@@ -200,9 +195,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
|||||||
),
|
),
|
||||||
if (state.activeTimeValidationError != null) ...[
|
if (state.activeTimeValidationError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ValidationErrorWidget(
|
ValidationErrorWidget(error: state.activeTimeValidationError!),
|
||||||
error: state.activeTimeValidationError!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
22
lib/features/diary/entities/diary_day_entity.dart
Normal file
22
lib/features/diary/entities/diary_day_entity.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'diary_day_entity.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class DiaryDayEntity with _$DiaryDayEntity {
|
||||||
|
factory DiaryDayEntity({
|
||||||
|
@JsonKey(name: "id") required int id,
|
||||||
|
@JsonKey(name: "date") required String date,
|
||||||
|
@JsonKey(name: "wakeup") String? wakeup,
|
||||||
|
@JsonKey(name: "retire") String? retire,
|
||||||
|
@JsonKey(name: "closed") required int closed,
|
||||||
|
@JsonKey(name: "comment") required String comment,
|
||||||
|
@JsonKey(name: "state") required TypeOfDiaryModel typeOfDiary,
|
||||||
|
@JsonKey(name: "meals") required List<MealModel> meals,
|
||||||
|
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
|
||||||
|
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
|
||||||
|
@JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet,
|
||||||
|
}) = _DiaryDayEntity;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'diary_future_diet_day_entity.freezed.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class DiaryFutureDietDayEntity with _$DiaryFutureDietDayEntity {
|
||||||
|
const factory DiaryFutureDietDayEntity({
|
||||||
|
required String date,
|
||||||
|
required TypeOfDiaryModel typeOfDiary,
|
||||||
|
required List<MealModel> meals,
|
||||||
|
required DiaryDietModel? diet,
|
||||||
|
required int? dayOfChosenDiet,
|
||||||
|
}) = _DiaryFutureDietDayEntity;
|
||||||
|
|
||||||
|
}
|
||||||
11
lib/features/diary/models/create_day_model.dart
Normal file
11
lib/features/diary/models/create_day_model.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'create_day_model.freezed.dart';
|
||||||
|
part 'create_day_model.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class CreateDayModel with _$CreateDayModel {
|
||||||
|
factory CreateDayModel({required String day}) = _CreateDayModel;
|
||||||
|
|
||||||
|
factory CreateDayModel.fromJson(Map<String, dynamic> json) => _$CreateDayModelFromJson(json);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'diary_day_model.freezed.dart';
|
part 'diary_day_model.freezed.dart';
|
||||||
@@ -30,7 +32,7 @@ abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel {
|
|||||||
@freezed
|
@freezed
|
||||||
abstract class DiaryDayModel with _$DiaryDayModel {
|
abstract class DiaryDayModel with _$DiaryDayModel {
|
||||||
const factory DiaryDayModel({
|
const factory DiaryDayModel({
|
||||||
@JsonKey(name: "id") required int id,
|
@JsonKey(name: "id") required int? id,
|
||||||
@JsonKey(name: "date") required String date,
|
@JsonKey(name: "date") required String date,
|
||||||
@JsonKey(name: "wakeup") String? wakeup,
|
@JsonKey(name: "wakeup") String? wakeup,
|
||||||
@JsonKey(name: "retire") String? retire,
|
@JsonKey(name: "retire") String? retire,
|
||||||
@@ -40,26 +42,36 @@ abstract class DiaryDayModel with _$DiaryDayModel {
|
|||||||
@JsonKey(name: "meals") required List<MealModel> meals,
|
@JsonKey(name: "meals") required List<MealModel> meals,
|
||||||
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
|
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
|
||||||
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
|
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
|
||||||
@JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet,
|
@JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet,
|
||||||
}) = _DiaryDayModel;
|
}) = _DiaryDayModel;
|
||||||
|
const DiaryDayModel._();
|
||||||
factory DiaryDayModel.fromJson(Map<String, dynamic> json) => _$DiaryDayModelFromJson(json);
|
factory DiaryDayModel.fromJson(Map<String, dynamic> json) => _$DiaryDayModelFromJson(json);
|
||||||
|
|
||||||
|
DiaryDayEntity? toEntity() {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DiaryDayEntity(
|
||||||
|
id: id!,
|
||||||
|
date: date,
|
||||||
|
closed: closed,
|
||||||
|
comment: comment,
|
||||||
|
typeOfDiary: typeOfDiary,
|
||||||
|
meals: meals,
|
||||||
|
chosenPlanId: chosenPlanId,
|
||||||
|
dayOfChosenDiet: dayOfChosenDiet,
|
||||||
|
wakeup: wakeup,
|
||||||
|
retire: retire,
|
||||||
|
diet: diet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DiaryFutureDietDayEntity toDiaryFutureDayEntity() {
|
||||||
|
return DiaryFutureDietDayEntity(date: date, typeOfDiary: typeOfDiary, meals: meals, dayOfChosenDiet: dayOfChosenDiet, diet: diet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
DiaryDietModel? parseDiet(dynamic json) {
|
||||||
abstract class DiaryFutureDietDayModel with _$DiaryFutureDietDayModel {
|
|
||||||
const factory DiaryFutureDietDayModel({
|
|
||||||
@JsonKey(name: "date") required String date,
|
|
||||||
@JsonKey(name: "state", fromJson: _parseDayTypeForFutureDay) required TypeOfDiaryModel typeOfDiary,
|
|
||||||
@JsonKey(name: "meals") required List<MealModel> meals,
|
|
||||||
@JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet,
|
|
||||||
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
|
|
||||||
}) = _DiaryFutureDietDayModel;
|
|
||||||
|
|
||||||
factory DiaryFutureDietDayModel.fromJson(Map<String, dynamic> json) => _$DiaryFutureDietDayModelFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
DiaryDietModel? _parseDiet(dynamic json) {
|
|
||||||
if (json == null) return null;
|
if (json == null) return null;
|
||||||
return DiaryDietModel.fromJson(json['diet']);
|
return DiaryDietModel.fromJson(json['diet']);
|
||||||
}
|
}
|
||||||
@@ -71,10 +83,6 @@ abstract class DiaryDietModel with _$DiaryDietModel {
|
|||||||
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
|
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
|
|
||||||
return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class MealModel with _$MealModel {
|
abstract class MealModel with _$MealModel {
|
||||||
const factory MealModel({
|
const factory MealModel({
|
||||||
@@ -141,7 +149,7 @@ abstract class TypeOfDiaryModel with _$TypeOfDiaryModel {
|
|||||||
const factory TypeOfDiaryModel({
|
const factory TypeOfDiaryModel({
|
||||||
@JsonKey(name: "has_chosen_diet") required bool hasChosenDiet,
|
@JsonKey(name: "has_chosen_diet") required bool hasChosenDiet,
|
||||||
@JsonKey(name: "has_chosen_plan") required bool hasChosenPlan,
|
@JsonKey(name: "has_chosen_plan") required bool hasChosenPlan,
|
||||||
@JsonKey(name: "last_state") required DiaryStateEnum diaryState,
|
@JsonKey(name: "last_state") required DiaryStateEnum? diaryState,
|
||||||
}) = _TypeOfDiaryModel;
|
}) = _TypeOfDiaryModel;
|
||||||
|
|
||||||
factory TypeOfDiaryModel.fromJson(Map<String, dynamic> json) => _$TypeOfDiaryModelFromJson(json);
|
factory TypeOfDiaryModel.fromJson(Map<String, dynamic> json) => _$TypeOfDiaryModelFromJson(json);
|
||||||
|
|||||||
11
lib/features/diary/models/get_day_model.dart
Normal file
11
lib/features/diary/models/get_day_model.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'get_day_model.freezed.dart';
|
||||||
|
part 'get_day_model.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class GetDayModel with _$GetDayModel {
|
||||||
|
factory GetDayModel({required String day}) = _GetDayModel;
|
||||||
|
|
||||||
|
factory GetDayModel.fromJson(Map<String, dynamic> json) => _$GetDayModelFromJson(json);
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
||||||
@@ -20,7 +23,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
class DiaryCubit extends Cubit<DiaryState> {
|
class DiaryCubit extends Cubit<DiaryState> {
|
||||||
late final DateTime today;
|
late final DateTime today;
|
||||||
late DateTime selectedDay;
|
late DateTime selectedDay;
|
||||||
late int selectedMeal;
|
int selectedMeal = 1;
|
||||||
final DiaryRepository diaryRepository;
|
final DiaryRepository diaryRepository;
|
||||||
final GlossaryRepository glossaryRepository;
|
final GlossaryRepository glossaryRepository;
|
||||||
final ProfileRepository profileRepository;
|
final ProfileRepository profileRepository;
|
||||||
@@ -39,19 +42,79 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
today = DateTime.now();
|
today = DateTime.now();
|
||||||
selectedDay = today;
|
selectedDay = today;
|
||||||
diaryRepository.forceFetchStream.listen((_) {
|
diaryRepository.forceFetchStream.listen((_) {
|
||||||
fetchDiary(true, diaryId);
|
fetchDiaryV2(needLoading: true, diaryId: diaryId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String comment = '';
|
String comment = '';
|
||||||
|
|
||||||
Future<bool?> setSelectedDay(DateTime day, {bool? forDietOnly}) async {
|
Future<bool> setSelectedDay(DateTime day) async {
|
||||||
|
final prevDay = selectedDay;
|
||||||
selectedDay = day;
|
selectedDay = day;
|
||||||
if (forDietOnly ?? false) {
|
final canSelect = await fetchDiaryV2(needLoading: true);
|
||||||
final result = await fetchDiaryForDietOnly();
|
|
||||||
return result;
|
if (!canSelect) {
|
||||||
|
selectedDay = prevDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return canSelect;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
|
||||||
|
final currentState = state;
|
||||||
|
if (needLoading) {
|
||||||
|
emit(const DiaryState.loading());
|
||||||
|
}
|
||||||
|
if (diaryId != null) {
|
||||||
|
final diaryDayById = await diaryRepository.getDiaryDay(diaryId);
|
||||||
|
final diaryEntity = diaryDayById.toEntity();
|
||||||
|
if (diaryEntity != null) {
|
||||||
|
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final diaryDay = await getDiary();
|
||||||
|
if (diaryDay == null) {
|
||||||
|
emit(const DiaryState.error(errorMessage: 'Diary not found'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final diaryEntity = diaryDay.toEntity();
|
||||||
|
if (diaryEntity != null) {
|
||||||
|
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) {
|
||||||
|
setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
emit(currentState);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateCurrentDiary() async {
|
||||||
|
try {
|
||||||
|
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
|
||||||
|
final diaryEntity = diaryDay.toEntity();
|
||||||
|
if (diaryEntity != null) {
|
||||||
|
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DiaryDayModel?> getDiary() async {
|
||||||
|
try {
|
||||||
|
await diaryRepository.createDiaryDay(day: selectedDay.toISO8601WithTimeZoneOffset());
|
||||||
|
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
|
||||||
|
return diaryDay;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 422) {
|
||||||
|
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
|
||||||
|
return diaryDay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await fetchDiary(true);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +128,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> fetchDiaryForDietOnly() async {
|
Future<bool> setDiaryForDietOnly(DiaryFutureDietDayEntity diaryDay) async {
|
||||||
final stringDate = selectedDay.format('yyyy-MM-dd');
|
|
||||||
final diaryDay = await diaryRepository.getDiaryByDateForDietOnly(stringDate);
|
|
||||||
if (!diaryDay.typeOfDiary.hasChosenDiet) {
|
if (!diaryDay.typeOfDiary.hasChosenDiet) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -146,22 +207,14 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
|
Future<void> setDiary({int? diaryId, required DiaryDayEntity diaryDay}) async {
|
||||||
this.diaryId ??= diaryId;
|
this.diaryId ??= diaryId;
|
||||||
final isConsultantView = this.diaryId != null;
|
final isConsultantView = this.diaryId != null;
|
||||||
if (needLoading) {
|
|
||||||
emit(const DiaryState.loading());
|
|
||||||
selectedMeal = 1;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final DiaryDayModel diaryDay;
|
|
||||||
bool isMoodEnabled = false;
|
bool isMoodEnabled = false;
|
||||||
bool isWakeUpAndRetireTimeEnabled = false;
|
bool isWakeUpAndRetireTimeEnabled = false;
|
||||||
if (diaryId != null) {
|
if (diaryId != null) {
|
||||||
diaryDay = await diaryRepository.getDiaryDay(diaryId);
|
|
||||||
} else {
|
} else {
|
||||||
final stringDate = selectedDay.format('yyyy-MM-dd');
|
|
||||||
diaryDay = await diaryRepository.getDiaryByDate(stringDate);
|
|
||||||
final profileSettings = await settingsRepository.getProfileSettings();
|
final profileSettings = await settingsRepository.getProfileSettings();
|
||||||
isMoodEnabled = profileSettings.isMoodFillable;
|
isMoodEnabled = profileSettings.isMoodFillable;
|
||||||
isWakeUpAndRetireTimeEnabled = profileSettings.isBedtimeFillable;
|
isWakeUpAndRetireTimeEnabled = profileSettings.isBedtimeFillable;
|
||||||
@@ -214,7 +267,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
|
|
||||||
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
|
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
|
||||||
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
|
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<NutritionDataModel> getNutrition(int index) async {
|
Future<NutritionDataModel> getNutrition(int index) async {
|
||||||
@@ -238,7 +291,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
productNutritionalValueId: productNutritionalValueId,
|
productNutritionalValueId: productNutritionalValueId,
|
||||||
);
|
);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateProductInMeal({
|
Future<void> updateProductInMeal({
|
||||||
@@ -272,27 +325,27 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
portionState: portionState,
|
portionState: portionState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
|
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
|
||||||
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
|
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
|
||||||
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
|
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
|
||||||
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
|
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
|
||||||
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
|
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
|
||||||
@@ -301,12 +354,12 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
|
|
||||||
Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
|
Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
|
||||||
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
|
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
|
Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
|
||||||
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
|
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GlossaryModerationModel?> sendProductToModeration({
|
Future<GlossaryModerationModel?> sendProductToModeration({
|
||||||
@@ -326,30 +379,32 @@ class DiaryCubit extends Cubit<DiaryState> {
|
|||||||
|
|
||||||
Future<void> fillTime({required int diaryId, required String time}) async {
|
Future<void> fillTime({required int diaryId, required String time}) async {
|
||||||
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
|
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fillWakeup({required int diaryId, required String time}) async {
|
Future<void> fillWakeup({required int diaryId, required String time}) async {
|
||||||
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
|
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fillRetire({required int diaryId, required String time}) async {
|
Future<void> fillRetire({required int diaryId, required String time}) async {
|
||||||
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
|
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
|
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
|
||||||
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
|
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fillMood({required int diaryId, required int mood}) async {
|
Future<void> fillMood({required int diaryId, required int mood}) async {
|
||||||
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
|
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
|
||||||
await fetchDiary();
|
await updateCurrentDiary();
|
||||||
}
|
}
|
||||||
|
|
||||||
void fillComment(String comment) {
|
void fillComment(String comment) {
|
||||||
this.comment = comment;
|
this.comment = comment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DiaryFetchingErrors { canNotCreateFutureDays, dayAlreadyExists }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/entities/diary_future_diet_day_entity.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
|
||||||
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
@@ -11,12 +12,10 @@ abstract class DiaryState with _$DiaryState {
|
|||||||
|
|
||||||
const factory DiaryState.loading() = DiaryStateLoading;
|
const factory DiaryState.loading() = DiaryStateLoading;
|
||||||
|
|
||||||
const factory DiaryState.error({
|
const factory DiaryState.error({required String errorMessage}) = DiaryStateError;
|
||||||
required String errorMessage,
|
|
||||||
}) = DiaryStateError;
|
|
||||||
|
|
||||||
const factory DiaryState.loaded({
|
const factory DiaryState.loaded({
|
||||||
required DiaryDayModel diary,
|
required DiaryDayEntity diary,
|
||||||
required NutritionDataModel nutrition,
|
required NutritionDataModel nutrition,
|
||||||
required DateTime selectedDay,
|
required DateTime selectedDay,
|
||||||
required int selectedMeal,
|
required int selectedMeal,
|
||||||
@@ -32,9 +31,6 @@ abstract class DiaryState with _$DiaryState {
|
|||||||
@Default(false) bool isLoadingAddingProductInMeal,
|
@Default(false) bool isLoadingAddingProductInMeal,
|
||||||
}) = DiaryStateLoaded;
|
}) = DiaryStateLoaded;
|
||||||
|
|
||||||
const factory DiaryState.loadedTomorrowWithDiet({
|
const factory DiaryState.loadedTomorrowWithDiet({required DiaryFutureDietDayEntity diary, required DateTime selectedDay, required int selectedMeal}) =
|
||||||
required DiaryFutureDietDayModel diary,
|
DiaryStateLoadedTomorrowWithDiet;
|
||||||
required DateTime selectedDay,
|
|
||||||
required int selectedMeal,
|
|
||||||
}) = DiaryStateLoadedTomorrowWithDiet;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,19 +35,12 @@ class DiaryScreen extends StatelessWidget {
|
|||||||
class UserDiaryScreen extends StatelessWidget {
|
class UserDiaryScreen extends StatelessWidget {
|
||||||
static const path = '/user-diary-screen';
|
static const path = '/user-diary-screen';
|
||||||
static const name = 'userDiaryScreen';
|
static const name = 'userDiaryScreen';
|
||||||
const UserDiaryScreen({
|
const UserDiaryScreen({super.key, required this.diaryId, required this.currentPlanDay});
|
||||||
super.key,
|
|
||||||
required this.diaryId,
|
|
||||||
required this.currentPlanDay,
|
|
||||||
});
|
|
||||||
final int diaryId;
|
final int diaryId;
|
||||||
final int currentPlanDay;
|
final int currentPlanDay;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DiaryScreenContent(
|
return DiaryScreenContent(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||||
diaryId: diaryId,
|
|
||||||
currentPlanDay: currentPlanDay,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,12 +53,8 @@ class DiaryScreenContent extends StatelessWidget {
|
|||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<DiaryCubit>(
|
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: diaryId)),
|
||||||
create: (BuildContext context) => sl<DiaryCubit>()..fetchDiary(true, diaryId, currentPlanDay),
|
BlocProvider(create: (context) => sl<ConsultantCommentsCubit>()),
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) => sl<ConsultantCommentsCubit>(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: BlocBuilder<DiaryCubit, DiaryState>(
|
child: BlocBuilder<DiaryCubit, DiaryState>(
|
||||||
builder: (BuildContext context, DiaryState state) {
|
builder: (BuildContext context, DiaryState state) {
|
||||||
@@ -74,16 +63,14 @@ class DiaryScreenContent extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
if (state is DiaryStateError) {
|
if (state is DiaryStateError) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiary(false, diaryId)),
|
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiaryV2(needLoading: false, diaryId: diaryId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey.shade200,
|
backgroundColor: Colors.grey.shade200,
|
||||||
appBar: diaryId == null
|
appBar: diaryId == null
|
||||||
? AppBar(
|
? AppBar(toolbarHeight: 0)
|
||||||
toolbarHeight: 0,
|
|
||||||
)
|
|
||||||
: AppBar(
|
: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
state is DiaryStateLoaded ? DateTime.parse(state.diary.date).toLocal().format('d MMMM') : '',
|
state is DiaryStateLoaded ? DateTime.parse(state.diary.date).toLocal().format('d MMMM') : '',
|
||||||
@@ -97,12 +84,7 @@ class DiaryScreenContent extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (diaryId == null) const CalendarWidget(),
|
if (diaryId == null) const CalendarWidget(),
|
||||||
if (state is DiaryStateLoading) ...[
|
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
|
||||||
const SizedBox(
|
|
||||||
height: 60,
|
|
||||||
),
|
|
||||||
const Loader()
|
|
||||||
],
|
|
||||||
if (state is DiaryStateLoaded)
|
if (state is DiaryStateLoaded)
|
||||||
WakeupAndRetireWidget(
|
WakeupAndRetireWidget(
|
||||||
isEnabled: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isWakeUpAndRetireEnabled,
|
isEnabled: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isWakeUpAndRetireEnabled,
|
||||||
@@ -114,40 +96,31 @@ class DiaryScreenContent extends StatelessWidget {
|
|||||||
showMood: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isMoodEnabled,
|
showMood: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isMoodEnabled,
|
||||||
showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented,
|
showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented,
|
||||||
),
|
),
|
||||||
if (state is DiaryStateLoadedTomorrowWithDiet)
|
if (state is DiaryStateLoadedTomorrowWithDiet) const MealWidget(showMood: false, showAddButton: false),
|
||||||
const MealWidget(
|
|
||||||
showMood: false,
|
|
||||||
showAddButton: false,
|
|
||||||
),
|
|
||||||
if (state is DiaryStateLoaded) const CaloriesChartWidget(),
|
if (state is DiaryStateLoaded) const CaloriesChartWidget(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Builder(builder: (context) {
|
Builder(
|
||||||
if (state is DiaryStateLoaded && state.diary.typeOfDiary.hasChosenPlan) {
|
builder: (context) {
|
||||||
if (state.diary.comment.isNotEmpty) {
|
if (state is DiaryStateLoaded && state.diary.typeOfDiary.hasChosenPlan) {
|
||||||
return CommentForConsultantWidget(
|
if (state.diary.comment.isNotEmpty) {
|
||||||
comment: state.diary.comment,
|
return CommentForConsultantWidget(comment: state.diary.comment);
|
||||||
);
|
}
|
||||||
|
if (state.isDiaryCommented || state.isDiaryFinished) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return const CommentForConsultantWidget();
|
||||||
}
|
}
|
||||||
if (state.isDiaryCommented || state.isDiaryFinished) {
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
},
|
||||||
}
|
),
|
||||||
return const CommentForConsultantWidget();
|
|
||||||
}
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}),
|
|
||||||
if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented)
|
if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12),
|
padding: const EdgeInsets.only(top: 12),
|
||||||
child: FinishDayButtonWidget(
|
child: FinishDayButtonWidget(diaryDay: state.diary),
|
||||||
diaryDay: state.diary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[
|
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const MainBackgroundBouble(
|
const MainBackgroundBouble(padding: EdgeInsets.all(16), child: DayCommentWidget()),
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: DayCommentWidget(),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
if (state is DiaryStateLoaded && state.consultantReview?.consultantComments.dayComment != null) ...[
|
if (state is DiaryStateLoaded && state.consultantReview?.consultantComments.dayComment != null) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -155,10 +128,7 @@ class DiaryScreenContent extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(s.dayComment, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||||
s.dayComment,
|
|
||||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
CommentContainer(comment: state.consultantReview?.consultantComments.dayComment ?? ''),
|
CommentContainer(comment: state.consultantReview?.consultantComments.dayComment ?? ''),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -25,26 +25,30 @@ class _AddProductModalState extends State<AddProductModal> {
|
|||||||
final quantityController = TextEditingController();
|
final quantityController = TextEditingController();
|
||||||
GlossaryModel? selectedGlossary;
|
GlossaryModel? selectedGlossary;
|
||||||
List<GlossaryModel> glossariesList = [];
|
List<GlossaryModel> glossariesList = [];
|
||||||
|
late final String? _previousProductName;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.portion != null) {
|
if (widget.portion != null) {
|
||||||
|
_previousProductName = widget.portion!.productName;
|
||||||
productNameController.text = widget.portion!.productName;
|
productNameController.text = widget.portion!.productName;
|
||||||
quantityController.text = '${widget.portion!.quantity}';
|
quantityController.text = '${widget.portion!.quantity}';
|
||||||
selectedGlossary = GlossaryModel(
|
final productNutritionalValueId = widget.portion?.productNutritionalValueId;
|
||||||
id: widget.portion!.id,
|
if (productNutritionalValueId != null) {
|
||||||
name: widget.portion!.productName,
|
selectedGlossary = GlossaryModel(
|
||||||
grInPortion: widget.portion!.portion == 'gr' ? 0 : 1,
|
id: productNutritionalValueId,
|
||||||
energyOn100Gr: 0,
|
name: widget.portion!.productName,
|
||||||
proteinsOn100Gr: 0,
|
grInPortion: widget.portion!.portion == 'gr' ? 0 : 1,
|
||||||
fatsOn100Gr: 0,
|
energyOn100Gr: 0,
|
||||||
carbohydratesOn100Gr: 0,
|
proteinsOn100Gr: 0,
|
||||||
energyOnPortion: 0,
|
fatsOn100Gr: 0,
|
||||||
proteinsOnPortion: 0,
|
carbohydratesOn100Gr: 0,
|
||||||
fatsOnPortion: 0,
|
energyOnPortion: 0,
|
||||||
carbohydratesOnPortion: 0,
|
proteinsOnPortion: 0,
|
||||||
);
|
fatsOnPortion: 0,
|
||||||
|
carbohydratesOnPortion: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +195,7 @@ class _AddProductModalState extends State<AddProductModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
if (selectedGlossary == null) {
|
if (selectedGlossary == null && _previousProductName != productNameController.text) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
|
|||||||
@@ -43,46 +43,24 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
|||||||
locale: 'ru_RU',
|
locale: 'ru_RU',
|
||||||
startingDayOfWeek: StartingDayOfWeek.monday,
|
startingDayOfWeek: StartingDayOfWeek.monday,
|
||||||
calendarFormat: _calendarFormat,
|
calendarFormat: _calendarFormat,
|
||||||
headerStyle: const HeaderStyle(
|
headerStyle: const HeaderStyle(titleCentered: true, formatButtonVisible: false, leftChevronVisible: false, rightChevronVisible: false),
|
||||||
titleCentered: true,
|
|
||||||
formatButtonVisible: false,
|
|
||||||
leftChevronVisible: false,
|
|
||||||
rightChevronVisible: false,
|
|
||||||
),
|
|
||||||
onDaySelected: (selectedDay, focusedDay) async {
|
onDaySelected: (selectedDay, focusedDay) async {
|
||||||
if (!isSameDay(_selectedDay, selectedDay)) {
|
if (!isSameDay(_selectedDay, selectedDay)) {
|
||||||
final today = DateTime.now();
|
final previousSelectedDay = _selectedDay;
|
||||||
if (selectedDay.isAfter(today)) {
|
final previousFocusedDay = _selectedDay;
|
||||||
if ((state is DiaryStateLoaded)) {
|
|
||||||
final result = await context.read<DiaryCubit>().setSelectedDay(_selectedDay, forDietOnly: true);
|
|
||||||
if (result == false && context.mounted) {
|
|
||||||
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_selectedDay = selectedDay;
|
|
||||||
_focusedDay = focusedDay;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
|
||||||
final result = await context.read<DiaryCubit>().setSelectedDay(_selectedDay, forDietOnly: true);
|
|
||||||
if (result == false && context.mounted) {
|
|
||||||
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_selectedDay = selectedDay;
|
|
||||||
_focusedDay = focusedDay;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDay = selectedDay;
|
_selectedDay = selectedDay;
|
||||||
_focusedDay = focusedDay;
|
_focusedDay = focusedDay;
|
||||||
});
|
});
|
||||||
context.read<DiaryCubit>().setSelectedDay(_selectedDay);
|
final canOpenNextDay = await context.read<DiaryCubit>().setSelectedDay(selectedDay);
|
||||||
|
if (!canOpenNextDay && context.mounted) {
|
||||||
|
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
|
||||||
|
setState(() {
|
||||||
|
_selectedDay = previousSelectedDay;
|
||||||
|
_focusedDay = previousFocusedDay;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedDayPredicate: (day) {
|
selectedDayPredicate: (day) {
|
||||||
@@ -101,26 +79,12 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
calendarStyle: CalendarStyle(
|
calendarStyle: CalendarStyle(
|
||||||
weekendDecoration: const BoxDecoration(
|
weekendDecoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
|
||||||
color: Colors.white,
|
defaultDecoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
|
||||||
shape: BoxShape.circle,
|
selectedDecoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||||
),
|
todayTextStyle: const TextStyle(color: Colors.black),
|
||||||
defaultDecoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
selectedDecoration: BoxDecoration(
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
todayTextStyle: const TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
todayDecoration: BoxDecoration(
|
todayDecoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(color: appTheme.blueA400, width: 2),
|
||||||
color: appTheme.blueA400,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
@@ -152,10 +116,7 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.only(top: 8, bottom: 16),
|
padding: const EdgeInsets.only(top: 8, bottom: 16),
|
||||||
child: Icon(
|
child: Icon(_calendarFormat == CalendarFormat.month ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, color: Colors.grey.shade600),
|
||||||
_calendarFormat == CalendarFormat.month ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class FinishDayButtonWidget extends StatefulWidget {
|
class FinishDayButtonWidget extends StatefulWidget {
|
||||||
final DiaryDayModel diaryDay;
|
final DiaryDayEntity diaryDay;
|
||||||
|
|
||||||
const FinishDayButtonWidget({super.key, required this.diaryDay});
|
const FinishDayButtonWidget({super.key, required this.diaryDay});
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ class GlossaryModerationRequestModalWidget extends StatefulWidget {
|
|||||||
final DiaryCubit diaryCubit;
|
final DiaryCubit diaryCubit;
|
||||||
final String productName;
|
final String productName;
|
||||||
|
|
||||||
const GlossaryModerationRequestModalWidget({
|
const GlossaryModerationRequestModalWidget({super.key, required this.productName, required this.diaryCubit});
|
||||||
super.key,
|
|
||||||
required this.productName,
|
|
||||||
required this.diaryCubit,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GlossaryModerationRequestModalWidget> createState() => _GlossaryModerationRequestModalWidgetState();
|
State<GlossaryModerationRequestModalWidget> createState() => _GlossaryModerationRequestModalWidgetState();
|
||||||
@@ -55,21 +51,13 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
const Text(
|
const Text(
|
||||||
'Добавление нового продукта',
|
'Добавление нового продукта',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(color: Color(0xFF323F4B), fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'Оу... а у нас нет в базе калорийности такого продукта. Если вы знаете, давайте её запишем?',
|
'Оу... а у нас нет в базе калорийности такого продукта. Если вы знаете, давайте её запишем?',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w400),
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Row(
|
Row(
|
||||||
@@ -77,25 +65,27 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Navigator.pop(context);
|
glossary = (await widget.diaryCubit.sendProductToModeration(
|
||||||
|
name: productNameController.text,
|
||||||
|
energy: 0,
|
||||||
|
proteins: 0,
|
||||||
|
fats: 0,
|
||||||
|
carbohydrates: 0,
|
||||||
|
))?.glossary;
|
||||||
|
setState(() {
|
||||||
|
glossaryState = GlossaryState.success;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(20)),
|
||||||
color: const Color(0xFFF6F6F6),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'В другой раз',
|
'Отказаться',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Color(0xFF6D6F71), fontSize: 18, fontWeight: FontWeight.w500),
|
||||||
color: Color(0xFF6D6F71),
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -111,20 +101,13 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
color: appTheme.blueA400,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Записать',
|
'Записать',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500),
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -146,18 +129,10 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
const Text(
|
const Text(
|
||||||
'Продукт успешно отправлен\nна модерацию',
|
'Продукт успешно отправлен\nна модерацию',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(color: Color(0xFF323F4B), fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Lottie.asset(
|
Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
|
||||||
'assets/lottie_animations/success.json',
|
|
||||||
width: 160,
|
|
||||||
height: 160,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -165,20 +140,13 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
color: appTheme.blueA400,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Закрыть',
|
'Закрыть',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500),
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -198,27 +166,16 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
const Text(
|
const Text(
|
||||||
'Добавление нового продукта',
|
'Добавление нового продукта',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(color: Color(0xFF323F4B), fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'Если у вас данные по нему\nпросим заполнить значения на 100гр.',
|
'Если у вас данные по нему\nпросим заполнить значения на 100гр.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w400),
|
||||||
color: Color(0xFF323F4B),
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
CustomTextFormField(
|
CustomTextFormField(hintText: 'Сырный суп', controller: productNameController),
|
||||||
hintText: 'Сырный суп',
|
|
||||||
controller: productNameController,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -245,11 +202,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomTextFormField(
|
child: CustomTextFormField(hintText: 'Жиры', controller: fatsController, keyboardType: const TextInputType.numberWithOptions(decimal: true)),
|
||||||
hintText: 'Жиры',
|
|
||||||
controller: fatsController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -270,8 +223,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
proteins: double.parse(proteinsController.text.replaceAll(',', '.')),
|
proteins: double.parse(proteinsController.text.replaceAll(',', '.')),
|
||||||
fats: double.parse(fatsController.text.replaceAll(',', '.')),
|
fats: double.parse(fatsController.text.replaceAll(',', '.')),
|
||||||
carbohydrates: double.parse(carbohydratesController.text.replaceAll(',', '.')),
|
carbohydrates: double.parse(carbohydratesController.text.replaceAll(',', '.')),
|
||||||
))
|
))?.glossary;
|
||||||
?.glossary;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
glossaryState = GlossaryState.success;
|
glossaryState = GlossaryState.success;
|
||||||
});
|
});
|
||||||
@@ -279,20 +231,13 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
color: appTheme.blueA400,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Сохранить',
|
'Сохранить',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:eatvio_app/core/app_router.dart';
|
|||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
@@ -35,7 +35,7 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
|
|||||||
diaryCubit = context.read<DiaryCubit>();
|
diaryCubit = context.read<DiaryCubit>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void showImagePicker(DiaryDayModel diaryDay) {
|
void showImagePicker(DiaryDayEntity diaryDay) {
|
||||||
showCupertinoModalPopup<void>(
|
showCupertinoModalPopup<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
|
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/models/create_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
||||||
@@ -69,14 +70,6 @@ class DiaryRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DiaryFutureDietDayModel> getDiaryByDateForDietOnly(String date) async {
|
|
||||||
try {
|
|
||||||
return (await diaryService.getDiaryIndexByDateForDietOnly(date: date)).last;
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<DiaryDayModel>> getDiaryIndexByDate(String date) async {
|
Future<List<DiaryDayModel>> getDiaryIndexByDate(String date) async {
|
||||||
return (await diaryService.getDiaryIndexByDate(date: date));
|
return (await diaryService.getDiaryIndexByDate(date: date));
|
||||||
}
|
}
|
||||||
@@ -164,7 +157,7 @@ class DiaryRepository {
|
|||||||
required PortionState portionState,
|
required PortionState portionState,
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
|
Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
|
||||||
data['product_nutritional_value_id'] = productNutritionalValueId;
|
if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId;
|
||||||
data['state'] = portionState.name;
|
data['state'] = portionState.name;
|
||||||
return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
|
return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
|
||||||
}
|
}
|
||||||
@@ -300,4 +293,69 @@ class DiaryRepository {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<DiaryDayModel> getDiaryDayV2({required String day}) async {
|
||||||
|
try {
|
||||||
|
final diaryDay = await diaryService.getDiaryDayV2(day: day);
|
||||||
|
return setCPFCToDiary(diaryDay);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createDiaryDay({required String day}) async {
|
||||||
|
try {
|
||||||
|
await diaryService.createDay(body: CreateDayModel(day: day));
|
||||||
|
await getDiaryByDate(day);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DiaryDayModel> setCPFCToDiary(DiaryDayModel diaryDay) async {
|
||||||
|
Set<int> ids = {};
|
||||||
|
for (var meal in diaryDay.meals) {
|
||||||
|
for (var portion in meal.portions) {
|
||||||
|
if (portion.productNutritionalValueId != null) {
|
||||||
|
ids.add(portion.productNutritionalValueId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ids.isNotEmpty) {
|
||||||
|
final glossary = await getGlossariesIndex(ids: ids.toList());
|
||||||
|
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
|
||||||
|
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
|
||||||
|
final meal = updatedMeals[mealIndex];
|
||||||
|
final updatedPortions = meal.portions.map((portion) {
|
||||||
|
if (portion.productNutritionalValueId != null) {
|
||||||
|
try {
|
||||||
|
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
|
||||||
|
return portion.copyWith(
|
||||||
|
productCaloriesInfo: ProductCaloriesInfoModel(
|
||||||
|
id: glossaryItem.id,
|
||||||
|
productName: glossaryItem.name,
|
||||||
|
calories: glossaryItem.energyOn100Gr,
|
||||||
|
proteins: glossaryItem.proteinsOn100Gr,
|
||||||
|
fats: glossaryItem.fatsOn100Gr,
|
||||||
|
carbohydrates: glossaryItem.carbohydratesOn100Gr,
|
||||||
|
energyOnPortion: glossaryItem.energyOnPortion,
|
||||||
|
proteinsOnPortion: glossaryItem.proteinsOnPortion,
|
||||||
|
fatsOnPortion: glossaryItem.fatsOnPortion,
|
||||||
|
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return portion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return portion;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
|
||||||
|
}
|
||||||
|
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
|
||||||
|
return updatedDiaryDay;
|
||||||
|
}
|
||||||
|
return diaryDay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
|
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
||||||
|
import 'package:eatvio_app/features/diary/models/create_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart';
|
import 'package:eatvio_app/features/diary/models/finish_diary_day_model.dart';
|
||||||
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
||||||
@@ -17,8 +18,6 @@ abstract class DiaryService {
|
|||||||
@GET('/api/diary/index/{date}')
|
@GET('/api/diary/index/{date}')
|
||||||
Future<List<DiaryDayModel>> getDiaryIndexByDate({@Path('date') required String date});
|
Future<List<DiaryDayModel>> getDiaryIndexByDate({@Path('date') required String date});
|
||||||
|
|
||||||
@GET('/api/diary/index/{date}')
|
|
||||||
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({@Path('date') required String date});
|
|
||||||
|
|
||||||
@GET('/api/diary/{diary}/show')
|
@GET('/api/diary/{diary}/show')
|
||||||
Future<DiaryDayWrapperModel> getDiaryDay({@Path('diary') required int diary});
|
Future<DiaryDayWrapperModel> getDiaryDay({@Path('diary') required int diary});
|
||||||
@@ -103,4 +102,10 @@ abstract class DiaryService {
|
|||||||
|
|
||||||
@GET('/api/glossaries/index')
|
@GET('/api/glossaries/index')
|
||||||
Future<GlossaryPaginationModel> getGlossariesIndex({@Queries() Map<String, dynamic>? filters});
|
Future<GlossaryPaginationModel> getGlossariesIndex({@Queries() Map<String, dynamic>? filters});
|
||||||
|
|
||||||
|
@POST('/api/v2/diary/create/day')
|
||||||
|
Future<void> createDay({@Body() required CreateDayModel body});
|
||||||
|
|
||||||
|
@GET('/api/v2/diary/index/{day?}')
|
||||||
|
Future<DiaryDayModel> getDiaryDayV2({@Query('day') required String day});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/features/feed/models/diets_pagination_model.dart';
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.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/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/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class AuthorCard extends StatelessWidget {
|
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 Author author;
|
||||||
final String title;
|
final String title;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final TextStyle? titleStyle;
|
final TextStyle? titleStyle;
|
||||||
|
final ProfileModel? customerProfile;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed('profileScreen', extra: author);
|
GoRouter.of(context).pushNamed('profileScreen', extra: customerProfile != null ? customerProfile!.slug : author.authorSlug);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: MediaQuery.sizeOf(context).width,
|
width: MediaQuery.sizeOf(context).width,
|
||||||
@@ -39,24 +41,48 @@ class AuthorCard extends StatelessWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: NetworkImage(
|
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,
|
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)),
|
title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
if (customerProfile == null) ...[
|
||||||
const SizedBox(width: 10),
|
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||||
if (author.authorPlaceInRating != null)
|
const SizedBox(width: 10),
|
||||||
InfoBouble(
|
|
||||||
radius: 12,
|
if (author.authorPlaceInRating != null)
|
||||||
backgroundColor: appTheme.whiteA700,
|
InfoBouble(
|
||||||
children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
|
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> {
|
class _CalendarWidgetState extends State<CalendarWidget> {
|
||||||
CalendarFormat _calendarFormat = CalendarFormat.week;
|
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||||
DateTime _focusedDay = DateTime.now();
|
late DateTime _focusedDay;
|
||||||
DateTime? _selectedDay = DateTime.now();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -23,7 +30,7 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 5),
|
padding: const EdgeInsets.only(top: 5),
|
||||||
child: TableCalendar(
|
child: TableCalendar(
|
||||||
firstDay: DateTime.now(),
|
firstDay: widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0],
|
||||||
lastDay: DateTime.utc(2030, 3, 14),
|
lastDay: DateTime.utc(2030, 3, 14),
|
||||||
locale: 'ru_RU',
|
locale: 'ru_RU',
|
||||||
startingDayOfWeek: StartingDayOfWeek.monday,
|
startingDayOfWeek: StartingDayOfWeek.monday,
|
||||||
|
|||||||
@@ -67,11 +67,19 @@ abstract class PacketOptions with _$PacketOptions {
|
|||||||
@JsonKey(name: "price") required dynamic price,
|
@JsonKey(name: "price") required dynamic price,
|
||||||
@JsonKey(name: "short") required String short,
|
@JsonKey(name: "short") required String short,
|
||||||
@JsonKey(name: "description_md") required String description,
|
@JsonKey(name: "description_md") required String description,
|
||||||
|
@JsonKey(name: "has_personal_diet", fromJson: _parseHasPersonalDietField) required bool hasPersonalDiet,
|
||||||
}) = _PacketOptions;
|
}) = _PacketOptions;
|
||||||
|
|
||||||
factory PacketOptions.fromJson(Map<String, dynamic> json) => _$PacketOptionsFromJson(json);
|
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) {
|
List<String> _parseItems(dynamic json) {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:easy_debounce/easy_debounce.dart';
|
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/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/views/cubit/views_cubit.dart';
|
||||||
import 'package:eatvio_app/core/widgets/add_item_button.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/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_diet/presentation/create_diet_screen.dart';
|
||||||
import 'package:eatvio_app/features/create_plan/presentation/create_plan_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';
|
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
|
||||||
@@ -70,6 +73,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||||
consultantsScrollController.addListener(_onScrollConsultants);
|
consultantsScrollController.addListener(_onScrollConsultants);
|
||||||
|
_checkRegistrationDate(context);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +81,15 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
|||||||
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
blogPostsScrollController.dispose();
|
blogPostsScrollController.dispose();
|
||||||
@@ -215,7 +228,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
|||||||
3 when !hidePlans => AddItemButton(
|
3 when !hidePlans => AddItemButton(
|
||||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
|
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/custom_popup.dart';
|
||||||
import 'package:eatvio_app/core/widgets/feed_icon_button.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/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_cubit.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.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';
|
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:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class BlogPostWidget extends StatefulWidget {
|
class BlogPostWidget extends StatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
@@ -58,17 +58,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
|||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed('profileScreen', extra: state.feedBlogPostList[widget.index].author.authorSlug);
|
||||||
'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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
@@ -134,14 +124,39 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Html(
|
child: Html(
|
||||||
data:
|
data: md.markdownToHtml(
|
||||||
(state.feedBlogPostList[widget.index].description.length > 200
|
(state.feedBlogPostList[widget.index].description.length > 200
|
||||||
? state.feedBlogPostList[widget.index].description.substring(0, 200)
|
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
||||||
: state.feedBlogPostList[widget.index].description)
|
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
|
||||||
.replaceAll('\n', '<br>'),
|
),
|
||||||
style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
|
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)
|
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/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/complaining/presentation/complaining_bottom_sheet.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/comment_model.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/core/comments/comments_cubit/comments_cubit.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
@@ -96,12 +95,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'profileScreen',
|
'profileScreen',
|
||||||
extra: Author(
|
extra: comment.authorSlug,
|
||||||
authorSlug: comment.authorSlug,
|
|
||||||
authorName: comment.authorName,
|
|
||||||
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl<AppConfig>().baseUrl + comment.authorProfileIcon,
|
|
||||||
authorIsMe: isMe,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: CircleAvatar(
|
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/app_config.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.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/feed/models/feed_consultant_model.dart';
|
||||||
import 'package:eatvio_app/features/profile/models/badge_model.dart';
|
import 'package:eatvio_app/features/profile/models/badge_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -22,12 +19,7 @@ class ConsultantWidget extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'profileScreen',
|
'profileScreen',
|
||||||
extra: Author(
|
extra: consultant.slug,
|
||||||
authorSlug: consultant.slug,
|
|
||||||
authorName: consultant.name,
|
|
||||||
authorProfileIcon: consultant.photo,
|
|
||||||
authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == consultant.slug ? true : false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|||||||
@@ -17,15 +17,12 @@ import 'package:flutter_html/flutter_html.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.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 Diet diet;
|
||||||
final int index;
|
final int index;
|
||||||
const DietsWidget({
|
const DietsWidget({super.key, required this.diet, required this.index});
|
||||||
super.key,
|
|
||||||
required this.diet,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DietsWidget> createState() => _DietsWidgetState();
|
State<DietsWidget> createState() => _DietsWidgetState();
|
||||||
@@ -33,18 +30,11 @@ class DietsWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _DietsWidgetState extends State<DietsWidget> {
|
class _DietsWidgetState extends State<DietsWidget> {
|
||||||
bool isMoreOptionsVisible = false;
|
bool isMoreOptionsVisible = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => GoRouter.of(context).pushNamed(
|
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': false}),
|
||||||
'aboutDietScreen',
|
|
||||||
extra: {
|
|
||||||
'slug': widget.diet.slug,
|
|
||||||
'index': widget.index,
|
|
||||||
'navigateToComments': false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -104,12 +94,7 @@ class _DietsWidgetState extends State<DietsWidget> {
|
|||||||
bottom: 16,
|
bottom: 16,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.sizeOf(context).width,
|
width: MediaQuery.sizeOf(context).width,
|
||||||
child: InfoRow(
|
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet),
|
||||||
diet: widget.diet,
|
|
||||||
itemsBackgroundColor: appTheme.whiteA700,
|
|
||||||
disableDescriptionOfTypes: true,
|
|
||||||
type: ProductType.diet,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -120,26 +105,48 @@ class _DietsWidgetState extends State<DietsWidget> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||||
widget.diet.name,
|
|
||||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (widget.diet.details.isNotEmpty) ...[
|
if (widget.diet.details.isNotEmpty) ...[
|
||||||
const SizedBox(
|
const SizedBox(height: 12),
|
||||||
height: 12,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Html(
|
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: {
|
||||||
"*": Style(
|
"body": Style(
|
||||||
margin: Margins.zero,
|
margin: Margins.zero,
|
||||||
color: appTheme.black,
|
padding: HtmlPaddings.zero,
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
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),
|
padding: EdgeInsets.only(left: 16, top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Показать ещё',
|
'Показать ещё',
|
||||||
style: TextStyle(
|
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
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);
|
await context.read<DietsCubit>().likeDiet(widget.diet.id, widget.index);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(width: 12),
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
icon: Icons.chat_bubble_outline,
|
icon: Icons.chat_bubble_outline,
|
||||||
text: widget.diet.totalComments.toString(),
|
text: widget.diet.totalComments.toString(),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': true}),
|
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': true}),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(width: 12),
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
|
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
|
||||||
onTap: () async {
|
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/info_row.dart';
|
||||||
import 'package:eatvio_app/core/widgets/feed_icon_button.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/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/features/feed/models/plan_pagination_model.dart';
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
|
||||||
@@ -20,16 +19,13 @@ import 'package:flutter_html/flutter_html.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class PlanWidget extends StatefulWidget {
|
class PlanWidget extends StatefulWidget {
|
||||||
final PlanModel plan;
|
final PlanModel plan;
|
||||||
final int index;
|
final int index;
|
||||||
|
|
||||||
const PlanWidget({
|
const PlanWidget({super.key, required this.plan, required this.index});
|
||||||
super.key,
|
|
||||||
required this.plan,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlanWidget> createState() => _PlanWidgetState();
|
State<PlanWidget> createState() => _PlanWidgetState();
|
||||||
@@ -42,14 +38,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => GoRouter.of(context).pushNamed(
|
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': false}),
|
||||||
'aboutPlanScreen',
|
|
||||||
extra: {
|
|
||||||
'slug': widget.plan.slug,
|
|
||||||
'index': widget.index,
|
|
||||||
'navigateToComments': false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -110,12 +99,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
bottom: 16,
|
bottom: 16,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.sizeOf(context).width,
|
width: MediaQuery.sizeOf(context).width,
|
||||||
child: InfoRow(
|
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
|
||||||
plan: widget.plan,
|
|
||||||
itemsBackgroundColor: appTheme.whiteA700,
|
|
||||||
disableDescriptionOfTypes: true,
|
|
||||||
type: ProductType.plan,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -126,26 +110,43 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18)),
|
||||||
widget.plan.name,
|
|
||||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (widget.plan.details.isNotEmpty) ...[
|
if (widget.plan.details.isNotEmpty) ...[
|
||||||
const SizedBox(
|
const SizedBox(height: 12),
|
||||||
height: 12,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Html(
|
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: {
|
||||||
"*": Style(
|
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
||||||
margin: Margins.zero,
|
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
||||||
color: appTheme.black,
|
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
||||||
fontSize: FontSize(16),
|
"h1": Style(fontSize: FontSize(24)),
|
||||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
"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),
|
padding: EdgeInsets.only(left: 16, top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Показать ещё',
|
'Показать ещё',
|
||||||
style: TextStyle(
|
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
height: 1,
|
|
||||||
color: Color(0xFF8B8B8B),
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -168,15 +164,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed('profileScreen', extra: widget.plan.author.authorSlug);
|
||||||
'profileScreen',
|
|
||||||
extra: Author(
|
|
||||||
authorSlug: widget.plan.author.authorSlug,
|
|
||||||
authorName: widget.plan.author.authorName,
|
|
||||||
authorProfileIcon: widget.plan.author.authorProfileIcon,
|
|
||||||
authorIsMe: widget.plan.author.authorIsMe,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
@@ -187,10 +175,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
height: 46,
|
height: 46,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
image: DecorationImage(
|
image: DecorationImage(image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover),
|
||||||
image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
@@ -200,12 +185,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
backgroundColor: appTheme.gray50,
|
backgroundColor: appTheme.gray50,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('${widget.plan.author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textXSSemibold.copyWith(color: appTheme.blueA400)),
|
||||||
'${widget.plan.author.authorPlaceInRating}-й в рейтинге',
|
|
||||||
style: TextStyles.textXSSemibold.copyWith(
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -218,21 +198,14 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
),
|
),
|
||||||
title: Padding(
|
title: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
child: Text(
|
child: Text(widget.plan.author.authorName, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
|
||||||
widget.plan.author.authorName,
|
|
||||||
style: TextStyles.textSSemibold.copyWith(
|
|
||||||
color: appTheme.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(left: 16),
|
||||||
left: 16,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
@@ -244,20 +217,17 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
await context.read<PlansCubit>().likePlan(widget.plan.slug, widget.index);
|
await context.read<PlansCubit>().likePlan(widget.plan.slug, widget.index);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(width: 12),
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
icon: Icons.chat_bubble_outline,
|
icon: Icons.chat_bubble_outline,
|
||||||
text: widget.plan.totalComments.toString(),
|
text: widget.plan.totalComments.toString(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
return GoRouter.of(context)
|
return GoRouter.of(
|
||||||
.pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
|
context,
|
||||||
|
).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(width: 12),
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
|
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|||||||
@@ -20,16 +20,13 @@ import 'package:flutter_html/flutter_html.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class RecipesWidget extends StatefulWidget {
|
class RecipesWidget extends StatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
final Recipe recipe;
|
final Recipe recipe;
|
||||||
|
|
||||||
const RecipesWidget({
|
const RecipesWidget({super.key, required this.recipe, required this.index});
|
||||||
super.key,
|
|
||||||
required this.recipe,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RecipesWidget> createState() => _RecipesWidgetState();
|
State<RecipesWidget> createState() => _RecipesWidgetState();
|
||||||
@@ -56,14 +53,8 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
}
|
}
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
onTap: () => GoRouter.of(context).pushNamed(
|
onTap: () =>
|
||||||
'aboutRecipesScreen',
|
GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': false}),
|
||||||
extra: {
|
|
||||||
'recipe': widget.recipe.slug,
|
|
||||||
'index': widget.index,
|
|
||||||
'navigateToComments': false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -139,10 +130,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(widget.recipe.title ?? '', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||||
widget.recipe.title ?? '',
|
|
||||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (widget.recipe.videoUrl != null && widget.recipe.videoUrl!.isNotEmpty) ...[
|
if (widget.recipe.videoUrl != null && widget.recipe.videoUrl!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -152,10 +140,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
children: [
|
children: [
|
||||||
SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16),
|
SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(s.there_is_a_video, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||||
s.there_is_a_video,
|
|
||||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -165,17 +150,41 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Html(
|
child: Html(
|
||||||
data: widget.recipe.description.length > 200
|
data: md.markdownToHtml(
|
||||||
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
|
widget.recipe.description.length > 200
|
||||||
: widget.recipe.description.replaceAll('\n', '<br>'),
|
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
|
||||||
|
: widget.recipe.description.replaceAll('\n', '<br>'),
|
||||||
|
),
|
||||||
style: {
|
style: {
|
||||||
"*": Style(
|
"body": Style(
|
||||||
color: appTheme.black,
|
margin: Margins.zero,
|
||||||
margin: Margins.all(0),
|
padding: HtmlPaddings.zero,
|
||||||
fontFamily: TextStyles.textSMedium.fontFamily,
|
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
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),
|
padding: EdgeInsets.only(left: 16, top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Показать ещё',
|
'Показать ещё',
|
||||||
style: TextStyle(
|
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
height: 1,
|
|
||||||
color: Color(0xFF8B8B8B),
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -211,14 +215,9 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
icon: Icons.chat_bubble_outline,
|
icon: Icons.chat_bubble_outline,
|
||||||
text: widget.recipe.commentsCount.toString(),
|
text: widget.recipe.commentsCount.toString(),
|
||||||
onTap: () => GoRouter.of(context).pushNamed(
|
onTap: () => GoRouter.of(
|
||||||
'aboutRecipesScreen',
|
context,
|
||||||
extra: {
|
).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': true}),
|
||||||
'recipe': widget.recipe.slug,
|
|
||||||
'index': widget.index,
|
|
||||||
'navigateToComments': true,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FeedIconButton(
|
FeedIconButton(
|
||||||
@@ -239,19 +238,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Icon(
|
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
|
||||||
Icons.visibility_rounded,
|
|
||||||
size: 16,
|
|
||||||
color: appTheme.gray500,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
widget.recipe.viewsCount.toString(),
|
widget.recipe.viewsCount.toString(),
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
|
||||||
fontSize: 12,
|
|
||||||
color: appTheme.gray500,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -268,26 +259,17 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
Container _iconButton(String iconPath, [String? text]) {
|
Container _iconButton(String iconPath, [String? text]) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
|
||||||
color: appTheme.whiteA700,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
height: 32,
|
height: 32,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset(iconPath),
|
||||||
iconPath,
|
|
||||||
),
|
|
||||||
if (text != null) const SizedBox(width: 8),
|
if (text != null) const SizedBox(width: 8),
|
||||||
if (text != null)
|
if (text != null)
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
|
||||||
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/app_config.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.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/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -21,13 +20,7 @@ class _BlockedUserWidgetState extends State<BlockedUserWidget> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => GoRouter.of(context).pushNamed(
|
onTap: () => GoRouter.of(context).pushNamed(
|
||||||
'profileScreen',
|
'profileScreen',
|
||||||
extra: Author(
|
extra: widget.profile.slug,
|
||||||
authorSlug: widget.profile.slug,
|
|
||||||
authorName: widget.profile.name,
|
|
||||||
authorProfileIcon: widget.profile.profilePhotoUrl,
|
|
||||||
authorIsMe: false,
|
|
||||||
isEntitySubscribedByMe: false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
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/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_deleted_bottom_sheet.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_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:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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:flutter_svg/svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class MyDietWidget extends StatefulWidget {
|
class MyDietWidget extends StatefulWidget {
|
||||||
final Diet diet;
|
final Diet diet;
|
||||||
|
|
||||||
const MyDietWidget({
|
const MyDietWidget({super.key, required this.diet});
|
||||||
super.key,
|
|
||||||
required this.diet,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MyDietWidget> createState() => _MyDietWidgetState();
|
State<MyDietWidget> createState() => _MyDietWidgetState();
|
||||||
@@ -42,10 +41,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -78,26 +74,48 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||||
widget.diet.name,
|
|
||||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (widget.diet.details.isNotEmpty) ...[
|
if (widget.diet.details.isNotEmpty) ...[
|
||||||
const SizedBox(
|
const SizedBox(height: 12),
|
||||||
height: 12,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Html(
|
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: {
|
||||||
"*": Style(
|
"body": Style(
|
||||||
margin: Margins.zero,
|
margin: Margins.zero,
|
||||||
color: appTheme.black,
|
padding: HtmlPaddings.zero,
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
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('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||||
}
|
}
|
||||||
} else if (state.selectedStateIndex == 2) {
|
} else if (state.selectedStateIndex == 2) {
|
||||||
|
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
|
||||||
} else {
|
} else {
|
||||||
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
|
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
|
||||||
}
|
}
|
||||||
@@ -230,11 +249,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
|||||||
},
|
},
|
||||||
height: 52,
|
height: 52,
|
||||||
width: 52,
|
width: 52,
|
||||||
leftIcon: SvgPicture.asset(
|
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
|
||||||
'assets/icons/trash_can.svg',
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
||||||
buttonStyle: ButtonStyle(
|
buttonStyle: ButtonStyle(
|
||||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
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
|
// Removed SVG icon usage after switching to text buttons
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class MyPlanWidget extends StatefulWidget {
|
class MyPlanWidget extends StatefulWidget {
|
||||||
final PlanModel plan;
|
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}),
|
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
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(
|
Positioned(
|
||||||
@@ -68,16 +73,41 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Html(
|
child: Html(
|
||||||
data: widget.plan.details.length > 200
|
data: md.markdownToHtml(
|
||||||
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
widget.plan.details.length > 200
|
||||||
: widget.plan.details.replaceAll('\n', '<br>'),
|
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
|
||||||
|
: widget.plan.details.replaceAll('\n', '<br>'),
|
||||||
|
),
|
||||||
style: {
|
style: {
|
||||||
"*": Style(
|
"body": Style(
|
||||||
margin: Margins.zero,
|
margin: Margins.zero,
|
||||||
color: appTheme.black,
|
padding: HtmlPaddings.zero,
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
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 'dart:developer';
|
||||||
|
|
||||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
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/order_info/cubit/order_info_state.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class OrderInfoCubit extends Cubit<OrderInfoState> {
|
class OrderInfoCubit extends Cubit<OrderInfoState> {
|
||||||
OrderInfoCubit({required this.chatRepository}) : super(OrderInfoState());
|
OrderInfoCubit({required this.chatRepository, required this.profileRepository}) : super(OrderInfoState());
|
||||||
final ChatRepository chatRepository;
|
final ChatRepository chatRepository;
|
||||||
|
final ProfileRepository profileRepository;
|
||||||
|
|
||||||
Future<String?> getPaymentLink() async {
|
Future<String?> getPaymentLink() async {
|
||||||
try {
|
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 {
|
try {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
final chosenPlan = await chatRepository.getChosenPlan(id: id);
|
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));
|
emit(state.copyWith(isLoading: false, chosenPlan: chosenPlan));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
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';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'order_info_state.freezed.dart';
|
part 'order_info_state.freezed.dart';
|
||||||
@@ -9,6 +10,7 @@ abstract class OrderInfoState with _$OrderInfoState {
|
|||||||
factory OrderInfoState({
|
factory OrderInfoState({
|
||||||
@Default(true) bool isLoading,
|
@Default(true) bool isLoading,
|
||||||
@Default(null) ChosenPlanModel? chosenPlan,
|
@Default(null) ChosenPlanModel? chosenPlan,
|
||||||
|
@Default(null) ProfileModel? customerProfile,
|
||||||
}) = _OrderInfoState;
|
}) = _OrderInfoState;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,31 +24,24 @@ import 'package:go_router/go_router.dart';
|
|||||||
class InfoByOrderScreen extends StatelessWidget {
|
class InfoByOrderScreen extends StatelessWidget {
|
||||||
static const name = 'infoByOrderScreen';
|
static const name = 'infoByOrderScreen';
|
||||||
static const path = '/info-by-order-screen';
|
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 bool? successfulPayment;
|
||||||
final int planId;
|
final int planId;
|
||||||
|
final String? customerSlug;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => sl<OrderInfoCubit>()..getChosenPlan(planId),
|
create: (context) => sl<OrderInfoCubit>()..getChosenPlan(id: planId, customerSlug: customerSlug),
|
||||||
child: InfoByOrderScreenContent(
|
child: InfoByOrderScreenContent(successfulPayment: successfulPayment, planId: planId, customerSlug: customerSlug),
|
||||||
successfulPayment: successfulPayment,
|
|
||||||
planId: planId,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfoByOrderScreenContent extends StatefulWidget {
|
class InfoByOrderScreenContent extends StatefulWidget {
|
||||||
static const name = 'infoByOrderScreen';
|
const InfoByOrderScreenContent({super.key, required this.planId, required this.successfulPayment, this.customerSlug});
|
||||||
static const path = '/info-by-order-screen';
|
|
||||||
const InfoByOrderScreenContent({
|
|
||||||
super.key,
|
|
||||||
required this.planId,
|
|
||||||
required this.successfulPayment,
|
|
||||||
});
|
|
||||||
final int planId;
|
final int planId;
|
||||||
final bool? successfulPayment;
|
final bool? successfulPayment;
|
||||||
|
final String? customerSlug;
|
||||||
@override
|
@override
|
||||||
State<InfoByOrderScreenContent> createState() => _InfoByOrderScreenContentState();
|
State<InfoByOrderScreenContent> createState() => _InfoByOrderScreenContentState();
|
||||||
}
|
}
|
||||||
@@ -64,39 +57,30 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
|||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
s.infoAboutOrder,
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pop();
|
GoRouter.of(context).pop();
|
||||||
}),
|
},
|
||||||
),
|
),
|
||||||
body: const Center(
|
|
||||||
child: Loader(),
|
|
||||||
),
|
),
|
||||||
|
body: const Center(child: Loader()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (plan == null) {
|
if (plan == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
s.infoAboutOrder,
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pop();
|
GoRouter.of(context).pop();
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text(
|
child: Text(s.sorry_we_did_not_find_the_plan, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
|
||||||
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(
|
return Scaffold(
|
||||||
backgroundColor: appTheme.gray100,
|
backgroundColor: appTheme.gray100,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
s.infoAboutOrder,
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pop();
|
GoRouter.of(context).pop();
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
|
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[CollectChargeBanner(chosenPlan: plan, price: chosenOption?.price ?? 0)],
|
||||||
CollectChargeBanner(
|
if (widget.successfulPayment != null) ...[const SizedBox(height: 8), PaymentBanner(successfulPayment: widget.successfulPayment ?? false)],
|
||||||
chosenPlan: plan,
|
|
||||||
price: chosenOption?.price ?? 0,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
if (widget.successfulPayment != null) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
PaymentBanner(
|
|
||||||
successfulPayment: widget.successfulPayment ?? false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
AuthorCard(
|
AuthorCard(
|
||||||
author: plan.plan.author,
|
author: plan.plan.author,
|
||||||
title: s.plan_seller,
|
title: state.customerProfile != null ? s.customer : s.plan_seller,
|
||||||
titleStyle: TextStyles.textSSemibold.copyWith(color: appTheme.gray),
|
titleStyle: TextStyles.textSSemibold.copyWith(color: appTheme.gray),
|
||||||
backgroundColor: appTheme.whiteA700,
|
backgroundColor: appTheme.whiteA700,
|
||||||
|
customerProfile: state.customerProfile,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
PlanProps(
|
PlanProps(planDuration: plan.plan.duration, planPrice: chosenOption?.price.toDouble() ?? 0, planStatus: planStatus),
|
||||||
planDuration: plan.plan.duration,
|
|
||||||
planPrice: chosenOption?.price.toDouble() ?? 0,
|
|
||||||
planStatus: planStatus),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
PlanInfo(
|
PlanInfo(title: plan.plan.name, description: plan.plan.details, imagePath: plan.plan.image),
|
||||||
title: plan.plan.name,
|
|
||||||
description: plan.plan.details,
|
|
||||||
imagePath: plan.plan.image,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
OptionWidget(
|
OptionWidget(option: chosenOption, disableDuration: true, duration: plan.plan.duration),
|
||||||
option: chosenOption,
|
|
||||||
disableDuration: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
PlanState(
|
PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,),
|
||||||
state: plan.chosenPlanState,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (widget.successfulPayment == null)
|
if (widget.successfulPayment == null)
|
||||||
CustomOutlinedButton(
|
CustomOutlinedButton(
|
||||||
@@ -184,13 +145,10 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => CancelPlanBottomSheet(
|
builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isStateAfterPaidState(), planId: plan.id),
|
||||||
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:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class PlanState extends StatelessWidget {
|
class PlanState extends StatefulWidget {
|
||||||
PlanState({super.key, required this.state});
|
const PlanState({super.key, required this.state, required this.hasPersonalDiet});
|
||||||
final ChosenPlanStateEnum state;
|
final ChosenPlanStateEnum state;
|
||||||
final List<List<ChosenPlanStateEnum>> states = [
|
final bool hasPersonalDiet;
|
||||||
[
|
|
||||||
ChosenPlanStateEnum.stateInited,
|
@override
|
||||||
ChosenPlanStateEnum.stateInCoordination,
|
State<PlanState> createState() => _PlanStateState();
|
||||||
],
|
}
|
||||||
[
|
|
||||||
ChosenPlanStateEnum.stateStartDateHasBeenSet,
|
class _PlanStateState extends State<PlanState> {
|
||||||
ChosenPlanStateEnum.statePendingPayment,
|
late final List<List<ChosenPlanStateEnum>> states;
|
||||||
],
|
|
||||||
[
|
@override
|
||||||
ChosenPlanStateEnum.statePaid,
|
void initState() {
|
||||||
],
|
super.initState();
|
||||||
[
|
if (widget.hasPersonalDiet) {
|
||||||
ChosenPlanStateEnum.stateStarted,
|
states = [
|
||||||
ChosenPlanStateEnum.stateInProgress,
|
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
|
||||||
ChosenPlanStateEnum.stateLastDayStarted,
|
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
|
||||||
ChosenPlanStateEnum.stateLastDayFinished,
|
[ChosenPlanStateEnum.statePaid],
|
||||||
],
|
[ChosenPlanStateEnum.personalDietSet],
|
||||||
[
|
[
|
||||||
ChosenPlanStateEnum.stateComplained,
|
ChosenPlanStateEnum.stateStarted,
|
||||||
ChosenPlanStateEnum.stateDone,
|
ChosenPlanStateEnum.stateInProgress,
|
||||||
ChosenPlanStateEnum.stateChargeWasCollected,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(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(
|
return MainBackgroundBouble(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: List.generate(states.length, (index) {
|
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];
|
final text = titles[index];
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -51,29 +74,16 @@ class PlanState extends StatelessWidget {
|
|||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||||
child: Center(
|
child: Center(child: Icon(Icons.check, color: appTheme.whiteA700, size: 14)),
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: appTheme.whiteA700,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(text, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||||
text,
|
|
||||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 12),
|
padding: const EdgeInsets.only(left: 12),
|
||||||
child: Container(
|
child: Container(height: 12, width: 1, color: color),
|
||||||
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,
|
stateComplained,
|
||||||
@JsonValue('done')
|
@JsonValue('done')
|
||||||
stateDone,
|
stateDone,
|
||||||
|
@JsonValue('personal_diet_setted')
|
||||||
|
personalDietSet,
|
||||||
@JsonValue('charge_was_collected')
|
@JsonValue('charge_was_collected')
|
||||||
stateChargeWasCollected;
|
stateChargeWasCollected;
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ enum ChosenPlanStateEnum {
|
|||||||
ChosenPlanStateEnum.stateLastDayFinished,
|
ChosenPlanStateEnum.stateLastDayFinished,
|
||||||
ChosenPlanStateEnum.stateDone,
|
ChosenPlanStateEnum.stateDone,
|
||||||
ChosenPlanStateEnum.stateInProgress,
|
ChosenPlanStateEnum.stateInProgress,
|
||||||
|
ChosenPlanStateEnum.personalDietSet,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +67,8 @@ enum ChosenPlanStateEnum {
|
|||||||
ChosenPlanStateEnum.stateLastDayStarted,
|
ChosenPlanStateEnum.stateLastDayStarted,
|
||||||
ChosenPlanStateEnum.stateLastDayFinished,
|
ChosenPlanStateEnum.stateLastDayFinished,
|
||||||
ChosenPlanStateEnum.stateComplained,
|
ChosenPlanStateEnum.stateComplained,
|
||||||
ChosenPlanStateEnum.stateDone
|
ChosenPlanStateEnum.stateDone,
|
||||||
|
ChosenPlanStateEnum.personalDietSet
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -505,12 +505,15 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
|||||||
switch (state.chosenOptionIndex) {
|
switch (state.chosenOptionIndex) {
|
||||||
1 => OptionWidget(
|
1 => OptionWidget(
|
||||||
option: state.planModel?.packet2Options,
|
option: state.planModel?.packet2Options,
|
||||||
|
duration: state.planModel?.duration ?? 0,
|
||||||
),
|
),
|
||||||
2 => OptionWidget(
|
2 => OptionWidget(
|
||||||
option: state.planModel?.packet3Options,
|
option: state.planModel?.packet3Options,
|
||||||
|
duration: state.planModel?.duration ?? 0,
|
||||||
),
|
),
|
||||||
_ => OptionWidget(
|
_ => OptionWidget(
|
||||||
option: state.planModel?.packet1Options,
|
option: state.planModel?.packet1Options,
|
||||||
|
duration: state.planModel?.duration ?? 0,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user