diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..bad9d0d2 Binary files /dev/null and b/.DS_Store differ diff --git a/.env b/.env new file mode 100644 index 00000000..e223a359 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +SERVER_ROOT='https://mobile-stage.eatvio.ru/api' +STORAGE_URL='https://mobile-stage.eatvio.ru/storage/' \ No newline at end of file diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 00000000..e8f9d11f --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,74 @@ +name: Android Release + +on: [push, pull_request] + +env: + flutter_version: "3.19.0" + java_version: "12.x" + flutter_channel: "stable" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v2 + with: + path: ${{ runner.tool_cache }}/flutter + key: flutter-${{ env.flutter_version }}-${{ env.flutter_channel }} + + - name: Install jq + run: | + apt update + apt-get install -y jq + + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "12.x" + cache: "gradle" + + - id: flutter-action + uses: actions/flutter-action@v2 + with: + channel: ${{ env.flutter_channel }} + flutter-version: ${{ env.flutter_version }} + cache: true + + - name: show cache keys + run: | + echo CACHE-PATH=${{ steps.flutter-action.outputs.CACHE-PATH }} + echo CACHE-KEY=${{ steps.flutter-action.outputs.CACHE-KEY }} + echo CHANNEL=${{ steps.flutter-action.outputs.CHANNEL }} + echo VERSION=${{ steps.flutter-action.outputs.VERSION }} + echo ARCHITECTURE=${{ steps.flutter-action.outputs.ARCHITECTURE }} + echo PUB-CACHE-PATH=${{ steps.flutter-action.outputs.PUB-CACHE-PATH }} + echo PUB-CACHE-KEY=${{ steps.flutter-action.outputs.PUB-CACHE-KEY }} + shell: bash + + - name: fix dir permissions + run: | + git config --global --add safe.directory /opt/hostedtoolcache/flutter/${{ env.flutter_channel }}-${{ env.flutter_version }}-${{ steps.flutter-action.outputs.ARCHITECTURE }} + flutter --version + + - name: Get dependencies + run: flutter pub get + + - name: Native Analyze + run: flutter analyze + + - name: Run Code Metrics + run: flutter pub run dart_code_metrics:metrics lib --disable-sunset-warning --fatal-warnings --fatal-performance + + - name: Test + run: | + touch .env + flutter test + +# - name: Build apk +# run: flutter build apk + +# Runs a set of commands using the runners shell +# - name: Start release build +# run: flutter build appbundle diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 00000000..b5f75243 --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,67 @@ +name: Eatvio App Android Test + +on: [pull_request] + +env: + flutter_version: "3.19.0" + java_version: "12.x" + flutter_channel: "stable" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v2 + with: + path: ${{ runner.tool_cache }}/flutter + key: flutter-${{ env.flutter_version }}-${{ env.flutter_channel }} + + - name: Install jq + run: | + apt update + apt-get install -y jq + + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "12.x" + cache: "gradle" + + - id: flutter-action + uses: actions/flutter-action@v2 + with: + channel: ${{ env.flutter_channel }} + flutter-version: ${{ env.flutter_version }} + cache: true + + - name: show cache keys + run: | + echo CACHE-PATH=${{ steps.flutter-action.outputs.CACHE-PATH }} + echo CACHE-KEY=${{ steps.flutter-action.outputs.CACHE-KEY }} + echo CHANNEL=${{ steps.flutter-action.outputs.CHANNEL }} + echo VERSION=${{ steps.flutter-action.outputs.VERSION }} + echo ARCHITECTURE=${{ steps.flutter-action.outputs.ARCHITECTURE }} + echo PUB-CACHE-PATH=${{ steps.flutter-action.outputs.PUB-CACHE-PATH }} + echo PUB-CACHE-KEY=${{ steps.flutter-action.outputs.PUB-CACHE-KEY }} + shell: bash + + - name: fix dir permissions + run: | + git config --global --add safe.directory /opt/hostedtoolcache/flutter/${{ env.flutter_channel }}-${{ env.flutter_version }}-${{ steps.flutter-action.outputs.ARCHITECTURE }} + flutter --version + + - name: Get dependencies + run: flutter pub get + + - name: Native Analyze + run: flutter analyze + + - name: Run Code Metrics + run: flutter pub run dart_code_metrics:metrics lib --disable-sunset-warning --fatal-warnings --fatal-performance + + - name: Test + run: | + touch .env + flutter test diff --git a/.metadata b/.metadata new file mode 100644 index 00000000..aa90aa80 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: android + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: ios + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: linux + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: macos + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: web + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: windows + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md index f6cc1597..39b2d892 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # eatvio_app ### Системные требования -- Flutter (Channel stable, 3.16.4, on Ubuntu 22.04.3 LTS 6.2.0-39-generic, locale en_US.UTF-8) +- Flutter (Channel stable, 3.19.0, on Ubuntu 22.04.3 LTS 6.2.0-39-generic, locale en_US.UTF-8) - Android Studio (version 2023.1) - VS Code (version 1.85.0) - Android SDK (version 34.0.0) - устанавливается из Android Studio (Tools->SDK Manager->Android Command Line Tools) @@ -32,26 +32,27 @@ Для тестирования на устройствах Android в корневой папке проекта выполните команду -flutter build apk +`# flutter build apk` -после чего .apk файл скомпилируется в директории build/app/outputs/flutter-apk/app-release.apk +после чего `.apk` файл скомпилируется в директории `build/app/outputs/flutter-apk/app-release.apk` Данный файл установит приложение на устройство с актуальной версией OS ### Библиотеки и инструменты - flutter_bloc - контроль состояний - cached_network_image - кеширование картинок из веба - flutter_svg - работа с изображениями - equatable - сравнение без оператора "==" - validators - валидация - hive - локальная бд (1) - hive_flutter - локальная бд (2) - isar_flutter_libs - локальная бд (3) - path_provider - динамичкские пути до файлов - fl_chart - построение графиков - flutter_spinkit - прелоадеры - flutter_dotenv - переменные окружения + - **flutter_bloc** - контроль состояний + - **cached_network_image** - кеширование картинок из веба + - **flutter_svg** - работа с изображениями + - **equatable** - сравнение без оператора "==" + - **validators** - валидация + - **hive** - локальная бд (1) + - **hive_flutter** - локальная бд (2) + - **isar_flutter_libs** - локальная бд (3) + - **path_provider** - динамичкские пути до файлов + - **fl_chart** - построение графиков + - **flutter_spinkit** - прелоадеры + - **flutter_dotenv** - переменные окружения + ### Поддержка diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..9722fb45 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,34 @@ +analyzer: + plugins: + - dart_code_metrics + +dart_code_metrics: + extends: + - package:dart_code_metrics_presets/flutter_all.yaml + - package:dart_code_metrics_presets/bloc.yaml + metrics: + cyclomatic-complexity: 20 + lines-of-executable-code: 50 + number-of-parameters: 4 + maximum-nesting-level: 5 + metrics-exclude: + - test/** +# extends: +# - recommended +# anti-patterns: +# - long-method +# - long-parameter-list +# metrics: +# cyclomatic-complexity: 20 +# lines-of-executable-code: 50 +# number-of-parameters: 4 +# maximum-nesting-level: 5 +# metrics-exclude: +# - test/** +# rules: +# - newline-before-return +# - no-boolean-literal-compare +# - no-empty-block +# - prefer-trailing-comma +# - prefer-conditional-expressions +# - no-equal-then-else diff --git a/android/app/src/main/kotlin/com/eatvioapp/eatvio_app/MainActivity.kt b/android/app/src/main/kotlin/com/eatvioapp/eatvio_app/MainActivity.kt new file mode 100644 index 00000000..f3e4d58d --- /dev/null +++ b/android/app/src/main/kotlin/com/eatvioapp/eatvio_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.eatvioapp.eatvio_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/eatvio_app_android.iml b/android/eatvio_app_android.iml new file mode 100644 index 00000000..18999696 --- /dev/null +++ b/android/eatvio_app_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 00000000..2f718152 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/images/img_attach.svg b/assets/images/img_attach.svg new file mode 100644 index 00000000..9f75ca7c --- /dev/null +++ b/assets/images/img_attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_glass.svg b/assets/images/img_glass.svg new file mode 100644 index 00000000..063af954 --- /dev/null +++ b/assets/images/img_glass.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/img_logo_eatvio.svg b/assets/images/img_logo_eatvio.svg index bc575bc9..6fcfd149 100644 --- a/assets/images/img_logo_eatvio.svg +++ b/assets/images/img_logo_eatvio.svg @@ -1,12 +1,56 @@ - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/assets/images/img_mic.svg b/assets/images/img_mic.svg new file mode 100644 index 00000000..a7c11027 --- /dev/null +++ b/assets/images/img_mic.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_nav_bell.svg b/assets/images/img_nav_bell.svg new file mode 100644 index 00000000..3304f0cd --- /dev/null +++ b/assets/images/img_nav_bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_nav_blue_gray_400_24x25.svg b/assets/images/img_nav_chat.svg similarity index 100% rename from assets/images/img_nav_blue_gray_400_24x25.svg rename to assets/images/img_nav_chat.svg diff --git a/assets/images/img_nav.svg b/assets/images/img_nav_diary.svg similarity index 100% rename from assets/images/img_nav.svg rename to assets/images/img_nav_diary.svg diff --git a/assets/images/img_nav_diets.svg b/assets/images/img_nav_diets.svg new file mode 100644 index 00000000..6300006a --- /dev/null +++ b/assets/images/img_nav_diets.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_nav_exit.svg b/assets/images/img_nav_exit.svg new file mode 100644 index 00000000..5559649e --- /dev/null +++ b/assets/images/img_nav_exit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/img_nav_feed.svg b/assets/images/img_nav_feed.svg new file mode 100644 index 00000000..55794777 --- /dev/null +++ b/assets/images/img_nav_feed.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/img_nav_finance.svg b/assets/images/img_nav_finance.svg new file mode 100644 index 00000000..79c9d63a --- /dev/null +++ b/assets/images/img_nav_finance.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/img_nav_logo.svg b/assets/images/img_nav_logo.svg new file mode 100644 index 00000000..b4b27e75 --- /dev/null +++ b/assets/images/img_nav_logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/img_nav_plans.svg b/assets/images/img_nav_plans.svg new file mode 100644 index 00000000..62e0c17a --- /dev/null +++ b/assets/images/img_nav_plans.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_nav_recipes.svg b/assets/images/img_nav_recipes.svg new file mode 100644 index 00000000..505da803 --- /dev/null +++ b/assets/images/img_nav_recipes.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_nav_settings.svg b/assets/images/img_nav_settings.svg new file mode 100644 index 00000000..619bd4e7 --- /dev/null +++ b/assets/images/img_nav_settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_nav_support.svg b/assets/images/img_nav_support.svg new file mode 100644 index 00000000..84859d74 --- /dev/null +++ b/assets/images/img_nav_support.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/img_send.svg b/assets/images/img_send.svg new file mode 100644 index 00000000..5dedda3d --- /dev/null +++ b/assets/images/img_send.svg @@ -0,0 +1,3 @@ + + + diff --git a/eatvio_app.iml b/eatvio_app.iml new file mode 100644 index 00000000..ed4ab455 --- /dev/null +++ b/eatvio_app.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ios/.DS_Store b/ios/.DS_Store new file mode 100644 index 00000000..8baff32f Binary files /dev/null and b/ios/.DS_Store differ diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index f2872cf4..8c6e5614 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 12.0 diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile index d97f17e2..279576f3 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,9 +32,6 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end end post_install do |installer| diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 36428fd8..29940b0f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 68F4DD6659288F4CFDC41C5C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7024DE075B2C1E5A149230E4 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -31,7 +32,10 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1DC098B20AE17A152A9E7BC6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4B6A3CFB60775CA626789750 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7024DE075B2C1E5A149230E4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC46FEE88A2E87A3510B5EC3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,31 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 68F4DD6659288F4CFDC41C5C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 28430AE518366E9EB19A31C9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7024DE075B2C1E5A149230E4 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6F201E010624CF963CC3A17B /* Pods */ = { + isa = PBXGroup; + children = ( + 1DC098B20AE17A152A9E7BC6 /* Pods-Runner.debug.xcconfig */, + 4B6A3CFB60775CA626789750 /* Pods-Runner.release.xcconfig */, + EC46FEE88A2E87A3510B5EC3 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +96,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 6F201E010624CF963CC3A17B /* Pods */, + 28430AE518366E9EB19A31C9 /* Frameworks */, ); sourceTree = ""; }; @@ -105,12 +131,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + C9531164650CD277994F87F5 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 8944C2327F94FA34E28A19A3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -127,7 +155,8 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,10 +200,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -183,8 +214,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 8944C2327F94FA34E28A19A3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -197,6 +246,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C9531164650CD277994F87F5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -235,6 +306,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -254,6 +326,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -264,6 +337,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -272,7 +346,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -288,13 +362,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -311,6 +389,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -330,6 +409,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -340,6 +420,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -354,7 +435,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -366,6 +447,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -385,6 +467,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -395,6 +478,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -403,10 +487,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -420,13 +505,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -447,13 +536,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..a6b826db 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - + + diff --git a/ios/Runner/.DS_Store b/ios/Runner/.DS_Store new file mode 100644 index 00000000..a8b55693 Binary files /dev/null and b/ios/Runner/.DS_Store differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 00000000..eef2a67c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png new file mode 100644 index 00000000..5edc1f6d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024 1.png new file mode 100644 index 00000000..6506e482 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..6506e482 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..0b06be4f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 1.png new file mode 100644 index 00000000..2bf25edb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 2.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 2.png new file mode 100644 index 00000000..2bf25edb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120 2.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..2bf25edb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 00000000..846eff6d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 00000000..2ee6146d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/152 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152 1.png new file mode 100644 index 00000000..de8394eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 00000000..de8394eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 00000000..8d43d731 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/167 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167 1.png new file mode 100644 index 00000000..925a123f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 00000000..925a123f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 00000000..b4869ae5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/180 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180 1.png new file mode 100644 index 00000000..4708a9b8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..4708a9b8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 00000000..8fa0cb21 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 00000000..434971aa Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 00000000..899dc84c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 00000000..dfeb88fe Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..9bb5b797 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 00000000..a71886e8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 1.png new file mode 100644 index 00000000..6b3dd97b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..6b3dd97b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 00000000..50f41c5b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 00000000..eea037f2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 00000000..f8815ff6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 00000000..8329380b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..35e4264a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/58 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58 1.png new file mode 100644 index 00000000..64997665 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..64997665 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..15ec4ff8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 00000000..fdde0d29 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png new file mode 100644 index 00000000..d0ad98b8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 00000000..559ff236 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 00000000..5c40b240 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 1.png new file mode 100644 index 00000000..dff295c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 1.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 2.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 2.png new file mode 100644 index 00000000..dff295c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80 2.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..dff295c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..38777e15 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 00000000..16398da6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png new file mode 100644 index 00000000..e33c3b1b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 004a4f77..3b2547e2 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -27,39 +27,43 @@ "size" : "29x29" }, { - "filename" : "Icon-App-20x20@2x.png", + "filename" : "80 1.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "120 1.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { - "filename" : "", + "filename" : "120 2.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { - "filename" : "", + "filename" : "180 1.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "40 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "58 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "80 2.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" @@ -71,16 +75,19 @@ "size" : "76x76" }, { + "filename" : "152 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "167 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "1024 1.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 6ce701e8..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8397f756..fb188418 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,6 +26,13 @@ LaunchScreen UIMainStoryboardFile Main + NSPhotoLibraryUsageDescription + UserPhotoUpload + UIBackgroundModes + + fetch + remote-notification + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/core/utils/image_constant.dart b/lib/core/utils/image_constant.dart index 37113a30..07aa9abb 100644 --- a/lib/core/utils/image_constant.dart +++ b/lib/core/utils/image_constant.dart @@ -14,6 +14,7 @@ class ImageConstant { static String imageNotFound = 'assets/images/image_not_found.png'; + static String imaGlass = 'assets/images/image_glass.svg'; // Настройки images static String imgClose = '$imagePath/img_close.svg'; @@ -79,7 +80,39 @@ class ImageConstant { static String imgEllipse3 = '$imagePath/img_ellipse_3.png'; - static String imgNav = '$imagePath/img_nav.svg'; + //------------------NAVIGATION--------------------------------- + + static String imgDiary = '$imagePath/img_nav_diary.svg'; + + static String imgFeed = '$imagePath/img_nav_feed.svg'; + + static String imgChat = '$imagePath/img_nav_chat.svg'; + + static String imgLogo = '$imagePath/img_nav_logo.svg'; + + static String imgBell = '$imagePath/img_nav_bell.svg'; + + static String imgSettings = '$imagePath/img_nav_settings.svg'; + + static String imgRecipes = '$imagePath/img_nav_recipes.svg'; + + static String imgDiets = '$imagePath/img_nav_diets.svg'; + + static String imgPlans = '$imagePath/img_nav_plans.svg'; + + static String imgFinance = '$imagePath/img_nav_finance.svg'; + + static String imgSupport = '$imagePath/img_nav_support.svg'; + + static String imgMic = '$imagePath/img_mic.svg'; + + static String imgSend = '$imagePath/img_send.svg'; + + static String imgAttach = '$imagePath/img_attach.svg'; + + static String imgExit = '$imagePath/img_nav_exit.svg'; + + //------------------------------------------------------------- static String imgNavBlueGray400 = '$imagePath/img_nav_blue_gray_400.svg'; diff --git a/lib/core/utils/pref_utils.dart b/lib/core/utils/pref_utils.dart index 5238073f..9c49a3d2 100644 --- a/lib/core/utils/pref_utils.dart +++ b/lib/core/utils/pref_utils.dart @@ -1,5 +1,3 @@ -//ignore: unused_import -import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; class PrefUtils { diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index ddc30061..a6de3e67 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -1,9 +1,9 @@ -import 'dart:ui' as ui; import 'package:flutter/material.dart'; // This functions are responsible to make UI responsive across all the mobile devices. -MediaQueryData mediaQueryData = MediaQueryData.fromWindow(ui.window); +MediaQueryData mediaQueryData = MediaQueryData.fromView( + WidgetsBinding.instance.platformDispatcher.views.single); // These are the Viewport values of your Figma Design. // These are used in the code as a reference to create your UI Responsively. diff --git a/lib/data/apiClient/api_client.dart b/lib/data/apiClient/api_client.dart deleted file mode 100644 index d629b311..00000000 --- a/lib/data/apiClient/api_client.dart +++ /dev/null @@ -1,3 +0,0 @@ -//import 'package:eatvio_app/core/app_export.dart'; - -class ApiClient {} diff --git a/lib/data/models/userRegistrationModel/user_registration_model.dart b/lib/data/models/userRegistrationModel/user_registration_model.dart index 0001f9ec..baeb3a16 100644 --- a/lib/data/models/userRegistrationModel/user_registration_model.dart +++ b/lib/data/models/userRegistrationModel/user_registration_model.dart @@ -86,6 +86,7 @@ class UserRegistrationSheet { 'favorite_foods': favoriteFoods, 'unfavorite_foods': unfavoriteFoods, 'additional_info': additionalInfo, + 'activity_coefficient': '1.2', }; } } diff --git a/lib/localization/app_localization.dart b/lib/localization/app_localization.dart index 51b4b1aa..4701a670 100644 --- a/lib/localization/app_localization.dart +++ b/lib/localization/app_localization.dart @@ -17,8 +17,16 @@ class AppLocalization { };*/ static AppLocalization of() { - return Localizations.of( - NavigatorService.navigatorKey.currentContext!, AppLocalization)!; + final context = NavigatorService.navigatorKey.currentContext; + if (context != null) { + return Localizations.of(context, AppLocalization)!; + } else { + // Обработка ситуации, когда context равен null + return AppLocalization(Locale( + 'ru', + '', + )); // Возвращаем дефолтное значение или обработку ошибки + } } static List languages() => _localizedValues.keys.toList(); diff --git a/lib/localization/ru/ru_translations.dart b/lib/localization/ru/ru_translations.dart index 85794150..3d28a232 100644 --- a/lib/localization/ru/ru_translations.dart +++ b/lib/localization/ru/ru_translations.dart @@ -133,4 +133,6 @@ final Map ru = { "admin": "Админ", "diary": "Дневник", "save": "Сохранить", + "pcs": "порций", + "gr": "гр", }; diff --git a/lib/main.dart b/lib/main.dart index 7b5326dc..3a5a94bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,8 @@ void main() async { await dotenv.load(fileName: ".env"); await Hive.initFlutter(path); SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, DeviceOrientation.portraitUp, ]); await PrefUtils().init(); diff --git a/lib/presentation/auth/auth_screen/auth_screen.dart b/lib/presentation/auth/auth_screen/auth_screen.dart index 4cbef203..e934925b 100644 --- a/lib/presentation/auth/auth_screen/auth_screen.dart +++ b/lib/presentation/auth/auth_screen/auth_screen.dart @@ -30,11 +30,13 @@ class AuthScreen extends StatelessWidget { return Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppBar(context), - body: Container( + body:GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( width: double.maxFinite, padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), child: Column(children: [ - SizedBox(height: 39.v), + SizedBox(height: 36.v), Text("user_authorization".tr, style: theme.textTheme.headlineSmall), SizedBox(height: 60.v), @@ -58,100 +60,125 @@ class AuthScreen extends StatelessWidget { controller: passwordController, ); }), - Spacer(), + SizedBox(height: 50.v), NextStepButton( text: "no_account".tr.toUpperCase(), margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - Navigator.pushNamed( - context, AppRoutes.languageSelectScreen); - }), - NextStepButton( + onPressed: onTapNoAccount(context)), + /*NextStepButton( text: "Тест дневника".toUpperCase(), margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - Navigator.pushNamed(context, AppRoutes.diaryEdit); - }), + onPressed: onTapDiaryTest(context)), + NextStepButton( + text: "Тест чата".toUpperCase(), + margin: + EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: onTapProfileTest(context)),*/ BlocBuilder(builder: (context, state) { return NextStepButton( text: "authentificate".tr.toUpperCase(), margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () async { - List errors = validateFields( - state.emailController!.text, - state.passwordController!.text); - if (errors.isEmpty) { - final credentials = { - 'email': state.emailController!.text, - 'password': state.passwordController!.text, - }; - final response = await http.post( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/login'), - headers: {'Content-Type': 'application/json'}, - body: json.encode(credentials), - encoding: utf8); - if (response.statusCode == 200) { - var res = await jsonDecode(response.body); - final box = await Hive.openBox('token'); - final token = res['token']; - box.put('token', token); - Navigator.pushNamed( - context, - AppRoutes.myProfile, - ); - } else { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text('wrong_credentials'.tr), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - } else { - String errorMessage = ''; - - for (var error in errors) { - errorMessage += error + '\n\n'; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); + onPressed: onTapAuth(state, context)); }), - ]))); + ])))); + } + + onTapAuth(AuthState state, BuildContext context) { + return () async { + List errors = validateFields( + state.emailController!.text, state.passwordController!.text); + if (errors.isEmpty) { + final Map credentials = { + 'email': state.emailController!.text, + 'password': state.passwordController!.text, + }; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/login'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(credentials), + encoding: utf8); + print(response.statusCode); + if (response.statusCode == 200) { + var res = await jsonDecode(response.body); + final box = await Hive.openBox('token'); + final token = res['token']; + box.put('token', token); + Navigator.pushNamed( + context, + AppRoutes.myProfile, + ); + } else { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Ошибка'), + content: Text('wrong_credentials'.tr), + actions: [ + TextButton( + onPressed: onTapOk(context), + child: Text('OK'), + ), + ], + ), + ); + } + } else { + String errorMessage = ''; + + for (var error in errors) { + errorMessage += error + '\n\n'; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Ошибка'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: onTapOk(context), + child: Text('OK'), + ), + ], + ), + ); + } + }; + } + + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + + onTapDiaryTest(BuildContext context) { + return () { + Navigator.pushNamed(context, AppRoutes.diaryEdit); + }; + } + + onTapProfileTest(BuildContext context) { + return () { + Navigator.pushNamed(context, AppRoutes.mySettings); + }; + } + + onTapNoAccount(BuildContext context) { + return () { + Navigator.pushNamed(context, AppRoutes.languageSelectScreen); + }; } /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, centerTitle: true, diff --git a/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart b/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart new file mode 100644 index 00000000..52cf81df --- /dev/null +++ b/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart @@ -0,0 +1,79 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/chat/chat_screen/models/chat_model.dart'; +import 'package:eatvio_app/presentation/chat/chat_screen/models/chat_message_model.dart'; +part 'chat_event.dart'; +part 'chat_state.dart'; + +/// A bloc that manages the state of a Chat according to the event that is dispatched to it. +class ChatBloc extends Bloc { + ChatBloc(ChatState initialState) : super(initialState) { + on(_onInitialize); + } + + _onInitialize( + ChatInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? chatID = state.chatModelObj?.chatID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/chat/show/$chatID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map && + data.containsKey('chatRoom') && + data['chatRoom'].containsKey('messages')) { + if (state is ChatLoadedState) { + var currentState = state as ChatLoadedState; + emit(currentState.copyWith( + chatModelObj: currentState.chatModelObj, + messages: currentState.messages)); + } else { + emit(ChatLoadedState( + messages: fillMessages(data['chatRoom']['messages']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(ChatErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(ChatErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(ChatErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + List fillMessages(List messagesJSON) { + List messages = []; + for (int i = 0; i < messagesJSON.length; i++) { + messages.add(ChatMessageModel( + id: messagesJSON[i]['id'], + data: messagesJSON[i]['data'], + mediaData: messagesJSON[i]['mediaData'] ?? [], + updatedAt: messagesJSON[i]['updated_at'], + authorId: messagesJSON[i]['author_id'])); + } + + return messages; + } +} diff --git a/lib/presentation/chat/chat_screen/bloc/chat_event.dart b/lib/presentation/chat/chat_screen/bloc/chat_event.dart new file mode 100644 index 00000000..1fe6df94 --- /dev/null +++ b/lib/presentation/chat/chat_screen/bloc/chat_event.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +part of 'chat_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Chat widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class ChatEvent extends Equatable {} + +/// Event that is dispatched when the Chat widget is first created. +class ChatInitialEvent extends ChatEvent { + @override + List get props => []; +} diff --git a/lib/presentation/chat/chat_screen/bloc/chat_state.dart b/lib/presentation/chat/chat_screen/bloc/chat_state.dart new file mode 100644 index 00000000..6f5718d1 --- /dev/null +++ b/lib/presentation/chat/chat_screen/bloc/chat_state.dart @@ -0,0 +1,53 @@ +// ignore_for_file: must_be_immutable + +part of 'chat_bloc.dart'; + +/// Represents the state of Chat in the application. +/// + +class ChatState extends Equatable { + ChatState({ + this.chatModelObj, + }); + + ChatModel? chatModelObj; + + @override + List get props => [chatModelObj]; + ChatState copyWith({ChatModel? chatModelObj}) { + return ChatState( + chatModelObj: chatModelObj ?? this.chatModelObj, + ); + } +} + +class ChatLoadedState extends ChatState { + ChatLoadedState({this.chatModelObj, this.messages}); + + ChatModel? chatModelObj; + List? messages; + + @override + List get props => [chatModelObj, messages]; + ChatLoadedState copyWith( + {ChatModel? chatModelObj, List? messages}) { + return ChatLoadedState( + chatModelObj: chatModelObj ?? this.chatModelObj, + messages: messages ?? this.messages, + ); + } +} + +class ChatErrorState extends ChatState { + final String errorMessage; + + ChatErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class ChatInitialState extends ChatState { + ChatInitialState({required ChatModel chatModelObj}) + : super(chatModelObj: chatModelObj); +} diff --git a/lib/presentation/chat/chat_screen/chat_screen.dart b/lib/presentation/chat/chat_screen/chat_screen.dart new file mode 100644 index 00000000..37c3df52 --- /dev/null +++ b/lib/presentation/chat/chat_screen/chat_screen.dart @@ -0,0 +1,192 @@ +import 'bloc/chat_bloc.dart'; +import 'models/chat_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_title_image.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; + +class ChatScreen extends StatelessWidget { + const ChatScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final chatID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => ChatBloc( + ChatState(chatModelObj: ChatModel(chatID: chatID.toString()))) + ..add(ChatInitialEvent()), + child: ChatScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is ChatInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is ChatLoadedState) { + // Данные загружены, можно строить экран. + return _buildChat(context, state); + } else if (state is ChatErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildChat(BuildContext context, ChatLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: _buildAppbar(context), + body: SizedBox( + width: double.maxFinite, + child: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + ], + ), + ), + ), + bottomNavigationBar: _buildTextField(context), + ); + } + + /// Section Widget + _buildAppbar(BuildContext context) { + return CustomAppBar( + leadingWidth: 26.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, + top: 18.v, + bottom: 25.v, + ), + onTap: () => onTapArrowleftone(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 12.h), + child: Row( + children: [ + AppbarTitleImage( + imagePath: ImageConstant.imgIconMoreHorizontal, + margin: EdgeInsets.only(bottom: 3.v), + ), + Padding( + padding: EdgeInsets.only( + left: 12.h, + top: 2.v, + ), + child: Column( + children: [ + AppbarSubtitle( + text: "Валерий Леонтьев", + ), + SizedBox(height: 3.v), + Text("Был в сети вчера", style: theme.textTheme.bodySmall!) + ], + ), + ) + ], + ), + ), + ); + } + + /// Section Widget + _buildTextField(BuildContext context) { + return Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric( + horizontal: 7.h, + vertical: 6.v, + ), + decoration: AppDecoration.outlineBlueGray, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomImageView( + imagePath: ImageConstant.imgAttach, + height: 29.adaptSize, + width: 29.adaptSize, + margin: EdgeInsets.only( + top: 1.v, + bottom: 6.v, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: 4.h, + bottom: 4.v, + ), + child: BlocSelector( + selector: (state) => TextEditingController(), + builder: (context, messageController) { + return CustomTextFormField( + controller: messageController, + hintText: "Напишите сообщение...", + textInputAction: TextInputAction.done, + ); + }, + ), + ), + ), + CustomImageView( + imagePath: ImageConstant.imgMic, + height: 24.v, + width: 18.h, + margin: EdgeInsets.only( + left: 11.h, + top: 4.v, + bottom: 8.v, + ), + ) + ], + ), + ); + } + + /// Common widget + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.chatScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.chatScreen: + return ChatScreen.builder(context); + default: + return ChatScreen(); + } + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/chat/chat_screen/models/chat_message_model.dart b/lib/presentation/chat/chat_screen/models/chat_message_model.dart new file mode 100644 index 00000000..e91596f3 --- /dev/null +++ b/lib/presentation/chat/chat_screen/models/chat_message_model.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +class ChatMessageModel extends Equatable { + final int id; + final String data; + final List mediaData; + final int authorId; + final String updatedAt; + + ChatMessageModel({ + required this.id, + required this.data, + required this.mediaData, + required this.authorId, + required this.updatedAt, + }); + + ChatMessageModel copyWith({ + int? id, + String? data, + List? mediaData, + int? authorId, + String? updatedAt, + }) { + return ChatMessageModel( + id: id ?? this.id, + data: data ?? this.data, + mediaData: mediaData ?? this.mediaData, + authorId: authorId ?? this.authorId, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + List get props => [id, data, mediaData, authorId, updatedAt]; +} diff --git a/lib/presentation/chat/chat_screen/models/chat_model.dart b/lib/presentation/chat/chat_screen/models/chat_model.dart new file mode 100644 index 00000000..970b9ada --- /dev/null +++ b/lib/presentation/chat/chat_screen/models/chat_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [chat_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class ChatModel extends Equatable { + final String chatID; + + ChatModel({required String chatID}) : chatID = chatID; + + ChatModel copyWith(String chatID) { + return ChatModel(chatID: chatID); + } + + @override + List get props => [chatID]; +} diff --git a/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart b/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart new file mode 100644 index 00000000..f543c5aa --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart @@ -0,0 +1,89 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/chat/chat_select_screen/models/chat_select_model.dart'; +import 'package:eatvio_app/presentation/chat/chat_select_screen/models/chat_entry.dart'; // ignore: must_be_immutable +part 'chat_select_event.dart'; +part 'chat_select_state.dart'; + +/// A bloc that manages the state of a ChatSelect according to the event that is dispatched to it. +/// + +class ChatSelectBloc extends Bloc { + ChatSelectBloc(ChatSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + List fillChatEntryList(List chatRooms) { + List chats = []; + for (int i = 0; i < chatRooms.length; i++) { + String text = chatRooms[i]['last_message']['data']; + if (chatRooms[i]['last_message']['is_author_from_user']) { + text = "Вы: " + text; + } + chats.add(ChatItemModel( + id: chatRooms[i]['id'], + userImage: chatRooms[i]['users']['to_chat_user'] + ['profile_photo_path'], + username: chatRooms[i]['users']['to_chat_user']['name'], + dateText: DateFormat('dd.MM').format( + DateTime.parse(chatRooms[i]['last_message']['updated_at'])), + greetingText: text)); + } + return chats; + } + + Future _onInitialize( + ChatSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/chat/index'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map && data.containsKey('chatRooms')) { + if (state is ChatSelectLoadedState) { + var currentState = state as ChatSelectLoadedState; + emit(currentState.copyWith( + chatSelectModelObj: currentState.chatSelectModelObj, + chatRooms: currentState.chatRooms, + )); + } else { + emit(ChatSelectLoadedState( + chatRooms: fillChatEntryList(data['chatRooms']), + //chatRooms: [], + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(ChatSelectErrorState( + 'Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(ChatSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(ChatSelectErrorState('Ошибка при получении данных о чатах: $e')); + } + } +} diff --git a/lib/presentation/chat/chat_select_screen/bloc/chat_select_event.dart b/lib/presentation/chat/chat_select_screen/bloc/chat_select_event.dart new file mode 100644 index 00000000..681736b1 --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/bloc/chat_select_event.dart @@ -0,0 +1,28 @@ +// ignore_for_file: must_be_immutable + +part of 'chat_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///ChatSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class ChatSelectEvent extends Equatable {} + +/// Event that is dispatched when the ChatSelect widget is first created. +class ChatSelectInitialEvent extends ChatSelectEvent { + @override + List get props => []; +} + +///event for dropdown selection +class ChangeDropDownEvent extends ChatSelectEvent { + ChangeDropDownEvent({required this.value}); + + SelectionPopupModel value; + + @override + List get props => [ + value, + ]; +} diff --git a/lib/presentation/chat/chat_select_screen/bloc/chat_select_state.dart b/lib/presentation/chat/chat_select_screen/bloc/chat_select_state.dart new file mode 100644 index 00000000..e5f1262a --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/bloc/chat_select_state.dart @@ -0,0 +1,58 @@ +// ignore_for_file: must_be_immutable + +part of 'chat_select_bloc.dart'; + +/// Represents the state of ChatSelect in the application. +class ChatSelectState extends Equatable { + ChatSelectState({ + this.chatSelectModelObj, + }); + + ChatSelectModel? chatSelectModelObj; + + @override + List get props => [chatSelectModelObj]; + ChatSelectState copyWith({ChatSelectModel? chatSelectModelObj}) { + return ChatSelectState( + chatSelectModelObj: chatSelectModelObj ?? this.chatSelectModelObj, + ); + } +} + +class ChatSelectInitialState extends ChatSelectState { + ChatSelectInitialState({required ChatSelectModel chatSelectModelObj}) + : super(chatSelectModelObj: chatSelectModelObj); +} + +class ChatSelectLoadedState extends ChatSelectState { + ChatSelectLoadedState({ + this.chatRooms, + this.searchController, + this.chatSelectModelObj, + }) : super(chatSelectModelObj: null); + List? chatRooms; + TextEditingController? searchController; + ChatSelectModel? chatSelectModelObj; + @override + List get props => [chatSelectModelObj, chatRooms, searchController]; + ChatSelectLoadedState copyWith({ + ChatSelectModel? chatSelectModelObj, + TextEditingController? searchController, + List? chatRooms, + }) { + return ChatSelectLoadedState( + chatSelectModelObj: chatSelectModelObj ?? this.chatSelectModelObj, + chatRooms: chatRooms ?? this.chatRooms, + searchController: searchController ?? this.searchController, + ); + } +} + +class ChatSelectErrorState extends ChatSelectState { + final String errorMessage; + + ChatSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/chat/chat_select_screen/chat_search_view.dart b/lib/presentation/chat/chat_select_screen/chat_search_view.dart new file mode 100644 index 00000000..b9447660 --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/chat_search_view.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class CustomSearchView extends StatelessWidget { + CustomSearchView( + {Key? key, + this.alignment, + this.width, + this.scrollPadding, + this.controller, + this.focusNode, + this.autofocus = false, + this.textStyle, + this.textInputType = TextInputType.text, + this.maxLines, + this.hintText, + this.hintStyle, + this.prefix, + this.prefixConstraints, + this.suffix, + this.suffixConstraints, + this.contentPadding, + this.borderDecoration, + this.fillColor, + this.filled = true, + this.validator, + this.onChanged}) + : super( + key: key, + ); + + final Alignment? alignment; + + final double? width; + + final TextEditingController? scrollPadding; + + final TextEditingController? controller; + + final FocusNode? focusNode; + + final bool? autofocus; + + final TextStyle? textStyle; + + final TextInputType? textInputType; + + final int? maxLines; + + final String? hintText; + + final TextStyle? hintStyle; + + final Widget? prefix; + + final BoxConstraints? prefixConstraints; + + final Widget? suffix; + + final BoxConstraints? suffixConstraints; + + final EdgeInsets? contentPadding; + + final InputBorder? borderDecoration; + + final Color? fillColor; + + final bool? filled; + + final FormFieldValidator? validator; + + final Function(String)? onChanged; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: searchViewWidget(context)) + : searchViewWidget(context); + } + + searchViewWidget(BuildContext context) => SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + scrollPadding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + controller: controller, + focusNode: focusNode, + onTapOutside: (event) => onTapOutside(), + autofocus: autofocus!, + style: textStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + keyboardType: textInputType, + maxLines: maxLines ?? 1, + decoration: decoration, + validator: validator, + onChanged: (String value) => onChanged?.call(value), + ), + ); + + void onTapOutside() { + if (focusNode != null) { + focusNode?.unfocus(); + } else { + FocusManager.instance.primaryFocus?.unfocus(); + } + } + + InputDecoration get decoration => InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + prefixIcon: prefix ?? + SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.search)), + ), + prefixIconConstraints: prefixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + suffixIcon: suffix ?? + Padding( + padding: EdgeInsets.only( + right: 15.h, + ), + child: IconButton( + onPressed: () => controller!.clear(), + icon: Icon( + Icons.clear, + color: Colors.grey.shade600, + ), + ), + ), + suffixIconConstraints: suffixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + top: 11.v, + right: 11.h, + bottom: 11.v, + ), + fillColor: fillColor ?? appTheme.gray100, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + ); +} diff --git a/lib/presentation/chat/chat_select_screen/chat_select_screen.dart b/lib/presentation/chat/chat_select_screen/chat_select_screen.dart new file mode 100644 index 00000000..dea9d3bc --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/chat_select_screen.dart @@ -0,0 +1,111 @@ +import 'bloc/chat_select_bloc.dart'; +import 'models/chat_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import '../chat_select_screen/chat_search_view.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/presentation/chat/chat_select_screen/models/chat_entry.dart'; // ignore: must_be_immutable +import 'package:eatvio_app/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart'; // ignore: must_be_immutable + +class ChatSelectScreen extends StatelessWidget { + const ChatSelectScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => ChatSelectBloc( + ChatSelectState(chatSelectModelObj: ChatSelectModel())) + ..add(ChatSelectInitialEvent()), + child: ChatSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is ChatSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is ChatSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildScreen(context, state); + } else if (state is ChatSelectErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildScreen(BuildContext context, ChatSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomSearchView( + controller: state.searchController, + hintText: "Поиск", + ), + SizedBox(height: 20.v), + _buildChatList(context, state), + ], + ), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildChatList(BuildContext context, ChatSelectLoadedState state) { + return ListView.separated( + shrinkWrap: true, + separatorBuilder: (context, index) { + return SizedBox( + height: 10.v, + ); + }, + itemCount: state.chatRooms?.length ?? 0, + itemBuilder: (context, index) { + ChatItemModel model = state.chatRooms?[index]; + return ChatEntryWidget( + model, + ); + }, + ); +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/chat/chat_select_screen/models/chat_entry.dart b/lib/presentation/chat/chat_select_screen/models/chat_entry.dart new file mode 100644 index 00000000..6738afb8 --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/models/chat_entry.dart @@ -0,0 +1,29 @@ +import 'package:eatvio_app/core/app_export.dart'; + +/// This class is used in the [userprofile_item_widget] screen. +// ignore_for_file: must_be_immutable + +class ChatItemModel { + ChatItemModel( + {this.userImage, + this.username, + this.dateText, + this.greetingText, + required this.id}) { + userImage = userImage ?? ImageConstant.imageNotFound; + username = username; + dateText = dateText; + greetingText = greetingText; + id = id; + } + + String? userImage; + + String? username; + + String? dateText; + + String? greetingText; + + String id; +} diff --git a/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart b/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart new file mode 100644 index 00000000..2b90449e --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/models/chat_select_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; +import 'package:eatvio_app/presentation/chat/chat_select_screen/models/chat_entry.dart'; + +/// This class defines the variables used in the [chat_select_screen], +/// and is typically used to hold data that is passed between different parts of the application. +class ChatSelectModel extends Equatable { + ChatSelectModel({this.chatList = const []}); + List chatList; + ChatSelectModel copyWith({List? chatList}) { + return ChatSelectModel( + chatList: chatList ?? this.chatList, + ); + } + + @override + List get props => [chatList]; +} diff --git a/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart b/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart new file mode 100644 index 00000000..52f9de3a --- /dev/null +++ b/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart @@ -0,0 +1,110 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/chat/chat_select_screen/models/chat_entry.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +class ChatEntryWidget extends StatelessWidget { + ChatEntryWidget(this.chatEntryObj, {Key? key}) + : super( + key: key, + ); + + ChatItemModel chatEntryObj; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.chatScreen, + arguments: chatEntryObj.id.toString(), + ), + child: Container( + padding: EdgeInsets.all(8.h), + decoration: AppDecoration.fillGray100.copyWith( + borderRadius: BorderRadiusStyle.circleBorder8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + /*CustomImageView( + imagePath: + dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, + height: 60.adaptSize, + width: 60.adaptSize, + radius: BorderRadius.circular( + 30.h, + ), + ),*/ + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: 12.h, + top: 2.v, + bottom: 11.v, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 227.h, + child: Row( + children: [ + Text( + chatEntryObj.username!.length <= 15 + ? chatEntryObj.username! + : chatEntryObj.username!.substring(0, 15) + + '...', + style: theme.textTheme.titleMedium, + ), + Spacer(), + CustomImageView( + imagePath: ImageConstant.imageNotFound, + height: 9.v, + width: 16.h, + margin: EdgeInsets.only( + top: 4.v, + bottom: 6.v, + ), + ), + Padding( + padding: EdgeInsets.only(left: 6.h), + child: Text( + chatEntryObj.dateText!, + style: theme.textTheme.bodyMedium, + ), + ) + ], + ), + ), + SizedBox(height: 6.v), + Text( + chatEntryObj.greetingText!, + style: CustomTextStyles.bodyMediumBluegray800, + ) + ], + ), + ), + ) + ]), + ), + ); + } +} diff --git a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart index 951c7ae6..44741105 100644 --- a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart +++ b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart @@ -1,5 +1,5 @@ import 'dart:async'; - +import 'package:intl/intl.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '/core/app_export.dart'; @@ -7,6 +7,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:eatvio_app/presentation/diary/diary_edit_screen/models/diary_edit_model.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +import 'package:hive/hive.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; part 'diary_edit_event.dart'; @@ -18,18 +19,26 @@ class DiaryEditBloc extends Bloc { on(_onInitialize); on(_changeSatiety); // пользователь нажимает на шкалу сытости on(_changeMood); // пользователь нажимает на шкалу настроения - on( - _changeMealTime); // пользователь изменяет время приема пиши + on(_changeMealTime); // пользовательяет времяа пиши on(_changeMeal); + on(_changePortion); on(_changeWakeup); // пользователь выбирает время пробуждения on(_changeRetire); // пользователь выбирает время отхода ко сну on(_toggleMood); on(_toggleWakeup); on(_addPortion); //пользователь нажимает "Добавить продукт" - on( - _removePortion); // пользователь нажимает на крестик около продукта + on(_removePortion); + on( + _removeImage); // пользователь нажимает на крестик около продукта on(_uploadImages); //пользователь нажимает "Добавить фото" on(_addComment); //пользователь нажимает "Добавить комментарий" + on( + _inputProduct); //пользователь нажимает "Добавить комментарий" + on(_finishDay); //пользователь нажимает "Добавить комментарий" + on(_sendModeration); + on( + reloadScreen); //пользователь нажимает "Добавить комментарий" + on(_clearInput); } Future _onInitialize( @@ -37,11 +46,187 @@ class DiaryEditBloc extends Bloc { Emitter emit, ) async { try { - final token = - "laravel_sanctum_Q22zOtJhQUcJnh0kseZFblFv5XAPaKbG3gTM5hga71ecbb49"; + // final token = + // "9|laravel_sanctum_KFXbccDcWEvTPfJmV99EQv1neAggmpR2TidVGDPN34fa201a"; + final box = await Hive.openBox('token'); + final token = box.get('token'); + //print(await fetchDiaryId(token)); + final int diaryId = await fetchDiaryId(token, emit); + try { + print("Diary ID fetched"); + final diary = await fetchDiaryData(token, diaryId); + print("Screen load success"); + if (diary is Map) { + final String diaryDate = diary['date'] ?? DateTime.now().toString(); + final chartValues = await fetchChartValues(token, diaryId, emit); + if (state is DiaryEditLoadedState) { + // Если текущее состояние уже загружено, обновляем его + var currentState = state as DiaryEditLoadedState; + emit((currentState).copyWith( + currentMeal: currentState.currentMeal, + wakeupExpanded: currentState.wakeupExpanded, + moodExpanded: currentState.moodExpanded, + moderationExpanded: currentState.moderationExpanded, + token: currentState.token, + meals: currentState.meals, + portions: currentState.portions, + photos: currentState.photos, + wakeupController: currentState.wakeupController, + retireController: currentState.retireController, + commentController: currentState.commentController, + mealTimeController: currentState.mealTimeController, + productNameController: currentState.productNameController, + caloriesController: currentState.caloriesController, + proteinsController: currentState.proteinsController, + fatsController: currentState.fatsController, + carbsController: currentState.carbsController, + selectedSatiety: currentState.selectedSatiety, + selectedMood: currentState.selectedMood, + diaryDate: currentState.diaryDate, + diaryId: currentState.diaryId, + productQuantityController: + currentState.productQuantityController, + recommendedChartValues: currentState.recommendedChartValues, + consumedChartValues: currentState.consumedChartValues, + autocompleteList: currentState.autocompleteList, + diaryEditModelObj: currentState.diaryEditModelObj)); + } else { + // Если текущее состояние - начальное, создаем новое загруженное состояние + final List meals = + fillMealList(diary['meals']); + emit(DiaryEditLoadedState( + currentMeal: meals[0], + portionController: SelectionPopupModel( + id: 0, title: "порций", isSelected: true), + wakeupExpanded: true, + moodExpanded: true, + moderationExpanded: false, + token: token, + recommendedChartValues: chartValues[0], + consumedChartValues: chartValues[1], + meals: diary['meals'], + portions: diary['meals'][0]['portions'], + photos: diary['meals'][0]['photos'], + wakeupController: toTime(diary['wakeup'] ?? "09:00"), + retireController: toTime(diary['retire'] ?? "23:00"), + mealTimeController: + toTime(diary['meals'][0]['time'] ?? "10:00"), + selectedSatiety: diary['meals'][0]['satiety'], + selectedMood: diary['meals'][0]['mood'], + commentController: + TextEditingController(text: diary['comment']), + productNameController: TextEditingController(), + productQuantityController: TextEditingController(), + caloriesController: TextEditingController(), + proteinsController: TextEditingController(), + fatsController: TextEditingController(), + carbsController: TextEditingController(), + diaryId: diaryId, + diaryDate: diaryDate, + autocompleteList: [], + diaryEditModelObj: state.diaryEditModelObj + ?.copyWith(mealList: meals, portionList: fillPortions()))); + } + } + } catch (e) { + // Ошибка при получении данных дневника + emit(DiaryEditErrorState('Ошибка при получении данных дневника: $e')); + } + } catch (e) { + // Ошибка при получении данных дневника + emit(DiaryEditErrorState('Ошибка при получении данных о дневнике: $e')); + } + } + + fetchDiaryId(String token, Emitter emit) async { + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/index'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map && data.containsKey('id')) { + final int diaryId = data['id']; + return diaryId; + } else { + emit(DiaryEditErrorState('Некоторые необходимые данные отсутствуют.')); + print('fail'); + return -1; + } + } else { + emit(DiaryEditErrorState('Ошибка сервера: ${response.statusCode}')); + print('fail'); + return -1; + } + } + + fetchDiaryData(String token, int diaryId) async { + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/$diaryId/show/'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + final diary = jsonDecode(response.body)['diary']; + + return diary; + } + + fetchChartValues( + String token, int diaryID, Emitter emit) async { + final responseChart = await http.get( + Uri.parse( + dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/dailyNutritionData/'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + if (responseChart.statusCode == 200) { + final recommendedChartValues = + jsonDecode(responseChart.body)['dailyNutritionRecommendation'] + .values + .map((value) => value as int) + .toList(); + final consumedChartValues = + jsonDecode(responseChart.body)['dailyNutritionIntake'] + .values + .map((value) => value as int) + .toList(); + return [recommendedChartValues, consumedChartValues]; + } else { + emit(DiaryEditErrorState('Ошибка при декодировании данных КБЖУ')); + print('fail'); + return -1; + } + } + + refreshScreen( + int mealID, + DiaryEditEvent event, + Emitter emit, + ) async { + await reloadScreen(event, emit); + } + + reloadScreen( + DiaryEditEvent event, + Emitter emit, + ) async { + print("Reloading..."); + try { + //final token = + // "9|laravel_sanctum_KFXbccDcWEvTPfJmV99EQv1neAggmpR2TidVGDPN34fa201a"; final box = await Hive.openBox('token'); + final box = await Hive.openBox('token'); + final token = box.get('token'); final response = await http.get( Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/index'), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); @@ -50,86 +235,93 @@ class DiaryEditBloc extends Bloc { final data = jsonDecode(response.body); if (data is Map && data.containsKey('id')) { final int diaryId = data['id']; - try { - final response = await http.get( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/show/$diaryId'), + + final diaryResponse = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/$diaryId/show/'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + + final diary = jsonDecode(diaryResponse.body)['diary']; + + if (diary is Map) { + final String diaryDate = diary['date'] ?? DateTime.now().toString(); + final responseChart = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + + '/diary/$diaryId/dailyNutritionData/'), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); - final diary = jsonDecode(response.body)['diary']; - if (diary is Map) { - print(diary['meals'][1].runtimeType); - final String diaryDate = - diary['date'] ?? DateTime.now().toString(); - // Продолжайте обработку данных дневника - if (state is DiaryEditLoadedState) { - // Если текущее состояние уже загружено, обновляем его - var currentState = state as DiaryEditLoadedState; - emit((currentState).copyWith( - currentMeal: currentState.currentMeal, - wakeupExpanded: currentState.wakeupExpanded, - moodExpanded: currentState.moodExpanded, - token: currentState.token, - meals: currentState.meals, - wakeupController: currentState.wakeupController, - retireController: currentState.retireController, - commentController: currentState.commentController, - mealTimeController: currentState.mealTimeController, - productNameController: currentState.productNameController, - selectedSatiety: currentState.selectedSatiety, - selectedMood: currentState.selectedMood, - diaryDate: currentState.diaryDate, - diaryId: currentState.diaryId, - productQuantityController: - currentState.productQuantityController, - diaryEditModelObj: currentState.diaryEditModelObj)); - } else { - // Если текущее состояние - начальное, создаем новое загруженное состояние - emit(DiaryEditLoadedState( - currentMeal: SelectionPopupModel( - id: 0, title: "Прием пищи 1", isSelected: true), - wakeupExpanded: true, - moodExpanded: true, - token: token, - meals: diary['meals'], - portions: diary['meals'][0]['portions'], - wakeupController: toTime(diary['wakeup'] ?? "09:00"), - retireController: toTime(diary['retire'] ?? "23:00"), - mealTimeController: - toTime(diary['meals'][0]['time'] ?? "10:00"), - selectedSatiety: diary['meals'][0]['satiety'], - selectedMood: diary['meals'][0]['mood'], - commentController: - TextEditingController(text: diary['comment']), - productNameController: TextEditingController(), - productQuantityController: TextEditingController(), - diaryId: diaryId, - diaryDate: diaryDate, - diaryEditModelObj: state.diaryEditModelObj - ?.copyWith(mealList: fillMealList()))); + if (responseChart.statusCode == 200) { + final recommendedChartValues = jsonDecode( + responseChart.body)['dailyNutritionRecommendation']; + final consumedChartValues = + jsonDecode(responseChart.body)['dailyNutritionIntake']; + final List meals = + fillMealList(diary['meals']); + try { + await Future(() => emit(DiaryEditLoadedState( + diaryEditModelObj: state.diaryEditModelObj?.copyWith( + mealList: meals, portionList: fillPortions()), + currentMeal: meals[0], + portionController: SelectionPopupModel( + id: 0, title: "порций", isSelected: true), + wakeupExpanded: true, + moodExpanded: true, + moderationExpanded: false, + token: token, + recommendedChartValues: recommendedChartValues.values + .map((value) => value as int) + .toList(), + consumedChartValues: consumedChartValues.values + .map((value) => value as int) + .toList(), + meals: diary['meals'], + portions: diary['meals'][0]['portions'], + photos: diary['meals'][0]['photos'], + wakeupController: toTime(diary['wakeup'] ?? "09:00"), + retireController: toTime(diary['retire'] ?? "23:00"), + mealTimeController: + toTime(diary['meals'][0]['time'] ?? "10:00"), + selectedSatiety: diary['meals'][0]['satiety'], + selectedMood: diary['meals'][0]['mood'], + commentController: + TextEditingController(text: diary['comment']), + productNameController: TextEditingController(), + productQuantityController: TextEditingController(), + caloriesController: TextEditingController(), + proteinsController: TextEditingController(), + fatsController: TextEditingController(), + carbsController: TextEditingController(), + diaryId: diaryId, + diaryDate: diaryDate, + isLoading: false, + autocompleteList: [], + ))); + + print("Updated"); + } catch (e) { + print("Ошибка при создании нового состояния $e"); } } else { - // Ошибка при декодировании JSON - emit(DiaryEditErrorState( - 'Ошибка при декодировании данных дневника')); + emit(DiaryEditErrorState('Ошибка при декодировании данных КБЖУ')); } - } catch (e) { - // Ошибка при получении данных дневника + } else { emit(DiaryEditErrorState( - 'Ошибка при получении данных дневника: $e')); + 'Ошибка при декодировании данных дневника')); } } else { - // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку emit( DiaryEditErrorState('Некоторые необходимые данные отсутствуют.')); } } else { - // Статус ответа не равен 200, обрабатываем ошибку emit(DiaryEditErrorState('Ошибка сервера: ${response.statusCode}')); } } catch (e) { - // Ошибка при получении данных дневника emit(DiaryEditErrorState('Ошибка при получении данных дневника: $e')); } } @@ -141,8 +333,6 @@ class DiaryEditBloc extends Bloc { var updatedState = currentState.copyWith(selectedSatiety: event.selectedElement); - emit(updatedState); // Передаем обновленное состояние в emit - // Отправляем данные по HTTP маршруту и ждем ответ var response = await sendSatietyData( currentState.token, @@ -151,8 +341,8 @@ class DiaryEditBloc extends Bloc { currentState.diaryId); if (response.statusCode == 200) { - currentState.selectedSatiety = event.selectedElement; - emit(currentState); // Передаем обновленное состояние в emit + updatedState.selectedSatiety = event.selectedElement; + emit(updatedState); // Передаем обновленное состояние в emit } } } @@ -163,21 +353,18 @@ class DiaryEditBloc extends Bloc { int? mealID, int diaryID, ) async { - String url = - dotenv.env['SERVER_ROOT']! + '$diaryID/meal/$mealID/satiety/fill'; - final credentials = { + String url = dotenv.env['SERVER_ROOT']! + + '/diary/$diaryID/meal/$mealID/satiety/fill'; + final Map credentials = { 'satiety': selectedSatiety, - 'meal_id': mealID, - 'diary_id': diaryID, }; - var response = await http.post(Uri.parse(url), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: json.encode(credentials), encoding: utf8); - return response; } @@ -188,8 +375,6 @@ class DiaryEditBloc extends Bloc { var updatedState = currentState.copyWith(selectedMood: event.selectedElement); - emit(updatedState); // Передаем обновленное состояние в emit - // Отправляем данные по HTTP маршруту и ждем ответ var response = await sendMoodData( currentState.token, @@ -198,8 +383,10 @@ class DiaryEditBloc extends Bloc { currentState.diaryId); if (response.statusCode == 200) { - currentState.selectedMood = event.selectedElement; - emit(currentState); // Передаем обновленное состояние в emit + print( + 'Mood change success! meal_id: ${currentState.currentMeal?.id}, mood: ${event.selectedElement}'); + updatedState.selectedMood = event.selectedElement; + emit(updatedState); // Передаем обновленное состояние в emit } } } @@ -211,15 +398,14 @@ class DiaryEditBloc extends Bloc { int diaryID, ) async { String url = - dotenv.env['SERVER_ROOT']! + '/$diaryID/meal/$mealID/mood/fill'; - final credentials = { + dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/meal/$mealID/mood/fill'; + final Map credentials = { 'mood': selectedMood, - 'meal_id': mealID, - 'diary_id': diaryID, }; var response = await http.post(Uri.parse(url), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: json.encode(credentials), @@ -232,19 +418,33 @@ class DiaryEditBloc extends Bloc { WakeupEvent event, Emitter emit) async { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; - var updatedState = - currentState.copyWith(wakeupController: event.selectedTime); - - emit(updatedState); // Передаем обновленное состояние в emit - - // Отправляем данные по HTTP маршруту и ждем ответ var response = await sendWakeupData( currentState.token, event.selectedTime, currentState.diaryId); - if (response.statusCode == 200) { - currentState.wakeupController = event.selectedTime; - emit(currentState); // Передаем обновленное состояние в emit - } + await reloadScreen(event, emit); + var updatedState = + currentState.copyWith(wakeupController: event.selectedTime); + print("Wakeup time changed to ${event.selectedTime}"); + emit(updatedState); // Передаем обновленное состояние в emit + } else + print("Wakeup time change error"); + } + } + + FutureOr _changeRetire( + RetireEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var response = await sendRetireData( + currentState.token, event.selectedTime, currentState.diaryId); + if (response.statusCode == 200) { + await reloadScreen(event, emit); + var updatedState = + currentState.copyWith(retireController: event.selectedTime); + print("Retire time changed to ${event.selectedTime}"); + emit(updatedState); // Передаем обновленное состояние в emit + } else + print("Retire time change error"); } } @@ -253,14 +453,17 @@ class DiaryEditBloc extends Bloc { TimeOfDay wakeup, int diaryID, ) async { - String url = dotenv.env['SERVER_ROOT']! + '/diary/???'; - final credentials = { - 'wakeup': wakeup, - 'diary_id': diaryID, + String url = dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/wakeUp/fill'; + final formatter = DateFormat('HH:mm'); + final wakeupString = + formatter.format(DateTime(2022, 1, 1, wakeup.hour, wakeup.minute)); + final Map credentials = { + 'wakeUp': wakeupString, }; var response = await http.post(Uri.parse(url), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: json.encode(credentials), @@ -269,39 +472,19 @@ class DiaryEditBloc extends Bloc { return response; } - FutureOr _changeRetire( - RetireEvent event, Emitter emit) async { - if (state is DiaryEditLoadedState) { - var currentState = state as DiaryEditLoadedState; - var updatedState = - currentState.copyWith(retireController: event.selectedTime); - - emit(updatedState); // Передаем обновленное состояние в emit - - // Отправляем данные по HTTP маршруту и ждем ответ - var response = await sendRetireData( - currentState.token, event.selectedTime, currentState.diaryId); - - if (response.statusCode == 200) { - currentState.retireController = event.selectedTime; - emit(currentState); // Передаем обновленное состояние в emit - } - } - } - Future sendRetireData( - String? token, - TimeOfDay retire, - int diaryID, - ) async { - String url = dotenv.env['SERVER_ROOT']! + '/diary/???'; - final credentials = { - 'wakeup': retire, - 'diary_id': diaryID, + String? token, TimeOfDay retire, int diaryID) async { + String url = dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/retire/fill'; + final formatter = DateFormat('HH:mm'); + final retireString = + formatter.format(DateTime(2022, 1, 1, retire.hour, retire.minute)); + final Map credentials = { + 'retire': retireString, }; var response = await http.post(Uri.parse(url), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: json.encode(credentials), @@ -316,16 +499,7 @@ class DiaryEditBloc extends Bloc { var currentState = state as DiaryEditLoadedState; var updatedState = currentState.copyWith(mealTimeController: event.time); - emit(updatedState); // Передаем обновленное состояние в emit - - // Отправляем данные по HTTP маршруту и ждем ответ - var response = await sendMealTimeData(currentState.token, event.time, - currentState.currentMeal?.id, currentState.diaryId); - - if (response.statusCode == 200) { - currentState.retireController = event.time; - emit(currentState); // Передаем обновленное состояние в emit - } + emit(updatedState); } } @@ -336,7 +510,7 @@ class DiaryEditBloc extends Bloc { int diaryID, ) async { String url = dotenv.env['SERVER_ROOT']! + '/diary/???'; - final credentials = { + final Map credentials = { 'time': mealTime, 'meal_id': mealID, 'diary_id': diaryID, @@ -344,6 +518,7 @@ class DiaryEditBloc extends Bloc { var response = await http.post(Uri.parse(url), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: json.encode(credentials), @@ -352,18 +527,46 @@ class DiaryEditBloc extends Bloc { return response; } - FutureOr _changeMeal(ChangeMeal event, Emitter emit) { + FutureOr _changeMeal( + ChangeMeal event, Emitter emit) async { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; + final meal = currentState.meals?.firstWhere( + (meal) => meal['id'] == event.value.id, + orElse: () => null); int? id = event.value.id; - emit(currentState.copyWith( + print(meal['photos']); + DiaryEditLoadedState updatedState = currentState.copyWith( currentMeal: event.value, - portions: event.meals?[event.value.id ?? 0]['portions'], - mealTimeController: - toTime(event.meals?[event.value.id ?? 0]['time'] ?? "1$id:00"), - selectedMood: event.meals?[event.value.id ?? 0]['mood'], - selectedSatiety: event.meals?[event.value.id ?? 0]['satiety'])); - currentState.currentMeal = event.value; + portions: meal['portions'], + photos: meal['photos'], + mealTimeController: toTime(meal['time'] ?? "1$id:00"), + selectedMood: meal['mood'], + selectedSatiety: meal['satiety']); + print(updatedState.photos); + emit(updatedState); + } + } + + + FutureOr _clearInput( + ClearInput event, Emitter emit) { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + DiaryEditLoadedState updatedState = currentState.copyWith( + ); + emit(updatedState); + } + } + + FutureOr _changePortion( + ChangePortion event, Emitter emit) { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + DiaryEditLoadedState updatedState = currentState.copyWith( + portionController: event.value, + ); + emit(updatedState); } } @@ -391,51 +594,345 @@ class DiaryEditBloc extends Bloc { AddPortionEvent event, Emitter emit) async { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; - int? id = currentState.currentMeal?.id; - var updatedState = currentState.copyWith( - currentMeal: currentState.diaryEditModelObj?.mealList[id ?? 0], - portions: currentState.meals?[id ?? 0]['portions'], - mealTimeController: - toTime(currentState.meals?[id ?? 0]['time'] ?? "1$id:00"), - selectedMood: currentState.meals?[id ?? 0]['mood'], - selectedSatiety: currentState.meals?[id ?? 0]['satiety']); - emit(updatedState); // Передаем обновленное состояние в emit - // Отправляем данные по HTTP маршруту и ждем ответ var response = await sendPortionData( - currentState.token, - event.name, - event.portion, - event.quantity, - currentState.currentMeal!.id, - currentState.diaryId); + currentState.token, + currentState.productNameController?.text, + currentState.portionController?.id == 0 ? 'pcs' : 'gr', + int.parse(currentState.productQuantityController!.text), + currentState.currentMeal!.id, + currentState.diaryId, + ); if (response.statusCode == 200) { - emit(currentState); // Передаем обновленное состояние в emit + emit(currentState.copyWith(isAdded: true)); + await reloadScreen(event, emit); + } else { + print( + 'Failed to add portion. Status Code: ${response.statusCode} ${response.body}'); } } } - Future sendPortionData( - String? token, - String name, - String portion, - int quantity, - int? mealID, - int diaryID, - ) async { + FutureOr sendPortionData(String? token, String? name, + String portion, int? quantity, int? mealID, int diaryID) async { String url = - dotenv.env['SERVER_ROOT']! + '/$diaryID/meal/$mealID/portions/create'; - final credentials = { - 'name': name, + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/portions/create'; + + final Map credentials = { + 'product_name': name, 'portion': portion, 'quantity': quantity, - 'meal_id': mealID, - 'diary_id': diaryID, + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } + + Future _inputProduct( + ProductInput event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + String? token = currentState.token; + + final Map credentials = {'name': event.input}; + final uri = Uri.parse( + '${dotenv.env['SERVER_ROOT']!}/diary/product_nutritional_values/filter?${Uri(queryParameters: credentials).query}'); + + final response = await http.get( + uri, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + + final productNutritionalValues = + jsonDecode(response.body)['product_nutritional_value']; + + if (productNutritionalValues is List) { + // Create a new controller with the input text and update the autocomplete list + var updatedState = currentState.copyWith( + productNameController: TextEditingController(text: event.input), + autocompleteList: productNutritionalValues + .map((product) => product['name']) + .toList(), + ); + if (event.input.length > 3) { + updatedState = updatedState.copyWith(moderationExpanded: true); + } else { + updatedState = updatedState.copyWith(moderationExpanded: false); + } + + emit(updatedState); // Emit the updated state + } + } + } + + Future _removePortion( + RemovePortionEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + int? mealId = + currentState.currentMeal?.id; // Rename id to mealId for clarity + + var response = await sendRemovePortion( + currentState.token, + event.portionId, + mealId, + currentState.diaryId, + ); + + if (response.statusCode == 204) { + await reloadScreen(event, emit); + } else { + print( + 'Failed to add portion. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + Future sendRemovePortion( + String? token, int? portionID, int? mealID, int diaryID) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/portions/$portionID/delete'; + + var response = await http.delete( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + encoding: utf8, + ); + + return response; + } + + FutureOr _removeImage( + RemoveImageEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + var response = await sendRemovePhoto( + currentState.token, + event.path, + event.photoID, + currentState.currentMeal?.id, + currentState.diaryId, + ); + print(response.statusCode); + if (response.statusCode == 204) { + await reloadScreen(event, emit); + } else { + print( + 'Failed to remove photo. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + Future sendRemovePhoto(String? token, String? path, + String? photoID, int? mealID, int diaryID) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/photo/delete'; + final Map credentials = { + 'id': photoID, + 'path': path, + }; + var response = await http.delete( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } + + Future _finishDay(FinishDay event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + var response = await sendFinishDay( + currentState.token, + currentState.diaryId, + currentState.wakeupController, + currentState.retireController, + currentState.commentController?.text, + ); + + if (response.statusCode == 200) { + await reloadScreen(event, emit); + } + } + } + + Future sendFinishDay(String? token, int diaryID, + TimeOfDay? wakeup, TimeOfDay? retire, String? comment) async { + final formatter = DateFormat('HH:mm'); + final wakeupString = + formatter.format(DateTime(2022, 1, 1, wakeup!.hour, wakeup.minute)); + final retireString = + formatter.format(DateTime(2022, 1, 1, retire!.hour, retire.minute)); + + final Map credentials = { + 'wakeup': wakeupString, + 'retire': retireString, + 'comment': comment ?? " ", + }; + + String url = '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/finishDay'; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } + + Future _uploadImages( + UploadImages event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + emit(currentState.copyWith(isLoading: true)); + await uploadImageToServer(event.image.path, currentState.diaryId, + currentState.currentMeal!.id!, currentState.token); + await reloadScreen(event, emit); + } + } + + Future uploadImageToServer( + String imagePath, int diaryID, int mealId, String? token) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealId/photo/upload'; + + var request = http.MultipartRequest('POST', Uri.parse(url)); + request.headers['Authorization'] = 'Bearer $token'; + + request.files.add(await http.MultipartFile.fromPath('photo', imagePath)); + + var client = http.Client(); + var streamedResponse = await client.send(request); + + if (streamedResponse.statusCode == 200) { + var response = await http.Response.fromStream(streamedResponse); + var imageUrl = response.body; + print('Image successfully uploaded'); + return imageUrl; + } else { + throw Exception( + 'Failed to upload image. Status code: ${streamedResponse.statusCode}}'); + } + } + + FutureOr _sendModeration( + SendModeration event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + final response = await _sendModerationData( + event.name, + event.energyOn100gr, + event.proteinsOn100gr, + event.fatsOn100gr, + event.carbohydratesOn100gr, + currentState.token, + ); + + if (response.statusCode == 200) + print("Moderation success"); + else + print("Moderation error"); + } + } + + Future _sendModerationData( + String name, + String energy_on_100gr, + String proteins_on_100gr, + String fats_on_100gr, + String carbohydrates_on_100gr, + String? token) async { + final Uri url = Uri.parse( + '${dotenv.env['SERVER_ROOT']!}/diary/product_nutritional_values/sendToModeration'); + final Uri serverURL = Uri.parse('${dotenv.env['SERVER_ROOT']!}'); + String host = serverURL.host; + final Map credentials = { + 'name': name, + 'energy_on_100gr': energy_on_100gr, + 'proteins_on_100gr': proteins_on_100gr, + 'fats_on_100gr': fats_on_100gr, + 'carbohydrates_on_100gr': carbohydrates_on_100gr, + }; + + var headers = { + //'Content-Length': bodyBytes, + 'Authorization': 'Bearer $token', + 'Host': host, + }; + + var response = await http.post( + url, + headers: headers, + body: credentials, + encoding: utf8, + ); + + if (response.statusCode == 200) { + return response; + } else { + throw Exception( + 'Failed to send a product for moderation ${response.statusCode}'); + } + } + + FutureOr _addComment( + AddComment event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var updatedState = currentState.copyWith( + commentController: TextEditingController(text: event.comment), + ); + + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await sendCommentData( + currentState.token, event.comment, currentState.diaryId); + + if (response.statusCode == 200) { + emit(updatedState); // Передаем обновленное состояние в emit + } + } + } + + Future sendCommentData( + String? token, String comment, int diaryID) async { + String url = dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/retire/fill'; + final Map credentials = { + 'comment': comment, }; var response = await http.post(Uri.parse(url), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: json.encode(credentials), @@ -443,51 +940,32 @@ class DiaryEditBloc extends Bloc { return response; } -} -FutureOr _removePortion( - RemovePortionEvent event, Emitter emit) {} + TimeOfDay toTime(String timeStr) { + List timeParts = timeStr.split(":"); + int hour = int.parse(timeParts[0]); + int minute = int.parse(timeParts[1]); -Future> _uploadImages( - UploadImages event, Emitter emit) async { - String imageUrl = await uploadImageToServer(event.image.path); - print(imageUrl); -} + TimeOfDay timeOfDay = TimeOfDay(hour: hour, minute: minute); + return timeOfDay; + } -FutureOr _addComment(AddComment event, Emitter emit) {} -Future uploadImageToServer(String imagePath) async { - var request = http.MultipartRequest( - 'POST', - Uri.parse('http://your-api-url.com/upload'), // Замените на ваш маршрут API - ); + List fillMealList(List meals) { + meals.sort(compareTypes); + return meals.map((meal) { + String title = "Прием пищи ${meal['type']}"; + return SelectionPopupModel(id: meal['id'], title: title); + }).toList(); + } - request.files.add(await http.MultipartFile.fromPath('image', imagePath)); + int compareTypes(a, b) { + return a['type'].compareTo(b['type']); + } - var response = await request.send(); - if (response.statusCode == 200) { - var responseData = await response.stream.bytesToString(); - return responseData; - } else { - throw Exception('Ошибка при загрузке изображения'); + List fillPortions() { + return [ + SelectionPopupModel(id: 0, title: "порций", isSelected: true), + SelectionPopupModel(id: 1, title: "грамм"), + ]; } } - -TimeOfDay toTime(String timeStr) { - List timeParts = timeStr.split(":"); - int hour = int.parse(timeParts[0]); - int minute = int.parse(timeParts[1]); - - TimeOfDay timeOfDay = TimeOfDay(hour: hour, minute: minute); - return timeOfDay; -} - -List fillMealList() { - return [ - SelectionPopupModel(id: 0, title: "Прием пищи 1", isSelected: true), - SelectionPopupModel(id: 1, title: "Прием пищи 2"), - SelectionPopupModel(id: 2, title: "Прием пищи 3"), - SelectionPopupModel(id: 3, title: "Прием пищи 4"), - SelectionPopupModel(id: 4, title: "Прием пищи 5"), - SelectionPopupModel(id: 5, title: "Прием пищи 6"), - ]; -} diff --git a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart index 236ecbcd..8185fa28 100644 --- a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart +++ b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart @@ -79,12 +79,9 @@ class ToggleWakeupExpandedEvent extends DiaryEditEvent { } class AddPortionEvent extends DiaryEditEvent { - AddPortionEvent(this.name, this.quantity, this.portion); - final String name; - final int quantity; - final String portion; + AddPortionEvent(); @override - List get props => [name, quantity, portion]; + List get props => []; } class RemovePortionEvent extends DiaryEditEvent { @@ -101,6 +98,21 @@ class UploadImages extends DiaryEditEvent { List get props => [image]; } +class RemoveImageEvent extends DiaryEditEvent { + RemoveImageEvent(this.photoID, this.path); + final String photoID; + final String path; + @override + List get props => [photoID, path]; +} + +class ProductInput extends DiaryEditEvent { + ProductInput(this.input); + final String input; + @override + List get props => [input]; +} + class ChangeMeal extends DiaryEditEvent { ChangeMeal({required this.value, required this.meals}); @@ -109,3 +121,51 @@ class ChangeMeal extends DiaryEditEvent { @override List get props => [value, meals]; } + + +class ClearInput extends DiaryEditEvent { + ClearInput({required this.value}); + + TextEditingController value; + @override + List get props => [value]; +} + +class ChangePortion extends DiaryEditEvent { + ChangePortion({required this.value}); + + SelectionPopupModel value; + @override + List get props => [value]; +} + +class FinishDay extends DiaryEditEvent { + FinishDay(); + + @override + List get props => []; +} + +class RefreshEvent extends DiaryEditEvent { + RefreshEvent(); + + @override + List get props => []; +} + +class SendModeration extends DiaryEditEvent { + SendModeration( + {required this.name, + required this.energyOn100gr, + required this.proteinsOn100gr, + required this.fatsOn100gr, + required this.carbohydratesOn100gr}); + String name; + String energyOn100gr; + String proteinsOn100gr; + String fatsOn100gr; + String carbohydratesOn100gr; + @override + List get props => + [name, energyOn100gr, proteinsOn100gr, fatsOn100gr, carbohydratesOn100gr]; +} diff --git a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart index 4ba46d61..61762f0e 100644 --- a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart +++ b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart @@ -24,16 +24,28 @@ class DiaryEditLoadedState extends DiaryEditState { this.token, this.meals, this.portions, + this.photos, + this.autocompleteList, this.wakeupController, this.retireController, this.commentController, this.mealTimeController, + this.portionController, this.productNameController, this.productQuantityController, this.selectedMood, this.currentMeal, + this.caloriesController, + this.fatsController, + this.proteinsController, + this.carbsController, + this.isLoading = false, + this.isAdded = false, + required this.recommendedChartValues, + required this.consumedChartValues, required this.diaryId, required this.selectedSatiety, + required this.moderationExpanded, required this.wakeupExpanded, required this.moodExpanded}) : super(diaryEditModelObj: null); @@ -43,66 +55,109 @@ class DiaryEditLoadedState extends DiaryEditState { String? token; List? meals; List? portions; + List? photos; + List? autocompleteList; + List recommendedChartValues; + List consumedChartValues; SelectionPopupModel? currentMeal; + SelectionPopupModel? portionController; TimeOfDay? wakeupController; TimeOfDay? retireController; TimeOfDay? mealTimeController; TextEditingController? commentController; TextEditingController? productNameController; TextEditingController? productQuantityController; + TextEditingController? caloriesController; + TextEditingController? fatsController; + TextEditingController? proteinsController; + TextEditingController? carbsController; int? selectedMood; int selectedSatiety; int diaryId; bool wakeupExpanded; + bool moderationExpanded; bool moodExpanded; + bool isLoading; + bool isAdded; @override List get props => [ diaryEditModelObj, diaryDate, token, meals, + portions, + photos, + recommendedChartValues, + consumedChartValues, + autocompleteList, wakeupController, retireController, mealTimeController, commentController, productNameController, productQuantityController, + portionController, + caloriesController, + fatsController, + proteinsController, + carbsController, selectedMood, selectedSatiety, diaryId, wakeupExpanded, moodExpanded, + moderationExpanded, + isLoading, + isAdded, currentMeal ]; DiaryEditLoadedState copyWith({ DiaryEditModel? diaryEditModelObj, SelectionPopupModel? currentMeal, + SelectionPopupModel? portionController, String? diaryDate, String? token, + List? recommendedChartValues, + List? consumedChartValues, List? meals, List? portions, + List? photos, + List? autocompleteList, TimeOfDay? wakeupController, TimeOfDay? retireController, TimeOfDay? mealTimeController, TextEditingController? commentController, TextEditingController? productNameController, TextEditingController? productQuantityController, + TextEditingController? caloriesController, + TextEditingController? fatsController, + TextEditingController? proteinsController, + TextEditingController? carbsController, int? selectedMood, int? selectedSatiety, int? diaryId, bool? wakeupExpanded, + bool? moderationExpanded, bool? moodExpanded, + bool? isLoading, + bool? isAdded, }) { return DiaryEditLoadedState( diaryEditModelObj: diaryEditModelObj ?? this.diaryEditModelObj, diaryDate: diaryDate ?? this.diaryDate, token: token ?? this.token, meals: meals ?? this.meals, + photos: photos ?? this.photos, + autocompleteList: autocompleteList ?? this.autocompleteList, portions: portions ?? this.portions, + recommendedChartValues: + recommendedChartValues ?? this.recommendedChartValues, + consumedChartValues: consumedChartValues ?? this.consumedChartValues, wakeupController: wakeupController ?? this.wakeupController, retireController: retireController ?? this.retireController, mealTimeController: mealTimeController ?? this.mealTimeController, commentController: commentController ?? this.commentController, + portionController: portionController ?? this.portionController, productNameController: productNameController ?? this.productNameController, productQuantityController: @@ -111,8 +166,15 @@ class DiaryEditLoadedState extends DiaryEditState { selectedSatiety: selectedSatiety ?? this.selectedSatiety, diaryId: diaryId ?? this.diaryId, currentMeal: currentMeal ?? this.currentMeal, + caloriesController: caloriesController ?? this.caloriesController, + fatsController: fatsController ?? this.fatsController, + proteinsController: proteinsController ?? this.proteinsController, + carbsController: carbsController ?? this.carbsController, wakeupExpanded: wakeupExpanded ?? this.wakeupExpanded, - moodExpanded: moodExpanded ?? this.moodExpanded); + moderationExpanded: moderationExpanded ?? this.moderationExpanded, + moodExpanded: moodExpanded ?? this.moodExpanded, + isAdded: isAdded ?? this.isAdded, + isLoading: isLoading ?? this.isLoading); } } diff --git a/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart b/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart index 04206dd4..1e81c626 100644 --- a/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart +++ b/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart @@ -1,28 +1,34 @@ +// ignore_for_file: unused_local_variable + import 'bloc/diary_edit_bloc.dart'; import 'models/diary_edit_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:fl_chart/fl_chart.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:eatvio_app/widgets/custom_edit_text_form_field.dart'; import 'package:eatvio_app/widgets/custom_edit_drop_down.dart'; import 'package:eatvio_app/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/widgets/custom_scale_element.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:carousel_slider/carousel_slider.dart'; // ignore: must_be_immutable class DiaryEditScreen extends StatelessWidget { - DiaryEditScreen({Key? key}) - : super( - key: key, - ); + final void Function() onInitialize; + DiaryEditScreen({Key? key, required this.onInitialize}) : super(key: key); static Widget builder(BuildContext context) { return BlocProvider( create: (context) => DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())) ..add(DiaryEditInitialEvent()), - child: DiaryEditScreen()); + child: DiaryEditScreen( + onInitialize: () {}, + )); } Widget build(BuildContext context) { @@ -30,9 +36,8 @@ class DiaryEditScreen extends StatelessWidget { builder: (context, state) { if (state is DiaryEditInitialState) { // Это начальное состояние, данные ещё не загружены. - return CircularProgressIndicator( + return Container( color: Colors.white, - backgroundColor: Colors.white, ); } else if (state is DiaryEditLoadedState) { // Данные загружены, можно строить экран. @@ -51,48 +56,84 @@ class DiaryEditScreen extends StatelessWidget { Widget _buildDiaryEditScreen( BuildContext context, DiaryEditLoadedState state) { mediaQueryData = MediaQuery.of(context); - return FormBuilder( - child: SafeArea( - child: Scaffold( - backgroundColor: Color.fromARGB(255, 240, 242, 245), - resizeToAvoidBottomInset: true, - body: SizedBox( - width: double.maxFinite, - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only(bottom: 5.v), - child: Column( - children: [ - _buildHeaderRow(context, state), - SizedBox(height: 30.v), - _buildWakupRetire(context, state), - SizedBox(height: 30.v), - //_buildImageStack(context), - _buildMeal(context, state), - SizedBox(height: 20.v), - _buildMood(context, state), - SizedBox(height: 30.v), - _buildNutritionChart(context), - SizedBox(height: 30.v), - _buildComment(context, state), - ], - ), + return RefreshIndicator( + onRefresh: refreshPage(context), + child: Stack( + children: [ + FormBuilder( + child: SafeArea( + child: Scaffold( + backgroundColor: Color.fromARGB(255, 240, 242, 245), + resizeToAvoidBottomInset: true, + body: SizedBox( + width: double.maxFinite, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(bottom: 5.v), + child: Column( + children: [ + _buildHeaderRow(context, state), + SizedBox(height: 30.v), + _buildWakupRetire(context, state), + SizedBox(height: 30.v), + _buildMeal(context, state), + SizedBox(height: 20.v), + _buildMood(context, state), + SizedBox(height: 30.v), + _buildNutritionChart(context, state), + SizedBox(height: 30.v), + _buildComment(context, state), + ], + ), + ), + ), + ), + ], ), ), + bottomNavigationBar: _buildBottomBar(context), ), - ], + ), ), - ), - bottomNavigationBar: _buildEndDay(context), + Positioned( + left: 16.h, + right: 16.h, + bottom: 50.v, // Adjusted position to lift the button higher + child: _buildEndDay(context), + ), + ], ), - )); + ); + } + + _buildEndDay(BuildContext context) { + return CustomOutlinedButton( + text: "Закончить день", + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 41.v, + ), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + decoration: BoxDecoration( + color: Colors.transparent, + ), + onPressed: onFinishDayPressed(context), + // Установите прозрачный фон + ); + } + + refreshPage(BuildContext context) { + return () async { + context.read().add(RefreshEvent()); + }; } /// Section Widget - Widget _buildHeaderRow(BuildContext context, DiaryEditLoadedState state) { + _buildHeaderRow(BuildContext context, DiaryEditLoadedState state) { final day = state.diaryId; return Container( margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), @@ -102,7 +143,7 @@ class DiaryEditScreen extends StatelessWidget { ), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), @@ -116,10 +157,7 @@ class DiaryEditScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ CustomImageView( - imagePath: ImageConstant.imgArrowLeft, - onTap: () { - NavigatorService.goBack(); - }), + imagePath: ImageConstant.imgArrowLeft, onTap: onTapBack()), Padding( padding: EdgeInsets.only(top: 1.v), child: Column( @@ -144,19 +182,97 @@ class DiaryEditScreen extends StatelessWidget { ); } + onTapBack() { + return () { + NavigatorService.goBack(); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + /// Section Widget - Widget _buildAddProduct(BuildContext context) { + _buildAddProduct(BuildContext context, DiaryEditLoadedState state) { return CustomOutlinedButton( text: "Добавить продукт", margin: EdgeInsets.symmetric(horizontal: 10.h), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: addProduct(context), ); } + addProduct(BuildContext context) { + return () { + context.read().add(AddPortionEvent()); + }; + } + /// Section Widget - Widget _buildAddPhoto(BuildContext context) { + _buildAddPhoto(BuildContext context, DiaryEditLoadedState state) { + bool isLoading = state.isLoading; return GestureDetector( - onTap: () async { + onTap: addImage(context, isLoading), + child: Stack( + children: [ + if (!isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + // Показывайте прелоадер, если загрузка активна + + Padding( + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + "Добавить фото", + selectionColor: appTheme.blueA400, + ), + ), + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ) + ], + ), + ), + if (isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Center( + child: CircularProgressIndicator( + color: appTheme + .blueGray300), // Замените на свой прелоадер при необходимости + ), + ), + ], + ), + ); + } + + addImage(BuildContext context, bool isLoading) { + if (!isLoading) { + return () async { final picker = ImagePicker(); List? images = await picker.pickMultiImage(); if (images.isNotEmpty) { @@ -164,75 +280,49 @@ class DiaryEditScreen extends StatelessWidget { context.read().add(UploadImages(image)); } } - }, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 10.h), - padding: EdgeInsets.symmetric( - horizontal: 40.h, - vertical: 80.v, - ), - decoration: AppDecoration.fillGray.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder5, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.only( - left: 7.h, - top: 5.v, - ), - child: Text( - "Добавить фото", - selectionColor: appTheme.blueA400, - ), - ), - Spacer(), - Icon( - Icons.photo_library, - color: appTheme.blueGray300, - ) - ], - ), - ), - ); + }; + } } _buildMeal(BuildContext context, DiaryEditLoadedState state) { final digitsOnly = FilteringTextInputFormatter.digitsOnly; - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 24.h, - vertical: 12.v, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Время приема пищи", - style: CustomTextStyles.bodyMediumBlack900, - ), - Padding( + List _options = + state.autocompleteList!.map((dynamic item) => item as String).toList(); + //print(_options); + FocusNode focusNodeName = FocusNode(); + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Время приема пищи", + style: CustomTextStyles.bodyMediumBlack900, + ), + Expanded( + // Adding Expanded widget + child: Padding( padding: EdgeInsets.symmetric(horizontal: 10.h), child: ConstrainedBox( constraints: BoxConstraints( @@ -251,136 +341,342 @@ class DiaryEditScreen extends StatelessWidget { ), ), ), - ], - ), + ), + ], ), - SizedBox(height: 20.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), - child: BlocSelector( - selector: (state) => state.diaryEditModelObj, - builder: (context, diaryEditModelObj) { - return CustomDropDown( - hintText: "Прием пищи", - items: diaryEditModelObj?.mealList, - contentPadding: EdgeInsets.symmetric( - horizontal: 10.h, - vertical: 9.v, + ), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: BlocSelector( + selector: (state) => state.diaryEditModelObj, + builder: (context, diaryEditModelObj) { + return CustomDropDown( + hintText: "Прием пищи", + items: diaryEditModelObj?.mealList, + contentPadding: EdgeInsets.symmetric( + horizontal: 10.h, + vertical: 9.v, + ), + defaultValue: diaryEditModelObj?.mealList[0], + onChanged: changeMeal(context, state), + ); + })), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: RawAutocomplete( + optionsBuilder: buildOptions(_options), + fieldViewBuilder: ( + BuildContext context, + TextEditingController productEditingController, + FocusNode focusNodeName, + VoidCallback onFieldSubmitted, + ) { + if (state.isAdded) { + print("Clear"); + } + return CustomTextFormField( + hintText: "Введите название продукта", + controller: productEditingController, + focusNode: focusNodeName, + onChanged: getAutocomplete(context, focusNodeName), + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: SizedBox( + height: 200.0, + child: ListView.builder( + padding: const EdgeInsets.all(8.0), + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final String option = options.elementAt(index); + return GestureDetector( + onTap: selectProduct(onSelected, option, state), + child: ListTile( + title: Text(option), + ), + ); + }, ), - defaultValue: diaryEditModelObj?.mealList[0], - onChanged: (value) { - context.read().add( - ChangeMeal(meals: state.meals, value: value)); - }, - ); - })), - SizedBox(height: 20.v), + ), + ), + ); + }), + ), + SizedBox(height: 20.v), + if (state.moderationExpanded) Padding( padding: EdgeInsets.symmetric(horizontal: 10.h), - child: CustomTextFormField( - hintText: "Введите названия продукта", - controller: state.productNameController, + child: GestureDetector( + onTap: () => openCaloriesDialog(context, state), + child: Text("Отправить на модерацию"), ), ), - SizedBox(height: 20.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ CustomTextFormField( - width: 146.h, + width: 150.h, hintText: "Кол-во", textInputAction: TextInputAction.done, + // focusNode: focusNodeQuantity, controller: state.productQuantityController, keyboardType: TextInputType.number, inputFormatters: [digitsOnly], ), Padding( - padding: EdgeInsets.only(right: 30.h), - child: Text( - "порций", - style: CustomTextStyles.bodyMediumBlack900, - )), + padding: EdgeInsets.only(left: 30.h), + child: BlocSelector( + selector: (state) => state.diaryEditModelObj, + builder: (context, diaryEditModelObj) { + return CustomDropDown( + items: diaryEditModelObj?.portionList, + defaultValue: diaryEditModelObj?.portionList?[0], + onChanged: changePortion(context, state), + ); + }, + ), + ), ], ), ), - SizedBox(height: 30.v), - _buildAddProduct(context), - SizedBox(height: 30.v), - ...state.portions! - .map((portion) => _buildProduct(context, portion)) - .toList(), - SizedBox(height: 30.v), - _buildAddPhoto(context), - SizedBox(height: 30.v), - Text( - "Ваша сытость после приёма пищи", - style: CustomTextStyles.bodyMediumBlack900, + ), + SizedBox(height: 30.v), + _buildAddProduct(context, state), + SizedBox(height: 30.v), + ...state.portions! + .map((portion) => _buildProduct(context, portion)) + .toList(), + SizedBox(height: 30.v), + /*...state.photos != null + ? state.photos!.isEmpty + ? [Text('Фото пока нет')] + : state.photos! + .map((photo) => _buildPhoto(context, photo)) + .toList() + : [Text('Фото нет')],*/ + _buildCarousel(context, state), + SizedBox(height: 20.v), + _buildAddPhoto(context, state), + SizedBox(height: 30.v), + Text( + "Ваша сытость после приёма пищи", + style: CustomTextStyles.bodyMediumBlack900, + ), + SizedBox(height: 15.v), + BlocBuilder( + builder: (context, state) { + if (state is DiaryEditLoadedState) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("😭"), + ScaleElement( + color: Colors.red, + isSelected: state.selectedSatiety == 0, + onTap: changeSatiety(context, 0), + ), + ScaleElement( + color: Colors.orange, + isSelected: state.selectedSatiety == 1, + onTap: changeSatiety(context, 1), + ), + ScaleElement( + color: Colors.yellow, + isSelected: state.selectedSatiety == 2, + onTap: changeSatiety(context, 2), + ), + ScaleElement( + color: Colors.lime, + isSelected: state.selectedSatiety == 3, + onTap: changeSatiety(context, 3), + ), + ScaleElement( + color: Colors.lightGreen, + isSelected: state.selectedSatiety == 4, + onTap: changeSatiety(context, 4), + ), + ScaleElement( + color: Colors.green, + isSelected: state.selectedSatiety == 5, + onTap: changeSatiety(context, 5), + ), + Text("😊"), + // Остальные элементы шкалы + ], + ); + } else { + return Container(); // Возвращаем пустой контейнер, если состояние не является DiaryEditLoadedState + } + }, + ) + ], + )); + } + + openCaloriesDialog(BuildContext context, DiaryEditLoadedState state) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text('Введите информацию о продукте'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomTextFormField( + hintText: "Название", + controller: state.productNameController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Энергетическая ценность на 100", + keyboardType: TextInputType.number, + controller: state.caloriesController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Белки на 100г", + keyboardType: TextInputType.number, + controller: state.proteinsController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Жиры на 100г", + keyboardType: TextInputType.number, + controller: state.fatsController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Углеводы на 100г", + keyboardType: TextInputType.number, + controller: state.carbsController, ), - SizedBox(height: 15.v), - BlocBuilder( - builder: (context, state) { - if (state is DiaryEditLoadedState) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("😭"), - ScaleElement( - color: Colors.red, - isSelected: state.selectedSatiety == 0, - onTap: () { - context.read().add(ChangeSatiety(0)); - }, - ), - ScaleElement( - color: Colors.orange, - isSelected: state.selectedSatiety == 1, - onTap: () { - context.read().add(ChangeSatiety(1)); - }, - ), - ScaleElement( - color: Colors.yellow, - isSelected: state.selectedSatiety == 2, - onTap: () { - context.read().add(ChangeSatiety(2)); - }, - ), - ScaleElement( - color: Colors.lime, - isSelected: state.selectedSatiety == 3, - onTap: () { - context.read().add(ChangeSatiety(3)); - }, - ), - ScaleElement( - color: Colors.lightGreen, - isSelected: state.selectedSatiety == 4, - onTap: () { - context.read().add(ChangeSatiety(4)); - }, - ), - ScaleElement( - color: Colors.green, - isSelected: state.selectedSatiety == 5, - onTap: () { - context.read().add(ChangeSatiety(5)); - }, - ), - Text("😊"), - // Остальные элементы шкалы - ], - ); - } else { - return Container(); // Возвращаем пустой контейнер, если состояние не является DiaryEditLoadedState - } - }, - ) ], ), - )); + actions: [ + TextButton( + onPressed: popDialog(context), + child: Text('Отмена'), + ), + TextButton( + onPressed: triggerModeration( + context, + state.productNameController!.text, + state.caloriesController!.text, + state.proteinsController!.text, + state.fatsController!.text, + state.carbsController!.text, + dialogContext), + child: Text('Отправить'), + ), + ], + ); + }, + ); + } + + triggerModeration( + BuildContext context, + String name, + String energyOn100gr, + String proteinsOn100gr, + String fatsOn100gr, + String carbohydratesOn100gr, + BuildContext dialogContext) { + return () { + // Call function to send moderation with the entered values + context.read().add(SendModeration( + name: name, + energyOn100gr: energyOn100gr, + proteinsOn100gr: proteinsOn100gr, + fatsOn100gr: fatsOn100gr, + carbohydratesOn100gr: carbohydratesOn100gr)); + Navigator.of(dialogContext).pop(); // Dismiss the dialog + }; + } + + popDialog(BuildContext dialogContext) { + return () { + Navigator.of(dialogContext) + .pop(); // Dismiss the dialog on Cancel button press + }; + } + + changeSatiety(BuildContext context, int satiety) { + return () { + context.read().add(ChangeSatiety(satiety)); + }; + } + + selectProduct(AutocompleteOnSelected onSelected, String option, + DiaryEditLoadedState state) { + state.productNameController?.text = option; + return () { + onSelected(option); + }; + } + + getAutocomplete(BuildContext context, FocusNode focusNode) { + return (value) { + /*print(state.autocompleteList + ?.map((dynamic item) => item as String) + .toList());*/ + if (value.length > 2) { + context.read().add(ProductInput(value)); + } + }; + } + + buildOptions(List _options) { + return (TextEditingValue textEditingValue) { + return _options.where((String option) { + return true; + }); + }; + } + + changeMeal(BuildContext context, DiaryEditLoadedState state) { + return (value) { + context + .read() + .add(ChangeMeal(meals: state.meals, value: value)); + }; + } + + changePortion(BuildContext context, DiaryEditLoadedState state) { + return (value) { + context.read().add(ChangePortion(value: value)); + }; + } + + _buildCarousel(BuildContext context, DiaryEditLoadedState state) { + if (state.photos!.isEmpty) { + return Text("Фото нет"); + } else { + return CarouselSlider( + options: CarouselOptions(height: 340.0), + items: state.photos!.map((photo) { + return _buildPhoto( + context, photo); // Assuming photos are stored as strings + }).toList(), + ); + } } _buildProduct(BuildContext context, Map portion) { @@ -398,15 +694,16 @@ class DiaryEditScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: EdgeInsets.only( - left: 7.h, - top: 5.v, - ), - child: Text( - portion['product_name'], - style: CustomTextStyles.titleMedium16, - ), - ), + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + portion['product_name'].length > 8 + ? portion['product_name'].substring(0, 8) + : portion['product_name'], + style: CustomTextStyles.titleMedium16, + )), Padding( padding: EdgeInsets.only( left: 20.h, @@ -414,16 +711,14 @@ class DiaryEditScreen extends StatelessWidget { bottom: 3.v, ), child: Text( - portion['quantity'] + portion['portion'], + portion['quantity'].toString() + + ' ' + + portion['portion'].toString().tr, ), ), Spacer(), GestureDetector( - onTap: () { - context - .read() - .add(RemovePortionEvent(portion['id'])); - }, + onTap: removePortion(context, portion), child: Padding( padding: const EdgeInsets.only(right: 10), child: Icon( @@ -436,7 +731,58 @@ class DiaryEditScreen extends StatelessWidget { ); } - Widget _buildNutritionChart(BuildContext context) { + _buildPhoto(BuildContext context, Map photo) { + return Stack( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h, vertical: 10.h), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Image.network( + dotenv.env['STORAGE_URL']! + photo['path'], + ), + ), + Positioned( + top: 5, + right: 5, + child: GestureDetector( + onTap: removePhoto(context, photo), + child: Container( + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + ), + child: Icon( + Icons.close, + color: appTheme.blueGray300, + ), + ), + ), + ), + ], + ); + } + + removePortion(BuildContext context, Map portion) { + return () { + context.read().add(RemovePortionEvent(portion['id'])); + }; + } + + removePhoto(BuildContext context, Map photo) { + return () { + context + .read() + .add(RemoveImageEvent(photo['id'].toString(), photo['path'])); + }; + } + + _buildNutritionChart(BuildContext context, DiaryEditLoadedState state) { + final List recommendedValues = state + .recommendedChartValues; // Рекомендуемые значения: калории, жиры, белки, углеводы + final List consumedValues = state.consumedChartValues; // return Container( margin: EdgeInsets.symmetric(horizontal: 16.h), padding: EdgeInsets.symmetric( @@ -445,7 +791,7 @@ class DiaryEditScreen extends StatelessWidget { ), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), @@ -477,142 +823,108 @@ class DiaryEditScreen extends StatelessWidget { left: 30.h, right: 12.h, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox(height: 101.v), - Container( - height: 47.v, - width: 12.h, - decoration: BoxDecoration( - color: theme.colorScheme.errorContainer, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(1.h), - ), - ), - ), - ], - ), - ), - Spacer( - flex: 33, - ), - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 66.v), - Container( - height: 82.v, - width: 12.h, - decoration: BoxDecoration( - color: appTheme.lightGreenA700, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(1.h), - ), - ), - ), - ], - ), - ), - Spacer( - flex: 33, - ), - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 14.v), - Container( - height: 134.v, - width: 12.h, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(1.h), - ), - ), - ), - ], - ), - ), - Spacer( - flex: 33, - ), - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Container( - height: 148.v, - width: 12.h, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular( - 1.h, + child: AspectRatio( + aspectRatio: 1.5, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + //getTitlesWidget: (value, meta) => newMethod(value), ), ), ), + borderData: FlBorderData(show: false), + barGroups: [ + BarChartGroupData(x: 0, barRods: [ + BarChartRodData( + toY: consumedValues[0] / recommendedValues[0] * 100, + color: checkConsumedValues( + [recommendedValues[0]], [consumedValues[1]]) + ? Colors.red + : Colors.blue, + ) + ]), + BarChartGroupData(x: 1, barRods: [ + BarChartRodData( + toY: consumedValues[1] / recommendedValues[1] * 100, + color: checkConsumedValues( + [recommendedValues[1]], [consumedValues[1]]) + ? Colors.red + : Colors.blue, + ) + ]), + BarChartGroupData(x: 2, barRods: [ + BarChartRodData( + toY: consumedValues[2] / recommendedValues[2] * 100, + color: checkConsumedValues( + [recommendedValues[2]], [consumedValues[2]]) + ? Colors.red + : Colors.blue, + ) + ]), + BarChartGroupData(x: 3, barRods: [ + BarChartRodData( + toY: consumedValues[3] / recommendedValues[3] * 100, + color: checkConsumedValues( + [recommendedValues[3]], [consumedValues[3]]) + ? Colors.red + : Colors.blue, + ) + ]), + ], ), - Padding( - padding: EdgeInsets.only( - left: 6.h, - bottom: 128.v, - ), - child: Text( - "*", - style: theme.textTheme.titleLarge, - ), - ), - ], + ), ), ), ), SizedBox(height: 8.v), + SizedBox(height: 2.v), Padding( - padding: EdgeInsets.only(left: 4.h), + padding: EdgeInsets.only( + left: 10.h, + right: 2.h, + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Калории", - style: theme.textTheme.titleSmall, + "Калорий", + style: theme.textTheme.bodySmall, ), Padding( - padding: EdgeInsets.only(left: 28.h), + padding: EdgeInsets.only( + left: 25.h, + bottom: 11.v, + ), child: Text( - "Белки", - style: theme.textTheme.titleSmall, + "Белков", + style: theme.textTheme.bodySmall, ), ), - Spacer(), - Text( - "Жиры", - style: theme.textTheme.titleSmall, + Padding( + padding: EdgeInsets.only( + left: 19.h, + bottom: 11.v, + ), + child: Text( + "Жиров", + style: theme.textTheme.bodySmall, + ), ), Padding( - padding: EdgeInsets.only(left: 25.h), + padding: EdgeInsets.only( + left: 14.h, + bottom: 11.v, + ), child: Text( - "Углеводы", - style: theme.textTheme.titleSmall, + "Углеводов", + style: theme.textTheme.bodySmall, ), ), ], @@ -629,7 +941,7 @@ class DiaryEditScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "255 из 250Ккал", + "${consumedValues[0].toInt()} из ${recommendedValues[0].toInt()}", style: theme.textTheme.bodySmall, ), Padding( @@ -638,7 +950,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "50 из 250 гр", + "${consumedValues[1].toInt()} из ${recommendedValues[1].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -648,7 +960,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "270 из 250 гр", + "${consumedValues[2].toInt()} из ${recommendedValues[2].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -658,7 +970,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "400 из 250 гр", + "${consumedValues[3].toInt()} из ${recommendedValues[3].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -666,263 +978,268 @@ class DiaryEditScreen extends StatelessWidget { ), ), SizedBox(height: 19.v), - Text( - "*Превышенная суточная норма углеводов", - style: theme.textTheme.labelLarge, - ), + if (checkConsumedValues(recommendedValues, consumedValues)) + Text( + "*Превышенная суточная норма", + style: theme.textTheme.labelLarge, + ) ], ), ); } /// Section Widget - Widget _buildComment(BuildContext context, DiaryEditLoadedState state) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 24.h, - vertical: 12.v, + _buildComment(BuildContext context, DiaryEditLoadedState state) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Дополнительная информация для консультанта", + style: theme.textTheme.titleMedium, + textAlign: TextAlign.center, ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Дополнительная информация для консультанта", - style: theme.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - Align( - alignment: Alignment.bottomCenter, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 20.v), - Container( - width: 320.h, - child: CustomTextFormField( - autofocus: false, - hintText: "Введите информацию для консультанта", - maxLines: 7, - borderDecoration: InputBorder.none, - controller: state.commentController, - ), - ), - ], - ), - ), - ], + SizedBox(height: 20.v), + Container( + width: 320.h, + child: CustomTextFormField( + autofocus: false, + hintText: "Введите информацию для консультанта", + maxLines: 7, + borderDecoration: InputBorder.none, + controller: state.commentController, + ), ), - )); + ], + ), + ); } - Widget _buildWakupRetire(BuildContext context, DiaryEditLoadedState state) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 24.h, - vertical: 12.v, + _buildWakupRetire(BuildContext context, DiaryEditLoadedState state) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], + ], + ), + child: Column( + children: [ + GestureDetector( + onTap: toggleWakeup(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + r"Подъем \ отход ко сну", + style: CustomTextStyles.bodyMediumBlack900, + ), + Icon(state.wakeupExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down), + ], + ), ), - child: Column( - children: [ - GestureDetector( - onTap: () { - BlocProvider.of(context) - .add(ToggleWakeupExpandedEvent()); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - r"Подъем \ отход ко сну", - style: CustomTextStyles.bodyMediumBlack900, + if (state.wakeupExpanded) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Column( + children: [ + SizedBox(height: 20.v), + GestureDetector( + onTap: () => _selectWakeupTime(context, state), + child: AbsorbPointer( + child: CustomTextFormField( + hintText: + state.wakeupController?.format(context).toString(), + prefix: Icon(Icons.sunny), + autofocus: false, + ), ), - Icon(state.wakeupExpanded - ? Icons.arrow_drop_up - : Icons.arrow_drop_down), - ], - ), - ), - if (state.wakeupExpanded) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Column( - children: [ - SizedBox(height: 20.v), - GestureDetector( - onTap: () => _selectWakeupTime(context, state), - child: AbsorbPointer( - child: CustomTextFormField( - hintText: state.wakeupController - ?.format(context) - .toString(), - prefix: Icon(Icons.sunny), - autofocus: false, - /*initialValue: state.wakeupController - ?.format(context) - .toString()*/ - ), - ), - ), - SizedBox(height: 20.v), - GestureDetector( - onTap: () => _selectRetireTime(context, state), - child: AbsorbPointer( - child: CustomTextFormField( - hintText: state.retireController - ?.format(context) - .toString(), - prefix: Icon(Icons.bed), - autofocus: false, - /*initialValue: state.retireController - ?.format(context) - .toString(),*/ - ), - ), - ), - SizedBox(height: 20.v), - ], ), - ), - ], - )), + SizedBox(height: 20.v), + GestureDetector( + onTap: () => _selectRetireTime(context, state), + child: AbsorbPointer( + child: CustomTextFormField( + hintText: + state.retireController?.format(context).toString(), + prefix: Icon(Icons.bed), + autofocus: false, + ), + ), + ), + SizedBox(height: 20.v), + ], + ), + ), + ], + ), ); } + toggleWakeup(BuildContext context) { + return () { + BlocProvider.of(context).add(ToggleWakeupExpandedEvent()); + }; + } + /// Section Widget - Widget _buildEndDay(BuildContext context) { - return CustomOutlinedButton( - text: "Закончить день", - margin: EdgeInsets.only( - left: 16.h, - right: 16.h, - bottom: 41.v, - ), - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - decoration: BoxDecoration( - color: Colors.transparent, - ), // Установите прозрачный фон - ); + + Function() onFinishDayPressed(BuildContext context) { + return () { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text("Подтвердите действие"), + content: Text("Вы уверены, что хотите завершить день?"), + actions: [ + TextButton( + child: Text("Отмена"), + onPressed: closeDialog(dialogContext), + ), + TextButton( + child: Text("Подтвердить"), + onPressed: sendFinishDayData(dialogContext, context), + ), + ], + ); + }, + ); + }; } - Widget _buildMood(BuildContext context, DiaryEditLoadedState state) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 24.h, - vertical: 12.v, + Function() sendFinishDayData( + BuildContext dialogContext, BuildContext context) { + return () { + closeDialog(dialogContext); + context.read().add(FinishDay()); + }; + } + + Function() closeDialog(BuildContext dialogContext) { + return () { + Navigator.of(dialogContext).pop(); + }; + } + + _buildMood(BuildContext context, DiaryEditLoadedState state) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), + ], + ), + child: Column(children: [ + GestureDetector( + onTap: toggleMood(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Ваше настроение после приема пищи", + style: CustomTextStyles.bodyMediumBlack900, ), + Icon(state.moodExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down), ], ), - child: Column(children: [ - GestureDetector( - onTap: () { - BlocProvider.of(context) - .add(ToggleMoodExpandedEvent()); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Ваше настроение после приема пищи", - style: CustomTextStyles.bodyMediumBlack900, - ), - Icon(state.moodExpanded - ? Icons.arrow_drop_up - : Icons.arrow_drop_down), - ], + ), + if (state.moodExpanded) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("😭"), + ScaleElement( + color: Colors.red, + isSelected: state.selectedMood == 0, + onTap: changeMood(context, 0), ), - ), - if (state.moodExpanded) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("😭"), - ScaleElement( - color: Colors.red, - isSelected: state.selectedMood == 0, - onTap: () { - context.read().add(ChangeMood(0)); - }, - ), - ScaleElement( - color: Colors.orange, - isSelected: state.selectedMood == 1, - onTap: () { - context.read().add(ChangeMood(1)); - }, - ), - ScaleElement( - color: Colors.yellow, - isSelected: state.selectedMood == 2, - onTap: () { - context.read().add(ChangeMood(2)); - }, - ), - ScaleElement( - color: Colors.lime, - isSelected: state.selectedMood == 3, - onTap: () { - context.read().add(ChangeMood(3)); - }, - ), - ScaleElement( - color: Colors.lightGreen, - isSelected: state.selectedMood == 4, - onTap: () { - context.read().add(ChangeMood(4)); - }, - ), - ScaleElement( - color: Colors.green, - isSelected: state.selectedMood == 5, - onTap: () { - context.read().add(ChangeMood(5)); - }, - ), - Text("😊"), - // Остальные элементы шкалы - ], + ScaleElement( + color: Colors.orange, + isSelected: state.selectedMood == 1, + onTap: changeMood(context, 1), ), - ]))); + ScaleElement( + color: Colors.yellow, + isSelected: state.selectedMood == 2, + onTap: changeMood(context, 2), + ), + ScaleElement( + color: Colors.lime, + isSelected: state.selectedMood == 3, + onTap: changeMood(context, 3), + ), + ScaleElement( + color: Colors.lightGreen, + isSelected: state.selectedMood == 4, + onTap: changeMood(context, 4), + ), + ScaleElement( + color: Colors.green, + isSelected: state.selectedMood == 5, + onTap: changeMood(context, 5), + ), + Text("😊"), + // Остальные элементы шкалы + ], + ), + ])); + } + + changeMood(BuildContext context, int satiety) { + return () { + context.read().add(ChangeMood(satiety)); + }; + } + + toggleMood(BuildContext context) { + return () { + BlocProvider.of(context).add(ToggleMoodExpandedEvent()); + }; } Future _selectWakeupTime( @@ -933,9 +1250,7 @@ class DiaryEditScreen extends StatelessWidget { ); if (pickedTime != null && pickedTime != state.wakeupController) { state.wakeupController = pickedTime; - context.read().add(WakeupEvent( - pickedTime, - )); + context.read().add(WakeupEvent(pickedTime)); } } @@ -963,3 +1278,15 @@ class DiaryEditScreen extends StatelessWidget { } } } + +bool checkConsumedValues( + List recommendedValues, List consumedValues) { + for (int i = 0; i < recommendedValues.length; i++) { + if (consumedValues[i] > recommendedValues[i]) { + // Если хотя бы одно потребленное значение превышает рекомендованное + return true; // Возвращаем false + } + } + // Если ни одно потребленное значение не превышает рекомендованное с соответствующим индексом + return false; // Возвращаем true +} diff --git a/lib/presentation/diary/diary_edit_screen/models/diary_edit_model.dart b/lib/presentation/diary/diary_edit_screen/models/diary_edit_model.dart index a175b889..6f68bc2d 100644 --- a/lib/presentation/diary/diary_edit_screen/models/diary_edit_model.dart +++ b/lib/presentation/diary/diary_edit_screen/models/diary_edit_model.dart @@ -6,17 +6,20 @@ import 'package:eatvio_app/data/models/selectionPopupModel/selection_popup_model /// This class defines the variables used in the [k0_page], /// and is typically used to hold data that is passed between different parts of the application. class DiaryEditModel extends Equatable { - DiaryEditModel({this.mealList = const []}); + DiaryEditModel({this.mealList = const [], this.portionList = const []}); List mealList; + List? portionList; DiaryEditModel copyWith({ List? mealList, + List? portionList, }) { return DiaryEditModel( mealList: mealList ?? this.mealList, + portionList: portionList ?? this.portionList, ); } @override - List get props => [mealList]; + List get props => [mealList, portionList]; } diff --git a/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart b/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart new file mode 100644 index 00000000..e517c1ac --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_create_screen/models/diet_create_model.dart'; +part 'diet_create_event.dart'; +part 'diet_create_state.dart'; + +/// A bloc that manages the state of a DietCreate according to the event that is dispatched to it. +class DietCreateBloc extends Bloc { + DietCreateBloc(DietCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createDiet); + } + + _onInitialize( + DietCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is DietCreateLoadedState) { + var currentState = state as DietCreateLoadedState; + emit(currentState.copyWith( + dietModelObj: currentState.dietModelObj, + name: currentState.name, + details: currentState.details, + )); + } else { + emit(DietCreateLoadedState( + name: TextEditingController(), + details: TextEditingController(), + )); + } + } + + FutureOr _createDiet( + CreateDiet event, Emitter emit) async { + if (state is DietCreateLoadedState) { + var currentState = state as DietCreateLoadedState; + + var response = await sendDiet( + currentState.name?.text, + currentState.details?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.dietSelectScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendDiet( + String? name, + String? details, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/diets/create'; + + final Map credentials = { + 'name': name, + 'details': details, + 'duration': '14', + 'state': 'active', + 'type': '{"meat": true}', + 'daily_schedule': + '{"1": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 494}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 12}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 432}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 195}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 710}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 119}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 287}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 445}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 500}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 764}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 735}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 127}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 89}]}, "2": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 436}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 700}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 766}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 194}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 426}], "snacks": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 58}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 152}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 925}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 224}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 918}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 81}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 308}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 579}]}, "3": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 266}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 790}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 26}], "snacks": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 549}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 446}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 92}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 403}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 894}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 457}]}, "4": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 698}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 43}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 994}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 965}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 42}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 948}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 28}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 54}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 112}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 891}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 538}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 635}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 586}]}, "5": {"lunch": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 900}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 242}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 375}], "snacks": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 604}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 510}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 887}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 554}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 731}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 574}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 274}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 276}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 80}]}, "6": {"lunch": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 359}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 358}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 639}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 861}], "dinner": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 122}], "snacks": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 980}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 837}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 444}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 577}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 706}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 839}]}, "7": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 118}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 664}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 665}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 986}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 296}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 807}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 493}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 227}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 385}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 448}], "breakfast": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 784}]}, "8": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 433}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 871}], "dinner": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 332}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 41}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 388}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 649}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 114}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 968}], "breakfast": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 180}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 269}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 998}]}, "9": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 68}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 505}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 554}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 579}], "snacks": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 751}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 144}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 677}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 263}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 304}], "breakfast": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 963}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 258}]}, "10": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 167}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 306}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 243}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 79}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 560}], "snacks": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 294}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 145}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 639}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 108}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 924}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 894}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 280}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 336}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 338}]}, "11": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 817}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 968}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 525}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 864}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 457}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 548}], "snacks": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 658}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 239}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 358}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 416}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 220}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 323}]}, "12": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 377}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 138}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 654}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 237}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 902}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 76}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 723}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 481}], "breakfast": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 571}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 515}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 188}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 411}]}, "13": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 379}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 231}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 147}], "dinner": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 871}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 679}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 359}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 561}], "snacks": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 414}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 827}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 649}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 161}], "breakfast": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 185}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 464}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 387}]}, "14": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 248}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 5}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 693}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 655}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 709}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 881}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 588}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 36}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 453}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 692}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 186}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 721}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 740}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 731}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 842}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 850}]}, "15": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 380}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 689}], "dinner": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 326}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 629}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 201}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 129}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 749}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 315}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 444}]}, "16": {"lunch": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 953}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 101}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 684}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 506}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 333}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 416}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 796}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 756}], "supper": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 858}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 619}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 800}], "breakfast": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 539}]}, "17": {"lunch": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 134}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 977}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 209}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 227}], "breakfast": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 195}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 917}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 706}]}, "18": {"lunch": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 239}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 798}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 155}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 959}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 958}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 984}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 134}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 432}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 242}]}, "19": {"lunch": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 906}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 152}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 610}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 605}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 498}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 616}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 512}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 146}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 200}], "supper": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 764}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 353}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 181}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 783}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 506}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 887}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 130}]}, "20": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 48}], "dinner": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 266}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 143}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 55}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 174}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 855}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 6}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 878}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}], "breakfast": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 442}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "milliliters", "quantity": 558}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 885}]}, "21": {"lunch": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 504}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 652}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 996}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 200}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 641}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 87}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 689}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 143}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 475}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 924}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 98}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 438}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 383}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 462}]}}' + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/diets/diet_create_screen/bloc/diet_create_event.dart b/lib/presentation/diets/diet_create_screen/bloc/diet_create_event.dart new file mode 100644 index 00000000..a346af77 --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///DietCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietCreateEvent extends Equatable {} + +/// Event that is dispatched when the DietCreate widget is first created. +class DietCreateInitialEvent extends DietCreateEvent { + @override + List get props => []; +} + +class CreateDiet extends DietCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/diets/diet_create_screen/bloc/diet_create_state.dart b/lib/presentation/diets/diet_create_screen/bloc/diet_create_state.dart new file mode 100644 index 00000000..be825abe --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_state.dart @@ -0,0 +1,65 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_create_bloc.dart'; + +/// Represents the state of DietCreate in the application. +/// + +class DietCreateState extends Equatable { + DietCreateState({ + this.dietModelObj, + }); + + DietCreateModel? dietModelObj; + + @override + List get props => [dietModelObj]; + DietCreateState copyWith({DietCreateModel? dietModelObj}) { + return DietCreateState( + dietModelObj: dietModelObj ?? this.dietModelObj, + ); + } +} + +class DietCreateLoadedState extends DietCreateState { + DietCreateLoadedState({ + this.dietModelObj, + this.name, + this.details, + }); + + DietCreateModel? dietModelObj; + TextEditingController? name; + TextEditingController? details; + @override + List get props => [ + dietModelObj, + name, + details, + ]; + DietCreateLoadedState copyWith({ + DietCreateModel? dietModelObj, + TextEditingController? name, + TextEditingController? details, + }) { + return DietCreateLoadedState( + dietModelObj: dietModelObj ?? this.dietModelObj, + name: name ?? this.name, + details: details ?? this.details, + ); + } +} + +class DietCreateErrorState extends DietCreateState { + final String errorMessage; + + DietCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class DietCreateInitialState extends DietCreateState { + DietCreateInitialState({required DietCreateModel dietModelObj}) + : super(dietModelObj: dietModelObj); +} diff --git a/lib/presentation/diets/diet_create_screen/diet_create_screen.dart b/lib/presentation/diets/diet_create_screen/diet_create_screen.dart new file mode 100644 index 00000000..6896701c --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/diet_create_screen.dart @@ -0,0 +1,97 @@ +import 'bloc/diet_create_bloc.dart'; +import 'models/diet_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; + +class DietCreateScreen extends StatelessWidget { + const DietCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final dietID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => DietCreateBloc(DietCreateState( + dietModelObj: DietCreateModel(dietID: dietID.toString()))) + ..add(DietCreateInitialEvent()), + child: DietCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DietCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildDietCreate(context, state); + } else if (state is DietCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildDietCreate(BuildContext context, DietCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(CreateDiet()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/diets/diet_create_screen/models/diet_create_model.dart b/lib/presentation/diets/diet_create_screen/models/diet_create_model.dart new file mode 100644 index 00000000..955e8d63 --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/models/diet_create_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [diet_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class DietCreateModel extends Equatable { + final String dietID; + + DietCreateModel({required String dietID}) : dietID = dietID; + + DietCreateModel copyWith(String dietID) { + return DietCreateModel(dietID: dietID); + } + + @override + List get props => [dietID]; +} diff --git a/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart new file mode 100644 index 00000000..d8167793 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_edit_screen/models/diet_edit_model.dart'; +part 'diet_edit_event.dart'; +part 'diet_edit_state.dart'; + +/// A bloc that manages the state of a DietEdit according to the event that is dispatched to it. +class DietEditBloc extends Bloc { + DietEditBloc(DietEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editDiet); + } + + _onInitialize( + DietEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? dietID = state.dietModelObj?.dietID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/show/$dietID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is DietEditLoadedState) { + var currentState = state as DietEditLoadedState; + emit(currentState.copyWith( + dietModelObj: currentState.dietModelObj, + diet: currentState.diet, + name: currentState.name, + duration: currentState.duration, + details: currentState.details)); + } else { + emit(DietEditLoadedState( + diet: data, + name: TextEditingController(text: data['name']), + details: TextEditingController(text: data['details']), + duration: TextEditingController(text: data['duration']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(DietEditErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(DietEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editDiet(EditDiet event, Emitter emit) async { + if (state is DietEditLoadedState) { + var currentState = state as DietEditLoadedState; + int duration = int.parse(currentState.duration!.text) * 7; + print(duration); + var response = await sendDiet(currentState.diet, currentState.name?.text, + currentState.details?.text, duration.toString()); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.dietScreen, + arguments: currentState.diet?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendDiet( + Map? diet, + String? name, + String? details, + String? duration, + ) async { + int id = diet?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/diets/update/$id'; + + final Map credentials = { + 'id': id, + 'name': name, + 'details': details, + 'duration': '14', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_event.dart b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_event.dart new file mode 100644 index 00000000..b2184444 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///DietEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietEditEvent extends Equatable {} + +/// Event that is dispatched when the DietEdit widget is first created. +class DietEditInitialEvent extends DietEditEvent { + @override + List get props => []; +} + +class EditDiet extends DietEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_state.dart b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_state.dart new file mode 100644 index 00000000..1c10a404 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_state.dart @@ -0,0 +1,64 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_edit_bloc.dart'; + +/// Represents the state of DietEdit in the application. +/// + +class DietEditState extends Equatable { + DietEditState({ + this.dietModelObj, + }); + + DietEditModel? dietModelObj; + + @override + List get props => [dietModelObj]; + DietEditState copyWith({DietEditModel? dietModelObj}) { + return DietEditState( + dietModelObj: dietModelObj ?? this.dietModelObj, + ); + } +} + +class DietEditLoadedState extends DietEditState { + DietEditLoadedState( + {this.dietModelObj, this.diet, this.name, this.details, this.duration}); + + DietEditModel? dietModelObj; + Map? diet; + TextEditingController? name; + TextEditingController? details; + TextEditingController? duration; + @override + List get props => [dietModelObj, diet, name, details, duration]; + DietEditLoadedState copyWith({ + DietEditModel? dietModelObj, + Map? diet, + TextEditingController? name, + TextEditingController? details, + TextEditingController? duration, + }) { + return DietEditLoadedState( + dietModelObj: dietModelObj ?? this.dietModelObj, + diet: diet ?? this.diet, + name: name ?? this.name, + details: details ?? this.details, + duration: duration ?? this.duration, + ); + } +} + +class DietEditErrorState extends DietEditState { + final String errorMessage; + + DietEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class DietEditInitialState extends DietEditState { + DietEditInitialState({required DietEditModel dietModelObj}) + : super(dietModelObj: dietModelObj); +} diff --git a/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart b/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart new file mode 100644 index 00000000..75f02810 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart @@ -0,0 +1,114 @@ +import 'bloc/diet_edit_bloc.dart'; +import 'models/diet_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DietEditScreen extends StatelessWidget { + const DietEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final dietID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => DietEditBloc(DietEditState( + dietModelObj: DietEditModel(dietID: dietID.toString()))) + ..add(DietEditInitialEvent()), + child: DietEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DietEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildDietEdit(context, state); + } else if (state is DietEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildDietEdit(BuildContext context, DietEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.diet?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditDiet()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/diets/diet_edit_screen/models/diet_edit_model.dart b/lib/presentation/diets/diet_edit_screen/models/diet_edit_model.dart new file mode 100644 index 00000000..8c02d1b1 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/models/diet_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [diet_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class DietEditModel extends Equatable { + final String dietID; + + DietEditModel({required String dietID}) : dietID = dietID; + + DietEditModel copyWith(String dietID) { + return DietEditModel(dietID: dietID); + } + + @override + List get props => [dietID]; +} diff --git a/lib/presentation/diets/diet_select_screen/bloc/diet_select_bloc.dart b/lib/presentation/diets/diet_select_screen/bloc/diet_select_bloc.dart new file mode 100644 index 00000000..49def2d1 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/bloc/diet_select_bloc.dart @@ -0,0 +1,73 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_select_screen/models/diet_select_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'diet_select_event.dart'; +part 'diet_select_state.dart'; + +/// A bloc that manages the state of a DietSelect according to the event that is dispatched to it. +class DietSelectBloc extends Bloc { + DietSelectBloc(DietSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + DietSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/index'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is DietSelectLoadedState) { + print('Got loaded'); + var currentState = state as DietSelectLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(DietSelectLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(DietSelectErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(DietSelectErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/diets/diet_select_screen/bloc/diet_select_event.dart b/lib/presentation/diets/diet_select_screen/bloc/diet_select_event.dart new file mode 100644 index 00000000..91372953 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/bloc/diet_select_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///DietSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietSelectEvent extends Equatable {} + +/// Event that is dispatched when the DietSelect widget is first created. +class DietSelectInitialEvent extends DietSelectEvent { + @override + List get props => []; +} + +class UploadAvatar extends DietSelectEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/diets/diet_select_screen/bloc/diet_select_state.dart b/lib/presentation/diets/diet_select_screen/bloc/diet_select_state.dart new file mode 100644 index 00000000..e7b709db --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/bloc/diet_select_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_select_bloc.dart'; + +class DietSelectState extends Equatable { + const DietSelectState({this.myProfileModelObj}); + + final DietSelectModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class DietSelectInitialState extends DietSelectState { + DietSelectInitialState({required DietSelectModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of DietSelect in the application. +class DietSelectLoadedState extends DietSelectState { + DietSelectLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + DietSelectModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + DietSelectLoadedState copyWith({ + DietSelectModel? myProfileModelObj, + List? posts, + }) { + return DietSelectLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class DietSelectErrorState extends DietSelectState { + final String errorMessage; + + const DietSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/diets/diet_select_screen/diet_select_screen.dart b/lib/presentation/diets/diet_select_screen/diet_select_screen.dart new file mode 100644 index 00000000..c44308d8 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/diet_select_screen.dart @@ -0,0 +1,184 @@ +import 'bloc/diet_select_bloc.dart'; +import 'models/diet_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class DietSelectScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => DietSelectBloc( + DietSelectState(myProfileModelObj: DietSelectModel())) + ..add(DietSelectInitialEvent()), + child: DietSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is DietSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, DietSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Диеты", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.dietCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildDietList(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildDietList(BuildContext context, DietSelectLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['details'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.dietScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.dietEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/diets/diet_select_screen/models/diet_select_model.dart b/lib/presentation/diets/diet_select_screen/models/diet_select_model.dart new file mode 100644 index 00000000..5dcaa802 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/models/diet_select_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class DietSelectModel extends Equatable { + DietSelectModel(); + + DietSelectModel copyWith() { + return DietSelectModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart b/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart new file mode 100644 index 00000000..a1216bd0 --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_show_screen/models/diet_show_model.dart'; +part 'diet_show_event.dart'; +part 'diet_show_state.dart'; + +/// A bloc that manages the state of a Diet according to the event that is dispatched to it. +class DietBloc extends Bloc { + DietBloc(DietState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + DietInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? dietID = state.dietModelObj?.dietID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/show/$dietID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is DietLoadedState) { + var currentState = state as DietLoadedState; + emit(currentState.copyWith( + dietModelObj: currentState.dietModelObj, + diet: currentState.diet)); + } else { + emit(DietLoadedState(diet: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(DietErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(DietErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.dietSelectScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/diets/diet_show_screen/bloc/diet_show_event.dart b/lib/presentation/diets/diet_show_screen/bloc/diet_show_event.dart new file mode 100644 index 00000000..6a131d5c --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_show_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Diet widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietEvent extends Equatable {} + +/// Event that is dispatched when the Diet widget is first created. +class DietInitialEvent extends DietEvent { + @override + List get props => []; +} + +class DeleteEntry extends DietEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/diets/diet_show_screen/bloc/diet_show_state.dart b/lib/presentation/diets/diet_show_screen/bloc/diet_show_state.dart new file mode 100644 index 00000000..49913f7f --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_show_bloc.dart'; + +/// Represents the state of Diet in the application. +/// + +class DietState extends Equatable { + DietState({ + this.dietModelObj, + }); + + DietModel? dietModelObj; + + @override + List get props => [dietModelObj]; + DietState copyWith({DietModel? dietModelObj}) { + return DietState( + dietModelObj: dietModelObj ?? this.dietModelObj, + ); + } +} + +class DietLoadedState extends DietState { + DietLoadedState({this.dietModelObj, this.diet}); + + DietModel? dietModelObj; + Map? diet; + @override + List get props => [dietModelObj, diet]; + DietLoadedState copyWith( + {DietModel? dietModelObj, Map? diet}) { + return DietLoadedState( + dietModelObj: dietModelObj ?? this.dietModelObj, + diet: diet ?? this.diet, + ); + } +} + +class DietErrorState extends DietState { + final String errorMessage; + + DietErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class DietInitialState extends DietState { + DietInitialState({required DietModel dietModelObj}) + : super(dietModelObj: dietModelObj); +} diff --git a/lib/presentation/diets/diet_show_screen/diet_show_screen.dart b/lib/presentation/diets/diet_show_screen/diet_show_screen.dart new file mode 100644 index 00000000..dcc20aea --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/diet_show_screen.dart @@ -0,0 +1,154 @@ +import 'bloc/diet_show_bloc.dart'; +import 'models/diet_show_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DietScreen extends StatelessWidget { + const DietScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final dietID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => DietBloc( + DietState(dietModelObj: DietModel(dietID: dietID.toString()))) + ..add(DietInitialEvent()), + child: DietScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DietLoadedState) { + // Данные загружены, можно строить экран. + return _buildDiet(context, state); + } else if (state is DietErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildDiet(BuildContext context, DietLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.diet?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.diet?['name'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.diet?['details'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.dietEditScreen, + arguments: state.diet?['id'].toString(), + ), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.diet?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.dietScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.dietScreen: + return DietScreen.builder(context); + default: + return DietScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/diets/diet_show_screen/models/diet_show_model.dart b/lib/presentation/diets/diet_show_screen/models/diet_show_model.dart new file mode 100644 index 00000000..803827f8 --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/models/diet_show_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [diet_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class DietModel extends Equatable { + final String dietID; + + DietModel({required String dietID}) : dietID = dietID; + + DietModel copyWith(String dietID) { + return DietModel(dietID: dietID); + } + + @override + List get props => [dietID]; +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart new file mode 100644 index 00000000..e07d2dc6 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart'; +part 'blog_post_create_event.dart'; +part 'blog_post_create_state.dart'; + +/// A bloc that manages the state of a BlogPostCreate according to the event that is dispatched to it. +class BlogPostCreateBloc + extends Bloc { + BlogPostCreateBloc(BlogPostCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createBlogPost); + } + + _onInitialize( + BlogPostCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is BlogPostCreateLoadedState) { + var currentState = state as BlogPostCreateLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + title: currentState.title, + description: currentState.description, + youtube_url: currentState.youtube_url, + token: currentState.token)); + } else { + emit(BlogPostCreateLoadedState( + token: token, + title: TextEditingController(), + description: TextEditingController(), + youtube_url: TextEditingController())); + } + } + + FutureOr _createBlogPost( + CreateBlogPost event, Emitter emit) async { + if (state is BlogPostCreateLoadedState) { + var currentState = state as BlogPostCreateLoadedState; + + var response = await sendBlogPost( + currentState.title?.text, + currentState.youtube_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.feedScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendBlogPost( + String? title, + String? youtube_url, + String? description, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/feed/create'; + final box = await Hive.openBox('token'); + final token = box.get('token'); + final Map credentials = { + 'title': title, + 'description': description, + 'youtube_url': youtube_url, + 'status': 'active', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart new file mode 100644 index 00000000..7631fac0 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///BlogPostCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class BlogPostCreateEvent extends Equatable {} + +/// Event that is dispatched when the BlogPostCreate widget is first created. +class BlogPostCreateInitialEvent extends BlogPostCreateEvent { + @override + List get props => []; +} + +class CreateBlogPost extends BlogPostCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart new file mode 100644 index 00000000..de3bab48 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart @@ -0,0 +1,68 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_create_bloc.dart'; + +/// Represents the state of BlogPostCreate in the application. +/// + +class BlogPostCreateState extends Equatable { + BlogPostCreateState({ + this.blog_postModelObj, + }); + + BlogPostCreateModel? blog_postModelObj; + + @override + List get props => [blog_postModelObj]; + BlogPostCreateState copyWith({BlogPostCreateModel? blog_postModelObj}) { + return BlogPostCreateState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + ); + } +} + +class BlogPostCreateLoadedState extends BlogPostCreateState { + BlogPostCreateLoadedState( + {this.token, + this.blog_postModelObj, + this.title, + this.description, + this.youtube_url}); + + BlogPostCreateModel? blog_postModelObj; + TextEditingController? title; + TextEditingController? description; + TextEditingController? youtube_url; + String? token; + @override + List get props => + [blog_postModelObj, title, description, youtube_url]; + BlogPostCreateLoadedState copyWith( + {BlogPostCreateModel? blog_postModelObj, + TextEditingController? title, + TextEditingController? description, + TextEditingController? youtube_url, + String? token}) { + return BlogPostCreateLoadedState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + title: title ?? this.title, + description: description ?? this.description, + youtube_url: youtube_url ?? this.youtube_url, + token: token ?? this.token, + ); + } +} + +class BlogPostCreateErrorState extends BlogPostCreateState { + final String errorMessage; + + BlogPostCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class BlogPostCreateInitialState extends BlogPostCreateState { + BlogPostCreateInitialState({required BlogPostCreateModel blog_postModelObj}) + : super(blog_postModelObj: blog_postModelObj); +} diff --git a/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart new file mode 100644 index 00000000..fc1d7c14 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart @@ -0,0 +1,168 @@ +import 'bloc/blog_post_create_bloc.dart'; +import 'models/blog_post_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; + +class BlogPostCreateScreen extends StatelessWidget { + const BlogPostCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final blog_postID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => BlogPostCreateBloc(BlogPostCreateState( + blog_postModelObj: + BlogPostCreateModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostCreateInitialEvent()), + child: BlogPostCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BlogPostCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is BlogPostCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPostCreate(context, state); + } else if (state is BlogPostCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildBlogPostCreate(BuildContext context, BlogPostCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Column( + children: [ + Text("Заголовок новости", + style: theme.textTheme.bodyLarge), + CustomTextFormField( + controller: state.title, + validator: _validateTitle, + ), + SizedBox(height: 8.0), + Text("Ссылка на YouTube", + style: theme.textTheme.bodyLarge), + CustomTextFormField( + controller: state.youtube_url, + validator: _validateYouTubeLink, + ), + SizedBox(height: 8.0), + Text("Описание", style: theme.textTheme.bodyLarge), + CustomTextFormField( + controller: state.description, + validator: _validateDescription, + maxLines: 6, + ), + SizedBox(height: 16.0), + // Add widget for image upload here + CustomOutlinedButton( + onPressed: () => _submitForm( + context, + state.title!.text, + state.description!.text, + state.youtube_url!.text), + buttonStyle: + CustomButtonStyles.outlinePrimaryTL15, + text: 'Создать', + ), + ], + ), + ])) + ])), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_submitForm( + BuildContext context, String title, String description, String youtubeURL) { + String? titleError = _validateTitle(title); + String? descriptionError = _validateDescription(description); + String? youtubeURLError = _validateYouTubeLink(youtubeURL); + + if (titleError != null || + descriptionError != null || + youtubeURLError != null) { + // Display error messages or handle invalid input as needed + // For example, show a snackbar with the error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(titleError ?? descriptionError ?? youtubeURLError!)), + ); + } else { + // Proceed with form submission + context.read().add(CreateBlogPost()); + } +} + +String? _validateTitle(value) { + if (value!.isEmpty) { + return 'Пожалуйста, введите заголовок'; + } + return null; +} + +String? _validateDescription(value) { + if (value!.isEmpty) { + return 'Пожалуйста, введите описание'; + } + return null; +} + +String? _validateYouTubeLink(value) { + if (value!.isEmpty) { + return 'Пожалуйста, введите ссылку'; + } else if (!_isValidUrl(value)) { + return 'Пожалуйста, введите корректную ссылку'; + } + return null; +} + +bool _isValidUrl(String url) { + final RegExp urlRegExp = + new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$"); + return urlRegExp.hasMatch(url); +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart b/lib/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart new file mode 100644 index 00000000..46b21c4e --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart @@ -0,0 +1,20 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [blog_post_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class BlogPostCreateModel extends Equatable { + final String blog_postID; + + BlogPostCreateModel({required String blog_postID}) + : blog_postID = blog_postID; + + BlogPostCreateModel copyWith(String blog_postID) { + return BlogPostCreateModel(blog_postID: blog_postID); + } + + @override + List get props => [blog_postID]; +} diff --git a/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart new file mode 100644 index 00000000..f3b193e4 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart'; +part 'blog_post_edit_event.dart'; +part 'blog_post_edit_state.dart'; + +/// A bloc that manages the state of a BlogPostEdit according to the event that is dispatched to it. +class BlogPostEditBloc extends Bloc { + BlogPostEditBloc(BlogPostEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editBlogPost); + } + + _onInitialize( + BlogPostEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? blog_postID = state.blog_postModelObj?.blog_postID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/show/$blog_postID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is BlogPostEditLoadedState) { + var currentState = state as BlogPostEditLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + blog_post: currentState.blog_post, + title: currentState.title, + description: currentState.description, + youtube_url: currentState.youtube_url)); + } else { + emit(BlogPostEditLoadedState( + blog_post: data, + title: TextEditingController(text: data['title']), + description: TextEditingController(text: data['description']), + youtube_url: TextEditingController(text: data['youtube_url']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(BlogPostEditErrorState( + 'Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(BlogPostEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(BlogPostEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editBlogPost( + EditBlogPost event, Emitter emit) async { + if (state is BlogPostEditLoadedState) { + var currentState = state as BlogPostEditLoadedState; + + var response = await sendBlogPost( + currentState.blog_post, + currentState.title?.text, + currentState.youtube_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.blogPostScreen, + arguments: currentState.blog_post?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendBlogPost( + Map? blog_post, + String? title, + String? youtube_url, + String? description, + ) async { + int id = blog_post?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/feed/update/$id'; + + final Map credentials = { + 'id': id, + 'title': title, + 'description': description, + 'youtube_url': youtube_url, + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_event.dart b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_event.dart new file mode 100644 index 00000000..c8961556 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///BlogPostEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class BlogPostEditEvent extends Equatable {} + +/// Event that is dispatched when the BlogPostEdit widget is first created. +class BlogPostEditInitialEvent extends BlogPostEditEvent { + @override + List get props => []; +} + +class EditBlogPost extends BlogPostEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_state.dart b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_state.dart new file mode 100644 index 00000000..321448ee --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_state.dart @@ -0,0 +1,69 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_edit_bloc.dart'; + +/// Represents the state of BlogPostEdit in the application. +/// + +class BlogPostEditState extends Equatable { + BlogPostEditState({ + this.blog_postModelObj, + }); + + BlogPostEditModel? blog_postModelObj; + + @override + List get props => [blog_postModelObj]; + BlogPostEditState copyWith({BlogPostEditModel? blog_postModelObj}) { + return BlogPostEditState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + ); + } +} + +class BlogPostEditLoadedState extends BlogPostEditState { + BlogPostEditLoadedState( + {this.blog_postModelObj, + this.blog_post, + this.title, + this.description, + this.youtube_url}); + + BlogPostEditModel? blog_postModelObj; + Map? blog_post; + TextEditingController? title; + TextEditingController? description; + TextEditingController? youtube_url; + @override + List get props => + [blog_postModelObj, blog_post, title, description, youtube_url]; + BlogPostEditLoadedState copyWith({ + BlogPostEditModel? blog_postModelObj, + Map? blog_post, + TextEditingController? title, + TextEditingController? description, + TextEditingController? youtube_url, + }) { + return BlogPostEditLoadedState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + blog_post: blog_post ?? this.blog_post, + title: title ?? this.title, + description: description ?? this.description, + youtube_url: youtube_url ?? this.youtube_url, + ); + } +} + +class BlogPostEditErrorState extends BlogPostEditState { + final String errorMessage; + + BlogPostEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class BlogPostEditInitialState extends BlogPostEditState { + BlogPostEditInitialState({required BlogPostEditModel blog_postModelObj}) + : super(blog_postModelObj: blog_postModelObj); +} diff --git a/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart b/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart new file mode 100644 index 00000000..1bba76d6 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart @@ -0,0 +1,120 @@ +import 'bloc/blog_post_edit_bloc.dart'; +import 'models/blog_post_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class BlogPostEditScreen extends StatelessWidget { + const BlogPostEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final blog_postID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => BlogPostEditBloc(BlogPostEditState( + blog_postModelObj: + BlogPostEditModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostEditInitialEvent()), + child: BlogPostEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BlogPostEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is BlogPostEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPostEdit(context, state); + } else if (state is BlogPostEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildBlogPostEdit(BuildContext context, BlogPostEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.blog_post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.title, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.description, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.youtube_url, + decoration: InputDecoration(labelText: 'Ссылка'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditBlogPost()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart b/lib/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart new file mode 100644 index 00000000..f9ef98c2 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [blog_post_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class BlogPostEditModel extends Equatable { + final String blog_postID; + + BlogPostEditModel({required String blog_postID}) : blog_postID = blog_postID; + + BlogPostEditModel copyWith(String blog_postID) { + return BlogPostEditModel(blog_postID: blog_postID); + } + + @override + List get props => [blog_postID]; +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart new file mode 100644 index 00000000..d0655339 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart @@ -0,0 +1,190 @@ +import 'dart:async'; + +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/blog_post_model.dart'; +part 'blog_post_event.dart'; +part 'blog_post_state.dart'; + +/// A bloc that manages the state of a BlogPost according to the event that is dispatched to it. +class BlogPostBloc extends Bloc { + BlogPostBloc(BlogPostState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + on(_createComment); + } + + _onInitialize( + BlogPostInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? blog_postID = state.blog_postModelObj?.blog_postID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/show/$blog_postID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is BlogPostLoadedState) { + var currentState = state as BlogPostLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + blog_post: currentState.blog_post, + author: currentState.author, + comments: currentState.comments, + token: currentState.token)); + } else { + emit(BlogPostLoadedState( + blog_post: data, + token: token, + commentController: TextEditingController(), + author: await _fetchAuthor(data['id']), + comments: fillCommentList(await _fetchComments(data['id'])))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(BlogPostErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(BlogPostErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(BlogPostErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _createComment( + CreateComment event, Emitter emit) async { + if (state is BlogPostLoadedState) { + var currentState = state as BlogPostLoadedState; + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await _sendCommentData( + currentState.token, event.body, currentState.blog_post?['id']); + if (response.statusCode == 200) { + var updatedComments = fillCommentList( + await _fetchComments(currentState.blog_post?['id'])); + var updatedState = currentState.copyWith( + comments: updatedComments, + commentController: + TextEditingController(), // Clear the comment input field + ); + emit(updatedState); // Emit the updated state to trigger a screen update + } + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.feedScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(BlogPostErrorState('Ошибка: ${response.statusCode}')); + } + } + + _fetchAuthor(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/getAuthor/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + Future _fetchComments(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/getComments/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + List fillCommentList(List comments) { + List list = []; + for (int i = 0; i < comments.length; i++) { + list.add(CommentModel( + id: comments[i]['id'].toString(), + userImage: comments[i]['profile_photo_path'], + username: comments[i]['name'], + dateText: DateFormat('dd.MM') + .format(DateTime.parse(comments[i]['updated_at'])), + greetingText: comments[i]['body'])); + } + return list; + } + + Future _sendCommentData( + String? token, String body, int id) async { + String url = dotenv.env['SERVER_ROOT']! + '/feed/createComment/$id'; + final Map credentials = { + 'id': id, + 'body': body, + }; + + var response = await http.post(Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8); + + return response; + } +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart new file mode 100644 index 00000000..e1435112 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart @@ -0,0 +1,44 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///BlogPost widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class BlogPostEvent extends Equatable {} + +/// Event that is dispatched when the BlogPost widget is first created. +class BlogPostInitialEvent extends BlogPostEvent { + @override + List get props => []; +} + +class DeleteEntry extends BlogPostEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} + +class FetchAuthor extends BlogPostEvent { + FetchAuthor(this.id); + final int id; + @override + List get props => [id]; +} + +class FetchComments extends BlogPostEvent { + FetchComments(this.id); + final int id; + @override + List get props => [id]; +} + +class CreateComment extends BlogPostEvent { + CreateComment(this.body); + final String body; + @override + List get props => [body]; +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart new file mode 100644 index 00000000..76226025 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart @@ -0,0 +1,70 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_bloc.dart'; + +/// Represents the state of BlogPost in the application. +/// + +class BlogPostState extends Equatable { + BlogPostState({ + this.blog_postModelObj, + }); + + BlogPostModel? blog_postModelObj; + + @override + List get props => [blog_postModelObj]; + BlogPostState copyWith({BlogPostModel? blog_postModelObj}) { + return BlogPostState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + ); + } +} + +class BlogPostLoadedState extends BlogPostState { + BlogPostLoadedState( + {this.blog_postModelObj, + this.blog_post, + this.author, + this.comments, + this.token, + this.commentController}); + + BlogPostModel? blog_postModelObj; + Map? blog_post; + Map? author; + List? comments; + TextEditingController? commentController; + String? token; + @override + List get props => [blog_postModelObj, blog_post, author, comments]; + BlogPostLoadedState copyWith( + {BlogPostModel? blog_postModelObj, + Map? blog_post, + Map? author, + TextEditingController? commentController, + String? token, + List? comments}) { + return BlogPostLoadedState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + blog_post: blog_post ?? this.blog_post, + author: author ?? this.author, + comments: comments ?? this.comments, + token: token ?? this.token, + commentController: commentController ?? this.commentController); + } +} + +class BlogPostErrorState extends BlogPostState { + final String errorMessage; + + BlogPostErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class BlogPostInitialState extends BlogPostState { + BlogPostInitialState({required BlogPostModel blog_postModelObj}) + : super(blog_postModelObj: blog_postModelObj); +} diff --git a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart new file mode 100644 index 00000000..04637420 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -0,0 +1,388 @@ +import 'bloc/blog_post_bloc.dart'; +import 'models/blog_post_model.dart'; +import 'widgets/comment_widget.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_edit_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class BlogPostScreen extends StatelessWidget { + const BlogPostScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final blog_postID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => BlogPostBloc(BlogPostState( + blog_postModelObj: + BlogPostModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostInitialEvent()), + child: BlogPostScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BlogPostInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is BlogPostLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPost(context, state); + } else if (state is BlogPostErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildBlogPost(BuildContext context, BlogPostLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 15.v), + _buildHeaderRow(context, state), + SizedBox(height: 15.v), + _buildBlogPostData(context, state), + SizedBox(height: 15.v), + _buildTextField(context, state), + SizedBox(height: 15.v), + _buildCommentSection(context, state), + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + _buildHeaderRow(BuildContext context, BlogPostLoadedState state) { + final String name = state.author?['name']; + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomImageView( + imagePath: ImageConstant.imgArrowLeft, + onTap: () => NavigatorService.goBack()), + Padding( + padding: EdgeInsets.only(top: 1.v), + child: Row( + children: [ + Align( + alignment: Alignment.centerRight, + child: Text( + "$name", + style: CustomTextStyles.titleMediumBlack, + ), + ), + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.author?['profile_photo_path'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + ], + ), + ), + ], + ), + ); + } + + _buildBlogPostData(BuildContext context, BlogPostLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Row( + children: [ + Text( + state.blog_post?['title'], + style: CustomTextStyles.titleMediumBlack, + ), + Spacer(), + PopupMenuButton( + onSelected: (value) => tapMenu(value, context, state), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], + ), + ], + ), + SizedBox(height: 8.0), + _buildImage(state), + SizedBox(height: 16.0), + Text( + state.blog_post?['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + Align( + alignment: Alignment.topLeft, + child: Text( + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(state.blog_post?['updated_at'])), + style: theme.textTheme.bodyMedium, + )), + SizedBox(height: 16.0), + Row(children: [ + Padding( + padding: EdgeInsets.only(left: 24), + child: Row(children: [ + Padding( + padding: EdgeInsets.only(right: 8), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + style: CustomTextStyles.titleMediumSemiBold, + ) + ]), + ), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 24), + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon(Icons.share_outlined), + ), + Text( + "Поделиться", + style: CustomTextStyles.titleMediumSemiBold, + ) + ])) + ]), + ]), + ); + } + + _buildImage(BlogPostLoadedState state) { + return CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.blog_post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + child: Image( + image: imageProvider, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 48.0), + child: Row( + children: [ + Icon( + Icons.remove_red_eye, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '24', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + Row( + children: [ + Icon( + Icons.favorite, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '24', + style: TextStyle(color: Colors.white), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(right: 48.0), + child: Row( + children: [ + Icon( + Icons.comment_outlined, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '${state.comments?.length}', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + placeholder: (context, url) => CircularProgressIndicator(), + ); + } + + void tapMenu(String value, BuildContext context, BlogPostLoadedState state) { + if (value == 'edit') { + Navigator.pushNamed( + context, + AppRoutes.blogPostEditScreen, + arguments: state.blog_post?['id'].toString(), + ); + } else if (value == 'delete') { + context.read().add(DeleteEntry(state.blog_post?['id'])); + } + } + + _buildTextField(BuildContext context, BlogPostLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + CustomTextFormField( + controller: state.commentController, + maxLines: 4, + ), + SizedBox(height: 15.h), + CustomOutlinedButton( + text: "Оставить отзыв", + margin: EdgeInsets.only(bottom: 5), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + decoration: BoxDecoration( + color: Colors.transparent, + ), + onPressed: () => context + .read() + .add(CreateComment(state.commentController!.text)), + // Установите прозрачный фон + ) + ], + ), + ], + ), + ); + } + + _buildCommentSection(BuildContext context, BlogPostLoadedState state) { + if (state.comments != null && state.comments!.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 0; i < state.comments!.length; i++) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CommentWidget(state.comments![i]), + SizedBox(height: 10.0), // Separator + ], + ), + ], + ); + } else { + // Return a message or placeholder widget when there are no comments + return Center( + child: Text('No comments available'), + ); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart b/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart new file mode 100644 index 00000000..67cbe661 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart @@ -0,0 +1,20 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; + +class BlogPostModel extends Equatable { + final String blog_postID; + BlogPostModel({required this.blog_postID, this.comments = const []}); + List comments; + + BlogPostModel copyWith({String? blog_postID, List? comments}) { + return BlogPostModel( + blog_postID: blog_postID ?? this.blog_postID, + comments: comments ?? this.comments, + ); + } + + @override + List get props => [blog_postID, comments]; +} diff --git a/lib/presentation/feed/blog_post_screen/models/comment_model.dart b/lib/presentation/feed/blog_post_screen/models/comment_model.dart new file mode 100644 index 00000000..3c8c5934 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/models/comment_model.dart @@ -0,0 +1,29 @@ +import 'package:eatvio_app/core/app_export.dart'; + +/// This class is used in the [userprofile_item_widget] screen. +// ignore_for_file: must_be_immutable + +class CommentModel { + CommentModel( + {this.userImage, + this.username, + this.dateText, + this.greetingText, + required this.id}) { + userImage = userImage ?? ImageConstant.imageNotFound; + username = username; + dateText = dateText; + greetingText = greetingText; + id = id; + } + + String? userImage; + + String? username; + + String? dateText; + + String? greetingText; + + String id; +} diff --git a/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart b/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart new file mode 100644 index 00000000..c224966c --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/widgets/comment_widget.dart @@ -0,0 +1,107 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +class CommentWidget extends StatelessWidget { + CommentWidget(this.commentModelObj, {Key? key}) + : super( + key: key, + ); + + CommentModel commentModelObj; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment + .spaceBetween, // Move the main axis alignment to space between + crossAxisAlignment: + CrossAxisAlignment.center, // Center the items vertically + children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + commentModelObj.userImage!, + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: 12.h, + top: 2.v, + bottom: 11.v, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 227.h, + child: Row( + children: [ + Text( + commentModelObj.username!.length <= 15 + ? commentModelObj.username! + : commentModelObj.username!.substring(0, 15) + + '...', + style: theme.textTheme.titleMedium, + ), + Spacer(), + Padding( + padding: EdgeInsets.only(left: 6.h), + child: Text( + commentModelObj.dateText!, + style: theme.textTheme.bodyMedium, + ), + ) + ], + ), + ), + SizedBox(height: 6.v), + Text( + commentModelObj.greetingText!, + style: CustomTextStyles.bodyMediumBluegray800, + ), + ], + ), + ), + ), + CustomImageView( + imagePath: ImageConstant.imgSend, + height: 30.v, + width: 30.h, + margin: EdgeInsets.only(left: 5)), + ], + ), + ); + } +} diff --git a/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart new file mode 100644 index 00000000..3932f495 --- /dev/null +++ b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart @@ -0,0 +1,73 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/models/feed_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'feed_event.dart'; +part 'feed_state.dart'; + +/// A bloc that manages the state of a Feed according to the event that is dispatched to it. +class FeedBloc extends Bloc { + FeedBloc(FeedState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + FeedInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/index'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is FeedLoadedState) { + print('Got loaded'); + var currentState = state as FeedLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(FeedLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(FeedErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(FeedErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(FeedErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/feed/feed_screen/bloc/feed_event.dart b/lib/presentation/feed/feed_screen/bloc/feed_event.dart new file mode 100644 index 00000000..cbb510f5 --- /dev/null +++ b/lib/presentation/feed/feed_screen/bloc/feed_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'feed_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Feed widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class FeedEvent extends Equatable {} + +/// Event that is dispatched when the Feed widget is first created. +class FeedInitialEvent extends FeedEvent { + @override + List get props => []; +} + +class UploadAvatar extends FeedEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/feed/feed_screen/bloc/feed_state.dart b/lib/presentation/feed/feed_screen/bloc/feed_state.dart new file mode 100644 index 00000000..dbc57ac5 --- /dev/null +++ b/lib/presentation/feed/feed_screen/bloc/feed_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'feed_bloc.dart'; + +class FeedState extends Equatable { + const FeedState({this.myProfileModelObj}); + + final FeedModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class FeedInitialState extends FeedState { + FeedInitialState({required FeedModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of Feed in the application. +class FeedLoadedState extends FeedState { + FeedLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + FeedModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + FeedLoadedState copyWith({ + FeedModel? myProfileModelObj, + List? posts, + }) { + return FeedLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class FeedErrorState extends FeedState { + final String errorMessage; + + const FeedErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart new file mode 100644 index 00000000..d5d25004 --- /dev/null +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -0,0 +1,318 @@ +import 'bloc/feed_bloc.dart'; +import 'models/feed_model.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_search_view.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/widgets/feed_scroll_item.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class FeedScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel())) + ..add(FeedInitialEvent()), + child: FeedScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is FeedInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is FeedLoadedState) { + // Данные загружены, можно строить экран. + return _buildFeedScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildFeedScreen(BuildContext context, FeedLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 20), + _buildSearch(context), + SizedBox(height: 20), + _buildMenu(context, state), + SizedBox(height: 20), + _buildBlogPostList(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildSearch(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CustomSearchView( + hintText: "Поиск записей", + ), + SizedBox(height: 10), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + text: "Найти запись", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "+", + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.blogPostCreateScreen, + ), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ) + ], + ), + ), + ); + } + + _buildBlogPostList(BuildContext context, FeedLoadedState state) { + List postWidgets = []; + + if (state.posts != null) { + for (var post in state.posts!) { + postWidgets.add(_buildFeedEntry(post, context)); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: postWidgets, + ); + } + + _buildFeedEntry(post, BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (post["author"]['profile_photo_path'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['author']['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text( + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(post?['post']['updated_at'])), + style: theme.textTheme.bodyMedium, + ), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.blogPostScreen, + arguments: post?['post']['id'].toString(), + ), + trailing: PopupMenuButton( + onSelected: (value) => print("tap"), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Align( + alignment: Alignment.center, + child: Text( + post?['post']['title'], + style: CustomTextStyles.titleMediumBlack, + ), + ), + Spacer(), + ], + ), + SizedBox(height: 8.0), + Text( + post?['post']['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (post?['post']['image'] ?? ''), + imageBuilder: (context, imageProvider) => Stack( + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8.0)), + child: Image( + image: imageProvider, + ), + ), + ], + ), + placeholder: (context, url) => CircularProgressIndicator(), + ), + SizedBox(height: 16.0), + Row(children: [ + Padding( + padding: EdgeInsets.only(left: 4), + child: Row(children: [ + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.comment_outlined), + ), + Text( + "Комментировать", + ) + ]), + ), + Spacer(), + Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + ) + ]), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 4), + child: Icon(Icons.share_outlined), + ), + ]), + ], + ), + ), + ])); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } + + _buildMenu(BuildContext context, FeedLoadedState state) { + return Container( + padding: EdgeInsets.all(12), + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ScrollItem(title: 'Все'), + ScrollItem(title: 'Рецепты'), + ScrollItem(title: 'Диеты'), + ScrollItem(title: 'Планы'), + ScrollItem(title: 'Записи'), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/feed/feed_screen/models/feed_model.dart b/lib/presentation/feed/feed_screen/models/feed_model.dart new file mode 100644 index 00000000..9e13172c --- /dev/null +++ b/lib/presentation/feed/feed_screen/models/feed_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class FeedModel extends Equatable { + FeedModel(); + + FeedModel copyWith() { + return FeedModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart b/lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart new file mode 100644 index 00000000..2c407394 --- /dev/null +++ b/lib/presentation/feed/feed_screen/widgets/feed_scroll_item.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class ScrollItem extends StatelessWidget { + final String title; + + const ScrollItem({required this.title}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + margin: EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Text( + title, + style: CustomTextStyles.titleMediumSemiBold, + ), + ); + } +} diff --git a/lib/presentation/feed/feed_screen/widgets/feed_search_view.dart b/lib/presentation/feed/feed_screen/widgets/feed_search_view.dart new file mode 100644 index 00000000..20cce2ed --- /dev/null +++ b/lib/presentation/feed/feed_screen/widgets/feed_search_view.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class CustomSearchView extends StatelessWidget { + CustomSearchView( + {Key? key, + this.alignment, + this.width, + this.scrollPadding, + this.controller, + this.focusNode, + this.autofocus = false, + this.textStyle, + this.textInputType = TextInputType.text, + this.maxLines, + this.hintText, + this.hintStyle, + this.prefix, + this.prefixConstraints, + this.suffix, + this.suffixConstraints, + this.contentPadding, + this.borderDecoration, + this.fillColor, + this.filled = true, + this.validator, + this.onChanged}) + : super( + key: key, + ); + + final Alignment? alignment; + + final double? width; + + final TextEditingController? scrollPadding; + + final TextEditingController? controller; + + final FocusNode? focusNode; + + final bool? autofocus; + + final TextStyle? textStyle; + + final TextInputType? textInputType; + + final int? maxLines; + + final String? hintText; + + final TextStyle? hintStyle; + + final Widget? prefix; + + final BoxConstraints? prefixConstraints; + + final Widget? suffix; + + final BoxConstraints? suffixConstraints; + + final EdgeInsets? contentPadding; + + final InputBorder? borderDecoration; + + final Color? fillColor; + + final bool? filled; + + final FormFieldValidator? validator; + + final Function(String)? onChanged; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: searchViewWidget(context)) + : searchViewWidget(context); + } + + searchViewWidget(BuildContext context) => SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + scrollPadding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + controller: controller, + focusNode: focusNode, + onTapOutside: (event) => onTapOutside(), + autofocus: autofocus!, + style: textStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + keyboardType: textInputType, + maxLines: maxLines ?? 1, + decoration: decoration, + validator: validator, + onChanged: (String value) => onChanged?.call(value), + ), + ); + + void onTapOutside() { + if (focusNode != null) { + focusNode?.unfocus(); + } else { + FocusManager.instance.primaryFocus?.unfocus(); + } + } + + InputDecoration get decoration => InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + prefixIcon: prefix ?? + SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.search)), + ), + prefixIconConstraints: prefixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + suffixIconConstraints: suffixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + top: 11.v, + right: 11.h, + bottom: 11.v, + ), + fillColor: fillColor ?? Colors.grey[200], + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + ); +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart new file mode 100644 index 00000000..d1065260 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/models/glossary_screen_model.dart'; +part 'glossary_screen_event.dart'; +part 'glossary_screen_state.dart'; + +/// A bloc that manages the state of a Glossary according to the event that is dispatched to it. +class GlossaryBloc extends Bloc { + GlossaryBloc(GlossaryState initialState) : super(initialState) { + on(_onInitialize); + on(_changeProductGroup); + } + + _onInitialize( + GlossaryInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final responseProductGroups = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/getProductGroups'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (responseProductGroups.statusCode == 200) { + final dataProductGroups = jsonDecode(responseProductGroups.body); + if (dataProductGroups is List) { + if (state is GlossaryLoadedState) { + var currentState = state as GlossaryLoadedState; + emit(currentState.copyWith( + glossaryModelObj: currentState.glossaryModelObj, + token: currentState.token, + productGroups: currentState.productGroups, + productList: currentState.productList)); + } else { + List productGroupsModel = + _fillProductGroups(dataProductGroups); + emit(GlossaryLoadedState( + token: token, + productGroups: productGroupsModel, + productList: [])); + await _getProductListById(token, emit, productGroupsModel[0].id); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(GlossaryErrorState('Список категорий некорректного формата')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(GlossaryErrorState( + 'Ошибка сервера: ${responseProductGroups.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(GlossaryErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + void _changeProductGroup( + ChangeProductGroup event, Emitter emit) async { + if (state is GlossaryLoadedState) { + var currentState = state as GlossaryLoadedState; + await _getProductListById(currentState.token, emit, event.id); + } + } + + Future _getProductListById( + token, Emitter emit, int? productGroupID) async { + final responseProductList = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/index/$productGroupID'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + final dataProductList = jsonDecode(responseProductList.body); + if (dataProductList is List) { + var currentState = state as GlossaryLoadedState; + emit(currentState.copyWith( + glossaryModelObj: currentState.glossaryModelObj, + productGroups: currentState.productGroups, + productList: dataProductList)); + } else { + emit(GlossaryErrorState('Список продуктов некорректного формата')); + } + } + + List _fillProductGroups(List productGroups) { + List result = []; + for (int i = 0; i < productGroups.length; i++) { + result.add( + SelectionPopupModel( + id: productGroups[i]['id'], + title: productGroups[i]['name'], + isSelected: i == 0, + ), + ); + } + return result; + } +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart new file mode 100644 index 00000000..f3fb4dd2 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart @@ -0,0 +1,30 @@ +// ignore_for_file: must_be_immutable + +part of 'glossary_screen_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Glossary widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class GlossaryEvent extends Equatable {} + +/// Event that is dispatched when the Glossary widget is first created. +class GlossaryInitialEvent extends GlossaryEvent { + @override + List get props => []; +} + +class DeleteEntry extends GlossaryEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} + +class ChangeProductGroup extends GlossaryEvent { + ChangeProductGroup(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart new file mode 100644 index 00000000..75363864 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart @@ -0,0 +1,64 @@ +// ignore_for_file: must_be_immutable + +part of 'glossary_screen_bloc.dart'; + +/// Represents the state of Glossary in the application. +/// + +class GlossaryState extends Equatable { + GlossaryState({ + this.glossaryModelObj, + }); + + GlossaryModel? glossaryModelObj; + + @override + List get props => [glossaryModelObj]; + GlossaryState copyWith({GlossaryModel? glossaryModelObj}) { + return GlossaryState( + glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, + ); + } +} + +class GlossaryLoadedState extends GlossaryState { + GlossaryLoadedState( + {this.glossaryModelObj, + this.productGroups, + this.productList, + this.token}); + + GlossaryModel? glossaryModelObj; + String? token; + List? productGroups; + List? productList; + @override + List get props => + [glossaryModelObj, productGroups, productList, token]; + GlossaryLoadedState copyWith( + {GlossaryModel? glossaryModelObj, + String? token, + List? productGroups, + List? productList}) { + return GlossaryLoadedState( + glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, + productGroups: productGroups ?? this.productGroups, + productList: productList ?? this.productList, + token: token ?? this.token, + ); + } +} + +class GlossaryErrorState extends GlossaryState { + final String errorMessage; + + GlossaryErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class GlossaryInitialState extends GlossaryState { + GlossaryInitialState({required GlossaryModel glossaryModelObj}) + : super(glossaryModelObj: glossaryModelObj); +} diff --git a/lib/presentation/glossary/glossary_screen/glossary_screen.dart b/lib/presentation/glossary/glossary_screen/glossary_screen.dart new file mode 100644 index 00000000..65dc65cb --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/glossary_screen.dart @@ -0,0 +1,333 @@ +import 'bloc/glossary_screen_bloc.dart'; +import 'models/glossary_screen_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/widgets/custom_drop_down.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart'; + +class GlossaryScreen extends StatelessWidget { + const GlossaryScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => + GlossaryBloc(GlossaryState(glossaryModelObj: GlossaryModel())) + ..add(GlossaryInitialEvent()), + child: GlossaryScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is GlossaryInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is GlossaryLoadedState) { + // Данные загружены, можно строить экран. + return _buildGlossary(context, state); + } else if (state is GlossaryErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildGlossary(BuildContext context, GlossaryLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 20), + _buildSearch(context), + SizedBox(height: 20), + _buildMenu(context), + SizedBox(height: 20), + _buildProducts(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildSearch(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CustomSearchView( + hintText: "Поиск записей", + ), + SizedBox(height: 10), + CustomOutlinedButton( + text: "Найти продукт", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ], + ), + ), + ); + } + + _buildMenu(BuildContext context) { + return Container( + padding: EdgeInsets.all(12), + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Поиск по алфавиту", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox( + height: 8, + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(33, _fillAlphabet), + ), + ), + ]), + ); + } + + _buildProducts(BuildContext context, GlossaryLoadedState state) { + return Container( + padding: EdgeInsets.all(12), + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Группа продуктов", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox( + height: 8, + ), + CustomDropDown( + items: state.productGroups, + defaultValue: state.productGroups?[0], + fillColor: Colors.grey[200]!, + hintText: 'Выберите категорию', + borderDecoration: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey[200]!, width: 1), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 9, + ), + onChanged: _changeProductGroup(context, state), + width: MediaQuery.of(context) + .size + .width, // Set a percentage value as per your layout + ), + SizedBox( + height: 8, + ), + Align( + alignment: Alignment.centerLeft, + child: Text("*КБЖУ указано за 100гр")), + SizedBox( + height: 8, + ), + _buildProductList(context, state) + ]), + ); + } + + _buildProductList(BuildContext context, GlossaryLoadedState state) { + List postWidgets = []; + + if (state.productList != null) { + for (var post in state.productList!) { + postWidgets.add(_buildProductEntry(post, context)); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: postWidgets, + ); + } + + _buildProductEntry(product, BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 3, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + ListTile( + trailing: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (product['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + product?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row(children: [ + Column(children: [ + Text("Ккал - ${product['energy_on_100gr'].toString()}"), + SizedBox(height: 8), + Text("Белки - ${product['proteins_on_100gr'].toString()}") + ]), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 24.0), + child: Column(children: [ + Text("Жиры - ${product['fats_on_100gr'].toString()}"), + SizedBox(height: 8), + Text( + "Углеводы - ${product['carbohydrates_on_100gr'].toString()}") + ]), + ) + ]) + ], + ), + ), + ])); + } + + Widget _fillAlphabet(int index) { + return ScrollItem(title: String.fromCharCode(1040 + index)); + } + + List fillProductGroups(List productGroups) { + List result = []; + + for (int i = 0; i < productGroups.length; i++) { + result.add( + SelectionPopupModel( + id: productGroups[i]['id'], + title: productGroups[i]['name'], + isSelected: true, + ), + ); + } + return result; + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.glossaryScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.glossaryScreen: + return GlossaryScreen.builder(context); + default: + return GlossaryScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + _changeProductGroup(BuildContext context, GlossaryLoadedState state) { + return (value) { + context.read().add(ChangeProductGroup(value.id)); + }; + } +} diff --git a/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart b/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart new file mode 100644 index 00000000..d19bbb54 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/models/glossary_screen_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [glossary_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class GlossaryModel extends Equatable { + GlossaryModel(); + GlossaryModel copyWith() { + return GlossaryModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart b/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart new file mode 100644 index 00000000..2c407394 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class ScrollItem extends StatelessWidget { + final String title; + + const ScrollItem({required this.title}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + margin: EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Text( + title, + style: CustomTextStyles.titleMediumSemiBold, + ), + ); + } +} diff --git a/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart b/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart new file mode 100644 index 00000000..20cce2ed --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class CustomSearchView extends StatelessWidget { + CustomSearchView( + {Key? key, + this.alignment, + this.width, + this.scrollPadding, + this.controller, + this.focusNode, + this.autofocus = false, + this.textStyle, + this.textInputType = TextInputType.text, + this.maxLines, + this.hintText, + this.hintStyle, + this.prefix, + this.prefixConstraints, + this.suffix, + this.suffixConstraints, + this.contentPadding, + this.borderDecoration, + this.fillColor, + this.filled = true, + this.validator, + this.onChanged}) + : super( + key: key, + ); + + final Alignment? alignment; + + final double? width; + + final TextEditingController? scrollPadding; + + final TextEditingController? controller; + + final FocusNode? focusNode; + + final bool? autofocus; + + final TextStyle? textStyle; + + final TextInputType? textInputType; + + final int? maxLines; + + final String? hintText; + + final TextStyle? hintStyle; + + final Widget? prefix; + + final BoxConstraints? prefixConstraints; + + final Widget? suffix; + + final BoxConstraints? suffixConstraints; + + final EdgeInsets? contentPadding; + + final InputBorder? borderDecoration; + + final Color? fillColor; + + final bool? filled; + + final FormFieldValidator? validator; + + final Function(String)? onChanged; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: searchViewWidget(context)) + : searchViewWidget(context); + } + + searchViewWidget(BuildContext context) => SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + scrollPadding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + controller: controller, + focusNode: focusNode, + onTapOutside: (event) => onTapOutside(), + autofocus: autofocus!, + style: textStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + keyboardType: textInputType, + maxLines: maxLines ?? 1, + decoration: decoration, + validator: validator, + onChanged: (String value) => onChanged?.call(value), + ), + ); + + void onTapOutside() { + if (focusNode != null) { + focusNode?.unfocus(); + } else { + FocusManager.instance.primaryFocus?.unfocus(); + } + } + + InputDecoration get decoration => InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + prefixIcon: prefix ?? + SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.search)), + ), + prefixIconConstraints: prefixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + suffixIconConstraints: suffixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + top: 11.v, + right: 11.h, + bottom: 11.v, + ), + fillColor: fillColor ?? Colors.grey[200], + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + ); +} diff --git a/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart b/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart new file mode 100644 index 00000000..b06092c3 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_create_screen/models/plan_create_model.dart'; +part 'plan_create_event.dart'; +part 'plan_create_state.dart'; + +/// A bloc that manages the state of a PlanCreate according to the event that is dispatched to it. +class PlanCreateBloc extends Bloc { + PlanCreateBloc(PlanCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createPlan); + } + + _onInitialize( + PlanCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is PlanCreateLoadedState) { + var currentState = state as PlanCreateLoadedState; + emit(currentState.copyWith( + planModelObj: currentState.planModelObj, + name: currentState.name, + details: currentState.details, + )); + } else { + emit(PlanCreateLoadedState( + name: TextEditingController(), + details: TextEditingController(), + )); + } + } + + FutureOr _createPlan( + CreatePlan event, Emitter emit) async { + if (state is PlanCreateLoadedState) { + var currentState = state as PlanCreateLoadedState; + + var response = await sendPlan( + currentState.name?.text, + currentState.details?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.planSelectScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendPlan( + String? name, + String? details, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/plans/create'; + + final Map credentials = { + 'name': name, + 'details': details, + 'duration': '14', + 'state': 'active', + 'type': '{"meat": true}', + 'daily_schedule': + '{"1": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 494}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 12}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 432}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 195}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 710}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 119}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 287}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 445}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 500}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 764}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 735}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 127}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 89}]}, "2": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 436}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 700}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 766}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 194}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 426}], "snacks": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 58}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 152}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 925}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 224}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 918}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 81}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 308}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 579}]}, "3": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 266}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 790}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 26}], "snacks": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 549}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 446}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 92}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 403}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 894}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 457}]}, "4": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 698}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 43}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 994}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 965}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 42}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 948}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 28}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 54}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 112}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 891}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 538}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 635}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 586}]}, "5": {"lunch": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 900}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 242}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 375}], "snacks": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 604}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 510}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 887}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 554}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 731}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 574}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 274}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 276}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 80}]}, "6": {"lunch": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 359}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 358}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 639}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 861}], "dinner": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 122}], "snacks": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 980}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 837}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 444}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 577}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 706}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 839}]}, "7": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 118}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 664}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 665}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 986}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 296}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 807}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 493}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 227}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 385}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 448}], "breakfast": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 784}]}, "8": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 433}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 871}], "dinner": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 332}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 41}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 388}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 649}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 114}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 968}], "breakfast": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 180}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 269}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 998}]}, "9": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 68}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 505}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 554}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 579}], "snacks": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 751}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 144}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 677}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 263}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 304}], "breakfast": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 963}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 258}]}, "10": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 167}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 306}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 243}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 79}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 560}], "snacks": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 294}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 145}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 639}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 108}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 924}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 894}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 280}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 336}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 338}]}, "11": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 817}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 968}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 525}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 864}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 457}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 548}], "snacks": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 658}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 239}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 358}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 416}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 220}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 323}]}, "12": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 377}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 138}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 654}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 237}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 902}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 76}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 723}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 481}], "breakfast": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 571}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 515}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 188}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 411}]}, "13": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 379}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 231}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 147}], "dinner": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 871}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 679}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 359}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 561}], "snacks": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 414}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 827}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 649}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 161}], "breakfast": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 185}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 464}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 387}]}, "14": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 248}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 5}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 693}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 655}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 709}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 881}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 588}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 36}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 453}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 692}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 186}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 721}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 740}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 731}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 842}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 850}]}, "15": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 380}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 689}], "dinner": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 326}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 629}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 201}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 129}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 749}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 315}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 444}]}, "16": {"lunch": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 953}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 101}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 684}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 506}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 333}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 416}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 796}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 756}], "supper": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 858}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 619}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 800}], "breakfast": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 539}]}, "17": {"lunch": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 134}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 977}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 209}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 227}], "breakfast": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 195}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 917}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 706}]}, "18": {"lunch": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 239}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 798}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 155}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 959}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 958}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 984}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 134}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 432}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 242}]}, "19": {"lunch": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 906}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 152}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 610}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 605}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 498}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 616}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 512}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 146}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 200}], "supper": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 764}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 353}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 181}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 783}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 506}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 887}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 130}]}, "20": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 48}], "dinner": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 266}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 143}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 55}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 174}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 855}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 6}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 878}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}], "breakfast": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 442}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "milliliters", "quantity": 558}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 885}]}, "21": {"lunch": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 504}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 652}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 996}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 200}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 641}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 87}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 689}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 143}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 475}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 924}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 98}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 438}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 383}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 462}]}}' + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/plans/plan_create_screen/bloc/plan_create_event.dart b/lib/presentation/plans/plan_create_screen/bloc/plan_create_event.dart new file mode 100644 index 00000000..0bcfb740 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///PlanCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanCreateEvent extends Equatable {} + +/// Event that is dispatched when the PlanCreate widget is first created. +class PlanCreateInitialEvent extends PlanCreateEvent { + @override + List get props => []; +} + +class CreatePlan extends PlanCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_create_screen/bloc/plan_create_state.dart b/lib/presentation/plans/plan_create_screen/bloc/plan_create_state.dart new file mode 100644 index 00000000..93e4aaf2 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_state.dart @@ -0,0 +1,65 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_create_bloc.dart'; + +/// Represents the state of PlanCreate in the application. +/// + +class PlanCreateState extends Equatable { + PlanCreateState({ + this.planModelObj, + }); + + PlanCreateModel? planModelObj; + + @override + List get props => [planModelObj]; + PlanCreateState copyWith({PlanCreateModel? planModelObj}) { + return PlanCreateState( + planModelObj: planModelObj ?? this.planModelObj, + ); + } +} + +class PlanCreateLoadedState extends PlanCreateState { + PlanCreateLoadedState({ + this.planModelObj, + this.name, + this.details, + }); + + PlanCreateModel? planModelObj; + TextEditingController? name; + TextEditingController? details; + @override + List get props => [ + planModelObj, + name, + details, + ]; + PlanCreateLoadedState copyWith({ + PlanCreateModel? planModelObj, + TextEditingController? name, + TextEditingController? details, + }) { + return PlanCreateLoadedState( + planModelObj: planModelObj ?? this.planModelObj, + name: name ?? this.name, + details: details ?? this.details, + ); + } +} + +class PlanCreateErrorState extends PlanCreateState { + final String errorMessage; + + PlanCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class PlanCreateInitialState extends PlanCreateState { + PlanCreateInitialState({required PlanCreateModel planModelObj}) + : super(planModelObj: planModelObj); +} diff --git a/lib/presentation/plans/plan_create_screen/models/plan_create_model.dart b/lib/presentation/plans/plan_create_screen/models/plan_create_model.dart new file mode 100644 index 00000000..b105414c --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/models/plan_create_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [plan_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class PlanCreateModel extends Equatable { + final String planID; + + PlanCreateModel({required String planID}) : planID = planID; + + PlanCreateModel copyWith(String planID) { + return PlanCreateModel(planID: planID); + } + + @override + List get props => [planID]; +} diff --git a/lib/presentation/plans/plan_create_screen/plan_create_screen.dart b/lib/presentation/plans/plan_create_screen/plan_create_screen.dart new file mode 100644 index 00000000..af04abc7 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/plan_create_screen.dart @@ -0,0 +1,97 @@ +import 'bloc/plan_create_bloc.dart'; +import 'models/plan_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; + +class PlanCreateScreen extends StatelessWidget { + const PlanCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final planID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => PlanCreateBloc(PlanCreateState( + planModelObj: PlanCreateModel(planID: planID.toString()))) + ..add(PlanCreateInitialEvent()), + child: PlanCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is PlanCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildPlanCreate(context, state); + } else if (state is PlanCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildPlanCreate(BuildContext context, PlanCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(CreatePlan()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart new file mode 100644 index 00000000..e8fa9eae --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_edit_screen/models/plan_edit_model.dart'; +part 'plan_edit_event.dart'; +part 'plan_edit_state.dart'; + +/// A bloc that manages the state of a PlanEdit according to the event that is dispatched to it. +class PlanEditBloc extends Bloc { + PlanEditBloc(PlanEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editPlan); + } + + _onInitialize( + PlanEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? planID = state.planModelObj?.planID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/show/$planID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is PlanEditLoadedState) { + var currentState = state as PlanEditLoadedState; + emit(currentState.copyWith( + planModelObj: currentState.planModelObj, + plan: currentState.plan, + name: currentState.name, + duration: currentState.duration, + details: currentState.details)); + } else { + emit(PlanEditLoadedState( + plan: data, + name: TextEditingController(text: data['name']), + details: TextEditingController(text: data['details']), + duration: + TextEditingController(text: data['duration'].toString()))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(PlanEditErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(PlanEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editPlan(EditPlan event, Emitter emit) async { + if (state is PlanEditLoadedState) { + var currentState = state as PlanEditLoadedState; + int duration = int.parse(currentState.duration!.text) * 7; + print(duration); + var response = await sendPlan(currentState.plan, currentState.name?.text, + currentState.details?.text, duration.toString()); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.planScreen, + arguments: currentState.plan?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendPlan( + Map? plan, + String? name, + String? details, + String? duration, + ) async { + int id = plan?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/plans/update/$id'; + + final Map credentials = { + 'id': id, + 'name': name, + 'details': details, + 'duration': '14', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_event.dart b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_event.dart new file mode 100644 index 00000000..2596b8aa --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///PlanEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanEditEvent extends Equatable {} + +/// Event that is dispatched when the PlanEdit widget is first created. +class PlanEditInitialEvent extends PlanEditEvent { + @override + List get props => []; +} + +class EditPlan extends PlanEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_state.dart b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_state.dart new file mode 100644 index 00000000..427bf0df --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_state.dart @@ -0,0 +1,64 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_edit_bloc.dart'; + +/// Represents the state of PlanEdit in the application. +/// + +class PlanEditState extends Equatable { + PlanEditState({ + this.planModelObj, + }); + + PlanEditModel? planModelObj; + + @override + List get props => [planModelObj]; + PlanEditState copyWith({PlanEditModel? planModelObj}) { + return PlanEditState( + planModelObj: planModelObj ?? this.planModelObj, + ); + } +} + +class PlanEditLoadedState extends PlanEditState { + PlanEditLoadedState( + {this.planModelObj, this.plan, this.name, this.details, this.duration}); + + PlanEditModel? planModelObj; + Map? plan; + TextEditingController? name; + TextEditingController? details; + TextEditingController? duration; + @override + List get props => [planModelObj, plan, name, details, duration]; + PlanEditLoadedState copyWith({ + PlanEditModel? planModelObj, + Map? plan, + TextEditingController? name, + TextEditingController? details, + TextEditingController? duration, + }) { + return PlanEditLoadedState( + planModelObj: planModelObj ?? this.planModelObj, + plan: plan ?? this.plan, + name: name ?? this.name, + details: details ?? this.details, + duration: duration ?? this.duration, + ); + } +} + +class PlanEditErrorState extends PlanEditState { + final String errorMessage; + + PlanEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class PlanEditInitialState extends PlanEditState { + PlanEditInitialState({required PlanEditModel planModelObj}) + : super(planModelObj: planModelObj); +} diff --git a/lib/presentation/plans/plan_edit_screen/models/plan_edit_model.dart b/lib/presentation/plans/plan_edit_screen/models/plan_edit_model.dart new file mode 100644 index 00000000..22c3d277 --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/models/plan_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [plan_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class PlanEditModel extends Equatable { + final String planID; + + PlanEditModel({required String planID}) : planID = planID; + + PlanEditModel copyWith(String planID) { + return PlanEditModel(planID: planID); + } + + @override + List get props => [planID]; +} diff --git a/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart b/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart new file mode 100644 index 00000000..71ac12b7 --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart @@ -0,0 +1,114 @@ +import 'bloc/plan_edit_bloc.dart'; +import 'models/plan_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class PlanEditScreen extends StatelessWidget { + const PlanEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final planID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => PlanEditBloc(PlanEditState( + planModelObj: PlanEditModel(planID: planID.toString()))) + ..add(PlanEditInitialEvent()), + child: PlanEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is PlanEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildPlanEdit(context, state); + } else if (state is PlanEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildPlanEdit(BuildContext context, PlanEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.plan?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditPlan()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/plans/plan_select_screen/bloc/plan_select_bloc.dart b/lib/presentation/plans/plan_select_screen/bloc/plan_select_bloc.dart new file mode 100644 index 00000000..40d5f6fd --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/bloc/plan_select_bloc.dart @@ -0,0 +1,70 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_select_screen/models/plan_select_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'plan_select_event.dart'; +part 'plan_select_state.dart'; + +/// A bloc that manages the state of a PlanSelect according to the event that is dispatched to it. +class PlanSelectBloc extends Bloc { + PlanSelectBloc(PlanSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + PlanSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/index'), + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is PlanSelectLoadedState) { + print('Got loaded'); + var currentState = state as PlanSelectLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(PlanSelectLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(PlanSelectErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(PlanSelectErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/plans/plan_select_screen/bloc/plan_select_event.dart b/lib/presentation/plans/plan_select_screen/bloc/plan_select_event.dart new file mode 100644 index 00000000..5cb4b41b --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/bloc/plan_select_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///PlanSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanSelectEvent extends Equatable {} + +/// Event that is dispatched when the PlanSelect widget is first created. +class PlanSelectInitialEvent extends PlanSelectEvent { + @override + List get props => []; +} + +class UploadAvatar extends PlanSelectEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/plans/plan_select_screen/bloc/plan_select_state.dart b/lib/presentation/plans/plan_select_screen/bloc/plan_select_state.dart new file mode 100644 index 00000000..5ab68c28 --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/bloc/plan_select_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_select_bloc.dart'; + +class PlanSelectState extends Equatable { + const PlanSelectState({this.myProfileModelObj}); + + final PlanSelectModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class PlanSelectInitialState extends PlanSelectState { + PlanSelectInitialState({required PlanSelectModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of PlanSelect in the application. +class PlanSelectLoadedState extends PlanSelectState { + PlanSelectLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + PlanSelectModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + PlanSelectLoadedState copyWith({ + PlanSelectModel? myProfileModelObj, + List? posts, + }) { + return PlanSelectLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class PlanSelectErrorState extends PlanSelectState { + final String errorMessage; + + const PlanSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/plans/plan_select_screen/models/plan_select_model.dart b/lib/presentation/plans/plan_select_screen/models/plan_select_model.dart new file mode 100644 index 00000000..238869f6 --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/models/plan_select_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class PlanSelectModel extends Equatable { + PlanSelectModel(); + + PlanSelectModel copyWith() { + return PlanSelectModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_select_screen/plan_select_screen.dart b/lib/presentation/plans/plan_select_screen/plan_select_screen.dart new file mode 100644 index 00000000..7f3486a2 --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/plan_select_screen.dart @@ -0,0 +1,184 @@ +import 'bloc/plan_select_bloc.dart'; +import 'models/plan_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class PlanSelectScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => PlanSelectBloc( + PlanSelectState(myProfileModelObj: PlanSelectModel())) + ..add(PlanSelectInitialEvent()), + child: PlanSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is PlanSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, PlanSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Планы", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.planCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildPlanList(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildPlanList(BuildContext context, PlanSelectLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['details'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.planScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.planEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart b/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart new file mode 100644 index 00000000..616b521a --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_show_screen/models/plan_show_model.dart'; +part 'plan_show_event.dart'; +part 'plan_show_state.dart'; + +/// A bloc that manages the state of a Plan according to the event that is dispatched to it. +class PlanBloc extends Bloc { + PlanBloc(PlanState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + PlanInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? planID = state.planModelObj?.planID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/show/$planID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is PlanLoadedState) { + var currentState = state as PlanLoadedState; + emit(currentState.copyWith( + planModelObj: currentState.planModelObj, + plan: currentState.plan)); + } else { + emit(PlanLoadedState(plan: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(PlanErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(PlanErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.planSelectScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/plans/plan_show_screen/bloc/plan_show_event.dart b/lib/presentation/plans/plan_show_screen/bloc/plan_show_event.dart new file mode 100644 index 00000000..fc83e024 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_show_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Plan widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanEvent extends Equatable {} + +/// Event that is dispatched when the Plan widget is first created. +class PlanInitialEvent extends PlanEvent { + @override + List get props => []; +} + +class DeleteEntry extends PlanEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/plans/plan_show_screen/bloc/plan_show_state.dart b/lib/presentation/plans/plan_show_screen/bloc/plan_show_state.dart new file mode 100644 index 00000000..cefb6cf6 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_show_bloc.dart'; + +/// Represents the state of Plan in the application. +/// + +class PlanState extends Equatable { + PlanState({ + this.planModelObj, + }); + + PlanModel? planModelObj; + + @override + List get props => [planModelObj]; + PlanState copyWith({PlanModel? planModelObj}) { + return PlanState( + planModelObj: planModelObj ?? this.planModelObj, + ); + } +} + +class PlanLoadedState extends PlanState { + PlanLoadedState({this.planModelObj, this.plan}); + + PlanModel? planModelObj; + Map? plan; + @override + List get props => [planModelObj, plan]; + PlanLoadedState copyWith( + {PlanModel? planModelObj, Map? plan}) { + return PlanLoadedState( + planModelObj: planModelObj ?? this.planModelObj, + plan: plan ?? this.plan, + ); + } +} + +class PlanErrorState extends PlanState { + final String errorMessage; + + PlanErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class PlanInitialState extends PlanState { + PlanInitialState({required PlanModel planModelObj}) + : super(planModelObj: planModelObj); +} diff --git a/lib/presentation/plans/plan_show_screen/models/plan_show_model.dart b/lib/presentation/plans/plan_show_screen/models/plan_show_model.dart new file mode 100644 index 00000000..6a6c37c2 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/models/plan_show_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [plan_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class PlanModel extends Equatable { + final String planID; + + PlanModel({required String planID}) : planID = planID; + + PlanModel copyWith(String planID) { + return PlanModel(planID: planID); + } + + @override + List get props => [planID]; +} diff --git a/lib/presentation/plans/plan_show_screen/plan_show_screen.dart b/lib/presentation/plans/plan_show_screen/plan_show_screen.dart new file mode 100644 index 00000000..d34e1a43 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/plan_show_screen.dart @@ -0,0 +1,153 @@ +import 'bloc/plan_show_bloc.dart'; +import 'models/plan_show_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class PlanScreen extends StatelessWidget { + const PlanScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final planID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => PlanBloc( + PlanState(planModelObj: PlanModel(planID: planID.toString()))) + ..add(PlanInitialEvent()), + child: PlanScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is PlanLoadedState) { + // Данные загружены, можно строить экран. + return _buildPlan(context, state); + } else if (state is PlanErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildPlan(BuildContext context, PlanLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.plan?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.plan?['name'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.plan?['details'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () { + // Действие для редактирования + }, + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.plan?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.planScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.planScreen: + return PlanScreen.builder(context); + default: + return PlanScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart b/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart index 03ae81b8..b0359fd1 100644 --- a/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart +++ b/lib/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart @@ -1,3 +1,6 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '/core/app_export.dart'; @@ -6,6 +9,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:hive/hive.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; part 'my_profile_event.dart'; part 'my_profile_state.dart'; @@ -14,6 +18,7 @@ part 'my_profile_state.dart'; class MyProfileBloc extends Bloc { MyProfileBloc(MyProfileState initialState) : super(initialState) { on(_onInitialize); + on(_uploadAvatar); } Future _onInitialize( @@ -23,24 +28,27 @@ class MyProfileBloc extends Bloc { try { final box = await Hive.openBox('token'); final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + //final token = + // "9|laravel_sanctum_KFXbccDcWEvTPfJmV99EQv1ne AggmpR2TidVGDPN34fa201a"; final response = await http.get( Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/index'), headers: { 'Authorization': 'Bearer $token', }, ); - if (response.statusCode == 200) { final data = jsonDecode(response.body); - // Проверка наличия ключевых полей в JSON if (data is Map && data.containsKey('user') && data.containsKey('userDetails') && data.containsKey('chartPoints') && data.containsKey('circumferences')) { - // Все ключевые поля присутствуют, продолжаем обработку if (state is MyProfileLoadedState) { - // Если текущее состояние уже загружено, обновляем его var currentState = state as MyProfileLoadedState; var plans = [] ..addAll(data["finishedPlans"]) @@ -49,9 +57,6 @@ class MyProfileBloc extends Bloc { ..addAll(data["paidPlan"]) ..addAll(data["requestedPlan"]) ..addAll(data["initedPlan"]); - - // Обновляем значения контроллеров только если они уже были созданы - emit((currentState).copyWith( chartPoints: currentState.chartPoints, chartDates: currentState.chartDates, @@ -89,6 +94,8 @@ class MyProfileBloc extends Bloc { hyperFormController: currentState.hyperFormController, selectedCity: currentState.selectedCity, selectedCountry: currentState.selectedCountry, + selectedActivity: currentState.selectedActivity, + myProfileModelObj: currentState.myProfileModelObj, )); } else { var plans = [] @@ -120,18 +127,6 @@ class MyProfileBloc extends Bloc { }) : data["chartPoints"]; - /*List weights = [0.0, 0.0, 0.0, 0.0]; - List dates = [ - "0000-00-00 00:00:00", - "0000-00-00 00:00:00", - "0000-00-00 00:00:00", - "0000-00-00 00:00:00" - ]; - for (int i = 0; i < data["chartPoints"].length; i++) { - weights[i] = data["chartPoints"][i]["weight"].toDouble(); - dates[i] = data["chartPoints"][i]["created_at"].toString(); - } - print(data);*/ List dates = [ "0000-00-00 00:00:00", "0000-00-00 00:00:00", @@ -167,13 +162,6 @@ class MyProfileBloc extends Bloc { } } - /*print(chartPoints); - print(circumferences);*/ - /*print(weights); - print(weights.runtimeType); - print(circumferences); - print(circumferences.runtimeType);*/ - // Если текущее состояние - начальное, создаем новое загруженное состояние emit(MyProfileLoadedState( chartPoints: chartPoints, chartDates: dates, @@ -227,6 +215,8 @@ class MyProfileBloc extends Bloc { TextEditingController(text: data['userDetails']['birthdate']), selectedCity: data['userDetails']['city'], selectedCountry: data['userDetails']['country'], + selectedActivity: determineActivityLevel( + data['userDetails']['activity_coefficient']), )); } } else { @@ -243,4 +233,49 @@ class MyProfileBloc extends Bloc { emit(MyProfileErrorState('Ошибка при получении данных профиля: $e')); } } + + String determineActivityLevel(double activityFactor) { + if (activityFactor <= 1.2) { + return "сидячий"; + } else if (activityFactor <= 1.375) { + return "низкий (1-3 тренировки в неделю по 20 мин)"; + } else if (activityFactor <= 1.55) { + return "умеренный (3-4 тренировки в неделю по 30-60 мин)"; + } else if (activityFactor <= 1.7) { + return "высокий (5-7 тренировок по 30-60 мин)"; + } else { + return "экстремальный (несколько интенсивных тренировок в день)"; + } + } // колхоз, но свитчкейсы в дарте странно ведут себя при сравнении double + + Future _uploadAvatar( + UploadAvatar event, Emitter emit) async { + if (state is MyProfileLoadedState) { + var currentState = state as MyProfileLoadedState; + + await uploadImageToServer(event.image.path, currentState.token); + } + } + + Future uploadImageToServer(String imagePath, String? token) async { + String url = '${dotenv.env['SERVER_ROOT']!}/profile/photo/upload'; + print(url); + var request = http.MultipartRequest('POST', Uri.parse(url)); + request.headers['Authorization'] = 'Bearer $token'; + + request.files.add(await http.MultipartFile.fromPath('photo', imagePath)); + + var client = http.Client(); + var streamedResponse = await client.send(request); + + if (streamedResponse.statusCode == 200) { + var response = await http.Response.fromStream(streamedResponse); + var imageUrl = response.body; + print('Image successfully uploaded'); + return imageUrl; + } else { + throw Exception( + 'Failed to upload image. Status code: ${streamedResponse.statusCode} ${streamedResponse}}'); + } + } } diff --git a/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart b/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart index 99c025a4..ca76f246 100644 --- a/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart +++ b/lib/presentation/profile/my_profile_screen/bloc/my_profile_event.dart @@ -14,3 +14,10 @@ class MyProfileInitialEvent extends MyProfileEvent { @override List get props => []; } + +class UploadAvatar extends MyProfileEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/profile/my_profile_screen/bloc/my_profile_state.dart b/lib/presentation/profile/my_profile_screen/bloc/my_profile_state.dart index 5350abe5..acf48231 100644 --- a/lib/presentation/profile/my_profile_screen/bloc/my_profile_state.dart +++ b/lib/presentation/profile/my_profile_screen/bloc/my_profile_state.dart @@ -32,6 +32,7 @@ class MyProfileLoadedState extends MyProfileState { this.token, this.selectedCountry, this.selectedCity, + this.selectedActivity, this.chestCircumferenceFormController, this.waistCircumferenceFormController, this.hipCircumferenceFormController, @@ -87,6 +88,7 @@ class MyProfileLoadedState extends MyProfileState { String? token; String? selectedCountry; String? selectedCity; + String? selectedActivity; String? hypoFormController; String? hyperFormController; @override @@ -122,6 +124,7 @@ class MyProfileLoadedState extends MyProfileState { additionalFormController, selectedCountry, selectedCity, + selectedActivity, hypoFormController, hyperFormController, ]; @@ -158,6 +161,7 @@ class MyProfileLoadedState extends MyProfileState { String? token, String? selectedCountry, String? selectedCity, + String? selectedActivity, String? hypoFormController, String? hyperFormController, }) { @@ -209,6 +213,7 @@ class MyProfileLoadedState extends MyProfileState { token: token ?? this.token, selectedCountry: selectedCountry ?? this.selectedCountry, selectedCity: selectedCity ?? this.selectedCity, + selectedActivity: selectedActivity ?? this.selectedActivity, hypoFormController: hypoFormController ?? this.hypoFormController, hyperFormController: hyperFormController ?? this.hyperFormController, ); diff --git a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart index 3fe87cf2..5d1f9bf3 100644 --- a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart @@ -2,17 +2,14 @@ import 'bloc/my_profile_bloc.dart'; import 'models/my_profile_model.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; -import 'package:eatvio_app/widgets/app_bar/appbar_title_circleimage.dart'; -import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; -import 'package:eatvio_app/widgets/app_bar/exit_button.dart'; -import 'package:eatvio_app/widgets/next_step_button.dart'; import 'package:eatvio_app/widgets/custom_text_form_field.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:hive/hive.dart'; import 'package:flutter/services.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:http/http.dart' as http; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; @@ -51,133 +48,156 @@ class MyProfileScreen extends StatelessWidget { ); } - Widget _buildMyProfileScreen( - BuildContext context, MyProfileLoadedState state) { + _buildMyProfileScreen(BuildContext context, MyProfileLoadedState state) { mediaQueryData = MediaQuery.of(context); return SafeArea( child: Scaffold( - appBar: _buildAppBar( - context, - ((state.userDetails?['first_name'] ?? '') + - ' ' + - (state.userDetails?['last_name'] ?? '')) ?? - 'loading'.tr), - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 18.v), - child: Column(children: [ - _buildMyBlog(context), - SizedBox(height: 15.v), - _buildSettings(context), - SizedBox(height: 30.v), - _buildCarousel(context, state), - SizedBox(height: 15.v), - _buildPlans( - context, - state.plans?.map((plan) => plan['name']).toList(), - state.plans?.map((plan) => plan['image']).toList(), - state.plans?.map((plan) => plan['state']).toList(), - ), - SizedBox(height: 5.v), - ])), + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column(children: [ + SizedBox(height: 30.v), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: _buildInfo(context, state)), + SizedBox(height: 15.v), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: _buildCarousel(context, state)), + SizedBox(height: 15.v), + if (!state.plans!.isEmpty) + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: _buildPlans( + context, + state.plans + ?.map((plan) => plan['name']) + .toList(), + state.plans + ?.map((plan) => plan['image']) + .toList(), + state.plans + ?.map((plan) => plan['state']) + .toList(), + )), + SizedBox(height: 5.v), + ])))), bottomNavigationBar: _buildBottomBar(context))); } /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context, String username) { - return CustomAppBar( - height: 54.v, - leadingWidth: 28.h, - title: Padding( - padding: EdgeInsets.only(left: 20.h), - child: Row(children: [ - AppbarTitleCircleimage(imagePath: ImageConstant.imgEllipse1), - AppbarSubtitle( - text: username, - margin: EdgeInsets.only(left: 35.h, top: 12.v, bottom: 9.v)) - ])), - actions: [ - ExitButton( - text: "exit".tr, - margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 17.v), - onTap: () async { - final box = await Hive.openBox('token'); - final token = box.get('token'); - final response = await http.post( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/logout'), - headers: { - 'Authorization': 'Bearer $token', - }, - ); - - if (response.statusCode == 204) { - print(response.statusCode); - box.delete('token'); // Удаляем токен из Hive - Navigator.pushNamed( - context, - AppRoutes.authScreen, - ); - } - }, - ) - ]); + _buildInfo(BuildContext context, MyProfileLoadedState state) { + return Column( + children: [ + GestureDetector( + onTap: () => addImage(context), + child: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.user?['profile_photo_path'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 60, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 60, + ), + ), + ), + AppbarSubtitle(text: state.user?['name'] ?? ''), + SizedBox(height: 20.v), + Text(state.userDetails?['additional_info'] ?? ''), + ], + ); } - /// Section Widget - Widget _buildMyBlog(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 7.h), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - CustomImageView( - imagePath: ImageConstant.imgGrid, - height: 24.adaptSize, - width: 24.adaptSize, - margin: EdgeInsets.symmetric(vertical: 3.v)), - Expanded( - child: NextStepButton( - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - text: "my_blog".tr.toUpperCase(), - margin: EdgeInsets.only(left: 10.h))) - ])); + addImage(BuildContext context) { + return () async { + final picker = ImagePicker(); + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + for (var image in images) { + context.read().add(UploadAvatar(image)); + } + } + }; } - /// Section Widget - Widget _buildSettings(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 7.h), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - CustomImageView( - imagePath: ImageConstant.imgSearch, - height: 24.adaptSize, - width: 24.adaptSize, - margin: EdgeInsets.symmetric(vertical: 3.v)), - Expanded( - child: NextStepButton( - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - Navigator.pushNamed(context, AppRoutes.mySettings); - }, - text: "my_settings".tr.toUpperCase(), - margin: EdgeInsets.only(left: 10.h))), - ])); + onTapSettings(BuildContext context) { + return () { + Navigator.pushNamed(context, AppRoutes.mySettings); + }; } - Widget _buildBottomBar(BuildContext context) { + _buildBottomBar(BuildContext context) { return CustomBottomBar(onChanged: (BottomBarEnum type) {}); } - Widget _buildCarousel(BuildContext contex, MyProfileLoadedState state) { + _buildCarousel(BuildContext contex, MyProfileLoadedState state) { return CarouselSlider( - options: CarouselOptions(height: 340.0), + options: CarouselOptions(height: 320.0), items: [1, 2, 3].map((chartType) { return Builder( builder: (BuildContext context) { switch (chartType) { case 1: // График веса return GestureDetector( - onTap: () { - _editWeight(context, state); - }, + onTap: () => _editWeight(context, state), child: Column(children: [ Text( "weight_chart".tr, @@ -220,9 +240,7 @@ class MyProfileScreen extends StatelessWidget { ]); case 3: //График обхватов return GestureDetector( - onTap: () { - _editCircumference(context, state); - }, + onTap: () => _editCircumference(context, state), child: Column(children: [ Text( "circumferences_chart".tr, @@ -244,7 +262,7 @@ class MyProfileScreen extends StatelessWidget { ); } - Widget _buildChartLegend() { + _buildChartLegend() { final colors = [Colors.pink, Colors.green, Colors.purple]; final titles = ["chest".tr, "waist".tr, "hips".tr]; final style = TextStyle( @@ -276,10 +294,8 @@ class MyProfileScreen extends StatelessWidget { ); } - /// Section Widget - Widget _buildChart( + _buildChart( BuildContext context, List chartPoints, List? dates) { -// Определение осей графика List spots = []; for (int i = 0; i < 4; i++) { spots.add( @@ -340,20 +356,7 @@ class MyProfileScreen extends StatelessWidget { touchTooltipData: LineTouchTooltipData( maxContentWidth: 100, tooltipBgColor: Colors.black, - getTooltipItems: (touchedSpots) { - return touchedSpots.map((LineBarSpot touchedSpot) { - final textStyle = TextStyle( - color: touchedSpot.bar.gradient?.colors[0] ?? - touchedSpot.bar.color, - fontWeight: FontWeight.bold, - fontSize: 14, - ); - return LineTooltipItem( - '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', - textStyle, - ); - }).toList(); - }, + getTooltipItems: mapSpots(), ), handleBuiltInTouches: true, getTouchLineStart: (data, index) => 0, @@ -410,9 +413,7 @@ class MyProfileScreen extends StatelessWidget { drawVerticalLine: true, horizontalInterval: 1.5, verticalInterval: 5, - checkToShowHorizontalLine: (value) { - return value.toInt() == 0; - }, + checkToShowHorizontalLine: valuteToInt(), getDrawingHorizontalLine: (_) => FlLine( color: Colors.blue.withOpacity(1), dashArray: [8, 2], @@ -423,9 +424,7 @@ class MyProfileScreen extends StatelessWidget { dashArray: [8, 2], strokeWidth: 0.8, ), - checkToShowVerticalLine: (value) { - return value.toInt() == 0; - }, + checkToShowVerticalLine: valuteToInt(), ), borderData: FlBorderData(show: false), ), @@ -436,7 +435,29 @@ class MyProfileScreen extends StatelessWidget { ); } - Widget _buildCircumferenceChart( + valuteToInt() { + return (value) { + return value.toInt() == 0; + }; + } + + List Function(List) mapSpots() { + return (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.gradient?.colors[0] ?? touchedSpot.bar.color, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem( + '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', + textStyle, + ); + }).toList(); + }; + } + + _buildCircumferenceChart( BuildContext context, chartPoints, List? dates) { // Определение осей графика final chestCircumferences = @@ -526,20 +547,7 @@ class MyProfileScreen extends StatelessWidget { touchTooltipData: LineTouchTooltipData( maxContentWidth: 100, tooltipBgColor: Colors.black, - getTooltipItems: (touchedSpots) { - return touchedSpots.map((LineBarSpot touchedSpot) { - final textStyle = TextStyle( - color: touchedSpot.bar.gradient?.colors[0] ?? - touchedSpot.bar.color, - fontWeight: FontWeight.bold, - fontSize: 14, - ); - return LineTooltipItem( - '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', - textStyle, - ); - }).toList(); - }, + getTooltipItems: mapTouchedSpots(), ), handleBuiltInTouches: true, getTouchLineStart: (data, index) => 0, @@ -618,9 +626,7 @@ class MyProfileScreen extends StatelessWidget { drawVerticalLine: true, horizontalInterval: 1.5, verticalInterval: 5, - checkToShowHorizontalLine: (value) { - return value.toInt() == 0; - }, + checkToShowHorizontalLine: valuteToInt(), getDrawingHorizontalLine: (_) => FlLine( color: Colors.blue.withOpacity(1), dashArray: [8, 2], @@ -631,9 +637,7 @@ class MyProfileScreen extends StatelessWidget { dashArray: [8, 2], strokeWidth: 0.8, ), - checkToShowVerticalLine: (value) { - return value.toInt() == 0; - }, + checkToShowVerticalLine: valuteToInt(), ), borderData: FlBorderData(show: false), ), @@ -644,8 +648,24 @@ class MyProfileScreen extends StatelessWidget { ); } + List Function(List) mapTouchedSpots() { + return (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.gradient?.colors[0] ?? touchedSpot.bar.color, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem( + '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', + textStyle, + ); + }).toList(); + }; + } + /// Section Widget - Widget _buildPlans(BuildContext context, List? plans, + _buildPlans(BuildContext context, List? plans, List? imagePaths, List? states) { return Padding( padding: EdgeInsets.only(left: 7.h), @@ -670,7 +690,6 @@ class MyProfileScreen extends StatelessWidget { Align( alignment: Alignment.topCenter, child: ListView.builder( - shrinkWrap: true, itemCount: plans?.length ?? 0, itemBuilder: (context, i) { String state = states?[i]; @@ -781,94 +800,66 @@ class MyProfileScreen extends StatelessWidget { showDialog( context: context, builder: (context) { - final digitsOnly = FilteringTextInputFormatter.digitsOnly; + //final digitsOnly = FilteringTextInputFormatter.digitsOnly; return AlertDialog( title: Center(child: Text("Добавить замер")), - content: SingleChildScrollView( - child: Column( - children: [ - Text("chest_circumference".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), - CustomTextFormField( - controller: state.chestCircumferenceController, - keyboardType: TextInputType.number, - inputFormatters: [ - digitsOnly // Ограничение ввода только цифрами - ], - ), - SizedBox(height: 15.v), - Text("waist_circumference".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), - CustomTextFormField( - controller: state.waistCircumferenceController, - keyboardType: TextInputType.number, - inputFormatters: [ - digitsOnly // Ограничение ввода только цифрами - ], - ), - SizedBox(height: 15.v), - Text("hip_circumference".tr, style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), - CustomTextFormField( - controller: state.hipCircumferenceController, - keyboardType: TextInputType.number, - inputFormatters: [ - digitsOnly // Ограничение ввода только цифрами - ], - ), - ], - ), - ), + + content: SizedBox( + width: 200, // Ширина диалогового окна + height: 300, // Высота диалогового окна + child: ListView( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 5.v), + child: Text("chest_circumference".tr, + style: theme.textTheme.bodyLarge), + ), + SizedBox(height: 5.v), + CustomTextFormField( + controller: state.chestCircumferenceController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter + .digitsOnly, // Ограничение ввода только цифрами + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 15.v), + child: Text("waist_circumference".tr, + style: theme.textTheme.bodyLarge), + ), + SizedBox(height: 5.v), + CustomTextFormField( + controller: state.waistCircumferenceController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter + .digitsOnly, // Ограничение ввода только цифрами + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 15.v), + child: Text("hip_circumference".tr, + style: theme.textTheme.bodyLarge), + ), + SizedBox(height: 5.v), + CustomTextFormField( + controller: state.hipCircumferenceController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter + .digitsOnly, // Ограничение ввода только цифрами + ], + ), + ], + )), actions: [ TextButton( - onPressed: () { - Navigator.pop(context); - }, + onPressed: onTapOk(context), child: Text("Отмена", style: theme.textTheme.bodyLarge), ), TextButton( - onPressed: () async { - Map data = { - "chest_circumference": - state.chestCircumferenceController?.text, - "waist_circumference": - state.waistCircumferenceController?.text, - "hip_circumference": state.hipCircumferenceController?.text, - }; - - // Преобразование Map в JSON - String jsonData = json.encode(data); - - // Отправка HTTP-запроса - try { - final token = state.token; - final response = await http.post( - Uri.parse(dotenv.env['SERVER_ROOT']! + - '/profile/addCircumference'), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token', - }, - body: jsonData, - ); - - if (response.statusCode == 201) { - Navigator.pushNamed( - context, - AppRoutes.myProfile, - ); - } else { - // Ошибка от сервера - print( - "Ошибка при отправке данных. Код ответа: ${response.statusCode}"); - } - } catch (e) { - // Обработка ошибок при отправке запроса - print("Ошибка при отправке запроса: $e"); - } - }, + onPressed: sendEditData(state, context), child: Text("Изменить", style: CustomTextStyles.titleMediumBlack.copyWith( color: Colors.blue, @@ -883,23 +874,66 @@ class MyProfileScreen extends StatelessWidget { ); } + sendEditData(MyProfileLoadedState state, BuildContext context) { + return () async { + Map data = { + "chest_circumference": state.chestCircumferenceController?.text, + "waist_circumference": state.waistCircumferenceController?.text, + "hip_circumference": state.hipCircumferenceController?.text, + }; + + // Преобразование Map в JSON + String jsonData = json.encode(data); + + // Отправка HTTP-запроса + try { + final token = state.token; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/addCircumference'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: jsonData, + ); + + if (response.statusCode == 201) { + Navigator.pushNamed( + context, + AppRoutes.myProfile, + ); + } else { + // Ошибка от сервера + print( + "Ошибка при отправке данных. Код ответа: ${response.statusCode}"); + } + } catch (e) { + // Обработка ошибок при отправке запроса + print("Ошибка при отправке запроса: $e"); + } + }; + } + _editWeight(BuildContext context, MyProfileLoadedState state) { showDialog( context: context, builder: (context) { - final digitsOnly = FilteringTextInputFormatter.digitsOnly; return AlertDialog( - title: Center(child: Text("add_measure".tr)), - content: SingleChildScrollView( - child: Column( + content: SizedBox( + width: 200, // Ширина диалогового окна + height: 100, // Высота диалогового окна + child: ListView( children: [ - Text("weight".tr, style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), + Padding( + padding: EdgeInsets.symmetric(vertical: 5), + child: Text("weight".tr, style: theme.textTheme.bodyLarge), + ), + SizedBox(height: 5), CustomTextFormField( controller: state.weightController, keyboardType: TextInputType.number, inputFormatters: [ - digitsOnly // Ограничение ввода только цифрами + FilteringTextInputFormatter.digitsOnly, ], ), ], @@ -907,47 +941,11 @@ class MyProfileScreen extends StatelessWidget { ), actions: [ TextButton( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.of(context).pop(), child: Text("Отмена", style: theme.textTheme.bodyLarge), ), TextButton( - onPressed: () async { - Map data = { - "weight": state.weightController!.text, - }; - // Преобразование Map в JSON - String jsonData = json.encode(data); - - // Отправка HTTP-запроса - try { - final token = state.token; - final response = await http.post( - Uri.parse( - dotenv.env['SERVER_ROOT']! + '/profile/addWeight'), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token', - }, - body: jsonData, - ); - - if (response.statusCode == 201) { - Navigator.pushNamed( - context, - AppRoutes.myProfile, - ); - } else { - // Ошибка от сервера - print( - "Ошибка при отправке данных. Код ответа: ${response.statusCode}"); - } - } catch (e) { - // Обработка ошибок при отправке запроса - print("Ошибка при отправке запроса: $e"); - } - }, + onPressed: () => saveWeight(state, context), child: Text("Изменить", style: CustomTextStyles.titleMediumBlack.copyWith( color: Colors.blue, @@ -955,10 +953,57 @@ class MyProfileScreen extends StatelessWidget { ), ], backgroundColor: Colors.white, - titlePadding: EdgeInsets.all( - 20.0), // Можете настроить отступы по вашему желанию + titlePadding: EdgeInsets.all(20.0), ); }, ); } + + void saveWeight(MyProfileLoadedState state, BuildContext context) { + addWeight(state, context); + Navigator.of(context).pop(); // Закрыть диалоговое окно после изменения веса + } + + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + + addWeight(MyProfileLoadedState state, BuildContext context) { + return () async { + Map data = { + "weight": state.weightController!.text, + }; + // Преобразование Map в JSON + String jsonData = json.encode(data); + + // Отправка HTTP-запроса + try { + final token = state.token; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/addWeight'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: jsonData, + ); + + if (response.statusCode == 201) { + Navigator.pushNamed( + context, + AppRoutes.myProfile, + ); + } else { + // Ошибка от сервера + print( + "Ошибка при отправке данных. Код ответа: ${response.statusCode}"); + } + } catch (e) { + // Обработка ошибок при отправке запроса + print("Ошибка при отправке запроса: $e"); + } + }; + } } diff --git a/lib/presentation/profile/my_profile_screen/my_settings_screen.dart b/lib/presentation/profile/my_profile_screen/my_settings_screen.dart index 78201e77..82ddc56b 100644 --- a/lib/presentation/profile/my_profile_screen/my_settings_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_settings_screen.dart @@ -5,14 +5,12 @@ import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:eatvio_app/widgets/next_step_button.dart'; +import 'package:eatvio_app/widgets/custom_about_list.dart'; +import 'package:eatvio_app/widgets/custom_expandable_list.dart'; +import 'package:eatvio_app/widgets/custom_notification_screen.dart'; +import 'package:eatvio_app/widgets/custom_section_title.dart'; +import 'package:eatvio_app/widgets/custom_language_drop_down.dart'; import 'package:flutter/material.dart'; -import 'dart:convert'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:hive/hive.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; class MySettingsScreen extends StatelessWidget { static Widget builder(BuildContext context) { @@ -43,54 +41,148 @@ class MySettingsScreen extends StatelessWidget { ); } - Widget _buildSetingsScreen(BuildContext context, MyProfileLoadedState state) { + _buildSetingsScreen(BuildContext context, MyProfileLoadedState state) { mediaQueryData = MediaQuery.of(context); return SafeArea( - child: Scaffold( - appBar: _buildAppBar(context, "my_settings".tr), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: [ - SectionTitle('Язык'), - LanguageDropdown(), - SectionTitle('Уведомления'), - NotificationSwitch('Уведомление об обновлениях'), - NotificationSwitch('Напоминания о заполнении'), - SectionTitle('Дополнительная информация'), - ExpandableList('Персональные данные'), - AboutList('О нас', ['ООО Итвио', '2023-н.в.']), - ], - ), + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 30), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context)), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row(children: [ + AppbarSubtitle( + text: state.user?['name'], + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v)) + ])), + )), + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + SectionTitle('Язык'), + LanguageDropdown(), + ], + ), + ), + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + SectionTitle('Уведомления'), + NotificationSwitch('Уведомление об обновлениях'), + NotificationSwitch('Напоминания о заполнении'), + ], + ), + ), + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + SectionTitle('Дополнительная информация'), + ExpandableList('Персональные данные'), + AboutList('О нас', ['ООО Итвио', '2023-н.в.']), + ], + ), + ), + ], ), ), - bottomNavigationBar: _buildBottomBar(context))); - } - - /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context, String username) { - return CustomAppBar( - height: 54.v, - leadingWidth: 28.h, - leading: AppbarLeadingImage( - imagePath: ImageConstant.imgArrowLeft, - margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), - title: Padding( - padding: EdgeInsets.only(left: 20.h), - child: Row(children: [ - AppbarSubtitle( - text: username, - margin: EdgeInsets.only(left: 80.h, top: 12.v, bottom: 9.v)) - ])), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), ); } - Widget _buildBottomBar(BuildContext context) { + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { return CustomBottomBar(onChanged: (BottomBarEnum type) {}); } @@ -98,570 +190,3 @@ class MySettingsScreen extends StatelessWidget { NavigatorService.goBack(); } } - -class SectionTitle extends StatelessWidget { - final String title; - - SectionTitle(this.title); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - title, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18), - ), - ); - } -} - -class LanguageDropdown extends StatefulWidget { - @override - _LanguageDropdownState createState() => _LanguageDropdownState(); -} - -class _LanguageDropdownState extends State { - String selectedLanguage = 'Русский'; - - @override - Widget build(BuildContext context) { - return DropdownButton( - value: selectedLanguage, - onChanged: (newValue) { - setState(() { - selectedLanguage = newValue!; - }); - }, - isDense: false, // Установите isDense в false - selectedItemBuilder: (BuildContext context) { - return ['Русский'].map((String language) { - return Align( - alignment: Alignment.center, // Выравнивание текста по центру - child: Text( - language, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ); - }).toList(); - }, - items: ['Русский'].map((String language) { - return DropdownMenuItem( - value: language, - child: Text(language), - ); - }).toList(), - ); - } -} - -class NotificationSwitch extends StatefulWidget { - final String title; - - NotificationSwitch(this.title); - - @override - _NotificationSwitchState createState() => _NotificationSwitchState(); -} - -class _NotificationSwitchState extends State { - bool switchValue = false; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold), - ), - Switch( - value: switchValue, - onChanged: (value) { - setState(() { - switchValue = value; - }); - }, - inactiveThumbColor: appTheme.blueA400, - inactiveTrackColor: Colors.white, - activeTrackColor: appTheme.blueA400, - activeColor: Colors.white, - ), - ], - ); - } -} - -class HyperSwitch extends StatefulWidget { - final String title; - final MyProfileLoadedState? state; - HyperSwitch(this.title, this.state); - - @override - _HyperState createState() => _HyperState(); -} - -class _HyperState extends State { - bool switchValue = false; - - @override - void initState() { - super.initState(); - if (widget.state?.hyperFormController == '1') { - switchValue = true; - } - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold), - ), - Switch( - value: switchValue, - onChanged: (value) { - setState(() { - switchValue = value; - widget.state!.hyperFormController = value ? "1" : "0"; - }); - }, - inactiveThumbColor: appTheme.blueA400, - inactiveTrackColor: Colors.white, - activeTrackColor: appTheme.blueA400, - activeColor: Colors.white, - ), - ], - ); - } -} - -class HypoSwitch extends StatefulWidget { - final String title; - final MyProfileLoadedState? state; - HypoSwitch(this.title, this.state); - - @override - _HypoState createState() => _HypoState(); -} - -class _HypoState extends State { - bool switchValue = false; - - @override - void initState() { - super.initState(); - if (widget.state?.hypoFormController == '1') { - switchValue = true; - } - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold), - ), - Switch( - value: switchValue, - onChanged: (value) { - setState(() { - switchValue = value; - widget.state!.hypoFormController = value ? "1" : "0"; - }); - }, - inactiveThumbColor: appTheme.blueA400, - inactiveTrackColor: Colors.white, - activeTrackColor: appTheme.blueA400, - activeColor: Colors.white, - ), - ], - ); - } -} - -class ExpandableList extends StatefulWidget { - final String title; - - ExpandableList(this.title); - - @override - _ExpandableListState createState() => _ExpandableListState(); -} - -class _ExpandableListState extends State { - bool isExpanded = false; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - GestureDetector( - onTap: () { - setState(() { - isExpanded = !isExpanded; - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.bold), - ), - Icon(isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), - ], - ), - ), - if (isExpanded) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: BlocBuilder( - builder: (context, state) { - if (state is MyProfileInitialState) { - // Это начальное состояние, данные ещё не загружены. - return CircularProgressIndicator( - color: Colors.white, - backgroundColor: Colors.white, - ); - } else if (state is MyProfileLoadedState) { - // Данные загружены, можно строить форму. - return _buildForm(context, state); - } else if (state is MyProfileErrorState) { - // Произошла ошибка при загрузке данных. - return Center(child: Text(state.errorMessage)); - } else { - // Другие состояния, если они есть. - return Container(); // Пустой контейнер для необработанных состояний. - } - }), - ), - Divider(), - ], - ); - } - - Widget _buildForm(BuildContext context, MyProfileLoadedState state) { - List cities = [ - 'Москва', - 'Санкт-Петербург', - 'Новосибирск', - 'Екатеринбург', - 'Нижний Новгород', - 'Казань', - 'Челябинск', - 'Омск', - 'Самара', - 'Ростов-на-Дону', - 'Уфа', - 'Красноярск', - 'Воронеж', - 'Пермь', - 'Волгоград', - 'Краснодар', - 'Саратов', - 'Тюмень', - 'Тольятти', - 'Ижевск', - 'Барнаул', - 'Ульяновск', - 'Иркутск', - 'Хабаровск', - 'Ярославль', - 'Владивосток', - 'Махачкала', - 'Томск', - 'Оренбург', - 'Кемерово', - ]; - List countries = [ - 'Россия', - ]; - final digitsOnly = FilteringTextInputFormatter.digitsOnly; - return Column(children: [ - TextField( - controller: state.firstNameFormController, - decoration: InputDecoration(labelText: 'first_name'.tr), - ), - TextField( - controller: state.lastNameFormController, - decoration: InputDecoration(labelText: 'last_name'.tr), - ), - TextField( - decoration: InputDecoration(labelText: 'date_of_birth'.tr), - controller: state.dateFormController, - onTap: () { - showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime.now(), - ).then((selectedDate) { - if (selectedDate != null) { - setState(() { - state.dateFormController!.text = - DateFormat('dd-MM-yyyy').format(selectedDate); - }); - } - }); - }), - DropdownButtonFormField( - value: state.selectedCountry, - hint: Text(state.selectedCountry ?? "Не выбрано"), - onChanged: (newValue) { - // Обработчик изменения выбранной страны - setSelectedCountry(state, newValue); - }, - items: countries.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - decoration: InputDecoration(labelText: 'country'.tr), - ), - DropdownButtonFormField( - hint: Text(state.selectedCity ?? "Не выбрано"), - value: state.selectedCity, - onChanged: (newValue) { - // Обработчик изменения выбранного города - setSelectedCity(state, newValue); - }, - items: cities.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - decoration: InputDecoration(labelText: 'city_of_living'.tr), - ), - TextField( - controller: state.weightFormController, - decoration: InputDecoration(labelText: 'weight_measurements'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.heightFormController, - decoration: InputDecoration(labelText: 'height_measurements'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.chestCircumferenceFormController, - decoration: InputDecoration(labelText: 'chest_circumference'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.waistCircumferenceFormController, - decoration: InputDecoration(labelText: 'waist_circumference'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.hipCircumferenceFormController, - decoration: InputDecoration(labelText: 'hip_circumference'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.upPressureFormController, - decoration: InputDecoration(labelText: 'up_pressure'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.downPressureFormController, - decoration: InputDecoration(labelText: 'down_pressure'.tr), - keyboardType: TextInputType.number, - inputFormatters: [digitsOnly]), - TextField( - controller: state.allergiesFormController, - decoration: InputDecoration(labelText: 'allergies_presence'.tr), - maxLines: null, - ), - TextField( - controller: state.pillsFormController, - decoration: InputDecoration(labelText: 'do_you_take_pills'.tr), - maxLines: null, - ), - TextField( - controller: state.sportsFormController, - decoration: InputDecoration(labelText: 'your_favorite_sports'.tr), - maxLines: null, - ), - TextField( - controller: state.favoriteFormController, - decoration: InputDecoration(labelText: 'favorite_foods'.tr), - maxLines: null, - ), - TextField( - controller: state.unfavoriteFormController, - decoration: InputDecoration(labelText: 'unfavorite_foods'.tr), - maxLines: null, - ), - TextField( - controller: state.additionalFormController, - decoration: InputDecoration(labelText: 'additional_information'.tr), - maxLines: null, - ), - Row( - children: [ - HypoSwitch('do_you_have_hypotension_tendency'.tr, state), - ], - ), - Row( - children: [ - HyperSwitch('do_you_have_hypertension_tendency'.tr, state), - ], - ), - NextStepButton( - onPressed: () { - sendProfileData(state); - }, - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - text: "save".tr.toUpperCase(), - margin: EdgeInsets.only(top: 20.h, bottom: 20.h)), - ]); - } - - void setSelectedCity(MyProfileLoadedState? state, String? newValue) { - setState(() { - state!.selectedCity = newValue; - }); - } - - void setSelectedCountry(MyProfileLoadedState? state, String? newValue) { - setState(() { - state!.selectedCountry = newValue; - }); - } - - Future sendProfileData(MyProfileLoadedState? state) async { - try { - // Создаем JSON объект с данными профиля - Map profileData = { - 'first_name': state?.firstNameFormController?.text, - 'last_name': state?.lastNameFormController?.text, - 'chest_circumference': state?.chestCircumferenceFormController?.text, - 'waist_circumference': state?.waistCircumferenceFormController?.text, - 'hip_circumference': state?.hipCircumferenceFormController?.text, - 'birthdate': state?.dateFormController?.text, - 'height': state?.heightFormController?.text, - 'weight': state?.weightFormController?.text, - 'blood_pressure_up': state?.upPressureFormController?.text, - 'blood_pressure_down': state?.downPressureFormController?.text, - 'about_alergy': state?.allergiesFormController?.text, - 'about_taking_pils': state?.pillsFormController?.text, - 'about_sport_hobbies': state?.sportsFormController?.text, - 'favorite_foods': state?.favoriteFormController?.text, - 'unfavorite_foods': state?.unfavoriteFormController?.text, - 'additional_info': state?.additionalFormController?.text, - 'country': state?.selectedCountry ?? 'Россия', - 'city': state?.selectedCity ?? 'Москва', - 'has_hypo': state?.hypoFormController, - 'has_hyper': state?.hyperFormController, - }; - final box = await Hive.openBox('token'); - final token = box.get('token'); - String apiUrl = dotenv.env['SERVER_ROOT']! + '/profile/update'; - http.Response response = await http.post( - Uri.parse(apiUrl), - body: json.encode(profileData), - encoding: utf8, - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token', - }, - ); - - print(response.body); - if (response.statusCode == 200) { - Navigator.pushNamed( - context, - AppRoutes.myProfile, - ); - } else { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text('Ошибка при отправке данных профиля'.tr), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - throw Exception('Ошибка при отправке данных профиля'); - } - } catch (error) { - throw Exception('Ошибка при отправке данных профиля: $error'); - } - } -} - -class AboutList extends StatefulWidget { - final String title; - final List items; - - AboutList(this.title, this.items); - - @override - _AboutListState createState() => _AboutListState(); -} - -class _AboutListState extends State { - bool isExpanded = false; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - GestureDetector( - onTap: () { - setState(() { - isExpanded = !isExpanded; - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.bold), - ), - Icon(isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), - ], - ), - ), - if (isExpanded) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Column( - //children: widget.items.map((item) => Text(item)).toList(), - children: [Text(widget.items.join('\n'))]), - ), - Divider(), - ], - ); - } -} diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart new file mode 100644 index 00000000..f637dbfe --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart'; +part 'recipe_create_event.dart'; +part 'recipe_create_state.dart'; + +/// A bloc that manages the state of a RecipeCreate according to the event that is dispatched to it. +class RecipeCreateBloc extends Bloc { + RecipeCreateBloc(RecipeCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createRecipe); + } + + _onInitialize( + RecipeCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + title: currentState.title, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps)); + } else { + final Map step = { + 'description': '', + 'image': '', + }; + emit(RecipeCreateLoadedState( + title: TextEditingController(), + description: TextEditingController(), + video_url: TextEditingController(), + steps: [step])); + } + } + + FutureOr _createRecipe( + CreateRecipe event, Emitter emit) async { + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + + var response = await sendRecipe( + currentState.title?.text, + currentState.video_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.recipeSelectScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendRecipe( + String? title, + String? video_url, + String? description, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/recipe/create'; + + final Map credentials = { + 'title': title, + 'description': description, + 'video_url': video_url, + 'status': 'active', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart new file mode 100644 index 00000000..fcea8078 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///RecipeCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeCreateEvent extends Equatable {} + +/// Event that is dispatched when the RecipeCreate widget is first created. +class RecipeCreateInitialEvent extends RecipeCreateEvent { + @override + List get props => []; +} + +class CreateRecipe extends RecipeCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart new file mode 100644 index 00000000..2a331cb4 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart @@ -0,0 +1,69 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_create_bloc.dart'; + +/// Represents the state of RecipeCreate in the application. +/// + +class RecipeCreateState extends Equatable { + RecipeCreateState({ + this.recipeModelObj, + }); + + RecipeCreateModel? recipeModelObj; + + @override + List get props => [recipeModelObj]; + RecipeCreateState copyWith({RecipeCreateModel? recipeModelObj}) { + return RecipeCreateState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + ); + } +} + +class RecipeCreateLoadedState extends RecipeCreateState { + RecipeCreateLoadedState( + {this.recipeModelObj, + this.title, + this.description, + this.video_url, + this.steps}); + + RecipeCreateModel? recipeModelObj; + TextEditingController? title; + TextEditingController? description; + TextEditingController? video_url; + List? steps; + @override + List get props => + [recipeModelObj, title, description, video_url, steps]; + RecipeCreateLoadedState copyWith({ + RecipeCreateModel? recipeModelObj, + TextEditingController? title, + TextEditingController? description, + TextEditingController? video_url, + List? steps, + }) { + return RecipeCreateLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + steps: steps ?? this.steps, + ); + } +} + +class RecipeCreateErrorState extends RecipeCreateState { + final String errorMessage; + + RecipeCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class RecipeCreateInitialState extends RecipeCreateState { + RecipeCreateInitialState({required RecipeCreateModel recipeModelObj}) + : super(recipeModelObj: recipeModelObj); +} diff --git a/lib/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart b/lib/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart new file mode 100644 index 00000000..1883aa86 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [recipe_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class RecipeCreateModel extends Equatable { + final String recipeID; + + RecipeCreateModel({required String recipeID}) : recipeID = recipeID; + + RecipeCreateModel copyWith(String recipeID) { + return RecipeCreateModel(recipeID: recipeID); + } + + @override + List get props => [recipeID]; +} diff --git a/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart new file mode 100644 index 00000000..b663db22 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart @@ -0,0 +1,376 @@ +import 'bloc/recipe_create_bloc.dart'; +import 'models/recipe_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; + +class RecipeCreateScreen extends StatelessWidget { + const RecipeCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final recipeID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => RecipeCreateBloc(RecipeCreateState( + recipeModelObj: RecipeCreateModel(recipeID: recipeID.toString()))) + ..add(RecipeCreateInitialEvent()), + child: RecipeCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is RecipeCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeCreate(context, state); + } else if (state is RecipeCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipeCreate(BuildContext context, RecipeCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + List steps = []; + if (state.steps != null) { + for (int i = 0; i < state.steps!.length; i++) { + steps.add(_buildStep(context, state, i)); + } + } + + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: ListView(children: [ + _buldAppBar(context), + SizedBox(height: 20.v), + _buildRecipeInfo(state, context), + SizedBox(height: 20.v), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: steps, + ), + SizedBox(height: 20.v), + _buildAdditional(context, state) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + _buldAppBar(BuildContext context) { + return Container( + margin: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Align( + alignment: Alignment.center, + child: AppbarSubtitle( + text: "Добавление рецепта", + )), + ), + ); + } + + _buildRecipeInfo(RecipeCreateLoadedState state, BuildContext context) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + Align(alignment: Alignment.centerLeft, child: Text("Изображение")), + _buildAddPhoto(context, state), + SizedBox(height: 16.0), + Align(alignment: Alignment.centerLeft, child: Text("Название рецепта")), + CustomTextFormField( + controller: state.title, + hintText: "Введите название рецепта", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + Align(alignment: Alignment.centerLeft, child: Text("Описание")), + CustomTextFormField( + controller: state.description, + hintText: "Введите описание рецепта", + maxLines: 4, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + ]), + ); + } +} + +_buildAddPhoto(BuildContext context, RecipeCreateLoadedState state) { + bool isLoading = false; + return GestureDetector( + child: Stack( + children: [ + if (!isLoading) + Container( + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + "Добавить фото", + selectionColor: appTheme.blueA400, + ), + ), + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ) + ], + ), + ), + /*if (isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Center( + child: CircularProgressIndicator( + color: appTheme + .blueGray300), // Замените на свой прелоадер при необходимости + ), + ),*/ + ], + ), + ); +} + +_buildStep(BuildContext context, RecipeCreateLoadedState state, int i) { + bool isLastStep = i == state.steps!.length - 1; + bool isFirstStep = i == 0; + + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Шаг ${i + 1}", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox(height: 8.0), + CustomTextFormField( + controller: state.description, + hintText: "Введите описание шага", + maxLines: 4, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + _buildAddPhoto(context, state), + if (!isFirstStep) + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "х", + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.blogPostCreateScreen, + ), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ), + if (isLastStep) // Добавляем кнопку "Добавить шаг" только для последнего шага + SizedBox(height: 16.0), + CustomOutlinedButton( + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); +} + +_buildAdditional(BuildContext context, RecipeCreateLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align(alignment: Alignment.centerLeft, child: Text("Теги")), + CustomTextFormField( + controller: state.title, + hintText: "Введите теги, через запятую", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 8.0), + Align( + alignment: Alignment.centerLeft, + child: Text("Ссылка на видео (необязательно)")), + CustomTextFormField( + controller: state.description, + hintText: "YouTube", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + CustomOutlinedButton( + text: "Добавить рецепт", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} + +Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; +} + +onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart new file mode 100644 index 00000000..8cafde30 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart @@ -0,0 +1,126 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart'; +part 'recipe_edit_event.dart'; +part 'recipe_edit_state.dart'; + +/// A bloc that manages the state of a RecipeEdit according to the event that is dispatched to it. +class RecipeEditBloc extends Bloc { + RecipeEditBloc(RecipeEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editRecipe); + } + + _onInitialize( + RecipeEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? recipeID = state.recipeModelObj?.recipeID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/show/$recipeID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + recipe: currentState.recipe, + title: currentState.title, + description: currentState.description, + video_url: currentState.video_url)); + } else { + emit(RecipeEditLoadedState( + recipe: data, + title: TextEditingController(text: data['title']), + description: TextEditingController(text: data['description']), + video_url: TextEditingController(text: data['video_url']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(RecipeEditErrorState( + 'Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(RecipeEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editRecipe( + EditRecipe event, Emitter emit) async { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + + var response = await sendRecipe( + currentState.recipe, + currentState.title?.text, + currentState.video_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.recipeScreen, + arguments: currentState.recipe?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendRecipe( + Map? recipe, + String? title, + String? video_url, + String? description, + ) async { + int id = recipe?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/recipe/update/$id'; + + final Map credentials = { + 'id': id, + 'title': title, + 'description': description, + 'video_url': video_url, + 'steps': + '[{"image": "steps/bT91V52zY2hIkjZezUxO4jW4njfhDfCe2PcMxciZ.jpg", "description": "Excepturi fuga sed quaerat impedit facere recusandae. Nihil exercitationem architecto accusantium ut. Qui consequuntur rerum quam asperiores repellendus molestias."}, {"image": "steps/7vaLlotrw65NXbqaD0dK4OTLVyxMgmB9wL3xBzg3.jpg", "description": "Sit hic sint ullam qui. Odit nihil possimus quo id itaque nisi non."}, {"image": "steps/et7cKUsnQkegcRAE72TS0mfAf2jLJXW93QDAGfjz.jpg", "description": "Ab dolores qui voluptatem sed quasi vero. Culpa minus optio in voluptatibus aut corrupti. Dolor expedita minus aut nesciunt quos quibusdam rerum."}, {"image": "steps/260401J6r71r9kuUt694VcsuMMQKTlQMrlIwfaKo.jpg", "description": "Eos dolores corrupti cupiditate aut. Nam nesciunt eveniet eos nam vitae ut. Quibusdam ab commodi ut natus. Nisi earum porro quidem et eligendi iste doloribus."}, {"image": "steps/Ol10BkOFeZ9EG4BAShgB1wnVwtCimoUMcSNDh2EB.jpg", "description": "Consectetur optio porro nobis voluptas quibusdam. Consequatur dolor dolorem laboriosam magnam iste atque."}, {"image": "steps/8PanRq2aWWdXG1xCYkRyJ0M3Z39y6jMPfnHCqlVi.jpg", "description": "A optio porro eligendi ullam. Sed numquam ullam quasi aperiam inventore corrupti enim. Alias id et consequatur perferendis. Ullam consequatur incidunt nesciunt ut et aut."}, {"image": "steps/19U2sYSGDkprS3L8SIFT3xz59OwQJ5xs0A8etZjr.jpg", "description": "In doloribus vel voluptas dolorum. Temporibus iusto est repellat quo sed. Perspiciatis enim sit beatae in dolor ut. Quis ad qui eligendi nisi. Est suscipit dolore ratione dolores suscipit molestiae."}, {"image": "steps/CRxJtjgQw84kG3teJVtl9GqwWUQ1WKpvOeBbwf9a.jpg", "description": "Aut quia maiores et aut. Quidem ab voluptates blanditiis. Est quam quia repellat exercitationem rerum et iste mollitia. Et sed dolores tempore nam et similique qui."}, {"image": "steps/NoG9rHtvHNLrYI2LujURT3DMQCmEe68UbpiXco2y.jpg", "description": "Nam et dolorum delectus. Labore fuga asperiores nam cumque sit. Eum hic officiis ea asperiores qui. Soluta totam enim quasi explicabo sed aut."}]', + 'ingredients': + '[{"name": "Cheeseburger", "unit": "pcs", "portion": 336}, {"name": "Little Cheeseburger", "unit": "gr", "portion": 623}, {"name": "Bacon Burger", "unit": "ml", "portion": 713}, {"name": "Little Bacon Burger", "unit": "pcs", "portion": 160}, {"name": "Hamburger", "unit": "tablespoon", "portion": 390}, {"name": "Bacon Cheese Dog", "unit": "teespons", "portion": 295}, {"name": "Hamburger", "unit": "pcs", "portion": 813}, {"name": "Cheeseburger", "unit": "gr", "portion": 951}]' + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart new file mode 100644 index 00000000..bcbe219c --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///RecipeEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeEditEvent extends Equatable {} + +/// Event that is dispatched when the RecipeEdit widget is first created. +class RecipeEditInitialEvent extends RecipeEditEvent { + @override + List get props => []; +} + +class EditRecipe extends RecipeEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart new file mode 100644 index 00000000..ec001270 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart @@ -0,0 +1,69 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_edit_bloc.dart'; + +/// Represents the state of RecipeEdit in the application. +/// + +class RecipeEditState extends Equatable { + RecipeEditState({ + this.recipeModelObj, + }); + + RecipeEditModel? recipeModelObj; + + @override + List get props => [recipeModelObj]; + RecipeEditState copyWith({RecipeEditModel? recipeModelObj}) { + return RecipeEditState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + ); + } +} + +class RecipeEditLoadedState extends RecipeEditState { + RecipeEditLoadedState( + {this.recipeModelObj, + this.recipe, + this.title, + this.description, + this.video_url}); + + RecipeEditModel? recipeModelObj; + Map? recipe; + TextEditingController? title; + TextEditingController? description; + TextEditingController? video_url; + @override + List get props => + [recipeModelObj, recipe, title, description, video_url]; + RecipeEditLoadedState copyWith({ + RecipeEditModel? recipeModelObj, + Map? recipe, + TextEditingController? title, + TextEditingController? description, + TextEditingController? video_url, + }) { + return RecipeEditLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + recipe: recipe ?? this.recipe, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + ); + } +} + +class RecipeEditErrorState extends RecipeEditState { + final String errorMessage; + + RecipeEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class RecipeEditInitialState extends RecipeEditState { + RecipeEditInitialState({required RecipeEditModel recipeModelObj}) + : super(recipeModelObj: recipeModelObj); +} diff --git a/lib/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart b/lib/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart new file mode 100644 index 00000000..72176186 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [recipe_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class RecipeEditModel extends Equatable { + final String recipeID; + + RecipeEditModel({required String recipeID}) : recipeID = recipeID; + + RecipeEditModel copyWith(String recipeID) { + return RecipeEditModel(recipeID: recipeID); + } + + @override + List get props => [recipeID]; +} diff --git a/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart new file mode 100644 index 00000000..31a723a9 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart @@ -0,0 +1,119 @@ +import 'bloc/recipe_edit_bloc.dart'; +import 'models/recipe_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class RecipeEditScreen extends StatelessWidget { + const RecipeEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final recipeID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => RecipeEditBloc(RecipeEditState( + recipeModelObj: RecipeEditModel(recipeID: recipeID.toString()))) + ..add(RecipeEditInitialEvent()), + child: RecipeEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is RecipeEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeEdit(context, state); + } else if (state is RecipeEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipeEdit(BuildContext context, RecipeEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.recipe?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.title, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.description, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.video_url, + decoration: InputDecoration(labelText: 'Ссылка'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditRecipe()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart new file mode 100644 index 00000000..fc9a96f4 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart @@ -0,0 +1,70 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'recipe_select_event.dart'; +part 'recipe_select_state.dart'; + +/// A bloc that manages the state of a RecipeSelect according to the event that is dispatched to it. +class RecipeSelectBloc extends Bloc { + RecipeSelectBloc(RecipeSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + RecipeSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/index'), + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is RecipeSelectLoadedState) { + print('Got loaded'); + var currentState = state as RecipeSelectLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(RecipeSelectLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(RecipeSelectErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(RecipeSelectErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_event.dart b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_event.dart new file mode 100644 index 00000000..460c79c8 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///RecipeSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeSelectEvent extends Equatable {} + +/// Event that is dispatched when the RecipeSelect widget is first created. +class RecipeSelectInitialEvent extends RecipeSelectEvent { + @override + List get props => []; +} + +class UploadAvatar extends RecipeSelectEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_state.dart b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_state.dart new file mode 100644 index 00000000..feb87b2e --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_select_bloc.dart'; + +class RecipeSelectState extends Equatable { + const RecipeSelectState({this.myProfileModelObj}); + + final RecipeSelectModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class RecipeSelectInitialState extends RecipeSelectState { + RecipeSelectInitialState({required RecipeSelectModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of RecipeSelect in the application. +class RecipeSelectLoadedState extends RecipeSelectState { + RecipeSelectLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + RecipeSelectModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + RecipeSelectLoadedState copyWith({ + RecipeSelectModel? myProfileModelObj, + List? posts, + }) { + return RecipeSelectLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class RecipeSelectErrorState extends RecipeSelectState { + final String errorMessage; + + const RecipeSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart b/lib/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart new file mode 100644 index 00000000..1e9a1d2d --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class RecipeSelectModel extends Equatable { + RecipeSelectModel(); + + RecipeSelectModel copyWith() { + return RecipeSelectModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart b/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart new file mode 100644 index 00000000..0623c053 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart @@ -0,0 +1,184 @@ +import 'bloc/recipe_select_bloc.dart'; +import 'models/recipe_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class RecipeSelectScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => RecipeSelectBloc( + RecipeSelectState(myProfileModelObj: RecipeSelectModel())) + ..add(RecipeSelectInitialEvent()), + child: RecipeSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is RecipeSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipeScreen(BuildContext context, RecipeSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Рецепты", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.recipeCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildRecipeList(context, state), + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildRecipeList(BuildContext context, RecipeSelectLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['title'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['description'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.recipeScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.recipeEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart new file mode 100644 index 00000000..a65348ed --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart'; +part 'recipe_show_event.dart'; +part 'recipe_show_state.dart'; + +/// A bloc that manages the state of a Recipe according to the event that is dispatched to it. +class RecipeBloc extends Bloc { + RecipeBloc(RecipeState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + RecipeInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? recipeID = state.recipeModelObj?.recipeID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/show/$recipeID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is RecipeLoadedState) { + var currentState = state as RecipeLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + recipe: currentState.recipe)); + } else { + emit(RecipeLoadedState(recipe: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(RecipeErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(RecipeErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.recipeSelectScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart new file mode 100644 index 00000000..6257e893 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_show_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Recipe widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeEvent extends Equatable {} + +/// Event that is dispatched when the Recipe widget is first created. +class RecipeInitialEvent extends RecipeEvent { + @override + List get props => []; +} + +class DeleteEntry extends RecipeEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart new file mode 100644 index 00000000..e8c1807f --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_show_bloc.dart'; + +/// Represents the state of Recipe in the application. +/// + +class RecipeState extends Equatable { + RecipeState({ + this.recipeModelObj, + }); + + RecipeModel? recipeModelObj; + + @override + List get props => [recipeModelObj]; + RecipeState copyWith({RecipeModel? recipeModelObj}) { + return RecipeState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + ); + } +} + +class RecipeLoadedState extends RecipeState { + RecipeLoadedState({this.recipeModelObj, this.recipe}); + + RecipeModel? recipeModelObj; + Map? recipe; + @override + List get props => [recipeModelObj, recipe]; + RecipeLoadedState copyWith( + {RecipeModel? recipeModelObj, Map? recipe}) { + return RecipeLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + recipe: recipe ?? this.recipe, + ); + } +} + +class RecipeErrorState extends RecipeState { + final String errorMessage; + + RecipeErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class RecipeInitialState extends RecipeState { + RecipeInitialState({required RecipeModel recipeModelObj}) + : super(recipeModelObj: recipeModelObj); +} diff --git a/lib/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart b/lib/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart new file mode 100644 index 00000000..1786388e --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [recipe_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class RecipeModel extends Equatable { + final String recipeID; + + RecipeModel({required String recipeID}) : recipeID = recipeID; + + RecipeModel copyWith(String recipeID) { + return RecipeModel(recipeID: recipeID); + } + + @override + List get props => [recipeID]; +} diff --git a/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart new file mode 100644 index 00000000..ee757764 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart @@ -0,0 +1,152 @@ +import 'bloc/recipe_show_bloc.dart'; +import 'models/recipe_show_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class RecipeScreen extends StatelessWidget { + const RecipeScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final recipeID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => RecipeBloc(RecipeState( + recipeModelObj: RecipeModel(recipeID: recipeID.toString()))) + ..add(RecipeInitialEvent()), + child: RecipeScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is RecipeLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipe(context, state); + } else if (state is RecipeErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipe(BuildContext context, RecipeLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.recipe?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.recipe?['title'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.recipe?['description'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.pushNamed( + context, AppRoutes.recipeEditScreen, + arguments: state.recipe?['id'].toString()), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.recipe?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.recipeScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.recipeScreen: + return RecipeScreen.builder(context); + default: + return RecipeScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/registration/additional_information_screen/additional_information_screen.dart b/lib/presentation/registration/additional_information_screen/additional_information_screen.dart index 0f6e122f..c1d7037b 100644 --- a/lib/presentation/registration/additional_information_screen/additional_information_screen.dart +++ b/lib/presentation/registration/additional_information_screen/additional_information_screen.dart @@ -66,47 +66,63 @@ class AdditionalInformationScreen extends StatelessWidget { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () async { - UserRegistrationSheet sheet = state - .additionalInformationModelObj! - .userRegistrationSheet; - sheet.additionalInfo = state.editTextController!.text; - final response = await http.post( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/register'), - headers: {'Content-Type': 'application/json'}, - body: json.encode(sheet), - ); - var res = await jsonDecode(response.body); - final box = await Hive.openBox('token'); - final token = res['token']; - box.put('token', token); - Navigator.pushNamed( - context, - AppRoutes.myProfile, - ); - }); + onPressed: onTapFinish(state, context)); }), ])))); } + onTapFinish(AdditionalInformationState state, BuildContext context) { + return () async { + UserRegistrationSheet sheet = + state.additionalInformationModelObj!.userRegistrationSheet; + sheet.additionalInfo = state.editTextController!.text; + + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/register'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: jsonEncode(sheet.toJson()), + encoding: utf8); + + var res = await jsonDecode(response.body); + if (response.statusCode == 200) { + final box = await Hive.openBox('token'); + final token = res['token']; + box.put('token', token); + Navigator.pushNamed( + context, + AppRoutes.myProfile, + ); + } else { + showErrorMessage(context, res['message']); + } + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 31.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Section Widget - Widget _buildTextFormField(BuildContext context) { + _buildTextFormField(BuildContext context) { return SizedBox( - height: 257.v, + height: 315.v, width: 343.h, child: Stack(alignment: Alignment.center, children: [ Align( @@ -127,7 +143,10 @@ class AdditionalInformationScreen extends StatelessWidget { builder: (context, editTextController) { return CustomTextFormField( controller: editTextController, - textInputAction: TextInputAction.done); + hintText: + "У меня крепкое здоровье, но последнее время часто утомляюсь, однако, это мне не мешает радоваться жизни", + textInputAction: TextInputAction.done, + maxLines: 8); }) ])) ])); @@ -140,19 +159,21 @@ class AdditionalInformationScreen extends StatelessWidget { /// Displays an alert dialog when the action is triggered. /// The dialog box contains a title and a message with the `Теперь вы можете пользоваться полным функционалом приложения` - showSuccessMessage(BuildContext context, String text) { + showErrorMessage(BuildContext context, String text) { showDialog( context: context, builder: (_) => AlertDialog( - title: const Text('Регистрация завершена!'), - content: const Text('1'), + title: const Text('Ошибка'), + content: Text(text), actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Ok')) + TextButton(onPressed: onTapOk(context), child: const Text('Ok')) ], )); } + + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } } diff --git a/lib/presentation/registration/allergies_screen/allergies_screen.dart b/lib/presentation/registration/allergies_screen/allergies_screen.dart index ee78724c..9f4c6b03 100644 --- a/lib/presentation/registration/allergies_screen/allergies_screen.dart +++ b/lib/presentation/registration/allergies_screen/allergies_screen.dart @@ -67,52 +67,63 @@ class _AllergiesScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = - state.allergiesModelObj!.userRegistrationSheet; - sheet.aboutAllergy = - state.enterInformationController!.text; - Navigator.pushNamed( - context, - AppRoutes.medicineIntakeScreen, - arguments: sheet, - ); - }); + onPressed: onTapContinue(state, context)); }), BlocBuilder( builder: (context, state) { return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.allergiesModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.medicineIntakeScreen, - arguments: sheet, - ); - }, + onTap: onTapSkip(state, context), child: Text("skip".tr, style: theme.textTheme.bodyLarge), ); }), ])))); } + onTapSkip(AllergiesState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.allergiesModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.medicineIntakeScreen, + arguments: sheet, + ); + }; + } + + onTapContinue(AllergiesState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.allergiesModelObj!.userRegistrationSheet; + sheet.aboutAllergy = state.enterInformationController!.text; + Navigator.pushNamed( + context, + AppRoutes.medicineIntakeScreen, + arguments: sheet, + ); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 31.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Section Widget - Widget _buildTextFormField(BuildContext context) { + _buildTextFormField(BuildContext context) { return Column(children: [ Text("fill_in_information".tr, style: CustomTextStyles.titleMediumSegoeUI), diff --git a/lib/presentation/registration/app_navigation_screen/app_navigation_screen.dart b/lib/presentation/registration/app_navigation_screen/app_navigation_screen.dart index 5dd89c25..ec34e364 100644 --- a/lib/presentation/registration/app_navigation_screen/app_navigation_screen.dart +++ b/lib/presentation/registration/app_navigation_screen/app_navigation_screen.dart @@ -151,7 +151,7 @@ class _AppNavigationScreenState extends State { } /// Section Widget - Widget _buildAppNavigation(BuildContext context) { + _buildAppNavigation(BuildContext context) { return Container( decoration: BoxDecoration( color: Color(0XFFFFFFFF), @@ -205,15 +205,13 @@ class _AppNavigationScreenState extends State { } /// Common widget - Widget _buildScreenTitle( + _buildScreenTitle( BuildContext context, { required String screenTitle, Function? onTapScreenTitle, }) { return GestureDetector( - onTap: () { - onTapScreenTitle!.call(); - }, + onTap: onTapScreenTitle!.call(), child: Container( decoration: BoxDecoration( color: Color(0XFFFFFFFF), diff --git a/lib/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart b/lib/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart index 47c83a35..f0c26a10 100644 --- a/lib/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart +++ b/lib/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart @@ -43,24 +43,30 @@ class _BloodPressureScreenState extends State { padding: EdgeInsets.symmetric(horizontal: 12.h, vertical: 9.v), child: Column(children: [ SizedBox(height: 7.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 4.h), - child: Container( - height: 16.v, - width: 343.h, - decoration: BoxDecoration( - color: theme.colorScheme.onPrimary, - borderRadius: BorderRadius.circular(8.h), - border: Border.all( - color: theme.colorScheme.primary, - width: 1.h)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8.h), - child: LinearProgressIndicator( - value: 0.62, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400))))), + Container( + height: 16.v, + width: 343.h, + padding: EdgeInsets.symmetric( + horizontal: + 4.h), // Apply padding directly to the Container + decoration: BoxDecoration( + color: theme.colorScheme.onPrimary, + borderRadius: BorderRadius.circular(8.h), + border: Border.all( + color: theme.colorScheme.primary, + width: 1.h, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.h), + child: LinearProgressIndicator( + value: 0.62, + backgroundColor: theme.colorScheme.onPrimary, + valueColor: + AlwaysStoppedAnimation(appTheme.blueA400), + ), + ), + ), SizedBox(height: 21.v), SizedBox( width: 224.h, @@ -130,11 +136,7 @@ class _BloodPressureScreenState extends State { items: bloodPressureModelObj?.dropdownItemList ?? [], - onChanged: (value) { - context - .read() - .add(ChangeHypoDownEvent(value: value)); - }); + onChanged: onChangeHyper(context)); })), SizedBox(height: 22.v), Text("do_you_have_hypertension_tendency".tr, @@ -161,10 +163,7 @@ class _BloodPressureScreenState extends State { items: bloodPressureModelObj?.dropdownItemList ?? [], - onChanged: (value) { - context.read().add( - ChangeHyperDropDownEvent(value: value)); - }); + onChanged: onChangeHypo(context)); })), Spacer(), BlocBuilder( @@ -174,79 +173,106 @@ class _BloodPressureScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - List errors = validateFields( - state.upPressureController!.text, - state.downPressureController!.text); - if (errors.isEmpty) { - UserRegistrationSheet sheet = state - .bloodPressureModelObj!.userRegistrationSheet; - sheet.bloodPressureUp = - state.upPressureController!.text; - sheet.bloodPressureDown = - state.downPressureController!.text; - Navigator.pushNamed( - context, - AppRoutes.favoriteSportsScreen, - arguments: sheet, - ); - } else { - String errorMessage = ''; - - for (var error in errors) { - errorMessage += error + '\n\n'; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); + onPressed: onTapContinue(state, context)); }), BlocBuilder( builder: (context, state) { return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.bloodPressureModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.allergiesScreen, - arguments: sheet, - ); - }, + onTap: onTapSkip(state, context), child: Text("skip".tr, style: theme.textTheme.bodyLarge), ); }), ])))); } + onTapSkip(BloodPressureState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.bloodPressureModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.allergiesScreen, + arguments: sheet, + ); + }; + } + + onTapContinue(BloodPressureState state, BuildContext context) { + return () { + List errors = validateFields( + state.upPressureController!.text, state.downPressureController!.text); + if (errors.isEmpty) { + UserRegistrationSheet sheet = + state.bloodPressureModelObj!.userRegistrationSheet; + sheet.bloodPressureUp = state.upPressureController!.text; + sheet.bloodPressureDown = state.downPressureController!.text; + Navigator.pushNamed( + context, + AppRoutes.favoriteSportsScreen, + arguments: sheet, + ); + } else { + String errorMessage = ''; + + for (var error in errors) { + errorMessage += error + '\n\n'; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Ошибка'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: onTapOK(context), + child: Text('OK'), + ), + ], + ), + ); + } + }; + } + + onTapOK(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + + onChangeHypo(BuildContext context) { + return (value) { + context + .read() + .add(ChangeHyperDropDownEvent(value: value)); + }; + } + + onChangeHyper(BuildContext context) { + return (value) { + context.read().add(ChangeHypoDownEvent(value: value)); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); diff --git a/lib/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart b/lib/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart index 77f48b43..17ce3dfb 100644 --- a/lib/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart +++ b/lib/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart @@ -105,68 +105,76 @@ class BodyCircumferencesScreen extends StatelessWidget { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - List errors = validateFields( - state.chestCircumferenceController!.text, - state.waistCircumferenceController!.text, - state.hipCircumferenceController!.text); - if (errors.isEmpty) { - UserRegistrationSheet sheet = state - .bodyCircumferencesModelObj! - .userRegistrationSheet; - sheet.chestCircumference = - state.chestCircumferenceController!.text; - sheet.waistCircumference = - state.waistCircumferenceController!.text; - sheet.hipCircumference = - state.hipCircumferenceController!.text; - Navigator.pushNamed( - context, - AppRoutes.weightScreen, - arguments: sheet, - ); - } else { - String errorMessage = ''; - - for (var error in errors) { - errorMessage += error + '\n\n'; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); + onPressed: onTapContinue(state, context)); }), ])))); } + onTapContinue(BodyCircumferencesState state, BuildContext context) { + return () { + List errors = validateFields( + state.chestCircumferenceController!.text, + state.waistCircumferenceController!.text, + state.hipCircumferenceController!.text); + if (errors.isEmpty) { + UserRegistrationSheet sheet = + state.bodyCircumferencesModelObj!.userRegistrationSheet; + sheet.chestCircumference = state.chestCircumferenceController!.text; + sheet.waistCircumference = state.waistCircumferenceController!.text; + sheet.hipCircumference = state.hipCircumferenceController!.text; + Navigator.pushNamed( + context, + AppRoutes.weightScreen, + arguments: sheet, + ); + } else { + String errorMessage = ''; + + for (var error in errors) { + errorMessage += error + '\n\n'; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Ошибка'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: onTapOk(context), + child: Text('OK'), + ), + ], + ), + ); + } + }; + } + + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); diff --git a/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_bloc.dart b/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_bloc.dart index 0a4fa9fa..ce4073f3 100644 --- a/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_bloc.dart +++ b/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_bloc.dart @@ -10,7 +10,6 @@ class CityOfLivingBloc extends Bloc { CityOfLivingBloc(CityOfLivingState initialState) : super(initialState) { on(_onInitialize); on(_changeDropDown); - on(_changeDropDown1); } _onInitialize( @@ -19,8 +18,8 @@ class CityOfLivingBloc extends Bloc { ) async { emit(state.copyWith( cityOfLivingModelObj: state.cityOfLivingModelObj?.copyWith( - dropdownItemList: fillDropdownItemList(), - dropdownItemList1: fillDropdownItemList1()))); + dropdownItemList: fillDropdownItemList(), + ))); } _changeDropDown( @@ -32,50 +31,7 @@ class CityOfLivingBloc extends Bloc { event.value.title; } - _changeDropDown1( - ChangeDropDown1Event event, - Emitter emit, - ) { - emit(state.copyWith(selectedDropDownValue1: event.value)); - state.cityOfLivingModelObj?.userRegistrationSheet.city = event.value.title; - } - List fillDropdownItemList() { return [SelectionPopupModel(id: 1, title: "Россия", isSelected: true)]; } - - List fillDropdownItemList1() { - return [ - SelectionPopupModel(id: 1, title: "Москва", isSelected: true), - SelectionPopupModel(id: 2, title: "Санкт-Петербург"), - SelectionPopupModel(id: 3, title: "Новосибирск"), - SelectionPopupModel(id: 4, title: "Екатеринбург"), - SelectionPopupModel(id: 5, title: "Нижний Новгород"), - SelectionPopupModel(id: 6, title: "Казань"), - SelectionPopupModel(id: 7, title: "Самара"), - SelectionPopupModel(id: 8, title: "Омск"), - SelectionPopupModel(id: 9, title: "Челябинск"), - SelectionPopupModel(id: 10, title: "Ростов-на-Дону"), - SelectionPopupModel(id: 11, title: "Уфа"), - SelectionPopupModel(id: 12, title: "Волгоград"), - SelectionPopupModel(id: 13, title: "Пермь"), - SelectionPopupModel(id: 14, title: "Красноярск"), - SelectionPopupModel(id: 15, title: "Воронеж"), - SelectionPopupModel(id: 16, title: "Саратов"), - SelectionPopupModel(id: 17, title: "Краснодар"), - SelectionPopupModel(id: 18, title: "Тольятти"), - SelectionPopupModel(id: 19, title: "Барнаул"), - SelectionPopupModel(id: 20, title: "Ижевск"), - SelectionPopupModel(id: 21, title: "Ульяновск"), - SelectionPopupModel(id: 22, title: "Тюмень"), - SelectionPopupModel(id: 23, title: "Иркутск"), - SelectionPopupModel(id: 24, title: "Хабаровск"), - SelectionPopupModel(id: 25, title: "Ярославль"), - SelectionPopupModel(id: 26, title: "Кемерово"), - SelectionPopupModel(id: 27, title: "Магнитогорск"), - SelectionPopupModel(id: 28, title: "Новокузнецк"), - SelectionPopupModel(id: 29, title: "Томск"), - SelectionPopupModel(id: 30, title: "Астрахань") - ]; - } } diff --git a/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_event.dart b/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_event.dart index c12d13e3..d8418128 100644 --- a/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_event.dart +++ b/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_event.dart @@ -26,15 +26,3 @@ class ChangeDropDownEvent extends CityOfLivingEvent { value, ]; } - -///event for dropdown selection -class ChangeDropDown1Event extends CityOfLivingEvent { - ChangeDropDown1Event({required this.value}); - - SelectionPopupModel value; - - @override - List get props => [ - value, - ]; -} diff --git a/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_state.dart b/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_state.dart index b7967ccf..5179b21e 100644 --- a/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_state.dart +++ b/lib/presentation/registration/city_of_living_screen/bloc/city_of_living_state.dart @@ -6,20 +6,20 @@ part of 'city_of_living_bloc.dart'; class CityOfLivingState extends Equatable { CityOfLivingState({ this.selectedDropDownValue, - this.selectedDropDownValue1, + this.cityController, this.cityOfLivingModelObj, }); SelectionPopupModel? selectedDropDownValue; - SelectionPopupModel? selectedDropDownValue1; + TextEditingController? cityController; CityOfLivingModel? cityOfLivingModelObj; @override List get props => [ selectedDropDownValue, - selectedDropDownValue1, + cityController, cityOfLivingModelObj, ]; CityOfLivingState copyWith({ @@ -30,8 +30,7 @@ class CityOfLivingState extends Equatable { return CityOfLivingState( selectedDropDownValue: selectedDropDownValue ?? this.selectedDropDownValue, - selectedDropDownValue1: - selectedDropDownValue1 ?? this.selectedDropDownValue1, + cityController: cityController ?? this.cityController, cityOfLivingModelObj: cityOfLivingModelObj ?? this.cityOfLivingModelObj, ); } diff --git a/lib/presentation/registration/city_of_living_screen/city_of_living_screen.dart b/lib/presentation/registration/city_of_living_screen/city_of_living_screen.dart index 1e14a9c1..20ab28e8 100644 --- a/lib/presentation/registration/city_of_living_screen/city_of_living_screen.dart +++ b/lib/presentation/registration/city_of_living_screen/city_of_living_screen.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/widgets/app_bar/appbar_title_image.dart'; import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; import 'package:eatvio_app/widgets/custom_drop_down.dart'; import 'package:eatvio_app/widgets/next_step_button.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; import 'package:flutter/material.dart'; @@ -25,6 +26,1125 @@ class CityOfLivingScreen extends StatelessWidget { @override Widget build(BuildContext context) { + List _options = [ + "Абаза", + "Абакан", + "Абдулино", + "Абинск", + "Агидель", + "Агрыз", + "Адыгейск", + "Азнакаево", + "Азов", + "Ак-Довурак", + "Аксай", + "Алагир", + "Алапаевск", + "Алатырь", + "Алдан", + "Алейск", + "Александров", + "Александровск", + "Александровск-Сахалинский", + "Алексеевка", + "Алексин", + "Алзамай", + "АлупкаОспаривается", + "АлуштаОспаривается", + "Альметьевск", + "Амурск", + "Анадырь", + "Анапа", + "Ангарск", + "Андреаполь", + "Анжеро-Судженск", + "Анива", + "Апатиты", + "Апрелевка", + "Апшеронск", + "Арамиль", + "Аргун", + "Ардатов", + "Ардон", + "Арзамас", + "Аркадак", + "Армавир", + "АрмянскОспаривается", + "Арсеньев", + "Арск", + "Артём", + "Артёмовск", + "Артёмовский", + "Архангельск", + "Асбест", + "Асино", + "Астрахань", + "Аткарск", + "Ахтубинск", + "Ачинск", + "Аша", + "Бабаево", + "Бабушкин", + "Бавлы", + "Багратионовск", + "Байкальск", + "Баймак", + "Бакал", + "Баксан", + "Балабаново", + "Балаково", + "Балахна", + "Балашиха", + "Балашов", + "Балей", + "Балтийск", + "Барабинск", + "Барнаул", + "Барыш", + "Батайск", + "БахчисарайОспаривается", + "Бежецк", + "Белая Калитва", + "Белая Холуница", + "Белгород", + "Белебей", + "Белёв", + "Белинский", + "Белово", + "Белогорск", + "БелогорскОспаривается", + "Белозерск", + "Белокуриха", + "Беломорск", + "Белоозёрский", + "Белорецк", + "Белореченск", + "Белоусово", + "Белоярский", + "Белый", + "Бердск", + "Березники", + "Берёзовский", + "Берёзовский", + "Беслан", + "Бийск", + "Бикин", + "Билибино", + "Биробиджан", + "Бирск", + "Бирюсинск", + "Бирюч", + "Благовещенск", + "Благовещенск", + "Благодарный", + "Бобров", + "Богданович", + "Богородицк", + "Богородск", + "Боготол", + "Богучар", + "Бодайбо", + "Бокситогорск", + "Болгар", + "Бологое", + "Болотное", + "Болохово", + "Болхов", + "Большой Камень", + "Бор", + "Борзя", + "Борисоглебск", + "Боровичи", + "Боровск", + "Бородино", + "Братск", + "Бронницы", + "Брянск", + "Бугульма", + "Бугуруслан", + "Будённовск", + "Бузулук", + "Буинск", + "Буй", + "Буйнакск", + "Бутурлиновка", + "Валдай", + "Валуйки", + "Велиж", + "Великие Луки", + "Великий Новгород", + "Великий Устюг", + "Вельск", + "Венёв", + "Верещагино", + "Верея", + "Верхнеуральск", + "Верхний Тагил", + "Верхний Уфалей", + "Верхняя Пышма", + "Верхняя Салда", + "Верхняя Тура", + "Верхотурье", + "Верхоянск", + "Весьегонск", + "Ветлуга", + "Видное", + "Вилюйск", + "Вилючинск", + "Вихоревка", + "Вичуга", + "Владивосток", + "Владикавказ", + "Владимир", + "Волгоград", + "Волгодонск", + "Волгореченск", + "Волжск", + "Волжский", + "Вологда", + "Володарск", + "Волоколамск", + "Волосово", + "Волхов", + "Волчанск", + "Вольск", + "Воркута", + "Воронеж", + "Ворсма", + "Воскресенск", + "Воткинск", + "Всеволожск", + "Вуктыл", + "Выборг", + "Выкса", + "Высоковск", + "Высоцк", + "Вытегра", + "Вышний Волочёк", + "Вяземский", + "Вязники", + "Вязьма", + "Вятские Поляны", + "Гаврилов Посад", + "Гаврилов-Ям", + "Гагарин", + "Гаджиево", + "Гай", + "Галич", + "Гатчина", + "Гвардейск", + "Гдов", + "Геленджик", + "Георгиевск", + "Глазов", + "Голицыно", + "Горбатов", + "Горно-Алтайск", + "Горнозаводск", + "Горняк", + "Городец", + "Городище", + "Городовиковск", + "Гороховец", + "Горячий Ключ", + "Грайворон", + "Гремячинск", + "Грозный", + "Грязи", + "Грязовец", + "Губаха", + "Губкин", + "Губкинский", + "Гудермес", + "Гуково", + "Гулькевичи", + "Гурьевск", + "Гурьевск", + "Гусев", + "Гусиноозёрск", + "Гусь-Хрустальный", + "Давлеканово", + "Дагестанские Огни", + "Далматово", + "Дальнегорск", + "Дальнереченск", + "Данилов", + "Данков", + "Дегтярск", + "Дедовск", + "Демидов", + "Дербент", + "Десногорск", + "ДжанкойОспаривается", + "Дзержинск", + "Дзержинский", + "Дивногорск", + "Дигора", + "Димитровград", + "Дмитриев", + "Дмитров", + "Дмитровск", + "Дно", + "Добрянка", + "Долгопрудный", + "Долинск", + "Домодедово", + "Донецк", + "Донской", + "Дорогобуж", + "Дрезна", + "Дубна", + "Дубовка", + "Дудинка", + "Духовщина", + "Дюртюли", + "Дятьково", + "ЕвпаторияОспаривается", + "Егорьевск", + "Ейск", + "Екатеринбург", + "Елабуга", + "Елец", + "Елизово", + "Ельня", + "Еманжелинск", + "Емва", + "Енисейск", + "Ермолино", + "Ершов", + "Ессентуки", + "Ефремов", + "Железноводск", + "Железногорск", + "Железногорск", + "Железногорск-Илимский", + "Жердевка", + "Жигулёвск", + "Жиздра", + "Жирновск", + "Жуков", + "Жуковка", + "Жуковский", + "Завитинск", + "Заводоуковск", + "Заволжск", + "Заволжье", + "Задонск", + "Заинск", + "Закаменск", + "Заозёрный", + "Заозёрск", + "Западная Двина", + "Заполярный", + "Зарайск", + "Заречный", + "Заречный", + "Заринск", + "Звенигово", + "Звенигород", + "Зверево", + "Зеленогорск", + "Зеленоградск", + "Зеленодольск", + "Зеленокумск", + "Зерноград", + "Зея", + "Зима", + "Златоуст", + "Злынка", + "Змеиногорск", + "Знаменск", + "Зубцов", + "Зуевка", + "Ивангород", + "Иваново", + "Ивантеевка", + "Ивдель", + "Игарка", + "Ижевск", + "Избербаш", + "Изобильный", + "Иланский", + "Инза", + "Иннополис", + "Инсар", + "Инта", + "Ипатово", + "Ирбит", + "Иркутск", + "Исилькуль", + "Искитим", + "Истра", + "Ишим", + "Ишимбай", + "Йошкар-Ола", + "Кадников", + "Казань", + "Калач", + "Калач-на-Дону", + "Калачинск", + "Калининград", + "Калининск", + "Калтан", + "Калуга", + "Калязин", + "Камбарка", + "Каменка", + "Каменногорск", + "Каменск-Уральский", + "Каменск-Шахтинский", + "Камень-на-Оби", + "Камешково", + "Камызяк", + "Камышин", + "Камышлов", + "Канаш", + "Кандалакша", + "Канск", + "Карабаново", + "Карабаш", + "Карабулак", + "Карасук", + "Карачаевск", + "Карачев", + "Каргат", + "Каргополь", + "Карпинск", + "Карталы", + "Касимов", + "Касли", + "Каспийск", + "Катав-Ивановск", + "Катайск", + "Качканар", + "Кашин", + "Кашира", + "Кедровый", + "Кемерово", + "Кемь", + "КерчьОспаривается", + "Кизел", + "Кизилюрт", + "Кизляр", + "Кимовск", + "Кимры", + "Кингисепп", + "Кинель", + "Кинешма", + "Киреевск", + "Киренск", + "Киржач", + "Кириллов", + "Кириши", + "Киров", + "Киров", + "Кировград", + "Кирово-Чепецк", + "Кировск", + "Кировск", + "Кирс", + "Кирсанов", + "Киселёвск", + "Кисловодск", + "Клин", + "Клинцы", + "Княгинино", + "Ковдор", + "Ковров", + "Ковылкино", + "Когалым", + "Кодинск", + "Козельск", + "Козловка", + "Козьмодемьянск", + "Кола", + "Кологрив", + "Коломна", + "Колпашево", + "Кольчугино", + "Коммунар", + "Комсомольск", + "Комсомольск-на-Амуре", + "Конаково", + "Кондопога", + "Кондрово", + "Константиновск", + "Копейск", + "Кораблино", + "Кореновск", + "Коркино", + "Королёв", + "Короча", + "Корсаков", + "Коряжма", + "Костерёво", + "Костомукша", + "Кострома", + "Котельники", + "Котельниково", + "Котельнич", + "Котлас", + "Котово", + "Котовск", + "Кохма", + "Красавино", + "Красноармейск", + "Красноармейск", + "Красновишерск", + "Красногорск", + "Краснодар", + "Краснозаводск", + "Краснознаменск", + "Краснознаменск", + "Краснокаменск", + "Краснокамск", + "КрасноперекопскОспаривается", + "Краснослободск", + "Краснослободск", + "Краснотурьинск", + "Красноуральск", + "Красноуфимск", + "Красноярск", + "Красный Кут", + "Красный Сулин", + "Красный Холм", + "Кремёнки", + "Кропоткин", + "Крымск", + "Кстово", + "Кубинка", + "Кувандык", + "Кувшиново", + "Кудрово", + "Кудымкар", + "Кузнецк", + "Куйбышев", + "Кукмор", + "Кулебаки", + "Кумертау", + "Кунгур", + "Купино", + "Курган", + "Курганинск", + "Курильск", + "Курлово", + "Куровское", + "Курск", + "Куртамыш", + "Курчалой", + "Курчатов", + "Куса", + "Кушва", + "Кызыл", + "Кыштым", + "Кяхта", + "Лабинск", + "Лабытнанги", + "Лагань", + "Ладушкин", + "Лаишево", + "Лакинск", + "Лангепас", + "Лахденпохья", + "Лебедянь", + "Лениногорск", + "Ленинск", + "Ленинск-Кузнецкий", + "Ленск", + "Лермонтов", + "Лесной", + "Лесозаводск", + "Лесосибирск", + "Ливны", + "Ликино-Дулёво", + "Липецк", + "Липки", + "Лиски", + "Лихославль", + "Лобня", + "Лодейное Поле", + "Лосино-Петровский", + "Луга", + "Луза", + "Лукоянов", + "Луховицы", + "Лысково", + "Лысьва", + "Лыткарино", + "Льгов", + "Любань", + "Люберцы", + "Любим", + "Людиново", + "Лянтор", + "Магадан", + "Магас", + "Магнитогорск", + "Майкоп", + "Майский", + "Макаров", + "Макарьев", + "Макушино", + "Малая Вишера", + "Малгобек", + "Малмыж", + "Малоархангельск", + "Малоярославец", + "Мамадыш", + "Мамоново", + "Мантурово", + "Мариинск", + "Мариинский Посад", + "Маркс", + "Махачкала", + "Мглин", + "Мегион", + "Медвежьегорск", + "Медногорск", + "Медынь", + "Межгорье", + "Междуреченск", + "Мезень", + "Меленки", + "Мелеуз", + "Менделеевск", + "Мензелинск", + "Мещовск", + "Миасс", + "Микунь", + "Миллерово", + "Минеральные Воды", + "Минусинск", + "Миньяр", + "Мирный", + "Мирный", + "Михайлов", + "Михайловка", + "Михайловск", + "Михайловск", + "Мичуринск", + "Могоча", + "Можайск", + "Можга", + "Моздок", + "Мончегорск", + "Морозовск", + "Моршанск", + "Мосальск", + "Москва", + "Муравленко", + "Мураши", + "Мурино", + "Мурманск", + "Муром", + "Мценск", + "Мыски", + "Мытищи", + "Мышкин", + "Набережные Челны", + "Навашино", + "Наволоки", + "Надым", + "Назарово", + "Назрань", + "Называевск", + "Нальчик", + "Нариманов", + "Наро-Фоминск", + "Нарткала", + "Нарьян-Мар", + "Находка", + "Невель", + "Невельск", + "Невинномысск", + "Невьянск", + "Нелидово", + "Неман", + "Нерехта", + "Нерчинск", + "Нерюнгри", + "Нестеров", + "Нефтегорск", + "Нефтекамск", + "Нефтекумск", + "Нефтеюганск", + "Нея", + "Нижневартовск", + "Нижнекамск", + "Нижнеудинск", + "Нижние Серги", + "Нижний Ломов", + "Нижний Новгород", + "Нижний Тагил", + "Нижняя Салда", + "Нижняя Тура", + "Николаевск", + "Николаевск-на-Амуре", + "Никольск", + "Никольск", + "Никольское", + "Новая Ладога", + "Новая Ляля", + "Новоалександровск", + "Новоалтайск", + "Новоаннинский", + "Нововоронеж", + "Новодвинск", + "Новозыбков", + "Новокубанск", + "Новокузнецк", + "Новокуйбышевск", + "Новомичуринск", + "Новомосковск", + "Новопавловск", + "Новоржев", + "Новороссийск", + "Новосибирск", + "Новосиль", + "Новосокольники", + "Новотроицк", + "Новоузенск", + "Новоульяновск", + "Новоуральск", + "Новохопёрск", + "Новочебоксарск", + "Новочеркасск", + "Новошахтинск", + "Новый Оскол", + "Новый Уренгой", + "Ногинск", + "Нолинск", + "Норильск", + "Ноябрьск", + "Нурлат", + "Нытва", + "Нюрба", + "Нягань", + "Нязепетровск", + "Няндома", + "Облучье", + "Обнинск", + "Обоянь", + "Обь", + "Одинцово", + "Озёрск", + "Озёрск", + "Озёры", + "Октябрьск", + "Октябрьский", + "Окуловка", + "Олёкминск", + "Оленегорск", + "Олонец", + "Омск", + "Омутнинск", + "Онега", + "Опочка", + "Орёл", + "Оренбург", + "Орехово-Зуево", + "Орлов", + "Орск", + "Оса", + "Осинники", + "Осташков", + "Остров", + "Островной", + "Острогожск", + "Отрадное", + "Отрадный", + "Оха", + "Оханск", + "Очёр", + "Павлово", + "Павловск", + "Павловский Посад", + "Палласовка", + "Партизанск", + "Певек", + "Пенза", + "Первомайск", + "Первоуральск", + "Перевоз", + "Пересвет", + "Переславль-Залесский", + "Пермь", + "Пестово", + "Петров Вал", + "Петровск", + "Петровск-Забайкальский", + "Петрозаводск", + "Петропавловск-Камчатский", + "Петухово", + "Петушки", + "Печора", + "Печоры", + "Пикалёво", + "Пионерский", + "Питкяранта", + "Плавск", + "Пласт", + "Плёс", + "Поворино", + "Подольск", + "Подпорожье", + "Покачи", + "Покров", + "Покровск", + "Полевской", + "Полесск", + "Полысаево", + "Полярные Зори", + "Полярный", + "Поронайск", + "Порхов", + "Похвистнево", + "Почеп", + "Починок", + "Пошехонье", + "Правдинск", + "Приволжск", + "Приморск", + "Приморск", + "Приморско-Ахтарск", + "Приозерск", + "Прокопьевск", + "Пролетарск", + "Протвино", + "Прохладный", + "Псков", + "Пугачёв", + "Пудож", + "Пустошка", + "Пучеж", + "Пушкино", + "Пущино", + "Пыталово", + "Пыть-Ях", + "Пятигорск", + "Радужный", + "Радужный", + "Райчихинск", + "Раменское", + "Рассказово", + "Ревда", + "Реж", + "Реутов", + "Ржев", + "Родники", + "Рославль", + "Россошь", + "Ростов-на-Дону", + "Ростов", + "Рошаль", + "Ртищево", + "Рубцовск", + "Рудня", + "Руза", + "Рузаевка", + "Рыбинск", + "Рыбное", + "Рыльск", + "Ряжск", + "Рязань", + "СакиОспаривается", + "Салават", + "Салаир", + "Салехард", + "Сальск", + "Самара", + "Санкт-Петербург", + "Саранск", + "Сарапул", + "Саратов", + "Саров", + "Сасово", + "Сатка", + "Сафоново", + "Саяногорск", + "Саянск", + "Светлогорск", + "Светлоград", + "Светлый", + "Светогорск", + "Свирск", + "Свободный", + "Себеж", + "СевастопольОспаривается", + "Северо-Курильск", + "Северобайкальск", + "Северодвинск", + "Североморск", + "Североуральск", + "Северск", + "Севск", + "Сегежа", + "Сельцо", + "Семёнов", + "Семикаракорск", + "Семилуки", + "Сенгилей", + "Серафимович", + "Сергач", + "Сергиев Посад", + "Сердобск", + "Серов", + "Серпухов", + "Сертолово", + "Сибай", + "Сим", + "СимферопольОспаривается", + "Сковородино", + "Скопин", + "Славгород", + "Славск", + "Славянск-на-Кубани", + "Сланцы", + "Слободской", + "Слюдянка", + "Смоленск", + "Снежинск", + "Снежногорск", + "Собинка", + "Советск", + "Советск", + "Советск", + "Советская Гавань", + "Советский", + "Сокол", + "Солигалич", + "Соликамск", + "Солнечногорск", + "Соль-Илецк", + "Сольвычегодск", + "Сольцы", + "Сорочинск", + "Сорск", + "Сортавала", + "Сосенский", + "Сосновка", + "Сосновоборск", + "Сосновый Бор", + "Сосногорск", + "Сочи", + "Спас-Деменск", + "Спас-Клепики", + "Спасск", + "Спасск-Дальний", + "Спасск-Рязанский", + "Среднеколымск", + "Среднеуральск", + "Сретенск", + "Ставрополь", + "Старая Купавна", + "Старая Русса", + "Старица", + "Стародуб", + "Старый КрымОспаривается", + "Старый Оскол", + "Стерлитамак", + "Стрежевой", + "Строитель", + "Струнино", + "Ступино", + "Суворов", + "СудакОспаривается", + "Суджа", + "Судогда", + "Суздаль", + "Сунжа", + "Суоярви", + "Сураж", + "Сургут", + "Суровикино", + "Сурск", + "Сусуман", + "Сухиничи", + "Сухой Лог", + "Сызрань", + "Сыктывкар", + "Сысерть", + "Сычёвка", + "Сясьстрой", + "Тавда", + "Таганрог", + "Тайга", + "Тайшет", + "Талдом", + "Талица", + "Тамбов", + "Тара", + "Тарко-Сале", + "Таруса", + "Татарск", + "Таштагол", + "Тверь", + "Теберда", + "Тейково", + "Темников", + "Темрюк", + "Терек", + "Тетюши", + "Тимашёвск", + "Тихвин", + "Тихорецк", + "Тобольск", + "Тогучин", + "Тольятти", + "Томари", + "Томмот", + "Томск", + "Топки", + "Торжок", + "Торопец", + "Тосно", + "Тотьма", + "Трёхгорный", + "Троицк", + "Трубчевск", + "Туапсе", + "Туймазы", + "Тула", + "Тулун", + "Туран", + "Туринск", + "Тутаев", + "Тында", + "Тырныауз", + "Тюкалинск", + "Тюмень", + "Уварово", + "Углегорск", + "Углич", + "Удачный", + "Удомля", + "Ужур", + "Узловая", + "Улан-Удэ", + "Ульяновск", + "Унеча", + "Урай", + "Урень", + "Уржум", + "Урус-Мартан", + "Урюпинск", + "Усинск", + "Усмань", + "Усолье-Сибирское", + "Усолье", + "Уссурийск", + "Усть-Джегута", + "Усть-Илимск", + "Усть-Катав", + "Усть-Кут", + "Усть-Лабинск", + "Устюжна", + "Уфа", + "Ухта", + "Учалы", + "Уяр", + "Фатеж", + "ФеодосияОспаривается", + "Фокино", + "Фокино", + "Фролово", + "Фрязино", + "Фурманов", + "Хабаровск", + "Хадыженск", + "Ханты-Мансийск", + "Харабали", + "Харовск", + "Хасавюрт", + "Хвалынск", + "Хилок", + "Химки", + "Холм", + "Холмск", + "Хотьково", + "Цивильск", + "Цимлянск", + "Циолковский", + "Чадан", + "Чайковский", + "Чапаевск", + "Чаплыгин", + "Чебаркуль", + "Чебоксары", + "Чегем", + "Чекалин", + "Челябинск", + "Чердынь", + "Черемхово", + "Черепаново", + "Череповец", + "Черкесск", + "Чёрмоз", + "Черноголовка", + "Черногорск", + "Чернушка", + "Черняховск", + "Чехов", + "Чистополь", + "Чита", + "Чкаловск", + "Чудово", + "Чулым", + "Чусовой", + "Чухлома", + "Шагонар", + "Шадринск", + "Шали", + "Шарыпово", + "Шарья", + "Шатура", + "Шахты", + "Шахунья", + "Шацк", + "Шебекино", + "Шелехов", + "Шенкурск", + "Шилка", + "Шимановск", + "Шиханы", + "Шлиссельбург", + "Шумерля", + "Шумиха", + "Шуя", + "Щёкино", + "ЩёлкиноОспаривается", + "Щёлково", + "Щигры", + "Щучье", + "Электрогорск", + "Электросталь", + "Электроугли", + "Элиста", + "Энгельс", + "Эртиль", + "Югорск", + "Южа", + "Южно-Сахалинск", + "Южно-Сухокумск", + "Южноуральск", + "Юрга", + "Юрьев-Польский", + "Юрьевец", + "Юрюзань", + "Юхнов", + "Ядрин", + "Якутск", + "ЯлтаОспаривается", + "Ялуторовск", + "Янаул", + "Яранск", + "Яровое", + "Ярославль", + "Ярцево", + "Ясногорск", + "Ясный", + "Яхрома" + ]; mediaQueryData = MediaQuery.of(context); return SafeArea( child: Scaffold( @@ -71,38 +1191,64 @@ class CityOfLivingScreen extends StatelessWidget { width: 12.adaptSize)), hintText: "Не выбрано".tr, items: cityOfLivingModelObj?.dropdownItemList ?? [], - onChanged: (value) { - context - .read() - .add(ChangeDropDownEvent(value: value)); - }); + onChanged: onDropDownChange(context)); }), SizedBox(height: 22.v), - Text("city".tr, style: theme.textTheme.bodyLarge), + Text("Населенный пункт", style: theme.textTheme.bodyLarge), SizedBox(height: 2.v), - BlocSelector( - selector: (state) => state.cityOfLivingModelObj, - builder: (context, cityOfLivingModelObj) { - return CustomDropDown( - icon: Container( - margin: - EdgeInsets.fromLTRB(30.h, 11.v, 17.h, 11.v), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(1.h)), - child: CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize)), - hintText: "Не выбрано".tr, - items: - cityOfLivingModelObj?.dropdownItemList1 ?? [], - onChanged: (value) { - context - .read() - .add(ChangeDropDown1Event(value: value)); - }); + BlocBuilder( + builder: (context, state) { + return RawAutocomplete( + optionsBuilder: (TextEditingValue textEditingValue) => + _options.where((String option) { + return option + .toLowerCase() + .startsWith(textEditingValue.text.toLowerCase()); }), + fieldViewBuilder: ( + BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + return CustomTextFormField( + controller: textEditingController, + focusNode: focusNode, + onFieldSubmitted: (String value) => + onFieldSubmitted(), + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: SizedBox( + height: 200.0, + child: ListView.builder( + padding: const EdgeInsets.all(8.0), + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final String option = + options.elementAt(index); + return GestureDetector( + onTap: () => onSelected(option), + child: ListTile( + title: Text(option), + ), + ); + }, + ), + ), + ), + ); + }, + ); + }), Spacer(flex: 65), BlocBuilder( builder: (context, state) { @@ -111,35 +1257,65 @@ class CityOfLivingScreen extends StatelessWidget { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = - state.cityOfLivingModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.bodyCircumferencesScreen, - arguments: sheet, - ); - }); + onPressed: onTapContinue(state, context)); }), ])))); } + onTapContinue(CityOfLivingState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.cityOfLivingModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.bodyCircumferencesScreen, + arguments: sheet, + ); + }; + } + + onDropDownChange(BuildContext context) { + return (value) { + context.read().add(ChangeDropDownEvent(value: value)); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); } } + +selectProduct(AutocompleteOnSelected onSelected, String option, + CityOfLivingState state) { + state.cityController?.text = option; + return () { + onSelected(option); + }; +} + +buildOptions(List _options) { + return (TextEditingValue textEditingValue) { + return _options.where((String option) { + return true; + }); + }; +} diff --git a/lib/presentation/registration/city_of_living_screen/models/city_of_living_model.dart b/lib/presentation/registration/city_of_living_screen/models/city_of_living_model.dart index 048621df..10366f3d 100644 --- a/lib/presentation/registration/city_of_living_screen/models/city_of_living_model.dart +++ b/lib/presentation/registration/city_of_living_screen/models/city_of_living_model.dart @@ -11,12 +11,1130 @@ class CityOfLivingModel extends Equatable { CityOfLivingModel({ required this.userRegistrationSheet, this.dropdownItemList = const [], - this.dropdownItemList1 = const [], + this.cityList = const [ + "Абаза", + "Абакан", + "Абдулино", + "Абинск", + "Агидель", + "Агрыз", + "Адыгейск", + "Азнакаево", + "Азов", + "Ак-Довурак", + "Аксай", + "Алагир", + "Алапаевск", + "Алатырь", + "Алдан", + "Алейск", + "Александров", + "Александровск", + "Александровск-Сахалинский", + "Алексеевка", + "Алексин", + "Алзамай", + "АлупкаОспаривается", + "АлуштаОспаривается", + "Альметьевск", + "Амурск", + "Анадырь", + "Анапа", + "Ангарск", + "Андреаполь", + "Анжеро-Судженск", + "Анива", + "Апатиты", + "Апрелевка", + "Апшеронск", + "Арамиль", + "Аргун", + "Ардатов", + "Ардон", + "Арзамас", + "Аркадак", + "Армавир", + "АрмянскОспаривается", + "Арсеньев", + "Арск", + "Артём", + "Артёмовск", + "Артёмовский", + "Архангельск", + "Асбест", + "Асино", + "Астрахань", + "Аткарск", + "Ахтубинск", + "Ачинск", + "Аша", + "Бабаево", + "Бабушкин", + "Бавлы", + "Багратионовск", + "Байкальск", + "Баймак", + "Бакал", + "Баксан", + "Балабаново", + "Балаково", + "Балахна", + "Балашиха", + "Балашов", + "Балей", + "Балтийск", + "Барабинск", + "Барнаул", + "Барыш", + "Батайск", + "БахчисарайОспаривается", + "Бежецк", + "Белая Калитва", + "Белая Холуница", + "Белгород", + "Белебей", + "Белёв", + "Белинский", + "Белово", + "Белогорск", + "БелогорскОспаривается", + "Белозерск", + "Белокуриха", + "Беломорск", + "Белоозёрский", + "Белорецк", + "Белореченск", + "Белоусово", + "Белоярский", + "Белый", + "Бердск", + "Березники", + "Берёзовский", + "Берёзовский", + "Беслан", + "Бийск", + "Бикин", + "Билибино", + "Биробиджан", + "Бирск", + "Бирюсинск", + "Бирюч", + "Благовещенск", + "Благовещенск", + "Благодарный", + "Бобров", + "Богданович", + "Богородицк", + "Богородск", + "Боготол", + "Богучар", + "Бодайбо", + "Бокситогорск", + "Болгар", + "Бологое", + "Болотное", + "Болохово", + "Болхов", + "Большой Камень", + "Бор", + "Борзя", + "Борисоглебск", + "Боровичи", + "Боровск", + "Бородино", + "Братск", + "Бронницы", + "Брянск", + "Бугульма", + "Бугуруслан", + "Будённовск", + "Бузулук", + "Буинск", + "Буй", + "Буйнакск", + "Бутурлиновка", + "Валдай", + "Валуйки", + "Велиж", + "Великие Луки", + "Великий Новгород", + "Великий Устюг", + "Вельск", + "Венёв", + "Верещагино", + "Верея", + "Верхнеуральск", + "Верхний Тагил", + "Верхний Уфалей", + "Верхняя Пышма", + "Верхняя Салда", + "Верхняя Тура", + "Верхотурье", + "Верхоянск", + "Весьегонск", + "Ветлуга", + "Видное", + "Вилюйск", + "Вилючинск", + "Вихоревка", + "Вичуга", + "Владивосток", + "Владикавказ", + "Владимир", + "Волгоград", + "Волгодонск", + "Волгореченск", + "Волжск", + "Волжский", + "Вологда", + "Володарск", + "Волоколамск", + "Волосово", + "Волхов", + "Волчанск", + "Вольск", + "Воркута", + "Воронеж", + "Ворсма", + "Воскресенск", + "Воткинск", + "Всеволожск", + "Вуктыл", + "Выборг", + "Выкса", + "Высоковск", + "Высоцк", + "Вытегра", + "Вышний Волочёк", + "Вяземский", + "Вязники", + "Вязьма", + "Вятские Поляны", + "Гаврилов Посад", + "Гаврилов-Ям", + "Гагарин", + "Гаджиево", + "Гай", + "Галич", + "Гатчина", + "Гвардейск", + "Гдов", + "Геленджик", + "Георгиевск", + "Глазов", + "Голицыно", + "Горбатов", + "Горно-Алтайск", + "Горнозаводск", + "Горняк", + "Городец", + "Городище", + "Городовиковск", + "Гороховец", + "Горячий Ключ", + "Грайворон", + "Гремячинск", + "Грозный", + "Грязи", + "Грязовец", + "Губаха", + "Губкин", + "Губкинский", + "Гудермес", + "Гуково", + "Гулькевичи", + "Гурьевск", + "Гурьевск", + "Гусев", + "Гусиноозёрск", + "Гусь-Хрустальный", + "Давлеканово", + "Дагестанские Огни", + "Далматово", + "Дальнегорск", + "Дальнереченск", + "Данилов", + "Данков", + "Дегтярск", + "Дедовск", + "Демидов", + "Дербент", + "Десногорск", + "ДжанкойОспаривается", + "Дзержинск", + "Дзержинский", + "Дивногорск", + "Дигора", + "Димитровград", + "Дмитриев", + "Дмитров", + "Дмитровск", + "Дно", + "Добрянка", + "Долгопрудный", + "Долинск", + "Домодедово", + "Донецк", + "Донской", + "Дорогобуж", + "Дрезна", + "Дубна", + "Дубовка", + "Дудинка", + "Духовщина", + "Дюртюли", + "Дятьково", + "ЕвпаторияОспаривается", + "Егорьевск", + "Ейск", + "Екатеринбург", + "Елабуга", + "Елец", + "Елизово", + "Ельня", + "Еманжелинск", + "Емва", + "Енисейск", + "Ермолино", + "Ершов", + "Ессентуки", + "Ефремов", + "Железноводск", + "Железногорск", + "Железногорск", + "Железногорск-Илимский", + "Жердевка", + "Жигулёвск", + "Жиздра", + "Жирновск", + "Жуков", + "Жуковка", + "Жуковский", + "Завитинск", + "Заводоуковск", + "Заволжск", + "Заволжье", + "Задонск", + "Заинск", + "Закаменск", + "Заозёрный", + "Заозёрск", + "Западная Двина", + "Заполярный", + "Зарайск", + "Заречный", + "Заречный", + "Заринск", + "Звенигово", + "Звенигород", + "Зверево", + "Зеленогорск", + "Зеленоградск", + "Зеленодольск", + "Зеленокумск", + "Зерноград", + "Зея", + "Зима", + "Златоуст", + "Злынка", + "Змеиногорск", + "Знаменск", + "Зубцов", + "Зуевка", + "Ивангород", + "Иваново", + "Ивантеевка", + "Ивдель", + "Игарка", + "Ижевск", + "Избербаш", + "Изобильный", + "Иланский", + "Инза", + "Иннополис", + "Инсар", + "Инта", + "Ипатово", + "Ирбит", + "Иркутск", + "Исилькуль", + "Искитим", + "Истра", + "Ишим", + "Ишимбай", + "Йошкар-Ола", + "Кадников", + "Казань", + "Калач", + "Калач-на-Дону", + "Калачинск", + "Калининград", + "Калининск", + "Калтан", + "Калуга", + "Калязин", + "Камбарка", + "Каменка", + "Каменногорск", + "Каменск-Уральский", + "Каменск-Шахтинский", + "Камень-на-Оби", + "Камешково", + "Камызяк", + "Камышин", + "Камышлов", + "Канаш", + "Кандалакша", + "Канск", + "Карабаново", + "Карабаш", + "Карабулак", + "Карасук", + "Карачаевск", + "Карачев", + "Каргат", + "Каргополь", + "Карпинск", + "Карталы", + "Касимов", + "Касли", + "Каспийск", + "Катав-Ивановск", + "Катайск", + "Качканар", + "Кашин", + "Кашира", + "Кедровый", + "Кемерово", + "Кемь", + "КерчьОспаривается", + "Кизел", + "Кизилюрт", + "Кизляр", + "Кимовск", + "Кимры", + "Кингисепп", + "Кинель", + "Кинешма", + "Киреевск", + "Киренск", + "Киржач", + "Кириллов", + "Кириши", + "Киров", + "Киров", + "Кировград", + "Кирово-Чепецк", + "Кировск", + "Кировск", + "Кирс", + "Кирсанов", + "Киселёвск", + "Кисловодск", + "Клин", + "Клинцы", + "Княгинино", + "Ковдор", + "Ковров", + "Ковылкино", + "Когалым", + "Кодинск", + "Козельск", + "Козловка", + "Козьмодемьянск", + "Кола", + "Кологрив", + "Коломна", + "Колпашево", + "Кольчугино", + "Коммунар", + "Комсомольск", + "Комсомольск-на-Амуре", + "Конаково", + "Кондопога", + "Кондрово", + "Константиновск", + "Копейск", + "Кораблино", + "Кореновск", + "Коркино", + "Королёв", + "Короча", + "Корсаков", + "Коряжма", + "Костерёво", + "Костомукша", + "Кострома", + "Котельники", + "Котельниково", + "Котельнич", + "Котлас", + "Котово", + "Котовск", + "Кохма", + "Красавино", + "Красноармейск", + "Красноармейск", + "Красновишерск", + "Красногорск", + "Краснодар", + "Краснозаводск", + "Краснознаменск", + "Краснознаменск", + "Краснокаменск", + "Краснокамск", + "КрасноперекопскОспаривается", + "Краснослободск", + "Краснослободск", + "Краснотурьинск", + "Красноуральск", + "Красноуфимск", + "Красноярск", + "Красный Кут", + "Красный Сулин", + "Красный Холм", + "Кремёнки", + "Кропоткин", + "Крымск", + "Кстово", + "Кубинка", + "Кувандык", + "Кувшиново", + "Кудрово", + "Кудымкар", + "Кузнецк", + "Куйбышев", + "Кукмор", + "Кулебаки", + "Кумертау", + "Кунгур", + "Купино", + "Курган", + "Курганинск", + "Курильск", + "Курлово", + "Куровское", + "Курск", + "Куртамыш", + "Курчалой", + "Курчатов", + "Куса", + "Кушва", + "Кызыл", + "Кыштым", + "Кяхта", + "Лабинск", + "Лабытнанги", + "Лагань", + "Ладушкин", + "Лаишево", + "Лакинск", + "Лангепас", + "Лахденпохья", + "Лебедянь", + "Лениногорск", + "Ленинск", + "Ленинск-Кузнецкий", + "Ленск", + "Лермонтов", + "Лесной", + "Лесозаводск", + "Лесосибирск", + "Ливны", + "Ликино-Дулёво", + "Липецк", + "Липки", + "Лиски", + "Лихославль", + "Лобня", + "Лодейное Поле", + "Лосино-Петровский", + "Луга", + "Луза", + "Лукоянов", + "Луховицы", + "Лысково", + "Лысьва", + "Лыткарино", + "Льгов", + "Любань", + "Люберцы", + "Любим", + "Людиново", + "Лянтор", + "Магадан", + "Магас", + "Магнитогорск", + "Майкоп", + "Майский", + "Макаров", + "Макарьев", + "Макушино", + "Малая Вишера", + "Малгобек", + "Малмыж", + "Малоархангельск", + "Малоярославец", + "Мамадыш", + "Мамоново", + "Мантурово", + "Мариинск", + "Мариинский Посад", + "Маркс", + "Махачкала", + "Мглин", + "Мегион", + "Медвежьегорск", + "Медногорск", + "Медынь", + "Межгорье", + "Междуреченск", + "Мезень", + "Меленки", + "Мелеуз", + "Менделеевск", + "Мензелинск", + "Мещовск", + "Миасс", + "Микунь", + "Миллерово", + "Минеральные Воды", + "Минусинск", + "Миньяр", + "Мирный", + "Мирный", + "Михайлов", + "Михайловка", + "Михайловск", + "Михайловск", + "Мичуринск", + "Могоча", + "Можайск", + "Можга", + "Моздок", + "Мончегорск", + "Морозовск", + "Моршанск", + "Мосальск", + "Москва", + "Муравленко", + "Мураши", + "Мурино", + "Мурманск", + "Муром", + "Мценск", + "Мыски", + "Мытищи", + "Мышкин", + "Набережные Челны", + "Навашино", + "Наволоки", + "Надым", + "Назарово", + "Назрань", + "Называевск", + "Нальчик", + "Нариманов", + "Наро-Фоминск", + "Нарткала", + "Нарьян-Мар", + "Находка", + "Невель", + "Невельск", + "Невинномысск", + "Невьянск", + "Нелидово", + "Неман", + "Нерехта", + "Нерчинск", + "Нерюнгри", + "Нестеров", + "Нефтегорск", + "Нефтекамск", + "Нефтекумск", + "Нефтеюганск", + "Нея", + "Нижневартовск", + "Нижнекамск", + "Нижнеудинск", + "Нижние Серги", + "Нижний Ломов", + "Нижний Новгород", + "Нижний Тагил", + "Нижняя Салда", + "Нижняя Тура", + "Николаевск", + "Николаевск-на-Амуре", + "Никольск", + "Никольск", + "Никольское", + "Новая Ладога", + "Новая Ляля", + "Новоалександровск", + "Новоалтайск", + "Новоаннинский", + "Нововоронеж", + "Новодвинск", + "Новозыбков", + "Новокубанск", + "Новокузнецк", + "Новокуйбышевск", + "Новомичуринск", + "Новомосковск", + "Новопавловск", + "Новоржев", + "Новороссийск", + "Новосибирск", + "Новосиль", + "Новосокольники", + "Новотроицк", + "Новоузенск", + "Новоульяновск", + "Новоуральск", + "Новохопёрск", + "Новочебоксарск", + "Новочеркасск", + "Новошахтинск", + "Новый Оскол", + "Новый Уренгой", + "Ногинск", + "Нолинск", + "Норильск", + "Ноябрьск", + "Нурлат", + "Нытва", + "Нюрба", + "Нягань", + "Нязепетровск", + "Няндома", + "Облучье", + "Обнинск", + "Обоянь", + "Обь", + "Одинцово", + "Озёрск", + "Озёрск", + "Озёры", + "Октябрьск", + "Октябрьский", + "Окуловка", + "Олёкминск", + "Оленегорск", + "Олонец", + "Омск", + "Омутнинск", + "Онега", + "Опочка", + "Орёл", + "Оренбург", + "Орехово-Зуево", + "Орлов", + "Орск", + "Оса", + "Осинники", + "Осташков", + "Остров", + "Островной", + "Острогожск", + "Отрадное", + "Отрадный", + "Оха", + "Оханск", + "Очёр", + "Павлово", + "Павловск", + "Павловский Посад", + "Палласовка", + "Партизанск", + "Певек", + "Пенза", + "Первомайск", + "Первоуральск", + "Перевоз", + "Пересвет", + "Переславль-Залесский", + "Пермь", + "Пестово", + "Петров Вал", + "Петровск", + "Петровск-Забайкальский", + "Петрозаводск", + "Петропавловск-Камчатский", + "Петухово", + "Петушки", + "Печора", + "Печоры", + "Пикалёво", + "Пионерский", + "Питкяранта", + "Плавск", + "Пласт", + "Плёс", + "Поворино", + "Подольск", + "Подпорожье", + "Покачи", + "Покров", + "Покровск", + "Полевской", + "Полесск", + "Полысаево", + "Полярные Зори", + "Полярный", + "Поронайск", + "Порхов", + "Похвистнево", + "Почеп", + "Починок", + "Пошехонье", + "Правдинск", + "Приволжск", + "Приморск", + "Приморск", + "Приморско-Ахтарск", + "Приозерск", + "Прокопьевск", + "Пролетарск", + "Протвино", + "Прохладный", + "Псков", + "Пугачёв", + "Пудож", + "Пустошка", + "Пучеж", + "Пушкино", + "Пущино", + "Пыталово", + "Пыть-Ях", + "Пятигорск", + "Радужный", + "Радужный", + "Райчихинск", + "Раменское", + "Рассказово", + "Ревда", + "Реж", + "Реутов", + "Ржев", + "Родники", + "Рославль", + "Россошь", + "Ростов-на-Дону", + "Ростов", + "Рошаль", + "Ртищево", + "Рубцовск", + "Рудня", + "Руза", + "Рузаевка", + "Рыбинск", + "Рыбное", + "Рыльск", + "Ряжск", + "Рязань", + "СакиОспаривается", + "Салават", + "Салаир", + "Салехард", + "Сальск", + "Самара", + "Санкт-Петербург", + "Саранск", + "Сарапул", + "Саратов", + "Саров", + "Сасово", + "Сатка", + "Сафоново", + "Саяногорск", + "Саянск", + "Светлогорск", + "Светлоград", + "Светлый", + "Светогорск", + "Свирск", + "Свободный", + "Себеж", + "СевастопольОспаривается", + "Северо-Курильск", + "Северобайкальск", + "Северодвинск", + "Североморск", + "Североуральск", + "Северск", + "Севск", + "Сегежа", + "Сельцо", + "Семёнов", + "Семикаракорск", + "Семилуки", + "Сенгилей", + "Серафимович", + "Сергач", + "Сергиев Посад", + "Сердобск", + "Серов", + "Серпухов", + "Сертолово", + "Сибай", + "Сим", + "СимферопольОспаривается", + "Сковородино", + "Скопин", + "Славгород", + "Славск", + "Славянск-на-Кубани", + "Сланцы", + "Слободской", + "Слюдянка", + "Смоленск", + "Снежинск", + "Снежногорск", + "Собинка", + "Советск", + "Советск", + "Советск", + "Советская Гавань", + "Советский", + "Сокол", + "Солигалич", + "Соликамск", + "Солнечногорск", + "Соль-Илецк", + "Сольвычегодск", + "Сольцы", + "Сорочинск", + "Сорск", + "Сортавала", + "Сосенский", + "Сосновка", + "Сосновоборск", + "Сосновый Бор", + "Сосногорск", + "Сочи", + "Спас-Деменск", + "Спас-Клепики", + "Спасск", + "Спасск-Дальний", + "Спасск-Рязанский", + "Среднеколымск", + "Среднеуральск", + "Сретенск", + "Ставрополь", + "Старая Купавна", + "Старая Русса", + "Старица", + "Стародуб", + "Старый КрымОспаривается", + "Старый Оскол", + "Стерлитамак", + "Стрежевой", + "Строитель", + "Струнино", + "Ступино", + "Суворов", + "СудакОспаривается", + "Суджа", + "Судогда", + "Суздаль", + "Сунжа", + "Суоярви", + "Сураж", + "Сургут", + "Суровикино", + "Сурск", + "Сусуман", + "Сухиничи", + "Сухой Лог", + "Сызрань", + "Сыктывкар", + "Сысерть", + "Сычёвка", + "Сясьстрой", + "Тавда", + "Таганрог", + "Тайга", + "Тайшет", + "Талдом", + "Талица", + "Тамбов", + "Тара", + "Тарко-Сале", + "Таруса", + "Татарск", + "Таштагол", + "Тверь", + "Теберда", + "Тейково", + "Темников", + "Темрюк", + "Терек", + "Тетюши", + "Тимашёвск", + "Тихвин", + "Тихорецк", + "Тобольск", + "Тогучин", + "Тольятти", + "Томари", + "Томмот", + "Томск", + "Топки", + "Торжок", + "Торопец", + "Тосно", + "Тотьма", + "Трёхгорный", + "Троицк", + "Трубчевск", + "Туапсе", + "Туймазы", + "Тула", + "Тулун", + "Туран", + "Туринск", + "Тутаев", + "Тында", + "Тырныауз", + "Тюкалинск", + "Тюмень", + "Уварово", + "Углегорск", + "Углич", + "Удачный", + "Удомля", + "Ужур", + "Узловая", + "Улан-Удэ", + "Ульяновск", + "Унеча", + "Урай", + "Урень", + "Уржум", + "Урус-Мартан", + "Урюпинск", + "Усинск", + "Усмань", + "Усолье-Сибирское", + "Усолье", + "Уссурийск", + "Усть-Джегута", + "Усть-Илимск", + "Усть-Катав", + "Усть-Кут", + "Усть-Лабинск", + "Устюжна", + "Уфа", + "Ухта", + "Учалы", + "Уяр", + "Фатеж", + "ФеодосияОспаривается", + "Фокино", + "Фокино", + "Фролово", + "Фрязино", + "Фурманов", + "Хабаровск", + "Хадыженск", + "Ханты-Мансийск", + "Харабали", + "Харовск", + "Хасавюрт", + "Хвалынск", + "Хилок", + "Химки", + "Холм", + "Холмск", + "Хотьково", + "Цивильск", + "Цимлянск", + "Циолковский", + "Чадан", + "Чайковский", + "Чапаевск", + "Чаплыгин", + "Чебаркуль", + "Чебоксары", + "Чегем", + "Чекалин", + "Челябинск", + "Чердынь", + "Черемхово", + "Черепаново", + "Череповец", + "Черкесск", + "Чёрмоз", + "Черноголовка", + "Черногорск", + "Чернушка", + "Черняховск", + "Чехов", + "Чистополь", + "Чита", + "Чкаловск", + "Чудово", + "Чулым", + "Чусовой", + "Чухлома", + "Шагонар", + "Шадринск", + "Шали", + "Шарыпово", + "Шарья", + "Шатура", + "Шахты", + "Шахунья", + "Шацк", + "Шебекино", + "Шелехов", + "Шенкурск", + "Шилка", + "Шимановск", + "Шиханы", + "Шлиссельбург", + "Шумерля", + "Шумиха", + "Шуя", + "Щёкино", + "ЩёлкиноОспаривается", + "Щёлково", + "Щигры", + "Щучье", + "Электрогорск", + "Электросталь", + "Электроугли", + "Элиста", + "Энгельс", + "Эртиль", + "Югорск", + "Южа", + "Южно-Сахалинск", + "Южно-Сухокумск", + "Южноуральск", + "Юрга", + "Юрьев-Польский", + "Юрьевец", + "Юрюзань", + "Юхнов", + "Ядрин", + "Якутск", + "ЯлтаОспаривается", + "Ялуторовск", + "Янаул", + "Яранск", + "Яровое", + "Ярославль", + "Ярцево", + "Ясногорск", + "Ясный", + "Яхрома" + ], }); List dropdownItemList; - List dropdownItemList1; + List cityList; CityOfLivingModel copyWith({ List? dropdownItemList, @@ -25,11 +1143,11 @@ class CityOfLivingModel extends Equatable { return CityOfLivingModel( userRegistrationSheet: userRegistrationSheet, dropdownItemList: dropdownItemList ?? this.dropdownItemList, - dropdownItemList1: dropdownItemList1 ?? this.dropdownItemList1, + cityList: cityList, ); } @override List get props => - [dropdownItemList, dropdownItemList1, userRegistrationSheet]; + [dropdownItemList, cityList, userRegistrationSheet]; } diff --git a/lib/presentation/registration/credentials_screen/credentials_screen.dart b/lib/presentation/registration/credentials_screen/credentials_screen.dart index 07607b4f..48979999 100644 --- a/lib/presentation/registration/credentials_screen/credentials_screen.dart +++ b/lib/presentation/registration/credentials_screen/credentials_screen.dart @@ -107,55 +107,62 @@ class CredentialsScreen extends StatelessWidget { margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - List errors = validateFields( - state.emailController!.text, - state.nameController!.text, - state.passwordController!.text, - state.passwordConfirmationController!.text); - if (errors.isEmpty) { - UserRegistrationSheet sheet = - state.credentialsModelObj!.userRegistrationSheet; - sheet.email = state.emailController!.text; - sheet.name = state.nameController!.text; - sheet.password = state.passwordController!.text; - sheet.passwordConfirmation = - state.passwordConfirmationController!.text; - Navigator.pushNamed( - context, - AppRoutes.nameScreen, - arguments: sheet, - ); - } else { - String errorMessage = ''; - - for (var error in errors) { - errorMessage += error + '\n\n'; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); + onPressed: onTapContinue(state, context)); }), ]))); } + onTapContinue(CredentialsState state, BuildContext context) { + return () { + List errors = validateFields( + state.emailController!.text, + state.nameController!.text, + state.passwordController!.text, + state.passwordConfirmationController!.text); + if (errors.isEmpty) { + UserRegistrationSheet sheet = + state.credentialsModelObj!.userRegistrationSheet; + sheet.email = state.emailController!.text; + sheet.name = state.nameController!.text; + sheet.password = state.passwordController!.text; + sheet.passwordConfirmation = state.passwordConfirmationController!.text; + Navigator.pushNamed( + context, + AppRoutes.nameScreen, + arguments: sheet, + ); + } else { + String errorMessage = ''; + + for (var error in errors) { + errorMessage += error + '\n\n'; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Ошибка'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: onTapOk(context), + child: Text('OK'), + ), + ], + ), + ); + } + }; + } + + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, centerTitle: true, diff --git a/lib/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart b/lib/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart index 49ef2848..df859872 100644 --- a/lib/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart +++ b/lib/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart @@ -69,37 +69,45 @@ class _DateOfBirthScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = state - .dateOfBirthModelObj!.userRegistrationSheet; - sheet.birthdate = selectedDate.toString(); - Navigator.pushNamed( - context, - AppRoutes.cityOfLivingScreen, - arguments: sheet, - ); - }); + onPressed: onTapContinue(state, context)); }), ])))); }); } + onTapContinue(DateOfBirthState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.dateOfBirthModelObj!.userRegistrationSheet; + sheet.birthdate = selectedDate.toString(); + Navigator.pushNamed( + context, + AppRoutes.cityOfLivingScreen, + arguments: sheet, + ); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Section Widget - Widget _buildDateInput(BuildContext context) { + _buildDateInput(BuildContext context) { return Container( width: 343.h, padding: EdgeInsets.symmetric(horizontal: 4.h, vertical: 1.v), @@ -119,9 +127,7 @@ class _DateOfBirthScreenState extends State { Align( alignment: Alignment.center, child: GestureDetector( - onTap: () { - openDatePickerDialog(context); - }, + onTap: onTapCalendar(context), child: Text( '${selectedDate.day}.${selectedDate.month}.${selectedDate.year}', style: theme.textTheme.titleMedium))), @@ -129,6 +135,12 @@ class _DateOfBirthScreenState extends State { ])); } + onTapCalendar(BuildContext context) { + return () { + openDatePickerDialog(context); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); @@ -139,15 +151,21 @@ class _DateOfBirthScreenState extends State { /// This function returns a `Future` that completes with `void`. Future openDatePickerDialog(BuildContext context) async { DateTime? dateTime = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(1924), - lastDate: DateTime( - DateTime.now().year, DateTime.now().month, DateTime.now().day)); + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1924), + lastDate: DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + ), + ); if (dateTime != null) { - setState(() { - selectedDate = dateTime; - }); + if (mounted) { + setState(() { + selectedDate = dateTime; + }); + } } } } diff --git a/lib/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart b/lib/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart index 1e189d19..dac3ebd0 100644 --- a/lib/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart +++ b/lib/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart @@ -68,7 +68,7 @@ class _FavoriteFoodsScreenState extends State { _buildTextFormFieldFav(context), SizedBox(height: 12.v), _buildTextFormFieldUnfav(context), - SizedBox(height: 17.v), + Spacer(), BlocBuilder( builder: (context, state) { return NextStepButton( @@ -76,54 +76,64 @@ class _FavoriteFoodsScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 40.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = state - .favoriteFoodsModelObj!.userRegistrationSheet; - sheet.favoriteFoods = - state.favoriteFoodsController!.text; - sheet.unfavoriteFoods = - state.unfavoriteFoodsController!.text; - Navigator.pushNamed( - context, - AppRoutes.additionalInformationScreen, - arguments: sheet, - ); - }); + onPressed: onTapContinue(state, context)); }), BlocBuilder( builder: (context, state) { return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.favoriteFoodsModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.additionalInformationScreen, - arguments: sheet, - ); - }, + onTap: onTapSkip(state, context), child: Text("skip".tr, style: theme.textTheme.bodyLarge), ); }), ])))); } + onTapSkip(FavoriteFoodsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.favoriteFoodsModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.additionalInformationScreen, + arguments: sheet, + ); + }; + } + + onTapContinue(FavoriteFoodsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.favoriteFoodsModelObj!.userRegistrationSheet; + sheet.favoriteFoods = state.favoriteFoodsController!.text; + sheet.unfavoriteFoods = state.unfavoriteFoodsController!.text; + Navigator.pushNamed( + context, + AppRoutes.additionalInformationScreen, + arguments: sheet, + ); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 31.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Section Widget - Widget _buildTextFormFieldFav(BuildContext context) { + _buildTextFormFieldFav(BuildContext context) { return Column(children: [ Text("favorite_foods".tr, style: CustomTextStyles.titleMediumSegoeUI), SizedBox(height: 2.v), @@ -140,7 +150,7 @@ class _FavoriteFoodsScreenState extends State { } /// Section Widget - Widget _buildTextFormFieldUnfav(BuildContext context) { + _buildTextFormFieldUnfav(BuildContext context) { return Column(children: [ Text("unfavorite_foods".tr, style: CustomTextStyles.titleMediumSegoeUI), SizedBox(height: 2.v), diff --git a/lib/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart b/lib/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart index 8374c74b..e4c6f1cf 100644 --- a/lib/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart +++ b/lib/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart @@ -71,31 +71,12 @@ class _FavoriteSportsScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = state - .favoriteSportsModelObj! - .userRegistrationSheet; - sheet.aboutSportHobbies = - state.enterinformationController!.text; - Navigator.pushNamed( - context, - AppRoutes.favoriteFoodsScreen, - arguments: sheet, - ); - }); + onPressed: onTapContinue(state, context)); }), BlocBuilder( builder: (context, state) { return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = state - .favoriteSportsModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.favoriteFoodsScreen, - arguments: sheet, - ); - }, + onTap: onTapSkip(state, context), child: Text("skip".tr, style: theme.textTheme.bodyLarge), ); @@ -103,21 +84,50 @@ class _FavoriteSportsScreenState extends State { ])))); } - PreferredSizeWidget _buildAppBar(BuildContext context) { + onTapSkip(FavoriteSportsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.favoriteSportsModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.favoriteFoodsScreen, + arguments: sheet, + ); + }; + } + + onTapContinue(FavoriteSportsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.favoriteSportsModelObj!.userRegistrationSheet; + sheet.aboutSportHobbies = state.enterinformationController!.text; + Navigator.pushNamed( + context, + AppRoutes.favoriteFoodsScreen, + arguments: sheet, + ); + }; + } + + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Section Widget - Widget _buildTextFormFieldSection(BuildContext context) { + _buildTextFormFieldSection(BuildContext context) { return Column(children: [ Text("fill_in_information".tr, style: CustomTextStyles.titleMediumSegoeUI), @@ -128,7 +138,8 @@ class _FavoriteSportsScreenState extends State { builder: (context, enterinformationController) { return CustomTextFormField( controller: enterinformationController, - hintText: "enter_information".tr, + hintText: + "Я занимаюсь плаванием , почти каждый вечер прогулки на свежем воздухе, пару раз в месяц играю в волейбол. Стараюсь не сидеть на месте!", textInputAction: TextInputAction.done, maxLines: 8); }) diff --git a/lib/presentation/registration/gender_screen/gender_screen.dart b/lib/presentation/registration/gender_screen/gender_screen.dart index 34dcc859..1db99ac0 100644 --- a/lib/presentation/registration/gender_screen/gender_screen.dart +++ b/lib/presentation/registration/gender_screen/gender_screen.dart @@ -63,10 +63,7 @@ class _GenderScreenState extends State { title: Text('Мужской'), value: 'male', groupValue: state.selectedRadioValue, - onChanged: (value) { - BlocProvider.of(context) - .add(GenderSelectEvent(value: 'male')); - }, + onChanged: onMale(context), secondary: Image.asset( 'assets/images/img_male.png', width: 40, @@ -77,10 +74,7 @@ class _GenderScreenState extends State { title: Text('Женский'), value: 'female', groupValue: state.selectedRadioValue, - onChanged: (value) { - BlocProvider.of(context) - .add(GenderSelectEvent(value: 'female')); - }, + onChanged: onFemale(context), secondary: Image.asset( 'assets/images/img_female.png', width: 40, @@ -95,37 +89,58 @@ class _GenderScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = - state.genderModelObj!.userRegistrationSheet; - sheet.gender = state.selectedRadioValue!; - Navigator.pushNamed( - context, - AppRoutes.dateOfBirthScreen, - arguments: sheet, - ); - }); + onPressed: onTapNext(state, context)); }), ])))); }); } + onTapNext(GenderState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.genderModelObj!.userRegistrationSheet; + sheet.gender = state.selectedRadioValue!; + Navigator.pushNamed( + context, + AppRoutes.dateOfBirthScreen, + arguments: sheet, + ); + }; + } + + onMale(BuildContext context) { + return (value) { + BlocProvider.of(context) + .add(GenderSelectEvent(value: 'male')); + }; + } + + onFemale(BuildContext context) { + return (value) { + BlocProvider.of(context) + .add(GenderSelectEvent(value: 'female')); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - NavigatorService.pushNamed( - AppRoutes.dateOfBirthScreen, - ); - }), + onTap: onTapBack()), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack() { + return () { + NavigatorService.pushNamed( + AppRoutes.dateOfBirthScreen, + ); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); diff --git a/lib/presentation/registration/height_screen/bloc/height_bloc.dart b/lib/presentation/registration/height_screen/bloc/height_bloc.dart index f9330fd6..70b8ba60 100644 --- a/lib/presentation/registration/height_screen/bloc/height_bloc.dart +++ b/lib/presentation/registration/height_screen/bloc/height_bloc.dart @@ -9,7 +9,6 @@ part 'height_state.dart'; class HeightBloc extends Bloc { HeightBloc(HeightState initialState) : super(initialState) { on(_onInitialize); - on(_changeDropDown); } _onInitialize( @@ -17,25 +16,7 @@ class HeightBloc extends Bloc { Emitter emit, ) async { emit(state.copyWith( - heightModelObj: state.heightModelObj - ?.copyWith(dropdownItemList: fillDropdownItemList()))); - } - - _changeDropDown( - ChangeDropDownEvent event, - Emitter emit, - ) { - emit(state.copyWith(selectedDropDownValue: event.value)); - state.heightModelObj?.userRegistrationSheet.height = event.value.title; - } - - List fillDropdownItemList() { - List selectionPopupModels = []; - - for (int i = 140; i <= 220; i++) { - selectionPopupModels - .add(SelectionPopupModel(id: i - 34, title: i.toString())); - } - return selectionPopupModels; + heightModelObj: state.heightModelObj?.copyWith(), + heightController: state.heightController)); } } diff --git a/lib/presentation/registration/height_screen/bloc/height_event.dart b/lib/presentation/registration/height_screen/bloc/height_event.dart index 80d209c4..1d65dd82 100644 --- a/lib/presentation/registration/height_screen/bloc/height_event.dart +++ b/lib/presentation/registration/height_screen/bloc/height_event.dart @@ -14,15 +14,3 @@ class HeightInitialEvent extends HeightEvent { @override List get props => []; } - -///event for dropdown selection -class ChangeDropDownEvent extends HeightEvent { - ChangeDropDownEvent({required this.value}); - - SelectionPopupModel value; - - @override - List get props => [ - value, - ]; -} diff --git a/lib/presentation/registration/height_screen/bloc/height_state.dart b/lib/presentation/registration/height_screen/bloc/height_state.dart index d0e3f45d..aa10592f 100644 --- a/lib/presentation/registration/height_screen/bloc/height_state.dart +++ b/lib/presentation/registration/height_screen/bloc/height_state.dart @@ -5,26 +5,25 @@ part of 'height_bloc.dart'; /// Represents the state of Height in the application. class HeightState extends Equatable { HeightState({ - this.selectedDropDownValue, + this.heightController, this.heightModelObj, }); - SelectionPopupModel? selectedDropDownValue; + TextEditingController? heightController; HeightModel? heightModelObj; @override List get props => [ - selectedDropDownValue, + heightController, heightModelObj, ]; HeightState copyWith({ - SelectionPopupModel? selectedDropDownValue, + TextEditingController? heightController, HeightModel? heightModelObj, }) { return HeightState( - selectedDropDownValue: - selectedDropDownValue ?? this.selectedDropDownValue, + heightController: heightController ?? this.heightController, heightModelObj: heightModelObj ?? this.heightModelObj, ); } diff --git a/lib/presentation/registration/height_screen/height_screen.dart b/lib/presentation/registration/height_screen/height_screen.dart index 574bf488..f5bafcb6 100644 --- a/lib/presentation/registration/height_screen/height_screen.dart +++ b/lib/presentation/registration/height_screen/height_screen.dart @@ -4,12 +4,13 @@ import 'package:eatvio_app/core/app_export.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_title_image.dart'; import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; -import 'package:eatvio_app/widgets/custom_drop_down.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; +import 'package:flutter/services.dart'; import 'package:eatvio_app/widgets/next_step_button.dart'; import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; import 'package:flutter/material.dart'; -class HeightScreen extends StatefulWidget { +class HeightScreen extends StatelessWidget { const HeightScreen({Key? key}) : super(key: key); static Widget builder(BuildContext context) { @@ -22,104 +23,122 @@ class HeightScreen extends StatefulWidget { child: HeightScreen()); } - @override - State createState() => _HeightScreenState(); -} - -class _HeightScreenState extends State { @override Widget build(BuildContext context) { mediaQueryData = MediaQuery.of(context); return SafeArea( - child: Scaffold( - appBar: _buildAppBar(context), - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), - child: Column(children: [ - SizedBox(height: 7.v), - Container( - height: 16.v, - width: 343.h, - decoration: BoxDecoration( - color: theme.colorScheme.onPrimary, - borderRadius: BorderRadius.circular(8.h), - border: Border.all( - color: theme.colorScheme.primary, width: 1.h)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8.h), - child: LinearProgressIndicator( - value: 0.55, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 39.v), - Column(children: [ - Text("height_measurements".tr, - style: theme.textTheme.headlineSmall), - SizedBox(height: 174.v), - Column(children: [ - Text("your_height".tr, style: theme.textTheme.bodyLarge), + child: Scaffold( + appBar: _buildAppBar(context), + body: Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), + child: Column( + children: [ + SizedBox(height: 7.v), + Container( + height: 16.v, + width: 343.h, + decoration: BoxDecoration( + color: theme.colorScheme.onPrimary, + borderRadius: BorderRadius.circular(8.h), + border: Border.all( + color: theme.colorScheme.primary, + width: 1.h, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.h), + child: LinearProgressIndicator( + value: 0.55, + backgroundColor: theme.colorScheme.onPrimary, + valueColor: AlwaysStoppedAnimation( + appTheme.blueA400, + ), + ), + ), + ), + SizedBox(height: 39.v), + Column( + children: [ + Text( + "height_measurements".tr, + style: theme.textTheme.headlineSmall, + ), + SizedBox(height: 174.v), + Column( + children: [ + Text( + "your_height".tr, + style: theme.textTheme.bodyLarge, + ), SizedBox(height: 4.v), - BlocSelector( - selector: (state) => state.heightModelObj, - builder: (context, heightModelObj) { - return CustomDropDown( - icon: Container( - margin: EdgeInsets.fromLTRB( - 30.h, 11.v, 17.h, 11.v), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(1.h)), - child: CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize)), - hintText: "170", - items: heightModelObj?.dropdownItemList ?? [], - onChanged: (value) { - context - .read() - .add(ChangeDropDownEvent(value: value)); - }); - }) - ]) - ]), - Spacer(), - BlocBuilder( - builder: (context, state) { - return NextStepButton( - text: "continue".tr.toUpperCase(), - margin: EdgeInsets.only( - left: 16.h, right: 16.h, bottom: 50.v), - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = - state.heightModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.bloodPressureScreen, - arguments: sheet, - ); - }); - }), - ])))); + BlocBuilder( + builder: (context, state) { + return CustomTextFormField( + width: 150.h, + textInputAction: TextInputAction.done, + controller: state.heightController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ); + }) + ], + ), + ], + ), + Spacer(), + BlocBuilder( + builder: (context, state) { + return NextStepButton( + text: "continue".tr.toUpperCase(), + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 50.v, + ), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: onTapNext(state, context), + ); + }, + ), + ], + ), + ), + ), + ); + } + + onTapNext(HeightState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.heightModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.bloodPressureScreen, + arguments: sheet, + ); + }; } /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); diff --git a/lib/presentation/registration/language_select_screen/language_select_screen.dart b/lib/presentation/registration/language_select_screen/language_select_screen.dart index cd8b9b4b..56e644a0 100644 --- a/lib/presentation/registration/language_select_screen/language_select_screen.dart +++ b/lib/presentation/registration/language_select_screen/language_select_screen.dart @@ -64,23 +64,22 @@ class LanguageSelectScreen extends StatelessWidget { languageSelectModelObj, ) { return CustomDropDown( - icon: Container( - margin: - EdgeInsets.fromLTRB(30.h, 11.v, 17.h, 11.v), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(1.h)), - child: CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize)), - hintText: "Не выбрано".tr, - items: - languageSelectModelObj?.dropdownItemList ?? [], - onChanged: (value) { - context - .read() - .add(ChangeDropDownEvent(value: value)); - }); + icon: Container( + margin: EdgeInsets.fromLTRB(30.h, 11.v, 17.h, 11.v), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1.h), + ), + child: CustomImageView( + imagePath: ImageConstant.imgFavorite, + height: 12.adaptSize, + width: 12.adaptSize, + ), + ), + hintText: "Не выбрано".tr, + items: languageSelectModelObj?.dropdownItemList ?? [], + onChanged: (value) => + _onDropDownChanged(context, value), + ); }), Spacer(flex: 63), ])), @@ -92,15 +91,24 @@ class LanguageSelectScreen extends StatelessWidget { margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = - state.languageSelectModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.credentialsScreen, - arguments: sheet, - ); - }); + onPressed: _saveAndPush(state, context)); }))); } + + void Function()? _saveAndPush( + LanguageSelectState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.languageSelectModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.credentialsScreen, + arguments: sheet, + ); + }; + } + + void _onDropDownChanged(BuildContext context, value) { + context.read().add(ChangeDropDownEvent(value: value)); + } } diff --git a/lib/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart b/lib/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart index fc0766f6..51df64aa 100644 --- a/lib/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart +++ b/lib/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart @@ -74,52 +74,63 @@ class _MedicineIntakeScreenState extends State { margin: EdgeInsets.only( left: 16.h, right: 16.h, bottom: 50.v), buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = state - .medicineIntakeModelObj!.userRegistrationSheet; - sheet.aboutTakingPils = - state.enterInformationController!.text; - Navigator.pushNamed( - context, - AppRoutes.bloodPressureScreen, - arguments: sheet, - ); - }); + onPressed: onTapNext(state, context)); }), BlocBuilder( builder: (context, state) { return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.medicineIntakeModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.bloodPressureScreen, - arguments: sheet, - ); - }, + onTap: onTapSkip(state, context), child: Text("skip".tr, style: theme.textTheme.bodyLarge), ); }), ])))); } + onTapSkip(MedicineIntakeState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.medicineIntakeModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.bloodPressureScreen, + arguments: sheet, + ); + }; + } + + onTapNext(MedicineIntakeState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = + state.medicineIntakeModelObj!.userRegistrationSheet; + sheet.aboutTakingPils = state.enterInformationController!.text; + Navigator.pushNamed( + context, + AppRoutes.bloodPressureScreen, + arguments: sheet, + ); + }; + } + /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 31.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Section Widget - Widget _buildTextFormField(BuildContext context) { + _buildTextFormField(BuildContext context) { return Column(children: [ Text("fill_in_information".tr, style: CustomTextStyles.titleMediumSegoeUI), diff --git a/lib/presentation/registration/name_screen/name_screen.dart b/lib/presentation/registration/name_screen/name_screen.dart index cc234a77..23612d72 100644 --- a/lib/presentation/registration/name_screen/name_screen.dart +++ b/lib/presentation/registration/name_screen/name_screen.dart @@ -11,10 +11,10 @@ import 'package:flutter/material.dart'; class NameScreen extends StatelessWidget { const NameScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { final sheet = ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + return BlocProvider( create: (context) => NameBloc( NameState( @@ -74,66 +74,28 @@ class NameScreen extends StatelessWidget { controller: lastNameController, ); }), - SizedBox(height: 21.v), Spacer(), BlocBuilder(builder: (context, state) { return NextStepButton( - text: "continue".tr.toUpperCase(), - margin: EdgeInsets.only( - left: 16.h, right: 16.h, bottom: 50.v), - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - List errors = validateFields( - state.firstNameController!.text, - state.lastNameController!.text); - if (errors.isEmpty) { - UserRegistrationSheet sheet = - state.nameModelObj!.userRegistrationSheet; - sheet.firstName = state.firstNameController!.text; - sheet.lastName = state.lastNameController!.text; - Navigator.pushNamed( - context, - AppRoutes.genderScreen, - arguments: sheet, - ); - } else { - String errorMessage = ''; - - for (var error in errors) { - errorMessage += error + '\n\n'; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); + text: "continue".tr.toUpperCase(), + margin: EdgeInsets.only( + left: 16.h, right: 16.h, bottom: 50.v), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: () => _onTapNext( + context, state), // Assign callback function here + ); }), ])))); } /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: () => onTapArrowLeft(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } @@ -143,26 +105,66 @@ class NameScreen extends StatelessWidget { NavigatorService.goBack(); } - List validateFields( - String firstName, - String lastName, - ) { + _onTapNext(BuildContext context, NameState state) { + if (state.firstNameController != null && state.lastNameController != null) { + List errors = validateFields( + state.firstNameController!.text, state.lastNameController!.text); + + if (errors.isEmpty) { + UserRegistrationSheet sheet = state.nameModelObj!.userRegistrationSheet; + sheet.firstName = state.firstNameController!.text; + sheet.lastName = state.lastNameController!.text; + + Navigator.pushNamed( + context, + AppRoutes.genderScreen, + arguments: sheet, + ); + } else { + String errorMessage = ''; + + for (var error in errors) { + errorMessage += error + '\n\n'; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Error'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: () => onTapOk(context), + child: Text('OK'), + ), + ], + ), + ); + } + } + } + + void onTapOk(BuildContext context) { + Navigator.pop(context); + } + + List validateFields(String firstName, String lastName) { List errors = []; if (firstName.isEmpty) { - errors.add('first_name_empty'.tr); + errors.add('First name cannot be empty'); } if (firstName.length < 1) { - errors.add('first_name_short'.tr); + errors.add('First name is too short'); } if (lastName.isEmpty) { - errors.add('last_name_empty'.tr); + errors.add('Last name cannot be empty'); } if (lastName.length < 1) { - errors.add('last_name_short'.tr); + errors.add('Last name is too short'); } return errors; diff --git a/lib/presentation/registration/splash_screen/bloc/splash_bloc.dart b/lib/presentation/registration/splash_screen/bloc/splash_bloc.dart index a767f763..9491b451 100644 --- a/lib/presentation/registration/splash_screen/bloc/splash_bloc.dart +++ b/lib/presentation/registration/splash_screen/bloc/splash_bloc.dart @@ -17,7 +17,7 @@ class SplashBloc extends Bloc { ) async { Future.delayed(const Duration(milliseconds: 3000), () { NavigatorService.popAndPushNamed( - AppRoutes.authScreen, + AppRoutes.myProfile, ); }); } diff --git a/lib/presentation/registration/weight_screen/bloc/weight_bloc.dart b/lib/presentation/registration/weight_screen/bloc/weight_bloc.dart index bf2cc1f9..69161cce 100644 --- a/lib/presentation/registration/weight_screen/bloc/weight_bloc.dart +++ b/lib/presentation/registration/weight_screen/bloc/weight_bloc.dart @@ -9,7 +9,6 @@ part 'weight_state.dart'; class WeightBloc extends Bloc { WeightBloc(WeightState initialState) : super(initialState) { on(_onInitialize); - on(_changeDropDown); } _onInitialize( @@ -17,25 +16,7 @@ class WeightBloc extends Bloc { Emitter emit, ) async { emit(state.copyWith( - weightModelObj: state.weightModelObj - ?.copyWith(dropdownItemList: fillDropdownItemList()))); - } - - _changeDropDown( - ChangeDropDownEvent event, - Emitter emit, - ) { - emit(state.copyWith(selectedDropDownValue: event.value)); - state.weightModelObj?.userRegistrationSheet.weight = event.value.title; - } - - List fillDropdownItemList() { - List selectionPopupModels = []; - - for (int i = 35; i <= 200; i++) { - selectionPopupModels - .add(SelectionPopupModel(id: i - 34, title: i.toString())); - } - return selectionPopupModels; + weightModelObj: state.weightModelObj?.copyWith(), + weightController: state.weightController)); } } diff --git a/lib/presentation/registration/weight_screen/bloc/weight_event.dart b/lib/presentation/registration/weight_screen/bloc/weight_event.dart index 93b69d22..1b554c5e 100644 --- a/lib/presentation/registration/weight_screen/bloc/weight_event.dart +++ b/lib/presentation/registration/weight_screen/bloc/weight_event.dart @@ -14,15 +14,3 @@ class WeightInitialEvent extends WeightEvent { @override List get props => []; } - -///event for dropdown selection -class ChangeDropDownEvent extends WeightEvent { - ChangeDropDownEvent({required this.value}); - - SelectionPopupModel value; - - @override - List get props => [ - value, - ]; -} diff --git a/lib/presentation/registration/weight_screen/bloc/weight_state.dart b/lib/presentation/registration/weight_screen/bloc/weight_state.dart index 06b9c2cf..5d782dfd 100644 --- a/lib/presentation/registration/weight_screen/bloc/weight_state.dart +++ b/lib/presentation/registration/weight_screen/bloc/weight_state.dart @@ -5,26 +5,25 @@ part of 'weight_bloc.dart'; /// Represents the state of Weight in the application. class WeightState extends Equatable { WeightState({ - this.selectedDropDownValue, + this.weightController, this.weightModelObj, }); - SelectionPopupModel? selectedDropDownValue; + TextEditingController? weightController; WeightModel? weightModelObj; @override List get props => [ - selectedDropDownValue, + weightController, weightModelObj, ]; WeightState copyWith({ - SelectionPopupModel? selectedDropDownValue, + TextEditingController? weightController, WeightModel? weightModelObj, }) { return WeightState( - selectedDropDownValue: - selectedDropDownValue ?? this.selectedDropDownValue, + weightController: weightController ?? this.weightController, weightModelObj: weightModelObj ?? this.weightModelObj, ); } diff --git a/lib/presentation/registration/weight_screen/weight_screen.dart b/lib/presentation/registration/weight_screen/weight_screen.dart index ab936350..c68ccb83 100644 --- a/lib/presentation/registration/weight_screen/weight_screen.dart +++ b/lib/presentation/registration/weight_screen/weight_screen.dart @@ -4,12 +4,13 @@ import 'package:eatvio_app/core/app_export.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_title_image.dart'; import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; -import 'package:eatvio_app/widgets/custom_drop_down.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; +import 'package:flutter/services.dart'; import 'package:eatvio_app/widgets/next_step_button.dart'; import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; import 'package:flutter/material.dart'; -class WeightScreen extends StatefulWidget { +class WeightScreen extends StatelessWidget { const WeightScreen({Key? key}) : super(key: key); static Widget builder(BuildContext context) { @@ -22,113 +23,124 @@ class WeightScreen extends StatefulWidget { child: WeightScreen()); } - @override - State createState() => _WeightScreenState(); -} - -class _WeightScreenState extends State { @override Widget build(BuildContext context) { mediaQueryData = MediaQuery.of(context); return SafeArea( - child: Scaffold( - appBar: _buildAppBar(context), - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), - child: Column(children: [ - SizedBox(height: 7.v), - Container( - height: 16.v, - width: 343.h, - decoration: BoxDecoration( - color: theme.colorScheme.onPrimary, - borderRadius: BorderRadius.circular(8.h), - border: Border.all( - color: theme.colorScheme.primary, width: 1.h)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8.h), - child: LinearProgressIndicator( - value: 0.48, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 39.v), - Column(children: [ - Text("weight_measurements".tr, - style: theme.textTheme.headlineSmall), - SizedBox(height: 174.v), - Column(children: [ - Text("your_weight".tr, style: theme.textTheme.bodyLarge), + child: Scaffold( + appBar: _buildAppBar(context), + body: Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), + child: Column( + children: [ + SizedBox(height: 7.v), + Container( + height: 16.v, + width: 343.h, + decoration: BoxDecoration( + color: theme.colorScheme.onPrimary, + borderRadius: BorderRadius.circular(8.h), + border: Border.all( + color: theme.colorScheme.primary, + width: 1.h, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.h), + child: LinearProgressIndicator( + value: 0.55, + backgroundColor: theme.colorScheme.onPrimary, + valueColor: AlwaysStoppedAnimation( + appTheme.blueA400, + ), + ), + ), + ), + SizedBox(height: 39.v), + Column( + children: [ + Text( + "weight_measurements".tr, + style: theme.textTheme.headlineSmall, + ), + SizedBox(height: 174.v), + Column( + children: [ + Text( + "your_weight".tr, + style: theme.textTheme.bodyLarge, + ), SizedBox(height: 4.v), - BlocSelector( - selector: (state) => state.weightModelObj, - builder: (context, weightModelObj) { - return CustomDropDown( - icon: Container( - margin: EdgeInsets.fromLTRB( - 30.h, 11.v, 17.h, 11.v), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(1.h)), - child: CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize)), - hintText: "Не выбрано", - items: weightModelObj?.dropdownItemList ?? [], - onChanged: (value) { - context - .read() - .add(ChangeDropDownEvent(value: value)); - }); - }) - ]) - ]), - Spacer(), - BlocBuilder( - builder: (context, state) { - return NextStepButton( - text: "continue".tr.toUpperCase(), - margin: EdgeInsets.only( - left: 16.h, right: 16.h, bottom: 50.v), - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - onPressed: () { - UserRegistrationSheet sheet = - state.weightModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.heightScreen, - arguments: sheet, - ); - }); - }), - ])))); + BlocBuilder( + builder: (context, state) { + return CustomTextFormField( + width: 150.h, + textInputAction: TextInputAction.done, + controller: state.weightController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ); + }) + ], + ), + ], + ), + Spacer(), + BlocBuilder( + builder: (context, state) { + return NextStepButton( + text: "continue".tr.toUpperCase(), + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 50.v, + ), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: onTapNext(state, context), + ); + }, + ), + ], + ), + ), + ), + ); + } + + onTapNext(WeightState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.weightModelObj!.userRegistrationSheet; + Navigator.pushNamed( + context, + AppRoutes.heightScreen, + arguments: sheet, + ); + }; } /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( leadingWidth: 29.h, leading: AppbarLeadingImage( imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), - onTap: () { - onTapArrowLeft(context); - }), + onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { NavigatorService.goBack(); } - - /// Navigates to the allergiesScreen when the action is triggered. - navigateToHeight(BuildContext context) { - NavigatorService.pushNamed( - AppRoutes.heightScreen, - ); - } } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index c754e47b..16235b16 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/presentation/diets/diet_select_screen/diet_select_screen.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart'; import 'package:eatvio_app/presentation/registration/city_of_living_screen/city_of_living_screen.dart'; @@ -20,6 +21,24 @@ import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; import 'package:eatvio_app/presentation/profile/my_profile_screen/my_profile_screen.dart'; import 'package:eatvio_app/presentation/profile/my_profile_screen/my_settings_screen.dart'; import 'package:eatvio_app/presentation/diary/diary_edit_screen/diary_edit_screen.dart'; +import 'package:eatvio_app/presentation/chat/chat_screen/chat_screen.dart'; +import 'package:eatvio_app/presentation/chat/chat_select_screen/chat_select_screen.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/feed_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_select_screen/recipe_select_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_select_screen/plan_select_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_edit_screen/plan_edit_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_create_screen/plan_create_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_show_screen/diet_show_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_edit_screen/diet_edit_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_create_screen/diet_create_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_show_screen/plan_show_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_show_screen/recipe_show_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_create_screen/recipe_create_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/blog_post_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/glossary_screen.dart'; class AppRoutes { // REGISTRATION @@ -72,28 +91,145 @@ class AppRoutes { static const String diaryEdit = '/diaryEdit'; + static const String chatScreen = '/chat'; + + static const String chatSelectScreen = '/chatSelect'; + + static const String feedScreen = '/feed'; + + static const String blogPostScreen = '/blogPost'; + + static const String blogPostEditScreen = '/blogPostEdit'; + + static const String blogPostCreateScreen = '/blogPostCreate'; + + static const String recipeSelectScreen = '/recipeSelect'; + + static const String recipeScreen = '/recipe'; + + static const String recipeEditScreen = '/recipeEdit'; + + static const String recipeCreateScreen = '/recipeCreate'; + + static const String dietSelectScreen = '/dietSelect'; + + static const String dietScreen = '/diet'; + + static const String dietEditScreen = '/dietEdit'; + + static const String dietCreateScreen = '/dietCreate'; + + static const String planSelectScreen = '/planSelect'; + + static const String planScreen = '/plan'; + + static const String planEditScreen = '/planEdit'; + + static const String planCreateScreen = '/planCreate'; + + static const String glossaryScreen = '/glossary'; + static Map get routes => { - dateOfBirthScreen: DateOfBirthScreen.builder, - cityOfLivingScreen: CityOfLivingScreen.builder, - bodyCircumferencesScreen: BodyCircumferencesScreen.builder, - weightScreen: WeightScreen.builder, - heightScreen: HeightScreen.builder, - nameScreen: NameScreen.builder, - credentialsScreen: CredentialsScreen.builder, - allergiesScreen: AllergiesScreen.builder, - medicineIntakeScreen: MedicineIntakeScreen.builder, - splashScreen: SplashScreen.builder, - authScreen: AuthScreen.builder, - genderScreen: GenderScreen.builder, - bloodPressureScreen: BloodPressureScreen.builder, - favoriteSportsScreen: FavoriteSportsScreen.builder, - favoriteFoodsScreen: FavoriteFoodsScreen.builder, - additionalInformationScreen: AdditionalInformationScreen.builder, - languageSelectScreen: LanguageSelectScreen.builder, - appNavigationScreen: AppNavigationScreen.builder, - initialRoute: SplashScreen.builder, - myProfile: MyProfileScreen.builder, - mySettings: MySettingsScreen.builder, - diaryEdit: DiaryEditScreen.builder, + dateOfBirthScreen: (context) => + _buildFadeRoute(context, DateOfBirthScreen.builder), + cityOfLivingScreen: (context) => + _buildFadeRoute(context, CityOfLivingScreen.builder), + bodyCircumferencesScreen: (context) => + _buildFadeRoute(context, BodyCircumferencesScreen.builder), + weightScreen: (context) => + _buildFadeRoute(context, WeightScreen.builder), + heightScreen: (context) => + _buildFadeRoute(context, HeightScreen.builder), + nameScreen: (context) => _buildFadeRoute(context, NameScreen.builder), + credentialsScreen: (context) => + _buildFadeRoute(context, CredentialsScreen.builder), + allergiesScreen: (context) => + _buildFadeRoute(context, AllergiesScreen.builder), + medicineIntakeScreen: (context) => + _buildFadeRoute(context, MedicineIntakeScreen.builder), + splashScreen: (context) => + _buildFadeRoute(context, SplashScreen.builder), + authScreen: (context) => _buildFadeRoute(context, AuthScreen.builder), + genderScreen: (context) => + _buildFadeRoute(context, GenderScreen.builder), + bloodPressureScreen: (context) => + _buildFadeRoute(context, BloodPressureScreen.builder), + favoriteSportsScreen: (context) => + _buildFadeRoute(context, FavoriteSportsScreen.builder), + favoriteFoodsScreen: (context) => + _buildFadeRoute(context, FavoriteFoodsScreen.builder), + additionalInformationScreen: (context) => + _buildFadeRoute(context, AdditionalInformationScreen.builder), + languageSelectScreen: (context) => + _buildFadeRoute(context, LanguageSelectScreen.builder), + appNavigationScreen: (context) => + _buildFadeRoute(context, AppNavigationScreen.builder), + initialRoute: (context) => + _buildFadeRoute(context, SplashScreen.builder), + myProfile: (context) => + _buildFadeRoute(context, MyProfileScreen.builder), + mySettings: (context) => + _buildFadeRoute(context, MySettingsScreen.builder), + diaryEdit: (context) => + _buildFadeRoute(context, DiaryEditScreen.builder), + chatScreen: (context) => _buildFadeRoute(context, ChatScreen.builder), + chatSelectScreen: (context) => + _buildFadeRoute(context, ChatSelectScreen.builder), + recipeSelectScreen: (context) => + _buildFadeRoute(context, RecipeSelectScreen.builder), + recipeScreen: (context) => + _buildFadeRoute(context, RecipeScreen.builder), + recipeEditScreen: (context) => + _buildFadeRoute(context, RecipeEditScreen.builder), + recipeCreateScreen: (context) => + _buildFadeRoute(context, RecipeCreateScreen.builder), + dietSelectScreen: (context) => + _buildFadeRoute(context, DietSelectScreen.builder), + dietScreen: (context) => _buildFadeRoute(context, DietScreen.builder), + dietEditScreen: (context) => + _buildFadeRoute(context, DietEditScreen.builder), + dietCreateScreen: (context) => + _buildFadeRoute(context, DietCreateScreen.builder), + planSelectScreen: (context) => + _buildFadeRoute(context, PlanSelectScreen.builder), + planScreen: (context) => _buildFadeRoute(context, PlanScreen.builder), + planEditScreen: (context) => + _buildFadeRoute(context, PlanEditScreen.builder), + planCreateScreen: (context) => + _buildFadeRoute(context, PlanCreateScreen.builder), + feedScreen: (context) => _buildFadeRoute(context, FeedScreen.builder), + blogPostScreen: (context) => + _buildFadeRoute(context, BlogPostScreen.builder), + blogPostEditScreen: (context) => + _buildFadeRoute(context, BlogPostEditScreen.builder), + blogPostCreateScreen: (context) => + _buildFadeRoute(context, BlogPostCreateScreen.builder), + glossaryScreen: (context) => + _buildFadeRoute(context, GlossaryScreen.builder), }; + + static Widget _buildFadeRoute(BuildContext context, WidgetBuilder builder) { + return FadeTransition( + opacity: AlwaysStoppedAnimation(1.0), + child: builder(context), + ); + } +} + +class FadeTransitionPage extends PageRouteBuilder { + final WidgetBuilder builder; + + FadeTransitionPage({required this.builder}) + : super( + pageBuilder: (BuildContext context, Animation animation, + Animation secondaryAnimation) { + return builder(context); + }, + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + return FadeTransition(opacity: animation, child: child); + }, + ); } diff --git a/lib/widgets/app_bar/appbar_leading_image.dart b/lib/widgets/app_bar/appbar_leading_image.dart index 5c9ff8e4..003d9f03 100644 --- a/lib/widgets/app_bar/appbar_leading_image.dart +++ b/lib/widgets/app_bar/appbar_leading_image.dart @@ -21,9 +21,7 @@ class AppbarLeadingImage extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () { - onTap!.call(); - }, + onTap: onTapCall, child: Padding( padding: margin ?? EdgeInsets.zero, child: CustomImageView( @@ -35,4 +33,8 @@ class AppbarLeadingImage extends StatelessWidget { ), ); } + + void onTapCall() { + onTap!.call(); + } } diff --git a/lib/widgets/app_bar/appbar_subtitle.dart b/lib/widgets/app_bar/appbar_subtitle.dart index 69c1b639..58779324 100644 --- a/lib/widgets/app_bar/appbar_subtitle.dart +++ b/lib/widgets/app_bar/appbar_subtitle.dart @@ -21,9 +21,7 @@ class AppbarSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - onTap!.call(); - }, + onTap: onTapCall, child: Padding( padding: margin ?? EdgeInsets.zero, child: Text( @@ -35,4 +33,8 @@ class AppbarSubtitle extends StatelessWidget { ), ); } + + void onTapCall() { + onTap!.call(); + } } diff --git a/lib/widgets/app_bar/appbar_title_circleimage.dart b/lib/widgets/app_bar/appbar_title_circleimage.dart index ee412291..02224f3c 100644 --- a/lib/widgets/app_bar/appbar_title_circleimage.dart +++ b/lib/widgets/app_bar/appbar_title_circleimage.dart @@ -7,7 +7,6 @@ class AppbarTitleCircleimage extends StatelessWidget { Key? key, this.imagePath, this.margin, - this.onTap, }) : super( key: key, ); @@ -16,15 +15,10 @@ class AppbarTitleCircleimage extends StatelessWidget { EdgeInsetsGeometry? margin; - Function? onTap; - @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadiusStyle.circleBorder8, - onTap: () { - onTap!.call(); - }, child: Padding( padding: margin ?? EdgeInsets.zero, child: CustomImageView( diff --git a/lib/widgets/app_bar/appbar_title_image.dart b/lib/widgets/app_bar/appbar_title_image.dart index 3ccfd581..4ef20de2 100644 --- a/lib/widgets/app_bar/appbar_title_image.dart +++ b/lib/widgets/app_bar/appbar_title_image.dart @@ -21,9 +21,7 @@ class AppbarTitleImage extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () { - onTap!.call(); - }, + onTap: onTapCall, child: Padding( padding: margin ?? EdgeInsets.zero, child: CustomImageView( @@ -35,4 +33,7 @@ class AppbarTitleImage extends StatelessWidget { ), ); } + + void onTapCall() { + } } diff --git a/lib/widgets/app_bar/exit_button.dart b/lib/widgets/app_bar/exit_button.dart index a93c9a84..ee0d27dd 100644 --- a/lib/widgets/app_bar/exit_button.dart +++ b/lib/widgets/app_bar/exit_button.dart @@ -21,9 +21,7 @@ class ExitButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - onTap!.call(); - }, + onTap: onTapCall, child: Padding( padding: margin ?? EdgeInsets.zero, child: Text( @@ -35,4 +33,8 @@ class ExitButton extends StatelessWidget { ), ); } + + void onTapCall() { + onTap!.call(); + } } diff --git a/lib/widgets/chart_axis.dart b/lib/widgets/chart_axis.dart new file mode 100644 index 00000000..5cc64480 --- /dev/null +++ b/lib/widgets/chart_axis.dart @@ -0,0 +1,145 @@ +/*import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +buildChart( + BuildContext context, List chartPoints, List? dates) { +// Определение осей графика + List spots = []; + for (int i = 0; i < 4; i++) { + spots.add( + FlSpot((i * 10 / 10) + 1, chartPoints[i]), + ); + } + + Widget bottomTitleWidgets( + double value, TitleMeta meta, double chartWidth, List? dates) { + if (value % 1 != 0) { + return Container(); + } + final style = TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: min(13, 13 * chartWidth / 300), + ); + int index = value.toInt(); + if (index >= 0 && index < dates!.length) { + // Проверка на допустимый индекс в массиве dates + String formattedDate = + DateFormat('dd.MM').format(DateTime.parse(dates[index])); + return SideTitleWidget( + axisSide: meta.axisSide, + space: 16, + child: Text(formattedDate, style: style), + ); + } else { + return Container(); + } + } + + Widget leftTitleWidgets(double value, TitleMeta meta, double chartWidth) { + final style = TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: min(18, 18 * chartWidth / 300), + ); + return SideTitleWidget( + axisSide: meta.axisSide, + space: 16, + child: Text(meta.formattedValue, style: style), + ); + } + + return Padding( + padding: const EdgeInsets.only( + left: 12, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: LayoutBuilder( + builder: (context, constraints) { + return LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + maxContentWidth: 100, + tooltipBgColor: Colors.black, + getTooltipItems: mapSpots(), + ), + handleBuiltInTouches: true, + getTouchLineStart: (data, index) => 0, + ), + lineBarsData: [ + LineChartBarData( + color: Colors.pink, + spots: spots, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: const FlDotData(show: false), + ), + ], + minY: chartPoints.reduce( + (value, element) => value < element ? value : element), + maxY: chartPoints.reduce( + (value, element) => value > element ? value : element), + minX: 1, + maxX: 4, + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) => + leftTitleWidgets(value, meta, constraints.maxWidth), + reservedSize: 56, + ), + drawBelowEverything: true, + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) => bottomTitleWidgets( + value, meta, constraints.maxWidth, dates), + reservedSize: 36, + interval: 1, + ), + drawBelowEverything: true, + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + horizontalInterval: 1.5, + verticalInterval: 5, + checkToShowHorizontalLine: valuteToInt(), + getDrawingHorizontalLine: (_) => FlLine( + color: Colors.blue.withOpacity(1), + dashArray: [8, 2], + strokeWidth: 0.8, + ), + getDrawingVerticalLine: (_) => FlLine( + color: Colors.blue.withOpacity(1), + dashArray: [8, 2], + strokeWidth: 0.8, + ), + checkToShowVerticalLine: valuteToInt(), + ), + borderData: FlBorderData(show: false), + ), + ); + }, + ), + ), + ); +} +*/ \ No newline at end of file diff --git a/lib/widgets/chart_bars.dart b/lib/widgets/chart_bars.dart new file mode 100644 index 00000000..7f4fc342 --- /dev/null +++ b/lib/widgets/chart_bars.dart @@ -0,0 +1,79 @@ +/*import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: Text('Nutrition Chart'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 300, + width: 300, + child: BarChart( + BarChartData( + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: SideTitles( + showTitles: true, + getTextStyles: (value) => + const TextStyle(color: Colors.black, fontSize: 14), + margin: 10, + showTitles: (double value) { + switch (value.toInt()) { + case 0: + return 'Calories'; + case 1: + return 'Protein'; + case 2: + return 'Fat'; + case 3: + return 'Carbs'; + default: + return ''; + } + }, + ), + leftTitles: SideTitles(showTitles: false), + ), + borderData: FlBorderData(show: false), + barGroups: [ + BarChartGroupData(x: 0, barRods: [ + BarChartRodData(y: 200, color: Colors.blue), + BarChartRodData(y: 150, color: Colors.blueAccent), + ]), + BarChartGroupData(x: 1, barRods: [ + BarChartRodData(y: 50, color: Colors.red), + BarChartRodData(y: 60, color: Colors.deepOrange), + ]), + BarChartGroupData(x: 2, barRods: [ + BarChartRodData(y: 80, color: Colors.green), + BarChartRodData(y: 70, color: Colors.lightGreen), + ]), + BarChartGroupData(x: 3, barRods: [ + BarChartRodData(y: 100, color: Colors.orange), + BarChartRodData(y: 120, color: Colors.orangeAccent), + ]), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} +*/ \ No newline at end of file diff --git a/lib/widgets/custom_about_list.dart b/lib/widgets/custom_about_list.dart new file mode 100644 index 00000000..f4b91b2c --- /dev/null +++ b/lib/widgets/custom_about_list.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class AboutList extends StatefulWidget { + final String title; + final List items; + + AboutList(this.title, this.items); + + @override + _AboutListState createState() => _AboutListState(); +} + +class _AboutListState extends State { + bool isExpanded = false; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: toggleList(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.bold), + ), + Icon(isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), + ], + ), + ), + if (isExpanded) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Column( + //children: widget.items.map((item) => Text(item)).toList(), + children: [Text(widget.items.join('\n'))]), + ), + Divider(), + ], + ); + } + + toggleList() { + return () { + setState(() { + isExpanded = !isExpanded; + }); + }; + } +} diff --git a/lib/widgets/custom_autocomplete.dart b/lib/widgets/custom_autocomplete.dart new file mode 100644 index 00000000..66e042dc --- /dev/null +++ b/lib/widgets/custom_autocomplete.dart @@ -0,0 +1,80 @@ +/* +class CustomTextFormField extends StatelessWidget { + // ... + + final AutocompleteOptions autocompleteOptions; + + const CustomTextFormField({ + // ... (остальные параметры) + required this.autocompleteOptions, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: autocompleteFieldWidget, + ) + : autocompleteFieldWidget; + } + + Widget get autocompleteFieldWidget => SizedBox( + width: width ?? double.maxFinite, + child: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + return const Iterable.empty(); + } + return autocompleteOptions.options.where((String option) { + return option + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }); + }, + onSelected: (String value) { + controller?.text = value; + onChanged?.call(value); + }, + fieldViewBuilder: (BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted) { + return TextFormField( + controller: textEditingController, + focusNode: focusNode, + autofocus: autofocus!, + style: textStyle ?? theme.textTheme.titleMedium, + obscureText: obscureText!, + textInputAction: textInputAction, + maxLines: maxLines ?? 1, + decoration: decoration, + validator: validator, + keyboardType: keyboardType, + onSaved: onSaved, + onChanged: onChanged, + inputFormatters: inputFormatters, + initialValue: initialValue, + onFieldSubmitted: (_) => onFieldSubmitted(), + ); + }, + optionsViewBuilder: (BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options) { + return Material( + elevation: 4.0, + child: ListView( + children: options.map((String option) { + return ListTile( + title: Text(option), + onTap: () { + onSelected(option); + }, + ); + }).toList(), + ), + ); + }, + ), + ); +}*/ diff --git a/lib/widgets/custom_bottom_bar.dart b/lib/widgets/custom_bottom_bar.dart index 67ae4e3f..d6e47d6d 100644 --- a/lib/widgets/custom_bottom_bar.dart +++ b/lib/widgets/custom_bottom_bar.dart @@ -1,6 +1,9 @@ import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_title_circleimage.dart'; +import 'package:eatvio_app/widgets/custom_navigation_menu.dart'; +// ignore: must_be_immutable class CustomBottomBar extends StatefulWidget { CustomBottomBar({this.onChanged}); @@ -15,41 +18,45 @@ class CustomBottomBarState extends State { List bottomMenuList = [ BottomMenuModel( - icon: ImageConstant.imgNav, - activeIcon: ImageConstant.imgNav, - title: "diary".tr, - type: BottomBarEnum.tf, - ), + icon: ImageConstant.imgLogo, + activeIcon: ImageConstant.imgLogo, + type: BottomBarEnum.tf, + image: false), BottomMenuModel( - icon: ImageConstant.imgNavBlueGray400, - activeIcon: ImageConstant.imgNavBlueGray400, - title: "my_blog".tr, - type: BottomBarEnum.tf, - ), + icon: ImageConstant.imgFeed, + activeIcon: ImageConstant.imgFeed, + title: "Лента", + type: BottomBarEnum.tf, + image: false), BottomMenuModel( - icon: ImageConstant.imgNavBlueGray40024x23, - activeIcon: ImageConstant.imgNavBlueGray40024x23, - title: "profile".tr, - type: BottomBarEnum.tf, - ), + icon: ImageConstant.imgDiary, + activeIcon: ImageConstant.imgDiary, + title: "diary".tr, + type: BottomBarEnum.tf, + image: false), BottomMenuModel( - icon: ImageConstant.imgNavPrimary, - activeIcon: ImageConstant.imgNavPrimary, - title: "admin".tr, - type: BottomBarEnum.tf, - ), + icon: ImageConstant.imgChat, + activeIcon: ImageConstant.imgChat, + title: "chat".tr, + type: BottomBarEnum.tf, + image: false), BottomMenuModel( - icon: ImageConstant.imgNavBlueGray40024x25, - activeIcon: ImageConstant.imgNavBlueGray40024x25, - title: "chat".tr, + icon: ImageConstant.imgBell, + activeIcon: ImageConstant.imgBell, + type: BottomBarEnum.tf, + image: false), + BottomMenuModel( + icon: ImageConstant.imgEllipse1, + activeIcon: ImageConstant.imgEllipse1, type: BottomBarEnum.tf, - ) + image: true, + ), ]; @override Widget build(BuildContext context) { return Container( - height: 80.v, + height: 83.v, decoration: BoxDecoration( border: Border.all( color: appTheme.whiteA700, @@ -64,60 +71,133 @@ class CustomBottomBarState extends State { elevation: 0, currentIndex: selectedIndex, type: BottomNavigationBarType.fixed, - items: List.generate(bottomMenuList.length, (index) { - return BottomNavigationBarItem( - icon: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CustomImageView( - imagePath: bottomMenuList[index].icon, - height: 24.v, - width: 19.h, - color: appTheme.blueGray400, - ), - Padding( - padding: EdgeInsets.only(top: 8.v), - child: Text( - bottomMenuList[index].title ?? "", - style: - CustomTextStyles.bodySmallMulishBluegray80012.copyWith( - color: appTheme.blueGray800, - ), - ), - ), - ], - ), - activeIcon: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CustomImageView( - imagePath: bottomMenuList[index].activeIcon, - height: 24.adaptSize, - width: 24.adaptSize, - color: theme.colorScheme.primary, - ), - Padding( - padding: EdgeInsets.only(top: 9.v), - child: Text( - bottomMenuList[index].title ?? "", - style: CustomTextStyles.bodySmallMulishPrimary.copyWith( - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), - label: '', - ); - }), - onTap: (index) { - selectedIndex = index; - widget.onChanged?.call(bottomMenuList[index].type); - setState(() {}); - }, + items: List.generate(bottomMenuList.length, bottomMenu), + onTap: changeMeny, ), ); } + + void changeMeny(index) { + setState(() { + selectedIndex = index; + }); + + switch (index) { + case 0: + Navigator.pushNamed(context, AppRoutes.myProfile); + break; + case 1: + Navigator.pushNamed(context, AppRoutes.feedScreen); + break; + case 2: + Navigator.pushNamed(context, AppRoutes.diaryEdit); + break; + case 3: + Navigator.pushNamed(context, AppRoutes.chatSelectScreen); + break; + } + } + + BottomNavigationBarItem bottomMenu(index) { + bool isSelected = selectedIndex == index; + + double iconHeight; + double iconWidth; + + if (index == 0) { + // Для логотипа- + iconHeight = 52.v; // Увеличиваем высоту + iconWidth = 48.h; // Меняем ширину, чтобы сохранить пропорции + } else if (index == bottomMenuList.length - 2) { + // Для колокольчика + iconHeight = 20.v; // Уменьшаем высоту + iconWidth = 20.h; // Меняем ширину, чтобы сохранить пропорции + } else if (index == bottomMenuList.length - 1) { + // Для колокольчика + iconHeight = 40.v; // Уменьшаем высоту + iconWidth = 40.h; // Меняем ширину, чтобы сохранить пропорции + } else { + // Для остальных иконок + iconHeight = 24.v; // Стандартная высота + iconWidth = 19.h; // Стандартная ширина + } + + return BottomNavigationBarItem( + icon: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!bottomMenuList[index].image) + CustomImageView( + imagePath: bottomMenuList[index].icon, + height: iconHeight, + width: iconWidth, + color: + isSelected ? theme.colorScheme.primary : appTheme.blueGray400, + ), + if (bottomMenuList[index].image) + GestureDetector( + onTap: () => showMenu( + context: context, + color: Colors.white, + shadowColor: Colors.white, + surfaceTintColor: Colors.white, + position: RelativeRect.fromLTRB( + // Указываем позицию справа по центру + MediaQuery.of(context).size.width - + 50, // Расположение от правого края + kToolbarHeight + 340, // Расположение от верхнего края + 0, // Не смещаем по горизонтали + 0, // Не смещаем по вертикали + ), + items: [ + PopupMenuItem( + child: + CustomNavigationMenu(), // Используйте ваше меню здесь + ) + ], + ), + child: AppbarTitleCircleimage( + imagePath: ImageConstant.imgEllipse1, + ), + ), + if (bottomMenuList[index].title != + null) // Проверяем, существует ли title + Padding( + padding: EdgeInsets.only(top: 8.v), + child: Text( + bottomMenuList[index].title!, + style: CustomTextStyles.bodySmallMulishBluegray80012.copyWith( + color: isSelected + ? theme.colorScheme.primary + : appTheme.blueGray800, + ), + ), + ), + ], + ), + activeIcon: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CustomImageView( + imagePath: bottomMenuList[index].activeIcon, + height: iconHeight, // Используем тот же размер для активной иконки + width: iconWidth, // Используем тот же размер для активной иконки + color: theme.colorScheme.primary, + ), + Padding( + padding: EdgeInsets.only(top: 9.v), + child: Text( + bottomMenuList[index].title ?? "", + style: CustomTextStyles.bodySmallMulishPrimary.copyWith( + color: theme.colorScheme.primary, + ), + ), + ), + ], + ), + label: '', + ); + } } enum BottomBarEnum { @@ -130,6 +210,7 @@ class BottomMenuModel { required this.activeIcon, this.title, required this.type, + required this.image, }); String icon; @@ -139,28 +220,6 @@ class BottomMenuModel { String? title; BottomBarEnum type; -} -class DefaultWidget extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container( - color: Colors.white, - padding: EdgeInsets.all(10), - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Please replace the respective Widget here', - style: TextStyle( - fontSize: 18, - ), - ), - ], - ), - ), - ); - } + bool image; } diff --git a/lib/widgets/custom_drop_down.dart b/lib/widgets/custom_drop_down.dart index dd5afc7d..759efc1a 100644 --- a/lib/widgets/custom_drop_down.dart +++ b/lib/widgets/custom_drop_down.dart @@ -23,6 +23,7 @@ class CustomDropDown extends StatelessWidget { this.filled = true, this.validator, this.onChanged, + this.defaultValue, }) : super( key: key, ); @@ -61,6 +62,8 @@ class CustomDropDown extends StatelessWidget { final bool? filled; + final SelectionPopupModel? defaultValue; + final FormFieldValidator? validator; final Function(SelectionPopupModel)? onChanged; @@ -70,74 +73,73 @@ class CustomDropDown extends StatelessWidget { return alignment != null ? Align( alignment: alignment ?? Alignment.center, - child: dropDownWidget, + child: _buildDropDownWidget(context), ) - : dropDownWidget; + : _buildDropDownWidget(context); } - Widget get dropDownWidget => SizedBox( - width: width ?? double.maxFinite, - child: DropdownButtonFormField( - focusNode: focusNode ?? FocusNode(), - icon: icon, - autofocus: autofocus!, - style: textStyle ?? theme.textTheme.titleMedium, - items: items?.map((SelectionPopupModel item) { - return DropdownMenuItem( - value: item, - child: Text( - item.title, - overflow: TextOverflow.ellipsis, - style: hintStyle ?? theme.textTheme.titleMedium, - ), - ); - }).toList(), - decoration: decoration, - validator: validator, - onChanged: (value) { - onChanged!(value!); - }, - ), - ); - InputDecoration get decoration => InputDecoration( - hintText: hintText ?? "", - hintStyle: hintStyle ?? theme.textTheme.titleMedium, - prefixIcon: prefix, - prefixIconConstraints: prefixConstraints, - suffixIcon: suffix, - suffixIconConstraints: suffixConstraints, - isDense: true, - contentPadding: contentPadding ?? - EdgeInsets.only( - left: 6.h, - top: 6.v, - bottom: 6.v, + _buildDropDownWidget(BuildContext context) { + return SizedBox( + width: width ?? MediaQuery.of(context).size.width - 20, + child: DropdownButtonFormField( + focusNode: focusNode ?? FocusNode(), + icon: icon, + autofocus: autofocus!, + isExpanded: true, + style: textStyle ?? Theme.of(context).textTheme.titleMedium, + items: items?.map((SelectionPopupModel item) { + return DropdownMenuItem( + value: item, + child: Text( + item.title, + overflow: TextOverflow.fade, + style: hintStyle ?? Theme.of(context).textTheme.titleMedium, ), - fillColor: fillColor ?? theme.colorScheme.onPrimary, - filled: filled, - border: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: appTheme.blueA400, - width: 1, - ), - ), - enabledBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: appTheme.blueA400, - width: 1, - ), - ), - focusedBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: appTheme.blueA400, - width: 1, - ), - ), - ); + ); + }).toList(), + decoration: _buildInputDecoration(context), + validator: validator, + onChanged: onDropDownChanged, + ), + ); + } + + void onDropDownChanged(value) { + onChanged!(value!); + } + + InputDecoration _buildInputDecoration(BuildContext context) { + return InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? Theme.of(context).textTheme.titleMedium, + prefixIcon: prefix, + prefixIconConstraints: prefixConstraints, + suffixIcon: suffix, + suffixIconConstraints: suffixConstraints, + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + left: 6, + top: 6, + bottom: 6, + ), + fillColor: fillColor ?? Theme.of(context).colorScheme.onPrimary, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: appTheme.blueA400, width: 1), + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: appTheme.blueA400, width: 1), + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: appTheme.blueA400, width: 1), + ), + ); + } } diff --git a/lib/widgets/custom_edit_drop_down.dart b/lib/widgets/custom_edit_drop_down.dart index 59c58593..7f56bbd8 100644 --- a/lib/widgets/custom_edit_drop_down.dart +++ b/lib/widgets/custom_edit_drop_down.dart @@ -73,75 +73,73 @@ class CustomDropDown extends StatelessWidget { return alignment != null ? Align( alignment: alignment ?? Alignment.center, - child: dropDownWidget, + child: _buildDropDownWidget(context), ) - : dropDownWidget; + : _buildDropDownWidget(context); } - Widget get dropDownWidget => SizedBox( - width: width ?? double.maxFinite, - child: DropdownButtonFormField( - focusNode: focusNode ?? FocusNode(), - icon: icon, - autofocus: autofocus!, - style: textStyle ?? theme.textTheme.titleMedium, - items: items?.map((SelectionPopupModel item) { - return DropdownMenuItem( - value: item, - child: Text( - item.title, - overflow: TextOverflow.ellipsis, - style: hintStyle ?? theme.textTheme.titleMedium, - ), - ); - }).toList(), - decoration: decoration, - validator: validator, - value: defaultValue, - onChanged: (value) { - onChanged!(value!); - }, - ), - ); - InputDecoration get decoration => InputDecoration( - hintText: hintText ?? "", - hintStyle: hintStyle ?? theme.textTheme.titleMedium, - prefixIcon: prefix, - prefixIconConstraints: prefixConstraints, - suffixIcon: suffix, - suffixIconConstraints: suffixConstraints, - isDense: true, - contentPadding: contentPadding ?? - EdgeInsets.only( - left: 6.h, - top: 6.v, - bottom: 6.v, + _buildDropDownWidget(BuildContext context) { + return SizedBox( + width: width ?? double.maxFinite, + child: DropdownButtonFormField( + focusNode: focusNode ?? FocusNode(), + icon: icon, + autofocus: autofocus!, + style: textStyle ?? Theme.of(context).textTheme.titleMedium, + items: items?.map((SelectionPopupModel item) { + return DropdownMenuItem( + value: item, + child: Text( + item.title, + overflow: TextOverflow.ellipsis, + style: hintStyle ?? Theme.of(context).textTheme.titleMedium, ), - fillColor: fillColor ?? theme.colorScheme.onPrimary, - filled: filled, - border: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: Colors.grey, - width: 1, - ), - ), - enabledBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: Colors.grey, - width: 1, - ), - ), - focusedBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: Colors.grey, - width: 1, - ), - ), - ); + ); + }).toList(), + decoration: _buildInputDecoration(context), + validator: validator, + value: defaultValue, + onChanged: onDropDownChanged, + ), + ); + } + + void onDropDownChanged(value) { + onChanged!(value!); + } + + InputDecoration _buildInputDecoration(BuildContext context) { + return InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? Theme.of(context).textTheme.titleMedium, + prefixIcon: prefix, + prefixIconConstraints: prefixConstraints, + suffixIcon: suffix, + suffixIconConstraints: suffixConstraints, + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + left: 6, + top: 6, + bottom: 6, + ), + fillColor: fillColor ?? Theme.of(context).colorScheme.onPrimary, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey, width: 1), + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey, width: 1), + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey, width: 1), + ), + ); + } } diff --git a/lib/widgets/custom_edit_text_form_field.dart b/lib/widgets/custom_edit_text_form_field.dart index 5a5687fb..0853d3e5 100644 --- a/lib/widgets/custom_edit_text_form_field.dart +++ b/lib/widgets/custom_edit_text_form_field.dart @@ -1,4 +1,3 @@ -import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,6 +20,7 @@ class CustomTextFormField extends StatelessWidget { this.prefixConstraints, this.suffix, this.suffixConstraints, + this.onFieldSubmitted, this.initialValue, this.contentPadding, this.inputFormatters, @@ -29,6 +29,7 @@ class CustomTextFormField extends StatelessWidget { this.filled = true, this.validator, this.keyboardType, + this.onChanged, this.onSaved, }) : super( key: key, @@ -66,6 +67,8 @@ class CustomTextFormField extends StatelessWidget { final Widget? suffix; + final Function(String)? onFieldSubmitted; + final BoxConstraints? suffixConstraints; final EdgeInsets? contentPadding; @@ -84,65 +87,68 @@ class CustomTextFormField extends StatelessWidget { final void Function(String?)? onSaved; + final ValueChanged? onChanged; + @override Widget build(BuildContext context) { return alignment != null ? Align( alignment: alignment ?? Alignment.center, - child: textFormFieldWidget, + child: _buildTextFormFieldWidget(context), ) - : textFormFieldWidget; + : _buildTextFormFieldWidget(context); } - Widget get textFormFieldWidget => SizedBox( - width: width ?? double.maxFinite, - child: TextFormField( - controller: controller, - focusNode: focusNode ?? FocusNode(), - autofocus: autofocus!, - style: textStyle ?? theme.textTheme.titleMedium, - obscureText: obscureText!, - textInputAction: textInputAction, - maxLines: maxLines ?? 1, - decoration: decoration, - validator: validator, - keyboardType: keyboardType, - onSaved: onSaved, - inputFormatters: inputFormatters, - initialValue: initialValue, - ), - ); - InputDecoration get decoration => InputDecoration( - hintText: hintText ?? "", - hintStyle: hintStyle ?? theme.textTheme.bodyLarge, - prefixIcon: prefix, - prefixIconConstraints: prefixConstraints, - suffixIcon: suffix, - suffixIconConstraints: suffixConstraints, - isDense: true, - contentPadding: contentPadding ?? EdgeInsets.all(6.h), - fillColor: fillColor ?? theme.colorScheme.onPrimary, - filled: filled, - border: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide(color: Colors.grey, width: 1), - ), - enabledBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: Colors.grey, - width: 1, - ), - ), - focusedBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: Colors.grey, - width: 1, - ), - ), - ); + _buildTextFormFieldWidget(BuildContext context) { + return SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + controller: controller, + focusNode: focusNode ?? FocusNode(), + autofocus: autofocus!, + style: textStyle ?? Theme.of(context).textTheme.titleMedium, + obscureText: obscureText!, + textInputAction: textInputAction, + maxLines: maxLines ?? 1, + decoration: _buildInputDecoration(context), + validator: validator, + keyboardType: keyboardType, + onSaved: onSaved, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + inputFormatters: inputFormatters, + initialValue: initialValue, + ), + ); + } + + InputDecoration _buildInputDecoration(BuildContext context) { + return InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? Theme.of(context).textTheme.bodyLarge, + prefixIcon: prefix, + prefixIconConstraints: prefixConstraints, + suffixIcon: suffix, + suffixIconConstraints: suffixConstraints, + isDense: true, + contentPadding: contentPadding ?? EdgeInsets.all(6), + fillColor: fillColor ?? Theme.of(context).colorScheme.onPrimary, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey, width: 1), + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey, width: 1), + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey, width: 1), + ), + ); + } } diff --git a/lib/widgets/custom_expandable_list.dart b/lib/widgets/custom_expandable_list.dart new file mode 100644 index 00000000..00d66cea --- /dev/null +++ b/lib/widgets/custom_expandable_list.dart @@ -0,0 +1,447 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart'; +import 'package:eatvio_app/widgets/custom_hyper_switch.dart'; +import 'package:eatvio_app/widgets/custom_hypo_switch.dart'; +import 'package:intl/intl.dart'; +import 'package:eatvio_app/widgets/next_step_button.dart'; +import 'dart:convert'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; + +class ExpandableList extends StatefulWidget { + final String title; + + ExpandableList(this.title); + + @override + _ExpandableListState createState() => _ExpandableListState(); +} + +class _ExpandableListState extends State { + bool isExpanded = false; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: toggleExpand(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.bold), + ), + Icon(isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), + ], + ), + ), + if (isExpanded) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: BlocBuilder( + builder: (context, state) { + if (state is MyProfileInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is MyProfileLoadedState) { + // Данные загружены, можно строить форму. + return _buildForm(context, state); + } else if (state is MyProfileErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }), + ), + Divider(), + ], + ); + } + + toggleExpand() { + return () { + setState(() { + isExpanded = !isExpanded; + }); + }; + } + + _buildForm(BuildContext context, MyProfileLoadedState state) { + List cities = [ + 'Москва', + 'Санкт-Петербург', + 'Новосибирск', + 'Екатеринбург', + 'Нижний Новгород', + 'Казань', + 'Челябинск', + 'Омск', + 'Самара', + 'Ростов-на-Дону', + 'Уфа', + 'Красноярск', + 'Воронеж', + 'Пермь', + 'Волгоград', + 'Краснодар', + 'Саратов', + 'Тюмень', + 'Тольятти', + 'Ижевск', + 'Барнаул', + 'Ульяновск', + 'Иркутск', + 'Хабаровск', + 'Ярославль', + 'Владивосток', + 'Махачкала', + 'Томск', + 'Оренбург', + 'Кемерово', + ]; + List countries = [ + 'Россия', + ]; + List activities = [ + 'сидячий', + 'низкий (1-3 тренировки в неделю по 20 мин)', + 'умеренный (3-4 тренировки в неделю по 30-60 мин)', + 'высокий (5-7 тренировок по 30-60 мин)', + 'экстремальный (несколько интенсивных тренировок в день)' + ]; + final digitsOnly = FilteringTextInputFormatter.digitsOnly; + return Column(children: [ + TextField( + controller: state.firstNameFormController, + decoration: InputDecoration(labelText: 'first_name'.tr), + ), + TextField( + controller: state.lastNameFormController, + decoration: InputDecoration(labelText: 'last_name'.tr), + ), + TextField( + decoration: InputDecoration(labelText: 'date_of_birth'.tr), + controller: state.dateFormController, + onTap: inputTime(context, state)), + DropdownButtonFormField( + value: state.selectedCountry, + hint: Text(state.selectedCountry ?? "Не выбрано"), + onChanged: setCountry(state), + items: countries.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + decoration: InputDecoration(labelText: 'country'.tr), + ), + DropdownButtonFormField( + hint: Text(state.selectedCity ?? "Не выбрано"), + value: state.selectedCity, + onChanged: setCity(state), + items: cities.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + decoration: InputDecoration(labelText: 'city_of_living'.tr), + ), + TextField( + controller: state.weightFormController, + decoration: InputDecoration(labelText: 'weight_measurements'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.heightFormController, + decoration: InputDecoration(labelText: 'height_measurements'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.chestCircumferenceFormController, + decoration: InputDecoration(labelText: 'chest_circumference'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.waistCircumferenceFormController, + decoration: InputDecoration(labelText: 'waist_circumference'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.hipCircumferenceFormController, + decoration: InputDecoration(labelText: 'hip_circumference'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.upPressureFormController, + decoration: InputDecoration(labelText: 'up_pressure'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.downPressureFormController, + decoration: InputDecoration(labelText: 'down_pressure'.tr), + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly]), + TextField( + controller: state.allergiesFormController, + decoration: InputDecoration(labelText: 'allergies_presence'.tr), + maxLines: null, + ), + TextField( + controller: state.pillsFormController, + decoration: InputDecoration(labelText: 'do_you_take_pills'.tr), + maxLines: null, + ), + TextField( + controller: state.sportsFormController, + decoration: InputDecoration(labelText: 'your_favorite_sports'.tr), + maxLines: null, + ), + TextField( + controller: state.favoriteFormController, + decoration: InputDecoration(labelText: 'favorite_foods'.tr), + maxLines: null, + ), + TextField( + controller: state.unfavoriteFormController, + decoration: InputDecoration(labelText: 'unfavorite_foods'.tr), + maxLines: null, + ), + TextField( + controller: state.additionalFormController, + decoration: InputDecoration(labelText: 'additional_information'.tr), + maxLines: null, + ), + DropdownButtonFormField( + hint: Text(state.selectedActivity ?? "Не выбрано"), + value: state.selectedActivity, + onChanged: setActivity(state), + items: activities.map>((String value) { + return DropdownMenuItem( + value: value, + child: SizedBox( + // Используем SizedBox для ограничения ширины элемента списка + width: MediaQuery.of(context).size.width * + 0.8, // Устанавливаем желаемую ширину элемента списка + child: Text(value), + ), + ); + }).toList(), + isExpanded: true, // Расширяем список на всю доступную ширину + decoration: InputDecoration(labelText: 'Активность'), + ), + Row( + children: [ + HypoSwitch('do_you_have_hypotension_tendency'.tr, state), + ], + ), + Row( + children: [ + HyperSwitch('do_you_have_hypertension_tendency'.tr, state), + ], + ), + NextStepButton( + onPressed: () => sendProfileData(state), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + text: "save".tr.toUpperCase(), + margin: EdgeInsets.only(top: 20.h, bottom: 20.h)), + ]); + } + + sendData(MyProfileLoadedState state) { + return () { + sendProfileData(state); + }; + } + + setCity(MyProfileLoadedState state) { + return (newValue) { + // Обработчик изменения выбранного города + setSelectedCity(state, newValue); + }; + } + + setCountry(MyProfileLoadedState state) { + return (newValue) { + // Обработчик изменения выбранной страны + setSelectedCountry(state, newValue); + }; + } + + setActivity(MyProfileLoadedState state) { + return (newValue) { + // Обработчик изменения выбранной страны + setSelectedActivity(state, newValue); + }; + } + + inputTime(BuildContext context, MyProfileLoadedState state) { + return () { + showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ).then((selectedDate) { + if (selectedDate != null) { + setState(() { + state.dateFormController!.text = + DateFormat('dd-MM-yyyy').format(selectedDate); + }); + } + }); + }; + } + + void setSelectedCity(MyProfileLoadedState? state, String? newValue) { + setState(() { + state!.selectedCity = newValue; + }); + } + + void setSelectedCountry(MyProfileLoadedState? state, String? newValue) { + setState(() { + state!.selectedCountry = newValue; + }); + } + + void setSelectedActivity(MyProfileLoadedState? state, String? newValue) { + setState(() { + state!.selectedActivity = newValue; + }); + } + + Future sendProfileData(MyProfileLoadedState? state) async { + try { + final profileData = _createProfileData(state); + final token = await _getTokenFromBox(); + final apiUrl = _buildApiUrl(); + final response = await _sendHttpRequest(profileData, apiUrl, token); + + if (response.statusCode == 200) { + _navigateToProfilePage(); + } else { + final errorMessage = _getErrorMessage(response); + _showErrorDialog(errorMessage); + throw Exception('Ошибка при отправке данных профиля'); + } + } catch (error) { + throw Exception('Ошибка при отправке данных профиля: $error'); + } + } + + Map _createProfileData(MyProfileLoadedState? state) { + return { + 'first_name': state?.firstNameFormController?.text, + 'last_name': state?.lastNameFormController?.text, + 'chest_circumference': state?.chestCircumferenceFormController?.text, + 'waist_circumference': state?.waistCircumferenceFormController?.text, + 'hip_circumference': state?.hipCircumferenceFormController?.text, + 'birthdate': state?.dateFormController?.text, + 'height': state?.heightFormController?.text, + 'weight': state?.weightFormController?.text, + 'blood_pressure_up': state?.upPressureFormController?.text, + 'blood_pressure_down': state?.downPressureFormController?.text, + 'about_alergy': state?.allergiesFormController?.text ?? 'Нет аллергий', + 'about_taking_pils': state?.pillsFormController?.text, + 'about_sport_hobbies': state?.sportsFormController?.text, + 'favorite_foods': state?.favoriteFormController?.text, + 'unfavorite_foods': state?.unfavoriteFormController?.text, + 'additional_info': state?.additionalFormController?.text, + 'country': state?.selectedCountry ?? 'Россия', + 'city': state?.selectedCity ?? 'Москва', + 'has_hypo': state?.hypoFormController, + 'has_hyper': state?.hyperFormController, + 'activity_coefficient': + determineActivityFactor(state?.selectedActivity ?? 'сидячий'), + }; + } + + Future _getTokenFromBox() async { + final box = await Hive.openBox('token'); + return box.get('token'); + } + + String _buildApiUrl() { + return dotenv.env['SERVER_ROOT']! + '/profile/update'; + } + + Future _sendHttpRequest( + Map profileData, String apiUrl, String? token) { + return http.post( + Uri.parse(apiUrl), + body: json.encode(profileData), + encoding: utf8, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + } + + void _navigateToProfilePage() { + Navigator.pushNamed( + context, + AppRoutes.myProfile, + ); + } + + String _getErrorMessage(http.Response response) { + final res = jsonDecode(response.body); + return res['message']; + } + + void _showErrorDialog(String errorMessage) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Ошибка'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: onTapOk(context), + child: Text('OK'), + ), + ], + ), + ); + } + + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + + double determineActivityFactor(String activityLevel) { + switch (activityLevel) { + case "сидячий": + return 1.2; + case "низкий (1-3 тренировки в неделю по 20 мин)": + return 1.375; + case "умеренный (3-4 тренировки в неделю по 30-60 мин)": + return 1.55; + case "высокий (5-7 тренировок по 30-60 мин)": + return 1.7; + case "экстремальный (несколько интенсивных тренировок в день)": + return 1.9; + default: + return 1.2; // Возвращаемое значение по умолчанию, если уровень активности не распознан + } + } +} diff --git a/lib/widgets/custom_hyper_switch.dart b/lib/widgets/custom_hyper_switch.dart new file mode 100644 index 00000000..92ebf000 --- /dev/null +++ b/lib/widgets/custom_hyper_switch.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart'; + +class HyperSwitch extends StatefulWidget { + final String title; + final MyProfileLoadedState? state; + HyperSwitch(this.title, this.state); + + @override + _HyperState createState() => _HyperState(); +} + +class _HyperState extends State { + bool switchValue = false; + + @override + void initState() { + super.initState(); + if (widget.state?.hyperFormController == '1') { + switchValue = true; + } + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold), + ), + Switch( + value: switchValue, + onChanged: toggleSwitch(), + inactiveThumbColor: appTheme.blueA400, + inactiveTrackColor: Colors.white, + activeTrackColor: appTheme.blueA400, + activeColor: Colors.white, + ), + ], + ); + } + + toggleSwitch() { + return (value) { + setState(() { + switchValue = value; + widget.state!.hyperFormController = value ? "1" : "0"; + }); + }; + } +} diff --git a/lib/widgets/custom_hypo_switch.dart b/lib/widgets/custom_hypo_switch.dart new file mode 100644 index 00000000..1c625feb --- /dev/null +++ b/lib/widgets/custom_hypo_switch.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart'; + +class HypoSwitch extends StatefulWidget { + final String title; + final MyProfileLoadedState? state; + HypoSwitch(this.title, this.state); + + @override + _HypoState createState() => _HypoState(); +} + +class _HypoState extends State { + bool switchValue = false; + + @override + void initState() { + super.initState(); + if (widget.state?.hypoFormController == '1') { + switchValue = true; + } + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold), + ), + Switch( + value: switchValue, + onChanged: toggleSwitch(), + inactiveThumbColor: appTheme.blueA400, + inactiveTrackColor: Colors.white, + activeTrackColor: appTheme.blueA400, + activeColor: Colors.white, + ), + ], + ); + } + + toggleSwitch() { + return (value) { + setState(() { + switchValue = value; + widget.state!.hypoFormController = value ? "1" : "0"; + }); + }; + } +} diff --git a/lib/widgets/custom_icon_button.dart b/lib/widgets/custom_icon_button.dart index cdf5b5b5..55766f18 100644 --- a/lib/widgets/custom_icon_button.dart +++ b/lib/widgets/custom_icon_button.dart @@ -10,22 +10,14 @@ class CustomIconButton extends StatelessWidget { this.decoration, this.child, this.onTap, - }) : super( - key: key, - ); + }) : super(key: key); final Alignment? alignment; - final double? height; - final double? width; - final EdgeInsetsGeometry? padding; - final BoxDecoration? decoration; - final Widget? child; - final VoidCallback? onTap; @override @@ -33,40 +25,40 @@ class CustomIconButton extends StatelessWidget { return alignment != null ? Align( alignment: alignment ?? Alignment.center, - child: iconButtonWidget, + child: _buildIconButtonWidget(), ) - : iconButtonWidget; + : _buildIconButtonWidget(); } - Widget get iconButtonWidget => SizedBox( - height: height ?? 0, - width: width ?? 0, - child: IconButton( - padding: EdgeInsets.zero, - icon: Container( - height: height ?? 0, - width: width ?? 0, - padding: padding ?? EdgeInsets.zero, - decoration: decoration ?? - BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(9), - border: Border.all( - color: Colors.blueGrey, - width: 1, - ), - ), - child: child, - ), - onPressed: onTap, + _buildIconButtonWidget() { + return SizedBox( + height: height ?? 0, + width: width ?? 0, + child: IconButton( + padding: EdgeInsets.zero, + icon: Container( + height: height ?? 0, + width: width ?? 0, + padding: padding ?? EdgeInsets.zero, + decoration: decoration ?? + BoxDecoration( + color: Colors.black, + borderRadius: const BorderRadius.all(Radius.circular(9)), + border: const Border.fromBorderSide( + BorderSide(color: Colors.blueGrey, width: 1)), + ), + child: child, ), - ); + onPressed: onTap, + ), + ); + } } /// Extension on [CustomIconButton] to facilitate inclusion of all types of border style etc extension IconButtonStyleHelper on CustomIconButton { static BoxDecoration get fillBlueA => BoxDecoration( color: Colors.blue, - borderRadius: BorderRadius.circular(14), + borderRadius: const BorderRadius.all(Radius.circular(14)), ); } diff --git a/lib/widgets/custom_image_view.dart b/lib/widgets/custom_image_view.dart index 5bed7a28..07b9a5d9 100644 --- a/lib/widgets/custom_image_view.dart +++ b/lib/widgets/custom_image_view.dart @@ -42,19 +42,21 @@ class CustomImageView extends StatelessWidget { return alignment != null ? Align( alignment: alignment!, - child: _buildWidget(), + child: Padding( + padding: margin ?? EdgeInsets.zero, + child: InkWell( + onTap: onTap, + child: _buildCircleImage(), + ), + ), ) - : _buildWidget(); - } - - Widget _buildWidget() { - return Padding( - padding: margin ?? EdgeInsets.zero, - child: InkWell( - onTap: onTap, - child: _buildCircleImage(), - ), - ); + : Padding( + padding: margin ?? EdgeInsets.zero, + child: InkWell( + onTap: onTap, + child: _buildCircleImage(), + ), + ); } ///build the image with border radius @@ -84,7 +86,7 @@ class CustomImageView extends StatelessWidget { } } - Widget _buildImageView() { + _buildImageView() { if (imagePath != null) { switch (imagePath!.imageType) { case ImageType.svg: @@ -96,7 +98,6 @@ class CustomImageView extends StatelessWidget { height: height, width: width, fit: fit ?? BoxFit.contain, - color: color, ), ); case ImageType.file: diff --git a/lib/widgets/custom_language_drop_down.dart b/lib/widgets/custom_language_drop_down.dart new file mode 100644 index 00000000..7f0e3e0f --- /dev/null +++ b/lib/widgets/custom_language_drop_down.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class LanguageDropdown extends StatefulWidget { + @override + _LanguageDropdownState createState() => _LanguageDropdownState(); +} + +class _LanguageDropdownState extends State { + String selectedLanguage = 'Русский'; + + @override + Widget build(BuildContext context) { + return DropdownButton( + value: selectedLanguage, + onChanged: selectLanguage(), + isDense: false, // Установите isDense в false + selectedItemBuilder: getList(), + items: ['Русский'].map((String language) { + return DropdownMenuItem( + value: language, + child: Text(language), + ); + }).toList(), + ); + } + + getList() { + return (BuildContext context) { + return ['Русский'].map((String language) { + return Align( + alignment: Alignment.center, // Выравнивание текста по центру + child: Text( + language, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ); + }).toList(); + }; + } + + selectLanguage() { + return (newValue) { + setState(() { + selectedLanguage = newValue!; + }); + }; + } +} diff --git a/lib/widgets/custom_navigation_menu.dart b/lib/widgets/custom_navigation_menu.dart new file mode 100644 index 00000000..4bbdc758 --- /dev/null +++ b/lib/widgets/custom_navigation_menu.dart @@ -0,0 +1,86 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_navigation_menu_item.dart'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; + +class CustomNavigationMenu extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MenuItem( + icon: ImageConstant.imgSettings, + text: 'Настройки профиля', + onTap: () => Navigator.pushNamed(context, AppRoutes.mySettings), + ), + MenuItem( + icon: ImageConstant.imgRecipes, + text: 'Мои рецепты', + onTap: () => + Navigator.pushNamed(context, AppRoutes.recipeSelectScreen), + // Действие при нажатии на "Мои рецепты" + ), + MenuItem( + icon: ImageConstant.imgDiets, + text: 'Мои диеты', + onTap: () => + Navigator.pushNamed(context, AppRoutes.dietSelectScreen), + // Действие при нажатии на "Мои рецепты" + ), + MenuItem( + icon: ImageConstant.imgPlans, + text: 'Мои планы', + onTap: () => + Navigator.pushNamed(context, AppRoutes.planSelectScreen), + // Действие при нажатии на "Мои рецепты" + ), + MenuItem( + icon: ImageConstant.imgFinance, + text: 'Глоссарий', + onTap: () => Navigator.pushNamed(context, AppRoutes.glossaryScreen), + ), + MenuItem( + icon: ImageConstant.imgSupport, + text: 'Поддержка', + onTap: () { + // Действие при нажатии на "Поддержка" + }, + ), + MenuItem( + icon: ImageConstant.imgExit, + text: 'Выход', + onTap: _onTapLogout(context), + // Действие при нажатии на "Выход" + ), + ], + ), + ); + } +} + +_onTapLogout(BuildContext context) { + return () async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/profile/logout'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + + if (response.statusCode == 204) { + print(response.statusCode); + box.delete('token'); // Удаляем токен из Hive + Navigator.pushNamed( + context, + AppRoutes.authScreen, + ); + } + }; +} diff --git a/lib/widgets/custom_navigation_menu_item.dart b/lib/widgets/custom_navigation_menu_item.dart new file mode 100644 index 00000000..f33d5342 --- /dev/null +++ b/lib/widgets/custom_navigation_menu_item.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_image_view.dart'; + +class MenuItem extends StatelessWidget { + final String icon; + final String text; + final VoidCallback onTap; + + const MenuItem({ + required this.icon, + required this.text, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.center, // Выравнивание по центру вертикально + children: [ + CustomImageView(imagePath: icon), + SizedBox( + width: + 8.0), // Добавляем небольшой отступ между иконкой и текстом + Text(text), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/custom_notification_screen.dart b/lib/widgets/custom_notification_screen.dart new file mode 100644 index 00000000..bb778441 --- /dev/null +++ b/lib/widgets/custom_notification_screen.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class NotificationSwitch extends StatefulWidget { + final String title; + + NotificationSwitch(this.title); + + @override + _NotificationSwitchState createState() => _NotificationSwitchState(); +} + +class _NotificationSwitchState extends State { + bool switchValue = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold), + ), + Switch( + value: switchValue, + onChanged: toggleSwitch(), + inactiveThumbColor: appTheme.blueA400, + inactiveTrackColor: Colors.white, + activeTrackColor: appTheme.blueA400, + activeColor: Colors.white, + ), + ], + ); + } + + toggleSwitch() { + return (value) { + setState(() { + switchValue = value; + }); + }; + } +} diff --git a/lib/widgets/custom_outlined_button.dart b/lib/widgets/custom_outlined_button.dart index 4fb4a85c..38a77041 100644 --- a/lib/widgets/custom_outlined_button.dart +++ b/lib/widgets/custom_outlined_button.dart @@ -31,43 +31,86 @@ class CustomOutlinedButton extends BaseButton { ); final BoxDecoration? decoration; - final Widget? leftIcon; - final Widget? rightIcon; - final Widget? label; @override Widget build(BuildContext context) { - return alignment != null - ? Align( - alignment: alignment ?? Alignment.center, - child: buildOutlinedButtonWidget, - ) - : buildOutlinedButtonWidget; - } - - Widget get buildOutlinedButtonWidget => Container( + if (alignment != null) { + return Align( + alignment: alignment ?? Alignment.center, + child: Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle!, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? CustomTextStyles.titleSmallBold, + ), + rightIcon ?? SizedBox.shrink(), + ], + ), + ), + ), + ); + } else { + return Container( height: this.height ?? 30.v, width: this.width ?? double.maxFinite, margin: margin, decoration: decoration, child: OutlinedButton( - style: buttonStyle, + style: buttonStyle!, onPressed: isDisabled ?? false ? null : onPressed ?? () {}, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - leftIcon ?? const SizedBox.shrink(), + leftIcon ?? SizedBox.shrink(), Text( text, style: buttonTextStyle ?? CustomTextStyles.titleSmallBold, ), - rightIcon ?? const SizedBox.shrink(), + rightIcon ?? SizedBox.shrink(), ], ), ), ); + } + } + + /*_buildAppBarbuildOutlinedButtonWidget() { + return Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle!, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? CustomTextStyles.titleSmallBold, + ), + rightIcon ?? SizedBox.shrink(), + ], + ), + ), + ); + }*/ } diff --git a/lib/widgets/custom_section_title.dart b/lib/widgets/custom_section_title.dart new file mode 100644 index 00000000..282f3d6a --- /dev/null +++ b/lib/widgets/custom_section_title.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class SectionTitle extends StatelessWidget { + final String title; + + SectionTitle(this.title); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + ); + } +} diff --git a/lib/widgets/custom_text_form_field.dart b/lib/widgets/custom_text_form_field.dart index a96c5a1c..86a2a237 100644 --- a/lib/widgets/custom_text_form_field.dart +++ b/lib/widgets/custom_text_form_field.dart @@ -28,57 +28,36 @@ class CustomTextFormField extends StatelessWidget { this.filled = true, this.validator, this.keyboardType, + this.onFieldSubmitted, this.onSaved, }) : super( key: key, ); final Alignment? alignment; - final double? width; - final TextEditingController? controller; - final FocusNode? focusNode; - final bool? autofocus; - final TextStyle? textStyle; - final bool? obscureText; - final List? inputFormatters; - final TextInputAction? textInputAction; - final TextInputType? textInputType; - final int? maxLines; - final String? hintText; - final TextStyle? hintStyle; - final Widget? prefix; - final BoxConstraints? prefixConstraints; - final Widget? suffix; - final BoxConstraints? suffixConstraints; - final EdgeInsets? contentPadding; - final InputBorder? borderDecoration; - final Color? fillColor; - final bool? filled; - final TextInputType? keyboardType; - final FormFieldValidator? validator; - + final Function(String)? onFieldSubmitted; final void Function(String?)? onSaved; @override @@ -86,62 +65,150 @@ class CustomTextFormField extends StatelessWidget { return alignment != null ? Align( alignment: alignment ?? Alignment.center, - child: textFormFieldWidget, + child: SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + controller: controller, + focusNode: focusNode ?? FocusNode(), + autofocus: autofocus!, + style: textStyle ?? theme.textTheme.titleMedium, + obscureText: obscureText!, + textInputAction: textInputAction, + maxLines: maxLines ?? 1, + onFieldSubmitted: onFieldSubmitted, + decoration: InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? theme.textTheme.bodyLarge, + prefixIcon: prefix, + prefixIconConstraints: prefixConstraints, + suffixIcon: suffix, + suffixIconConstraints: suffixConstraints, + isDense: true, + contentPadding: contentPadding ?? EdgeInsets.all(6.h), + fillColor: fillColor ?? theme.colorScheme.onPrimary, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + ), + keyboardType: keyboardType, + validator: validator, + onSaved: onSaved, + inputFormatters: inputFormatters, + ), + ), ) - : textFormFieldWidget; + : SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + controller: controller, + focusNode: focusNode ?? FocusNode(), + autofocus: autofocus!, + style: textStyle ?? theme.textTheme.titleMedium, + obscureText: obscureText!, + textInputAction: textInputAction, + maxLines: maxLines ?? 1, + decoration: InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? theme.textTheme.bodyLarge, + prefixIcon: prefix, + prefixIconConstraints: prefixConstraints, + suffixIcon: suffix, + suffixIconConstraints: suffixConstraints, + isDense: true, + contentPadding: contentPadding ?? EdgeInsets.all(6.h), + fillColor: fillColor ?? theme.colorScheme.onPrimary, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + ), + keyboardType: keyboardType, + validator: validator, + onSaved: onSaved, + inputFormatters: inputFormatters, + ), + ); } - - Widget get textFormFieldWidget => SizedBox( - width: width ?? double.maxFinite, - child: TextFormField( - controller: controller, - focusNode: focusNode ?? FocusNode(), - autofocus: autofocus!, - style: textStyle ?? theme.textTheme.titleMedium, - obscureText: obscureText!, - textInputAction: textInputAction, - maxLines: maxLines ?? 1, - decoration: decoration, - validator: validator, - keyboardType: keyboardType, - onSaved: onSaved, - inputFormatters: inputFormatters, - ), - ); - InputDecoration get decoration => InputDecoration( - hintText: hintText ?? "", - hintStyle: hintStyle ?? theme.textTheme.bodyLarge, - prefixIcon: prefix, - prefixIconConstraints: prefixConstraints, - suffixIcon: suffix, - suffixIconConstraints: suffixConstraints, - isDense: true, - contentPadding: contentPadding ?? EdgeInsets.all(6.h), - fillColor: fillColor ?? theme.colorScheme.onPrimary, - filled: filled, - border: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: appTheme.blueA400, - width: 1, - ), - ), - enabledBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: appTheme.blueA400, - width: 1, - ), - ), - focusedBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8.h), - borderSide: BorderSide( - color: appTheme.blueA400, - width: 1, - ), - ), - ); } + /* InputDecoration _buildInputDecoration() { + return InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? theme.textTheme.bodyLarge, + prefixIcon: prefix, + prefixIconConstraints: prefixConstraints, + suffixIcon: suffix, + suffixIconConstraints: suffixConstraints, + isDense: true, + contentPadding: contentPadding ?? EdgeInsets.all(6.h), + fillColor: fillColor ?? theme.colorScheme.onPrimary, + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide( + color: appTheme.blueA400, + width: 1, + ), + ), + ); + } +}*/ \ No newline at end of file diff --git a/lib/widgets/next_step_button.dart b/lib/widgets/next_step_button.dart index ce972835..455f8f94 100644 --- a/lib/widgets/next_step_button.dart +++ b/lib/widgets/next_step_button.dart @@ -31,43 +31,59 @@ class NextStepButton extends BaseButton { ); final BoxDecoration? decoration; - final Widget? leftIcon; - final Widget? rightIcon; - final Widget? label; @override Widget build(BuildContext context) { - return alignment != null + return (alignment != null) ? Align( - alignment: alignment ?? Alignment.center, - child: buildOutlinedButtonWidget, - ) - : buildOutlinedButtonWidget; - } - - Widget get buildOutlinedButtonWidget => Container( - height: this.height ?? 30.v, - width: this.width ?? double.maxFinite, - margin: margin, - decoration: decoration, - child: OutlinedButton( - style: buttonStyle, - onPressed: isDisabled ?? false ? null : onPressed ?? () {}, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - leftIcon ?? const SizedBox.shrink(), - Text( - text, - style: buttonTextStyle ?? theme.textTheme.titleSmall, + alignment: alignment!, + child: Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? const SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? theme.textTheme.titleSmall, + ), + rightIcon ?? const SizedBox.shrink(), + ], + ), ), - rightIcon ?? const SizedBox.shrink(), - ], - ), - ), - ); + ), + ) + : Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? const SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? theme.textTheme.titleSmall, + ), + rightIcon ?? const SizedBox.shrink(), + ], + ), + ), + ); + } } diff --git a/lib/widgets/nutrition_chart.dart b/lib/widgets/nutrition_chart.dart new file mode 100644 index 00000000..455f8f94 --- /dev/null +++ b/lib/widgets/nutrition_chart.dart @@ -0,0 +1,89 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/base_button.dart'; +import 'package:flutter/material.dart'; + +class NextStepButton extends BaseButton { + NextStepButton({ + Key? key, + this.decoration, + this.leftIcon, + this.rightIcon, + this.label, + VoidCallback? onPressed, + ButtonStyle? buttonStyle, + TextStyle? buttonTextStyle, + bool? isDisabled, + Alignment? alignment, + double? height, + double? width, + EdgeInsets? margin, + required String text, + }) : super( + text: text, + onPressed: onPressed, + buttonStyle: buttonStyle, + isDisabled: isDisabled, + buttonTextStyle: buttonTextStyle, + height: height, + alignment: alignment, + width: width, + margin: margin, + ); + + final BoxDecoration? decoration; + final Widget? leftIcon; + final Widget? rightIcon; + final Widget? label; + + @override + Widget build(BuildContext context) { + return (alignment != null) + ? Align( + alignment: alignment!, + child: Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? const SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? theme.textTheme.titleSmall, + ), + rightIcon ?? const SizedBox.shrink(), + ], + ), + ), + ), + ) + : Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? const SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? theme.textTheme.titleSmall, + ), + rightIcon ?? const SizedBox.shrink(), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 711ce10f..8f1a6f45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.0.0+1 +version: 0.0.0+1 environment: sdk: '>=2.12.0 <3.0.0' dependencies: @@ -25,8 +25,12 @@ dependencies: carousel_slider: ^4.2.1 flutter_form_builder: ^9.2.1 image_picker: ^1.0.7 + flutter_typeahead: ^5.2.0 + bloc_test: ^9.1.6 dev_dependencies: + dart_code_metrics: ^5.7.6 + dart_code_metrics_presets: ^1.8.0 flutter_test: sdk: flutter flutter: diff --git a/test/add_info_test.dart b/test/add_info_test.dart new file mode 100644 index 00000000..a4d576d7 --- /dev/null +++ b/test/add_info_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/additional_information_screen/additional_information_screen.dart'; +import 'package:eatvio_app/presentation/registration/additional_information_screen/bloc/additional_information_bloc.dart'; +import 'package:eatvio_app/presentation/registration/additional_information_screen/models/additional_information_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = AdditionalInformationBloc(AdditionalInformationState( + additionalInformationModelObj: AdditionalInformationModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: AdditionalInformationScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/allergies_test.dart b/test/allergies_test.dart new file mode 100644 index 00000000..d020b5ff --- /dev/null +++ b/test/allergies_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/allergies_screen/allergies_screen.dart'; +import 'package:eatvio_app/presentation/registration/allergies_screen/bloc/allergies_bloc.dart'; +import 'package:eatvio_app/presentation/registration/allergies_screen/models/allergies_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = AllergiesBloc(AllergiesState( + allergiesModelObj: AllergiesModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: AllergiesScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/auth_test.dart b/test/auth_test.dart new file mode 100644 index 00000000..22de3a00 --- /dev/null +++ b/test/auth_test.dart @@ -0,0 +1,44 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'package:eatvio_app/presentation/auth/auth_screen/bloc/auth_bloc.dart'; +import 'package:eatvio_app/presentation/auth/auth_screen/models/auth_model.dart'; + +class MockAuthBloc extends MockBloc implements AuthBloc {} + +void main() { + group('AuthScreen', () { + testWidgets('renders AuthScreen with initial state', + (WidgetTester tester) async { + final authBloc = AuthBloc(AuthState(authModelObj: AuthModel())); + + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: [ + AppLocalizationDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + Locale( + 'ru', + '', + ), + ], + home: BlocProvider( + create: (context) => authBloc, + child: Builder( + builder: (context) { + return AuthScreen(); + }, + ), + ), + ), + ); + }); + }); +} diff --git a/test/blood_pressure_test.dart b/test/blood_pressure_test.dart new file mode 100644 index 00000000..14693437 --- /dev/null +++ b/test/blood_pressure_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart'; +import 'package:eatvio_app/presentation/registration/blood_pressure_screen/bloc/blood_pressure_bloc.dart'; +import 'package:eatvio_app/presentation/registration/blood_pressure_screen/models/blood_pressure_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = BloodPressureBloc(BloodPressureState( + bloodPressureModelObj: BloodPressureModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: BloodPressureScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/body_circumferences_test.dart b/test/body_circumferences_test.dart new file mode 100644 index 00000000..e727e214 --- /dev/null +++ b/test/body_circumferences_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart'; +import 'package:eatvio_app/presentation/registration/body_circumferences_screen/bloc/body_circumferences_bloc.dart'; +import 'package:eatvio_app/presentation/registration/body_circumferences_screen/models/body_circumferences_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = BodyCircumferencesBloc(BodyCircumferencesState( + bodyCircumferencesModelObj: BodyCircumferencesModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: BodyCircumferencesScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/city_of_living_test.dart b/test/city_of_living_test.dart new file mode 100644 index 00000000..d198eb2b --- /dev/null +++ b/test/city_of_living_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/city_of_living_screen/city_of_living_screen.dart'; +import 'package:eatvio_app/presentation/registration/city_of_living_screen/bloc/city_of_living_bloc.dart'; +import 'package:eatvio_app/presentation/registration/city_of_living_screen/models/city_of_living_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = CityOfLivingBloc(CityOfLivingState( + cityOfLivingModelObj: CityOfLivingModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: CityOfLivingScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/credentials_test.dart b/test/credentials_test.dart new file mode 100644 index 00000000..851a7df6 --- /dev/null +++ b/test/credentials_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/credentials_screen/credentials_screen.dart'; +import 'package:eatvio_app/presentation/registration/credentials_screen/bloc/credentials_bloc.dart'; +import 'package:eatvio_app/presentation/registration/credentials_screen/models/credentials_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = CredentialsBloc(CredentialsState( + credentialsModelObj: CredentialsModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: CredentialsScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/date_of_birth_test.dart b/test/date_of_birth_test.dart new file mode 100644 index 00000000..4d79b572 --- /dev/null +++ b/test/date_of_birth_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart'; +import 'package:eatvio_app/presentation/registration/date_of_birth_screen/bloc/date_of_birth_bloc.dart'; +import 'package:eatvio_app/presentation/registration/date_of_birth_screen/models/date_of_birth_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = DateOfBirthBloc(DateOfBirthState( + dateOfBirthModelObj: DateOfBirthModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: DateOfBirthScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/diary_test.dart b/test/diary_test.dart new file mode 100644 index 00000000..224b05a3 --- /dev/null +++ b/test/diary_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/diary/diary_edit_screen/diary_edit_screen.dart'; +import 'package:eatvio_app/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart'; +import 'package:eatvio_app/presentation/diary/diary_edit_screen/models/diary_edit_model.dart'; + +void main() { + group('DiaryEditScreen', () { + /*testWidgets('renders CircularProgressIndicator when in initial state', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: DiaryEditScreen(), + ), + ); + });*/ + + testWidgets('renders _buildDiaryEditScreen when in loaded state', + (WidgetTester tester) async { + final diaryEditBloc = + DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())); + final loadedState = DiaryEditLoadedState( + diaryId: 1, + selectedSatiety: 4, + autocompleteList: [], + photos: [], + portions: [], + wakeupExpanded: false, + moderationExpanded: false, + recommendedChartValues: [10, 10, 10, 10], + consumedChartValues: [1, 2, 3, 4], + moodExpanded: false); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: diaryEditBloc, + child: DiaryEditScreen( + onInitialize: () {}, + ), + ), + ), + ); + + diaryEditBloc.emit(loadedState); + await tester.pump(); + + //expect(find.byType(_buildDiaryEditScreen), findsOneWidget); + }); + + /*testWidgets('renders error message when in error state', + (WidgetTester tester) async { + final diaryEditBloc = + DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())); + final errorState = DiaryEditErrorState("error"); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: diaryEditBloc, + child: DiaryEditScreen(), + ), + ), + ); + + diaryEditBloc.emit(errorState); + await tester.pump(); + + expect(find.text('Ошибка'), findsOneWidget); + });*/ + + // Add more tests for other scenarios if needed + }); +} diff --git a/test/favorite_foods_test.dart b/test/favorite_foods_test.dart new file mode 100644 index 00000000..2fbd838d --- /dev/null +++ b/test/favorite_foods_test.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart'; +import 'package:eatvio_app/presentation/registration/favorite_foods_screen/bloc/favorite_foods_bloc.dart'; +import 'package:eatvio_app/presentation/registration/favorite_foods_screen/models/favorite_foods_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = FavoriteFoodsBloc(FavoriteFoodsState( + favoriteFoodsModelObj: FavoriteFoodsModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: FavoriteFoodsScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/favorite_sports_test.dart b/test/favorite_sports_test.dart new file mode 100644 index 00000000..ed7ff4cd --- /dev/null +++ b/test/favorite_sports_test.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart'; +import 'package:eatvio_app/presentation/registration/favorite_sports_screen/bloc/favorite_sports_bloc.dart'; +import 'package:eatvio_app/presentation/registration/favorite_sports_screen/models/favorite_sports_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = FavoriteSportsBloc(FavoriteSportsState( + favoriteSportsModelObj: FavoriteSportsModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteSports", + unfavoriteFoods: "unfavoriteSports", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: FavoriteSportsScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/gender_test.dart b/test/gender_test.dart new file mode 100644 index 00000000..e05ba6fb --- /dev/null +++ b/test/gender_test.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/gender_screen/gender_screen.dart'; +import 'package:eatvio_app/presentation/registration/gender_screen/bloc/gender_bloc.dart'; +import 'package:eatvio_app/presentation/registration/gender_screen/models/gender_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = GenderBloc(GenderState( + genderModelObj: GenderModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteSports", + unfavoriteFoods: "unfavoriteSports", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: GenderScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/height_test.dart b/test/height_test.dart new file mode 100644 index 00000000..9338fa71 --- /dev/null +++ b/test/height_test.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/height_screen/height_screen.dart'; +import 'package:eatvio_app/presentation/registration/height_screen/bloc/height_bloc.dart'; +import 'package:eatvio_app/presentation/registration/height_screen/models/height_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = HeightBloc(HeightState( + heightModelObj: HeightModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + height: "height", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + gender: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteSports", + unfavoriteFoods: "unfavoriteSports", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: HeightScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/language_select_test.dart b/test/language_select_test.dart new file mode 100644 index 00000000..447592f8 --- /dev/null +++ b/test/language_select_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/language_select_screen/language_select_screen.dart'; +import 'package:eatvio_app/presentation/registration/language_select_screen/bloc/language_select_bloc.dart'; +import 'package:eatvio_app/presentation/registration/language_select_screen/models/language_select_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = LanguageSelectBloc(LanguageSelectState( + languageSelectModelObj: + LanguageSelectModel())); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: LanguageSelectScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/launch_test.dart b/test/launch_test.dart new file mode 100644 index 00000000..4f1eafee --- /dev/null +++ b/test/launch_test.dart @@ -0,0 +1,15 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/main.dart' as app; + +void main() { + testWidgets('Smoke test', (WidgetTester tester) async { + // Запустить приложение + app.main(); + + // Подождать, пока приложение загрузится + await tester.pumpAndSettle(); + + // Проверить, что нет ошибок + expect(tester.takeException(), isNull); + }); +} diff --git a/test/medicine_intake_test.dart b/test/medicine_intake_test.dart new file mode 100644 index 00000000..fd92e719 --- /dev/null +++ b/test/medicine_intake_test.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart'; +import 'package:eatvio_app/presentation/registration/medicine_intake_screen/bloc/medicine_intake_bloc.dart'; +import 'package:eatvio_app/presentation/registration/medicine_intake_screen/models/medicine_intake_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = MedicineIntakeBloc(MedicineIntakeState( + medicineIntakeModelObj: MedicineIntakeModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: MedicineIntakeScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/name_test.dart b/test/name_test.dart new file mode 100644 index 00000000..408a01cb --- /dev/null +++ b/test/name_test.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/name_screen/name_screen.dart'; +import 'package:eatvio_app/presentation/registration/name_screen/bloc/name_bloc.dart'; +import 'package:eatvio_app/presentation/registration/name_screen/models/name_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = NameBloc(NameState( + nameModelObj: NameModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: NameScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/profile_test.dart b/test/profile_test.dart new file mode 100644 index 00000000..20f3f34f --- /dev/null +++ b/test/profile_test.dart @@ -0,0 +1,26 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/my_profile_screen.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/models/my_profile_model.dart'; + +void main() { + group('MyProfileScreen', () { + testWidgets('renders CircularProgressIndicator in initial state', + (WidgetTester tester) async { + final myProfileBloc = MyProfileBloc(MyProfileState( + myProfileModelObj: + MyProfileModel())); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: MyProfileScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/settings_test.dart b/test/settings_test.dart new file mode 100644 index 00000000..b866ba40 --- /dev/null +++ b/test/settings_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/my_settings_screen.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart'; +import 'package:eatvio_app/presentation/profile/my_profile_screen/models/my_profile_model.dart'; + +void main() { + group('MyProfileScreen', () { + testWidgets('renders CircularProgressIndicator in initial state', + (WidgetTester tester) async { + final myProfileBloc = MyProfileBloc(MyProfileState( + myProfileModelObj: + MyProfileModel())); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: MySettingsScreen(), + ), + ), + ); + }); + }); +} diff --git a/test/weight_test.dart b/test/weight_test.dart new file mode 100644 index 00000000..adf8f8f1 --- /dev/null +++ b/test/weight_test.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:eatvio_app/presentation/registration/weight_screen/weight_screen.dart'; +import 'package:eatvio_app/presentation/registration/weight_screen/bloc/weight_bloc.dart'; +import 'package:eatvio_app/presentation/registration/weight_screen/models/weight_model.dart'; + +void main() { + group('AdditionalInfoScreen', () { + testWidgets('renders screen in initial state', (WidgetTester tester) async { + final myProfileBloc = WeightBloc(WeightState( + weightModelObj: WeightModel( + userRegistrationSheet: UserRegistrationSheet( + name: "name", + firstName: "firstName", + lastName: "lastName", + email: "email", + password: "password", + passwordConfirmation: "passwordConfirmation", + termsAccepted: "termsAccepted", + birthdate: "birthdate", + gender: "gender", + country: "country", + city: "city", + lang: "lang", + weight: "weight", + height: "height", + chestCircumference: 'asdasd', + waistCircumference: "waistCircumference", + hipCircumference: "hipCircumference", + bloodPressureUp: "bloodPressureUp", + bloodPressureDown: "bloodPressureDown", + hasHypo: "hasHypo", + hasHyper: "hasHyper", + aboutAllergy: "aboutAllergy", + aboutTakingPils: "aboutTakingPils", + aboutSportHobbies: "aboutSportHobbies", + favoriteFoods: "favoriteFoods", + unfavoriteFoods: "unfavoriteFoods", + additionalInfo: + "additionalInfo")))); // Ensure that MyProfileModel is non-nullable + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider.value( + value: myProfileBloc, + child: WeightScreen(), + ), + ), + ); + }); + }); +} +//sports \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index e93dc93b..00000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:eatvio_app/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}