diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..506ab56a 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 index e8f9d11f..955d74d5 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,9 +1,9 @@ name: Android Release -on: [push, pull_request] +on: [ push, pull_request ] env: - flutter_version: "3.19.0" + flutter_version: "3.22.0" java_version: "12.x" flutter_channel: "stable" @@ -13,22 +13,37 @@ jobs: steps: - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: ${{ env.java_version }} + cache: 'gradle' + + - name: Install Android SDK + run: | + apt-get update + apt-get install -y openjdk-11-jdk wget + wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip + unzip commandlinetools.zip + mkdir -p $HOME/Android/Sdk/cmdline-tools/latest + mv cmdline-tools/* $HOME/Android/Sdk/cmdline-tools/latest/ + export ANDROID_SDK_ROOT=$HOME/Android/Sdk + export PATH=$PATH:$HOME/Android/Sdk/cmdline-tools/latest/bin + sdkmanager "platform-tools" "platforms;android-30" "build-tools;30.0.3" + + - name: Set up Android environment variables + run: | + echo "ANDROID_SDK_ROOT=$HOME/Android/Sdk" >> $GITHUB_ENV + echo "PATH=$PATH:$HOME/Android/Sdk/platform-tools:$HOME/Android/Sdk/build-tools/30.0.3" >> $GITHUB_ENV + echo "ANDROID_HOME=$HOME/Android/Sdk" >> $GITHUB_ENV + - 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: @@ -66,9 +81,12 @@ jobs: touch .env flutter test -# - name: Build apk -# run: flutter build apk + - name: Build apk + run: flutter build apk -# Runs a set of commands using the runners shell -# - name: Start release build -# run: flutter build appbundle + - name: Rename and move apk + run: | + TIMESTAMP=$(date +'%Y-%m-%d-%H-%M') + APK_NAME="eatvio_app_${TIMESTAMP}.apk" + mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME + echo "APK build finished $APK_NAME" diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index b5f75243..a8fbb7d6 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -1,9 +1,9 @@ name: Eatvio App Android Test -on: [pull_request] +on: [ pull_request ] env: - flutter_version: "3.19.0" + flutter_version: "3.22.0" java_version: "12.x" flutter_channel: "stable" @@ -13,6 +13,38 @@ jobs: steps: - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: ${{ env.java_version }} + cache: "gradle" + + - name: Install Android SDK + run: | + apt-get update + apt-get install -y openjdk-11-jdk wget + wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip + unzip commandlinetools.zip + mkdir -p $HOME/Android/Sdk/cmdline-tools/latest + mv cmdline-tools/* $HOME/Android/Sdk/cmdline-tools/latest/ + export ANDROID_SDK_ROOT=$HOME/Android/Sdk + export PATH=$PATH:$HOME/Android/Sdk/cmdline-tools/latest/bin + yes | sdkmanager --licenses 2>/dev/null || true + + - name: Set up Android environment variables + run: | + echo "ANDROID_SDK_ROOT=$HOME/Android/Sdk" >> $GITHUB_ENV + echo "PATH=$PATH:$HOME/Android/Sdk/cmdline-tools/latest/bin:$HOME/Android/Sdk/platform-tools:$HOME/Android/Sdk/build-tools/30.0.3" >> $GITHUB_ENV + echo "ANDROID_HOME=$HOME/Android/Sdk" >> $GITHUB_ENV + + - name: Debug Environment Variables + run: | + echo "ANDROID_SDK_ROOT: $ANDROID_SDK_ROOT" + echo "ANDROID_HOME: $ANDROID_HOME" + echo "PATH: $PATH" + - uses: actions/cache@v2 with: path: ${{ runner.tool_cache }}/flutter @@ -20,15 +52,8 @@ jobs: - 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: @@ -36,7 +61,7 @@ jobs: flutter-version: ${{ env.flutter_version }} cache: true - - name: show cache keys + - name: Show cache keys run: | echo CACHE-PATH=${{ steps.flutter-action.outputs.CACHE-PATH }} echo CACHE-KEY=${{ steps.flutter-action.outputs.CACHE-KEY }} @@ -47,10 +72,11 @@ jobs: echo PUB-CACHE-KEY=${{ steps.flutter-action.outputs.PUB-CACHE-KEY }} shell: bash - - name: fix dir permissions + - 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 + flutter config --android-sdk="$ANDROID_HOME" + flutter doctor -v - name: Get dependencies run: flutter pub get @@ -65,3 +91,21 @@ jobs: run: | touch .env flutter test + + - name: Build apk + run: | + flutter build apk + + - name: Rename and move apk + run: | + TIMESTAMP=$(date +'%Y-%m-%d-%H-%M') + APK_NAME="eatvio_app_${TIMESTAMP}.apk" + mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME + echo "APK build finished $APK_NAME" + echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV + + - name: Upload APK artifact + uses: actions/upload-artifact@v3 + with: + name: EatvioApp-APK + path: ${{ env.APK_NAME }} 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/analysis_options.yaml b/analysis_options.yaml index 9722fb45..22f04010 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,6 @@ analyzer: + errors: + invalid_annotation_target: ignore plugins: - dart_code_metrics diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c483ee5..7652369e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,67 +1,59 @@ -plugins{ - id 'com.android.application' - id 'kotlin-android' +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" } + def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') +def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> + localPropertiesFile.withReader("UTF-8") { reader -> localProperties.load(reader) } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = "1" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') +def flutterVersionName = localProperties.getProperty("flutter.versionName") if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = "1.0" } -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion flutter.compileSdkVersion + namespace = "com.eatvioapp.app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.eatvioapp.app" - minSdkVersion 23 - targetSdkVersion flutter.targetSdkVersion - multiDexEnabled true - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + applicationId = "com.eatvioapp.app" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' + source = "../.." } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.multidex:multidex:2.0.1" -} 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/build.gradle b/android/build.gradle index d7f8aafa..5869be3d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.20' - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() 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/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6b665338..aeaff6f8 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..536165d3 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" 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/fonts/MulishBold.ttf b/assets/fonts/MulishBold.ttf index 0c1eed73..e69de29b 100644 Binary files a/assets/fonts/MulishBold.ttf and b/assets/fonts/MulishBold.ttf differ diff --git a/assets/fonts/MulishRegular.ttf b/assets/fonts/MulishRegular.ttf index 0971518b..e69de29b 100644 Binary files a/assets/fonts/MulishRegular.ttf and b/assets/fonts/MulishRegular.ttf differ diff --git a/assets/fonts/MulishSemiBold.ttf b/assets/fonts/MulishSemiBold.ttf index 9ac1fc8f..e69de29b 100644 Binary files a/assets/fonts/MulishSemiBold.ttf and b/assets/fonts/MulishSemiBold.ttf differ diff --git a/assets/fonts/SegoeUISemibold.ttf b/assets/fonts/SegoeUISemibold.ttf index 46b3b993..e69de29b 100644 Binary files a/assets/fonts/SegoeUISemibold.ttf and b/assets/fonts/SegoeUISemibold.ttf differ diff --git a/assets/images/img_2456927096976.svg b/assets/images/img_2456927096976.svg index 17ee6769..f869ff41 100644 --- a/assets/images/img_2456927096976.svg +++ b/assets/images/img_2456927096976.svg @@ -1,5 +1,12 @@ - - - - + + + + + + + + + + + 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 701b5d1d..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/bpc b/bpc new file mode 100644 index 00000000..7221be5e --- /dev/null +++ b/bpc @@ -0,0 +1,104 @@ +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'; + +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 Scaffold( + resizeToAvoidBottomInset: false, + body: SafeArea( + child: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + 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.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(CreateBlogPost()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + ), + bottomNavigationBar: _buildBottomBar(context)); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/devtools_options.yaml b/devtools_options.yaml index 7e7e7f67..5c27c3e3 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1 +1,2 @@ extensions: + - provider: true \ No newline at end of file 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/fc b/fc new file mode 100644 index 00000000..07c67735 --- /dev/null +++ b/fc @@ -0,0 +1,313 @@ +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 Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SafeArea( + child: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSearch(), + SizedBox(height: 20), + _buildMenu(context, state), + SizedBox(height: 20), + _buildBlogPostList(context, state) + ], + ), + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ); + } + + _buildSearch() { + 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: "+", + 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/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 new file mode 100644 index 00000000..279576f3 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 36428fd8..68ec53f4 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 = 1510; 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..5e31d3d3 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 08477d7a..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 @@ -41,5 +48,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + 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/app_router.dart b/lib/app_router.dart new file mode 100644 index 00000000..3c60e6e6 --- /dev/null +++ b/lib/app_router.dart @@ -0,0 +1,255 @@ +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart'; +import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'package:eatvio_app/presentation/diary/diary_edit_screen/diary_edit_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_create_screen/diet_create_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_select_screen/diet_select_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_create_screen/blog_post_create_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_screen/blog_post_screen.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/feed_screen.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/glossary_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_select_screen/plan_select_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/recipe/recipe_select_screen/recipe_select_screen.dart'; +import 'package:eatvio_app/presentation/registration/additional_information_screen/additional_information_screen.dart'; +import 'package:eatvio_app/presentation/registration/allergies_screen/allergies_screen.dart'; +import 'package:eatvio_app/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart'; +import 'package:eatvio_app/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart'; +import 'package:eatvio_app/presentation/registration/city_of_living_screen/city_of_living_screen.dart'; +import 'package:eatvio_app/presentation/registration/credentials_screen/credentials_screen.dart'; +import 'package:eatvio_app/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart'; +import 'package:eatvio_app/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart'; +import 'package:eatvio_app/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart'; +import 'package:eatvio_app/presentation/registration/gender_screen/gender_screen.dart'; +import 'package:eatvio_app/presentation/registration/height_screen/height_screen.dart'; +import 'package:eatvio_app/presentation/registration/language_select_screen/language_select_screen.dart'; +import 'package:eatvio_app/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart'; +import 'package:eatvio_app/presentation/registration/name_screen/name_screen.dart'; +import 'package:eatvio_app/presentation/registration/weight_screen/weight_screen.dart'; +import 'package:eatvio_app/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class AppRouter { + static GlobalKey navigatorKey = GlobalKey(); + + static GoRouter appRouter = GoRouter( + initialLocation: '/auth-screen', + navigatorKey: navigatorKey, + routes: [ + GoRoute( + path: '/auth-screen', + name: 'authScreen', + redirect: (context, state) async { + if (await TokenStorage.haveTokenInStorage()) return '/my-profile-screen'; + return null; + }, + builder: (context, state) => AuthScreen.builder(context), + routes: [ + GoRoute( + path: 'language-select-screen', + name: 'languageSelectScreen', + builder: (context, state) => LanguageSelectScreen.builder(context), + routes: [ + GoRoute( + path: 'credentials-screen', + name: 'credentialsScreen', + builder: (context, state) => CredentialsScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'name-screen', + name: 'nameScreen', + builder: (context, state) => NameScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'gender-screen', + name: 'genderScreen', + builder: (context, state) => GenderScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'date-of-birth-screen', + name: 'dateOfBirthScreen', + builder: (context, state) => DateOfBirthScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'city-of-living-screen', + name: 'cityOfLivingScreen', + builder: (context, state) => CityOfLivingScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'body-circumferences-screen', + name: 'bodyCircumferencesScreen', + builder: (context, state) => BodyCircumferencesScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'weight-screen', + name: 'weightScreen', + builder: (context, state) => WeightScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'height-screen', + name: 'heightScreen', + builder: (context, state) => HeightScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'blood-pressure-screen', + name: 'bloodPressureScreen', + builder: (context, state) => BloodPressureScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'favorite-sports-screen', + name: 'favoriteSportsScreen', + builder: (context, state) => FavoriteSportsScreen.builder(context, state.extra), + routes: [ + GoRoute( + name: 'allergiesScreen', + path: 'allergiesScreen', + builder: (context, state) => AllergiesScreen.builder(context, state.extra), + routes: [ + GoRoute( + name: 'medicineIntakeScreen', + path: 'medicine-intake-screen', + builder: (context, state) => MedicineIntakeScreen.builder(context, state.extra), + ), + GoRoute( + name: 'favoriteFoodsScreen', + path: 'favorite-foods-screen', + builder: (context, state) => FavoriteFoodsScreen.builder(context, state.extra), + routes: [ + GoRoute( + path: 'additional-information-screen', + name: 'additionalInformationScreen', + builder: (context, state) => AdditionalInformationScreen.builder(context, state.extra), + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ShellRoute( + builder: (context, state, child) { + return BottomNavigationBarWidget(child: child); + }, + routes: [ + GoRoute( + path: '/my-profile-screen', + name: 'myProfileScreen', + builder: (context, state) => MyProfileScreen(), + ), + GoRoute( + path: '/feed-screen', + name: 'feedScreen', + builder: (context, state) => FeedScreen(), + routes: [ + GoRoute( + path: 'blog-post-create-screen', + name: 'blogPostCreateScreen', + builder: (context, state) => BlogPostCreateScreen.builder(context), + ), + GoRoute( + path: 'blog-post-screen', + name: 'blogPostScreen', + builder: (context, state) => BlogPostScreen.builder( + context, + state.extra.toString(), + ), + routes: [ + GoRoute( + path: 'blog-post-edit-screen', + name: 'blogPostEditScreen', + builder: (context, state) => BlogPostEditScreen.builder( + context, + state.extra, + ), + ), + ], + ), + ], + ), + GoRoute( + path: '/diary-screen', + name: 'diaryScreen', + builder: (context, state) => DiaryEditScreen(), + ), + GoRoute( + path: '/chat-select-screen', + name: 'chatSelectScreen', + builder: (context, state) => ChatSelectScreen(), + routes: [ + GoRoute( + path: 'chat-screen', + name: 'chatScreen', + builder: (context, state) => ChatScreen(chatId: state.extra as String), + ), + ], + ), + ], + ), + GoRoute( + path: '/my-settings-screen', + name: 'mySettingsScreen', + builder: (context, state) => MySettingsScreen.builder(context), + ), + GoRoute( + path: '/recipe-select-screen', + name: 'recipeSelectScreen', + builder: (context, state) => RecipeSelectScreen.builder(context), + ), + GoRoute( + path: '/diet-select-screen', + name: 'dietSelectScreen', + builder: (context, state) => DietSelectScreen.builder(context), + ), + GoRoute( + path: '/diet-create-screen', + name: 'dietCreateScreen', + builder: (context, state) => DietCreateScreen.builder(context), + ), + GoRoute( + path: '/plan-select-screen', + name: 'planSelectScreen', + builder: (context, state) => PlanSelectScreen.builder(context), + ), + GoRoute( + path: '/glossary-screen', + name: 'glossaryScreen', + builder: (context, state) => GlossaryScreen.builder(context), + ), + GoRoute( + path: '/photo-view-screen/:index', + name: 'photoViewScreen', + builder: (context, state) => PhotoViewScreen( + imageUrls: state.extra as List, + index: int.parse(state.pathParameters['index']!), + ), + ), + ], + ); +} diff --git a/lib/core/app_export.dart b/lib/core/app_export.dart index 6d115305..233b58c8 100644 --- a/lib/core/app_export.dart +++ b/lib/core/app_export.dart @@ -1,16 +1,13 @@ -export 'package:connectivity_plus/connectivity_plus.dart'; export 'package:flutter_bloc/flutter_bloc.dart'; export 'package:eatvio_app/core/constants/constants.dart'; export 'package:eatvio_app/core/errors/exceptions.dart'; export 'package:eatvio_app/core/network/network_info.dart'; export 'package:eatvio_app/core/utils/image_constant.dart'; export 'package:eatvio_app/core/utils/logger.dart'; -export 'package:eatvio_app/core/utils/navigator_service.dart'; export 'package:eatvio_app/core/utils/pref_utils.dart'; export 'package:eatvio_app/core/utils/size_utils.dart'; export 'package:eatvio_app/data/models/selectionPopupModel/selection_popup_model.dart'; export 'package:eatvio_app/localization/app_localization.dart'; -export 'package:eatvio_app/routes/app_routes.dart'; export 'package:eatvio_app/theme/app_decoration.dart'; export 'package:eatvio_app/theme/custom_text_style.dart'; export 'package:eatvio_app/theme/theme_helper.dart'; diff --git a/lib/core/token_storage.dart b/lib/core/token_storage.dart new file mode 100644 index 00000000..82d2935e --- /dev/null +++ b/lib/core/token_storage.dart @@ -0,0 +1,32 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/locator_service.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +abstract class TokenStorage { + static addTokenToDioHeader(String token) { + sl().options.headers['Authorization'] = 'Bearer $token'; + } + + static haveTokenInStorage() async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + return false; + } else { + addTokenToDioHeader(token); + return true; + } + } + + static getTokenFromStorage() async => await Hive.openBox('token') + ..get('token'); + + static saveTokenInStorage(String token) async { + await Hive.openBox('token') + ..put('token', token); + addTokenToDioHeader(token); + } + + static removeTokenInStorage(String token) async => await Hive.openBox('token') + ..delete('token'); +} 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/navigator_service.dart b/lib/core/utils/navigator_service.dart deleted file mode 100644 index e5ea8350..00000000 --- a/lib/core/utils/navigator_service.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -class NavigatorService { - static GlobalKey navigatorKey = GlobalKey(); - - static Future pushNamed(String routeName, - {dynamic arguments}) async { - return navigatorKey.currentState - ?.pushNamed(routeName, arguments: arguments); - } - - static void goBack() { - return navigatorKey.currentState?.pop(); - } - - static Future pushNamedAndRemoveUntil(String routeName, - {bool routePredicate = false, dynamic arguments}) async { - return navigatorKey.currentState?.pushNamedAndRemoveUntil( - routeName, (route) => routePredicate, - arguments: arguments); - } - - static Future popAndPushNamed(String routeName, - {dynamic arguments}) async { - return navigatorKey.currentState - ?.popAndPushNamed(routeName, arguments: arguments); - } -} 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/progress_dialog_utils.dart b/lib/core/utils/progress_dialog_utils.dart index b111e966..ce4da750 100644 --- a/lib/core/utils/progress_dialog_utils.dart +++ b/lib/core/utils/progress_dialog_utils.dart @@ -1,17 +1,15 @@ +import 'package:eatvio_app/app_router.dart'; import 'package:flutter/material.dart'; -import 'package:eatvio_app/core/app_export.dart'; class ProgressDialogUtils { static bool isProgressVisible = false; ///common method for showing progress dialog - static void showProgressDialog( - {BuildContext? context, isCancellable = false}) async { - if (!isProgressVisible && - NavigatorService.navigatorKey.currentState?.overlay?.context != null) { + static void showProgressDialog({BuildContext? context, isCancellable = false}) async { + if (!isProgressVisible && AppRouter.navigatorKey.currentState?.overlay?.context != null) { showDialog( barrierDismissible: isCancellable, - context: NavigatorService.navigatorKey.currentState!.overlay!.context, + context: AppRouter.navigatorKey.currentState!.overlay!.context, builder: (BuildContext context) { return Center( child: CircularProgressIndicator.adaptive( @@ -28,9 +26,7 @@ class ProgressDialogUtils { ///common method for hiding progress dialog static void hideProgressDialog() { - if (isProgressVisible) - Navigator.pop( - NavigatorService.navigatorKey.currentState!.overlay!.context); + if (isProgressVisible) Navigator.pop(AppRouter.navigatorKey.currentState!.overlay!.context); isProgressVisible = false; } } 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/tokenModel/token_model.dart b/lib/data/models/tokenModel/token_model.dart index 17ab28a1..3aa1d3f6 100644 --- a/lib/data/models/tokenModel/token_model.dart +++ b/lib/data/models/tokenModel/token_model.dart @@ -1,5 +1,5 @@ import 'package:hive/hive.dart'; - +/// https://pub.dev/packages/flutter_secure_storage TODO replace hive to secure_storage for token storage @HiveType(typeId: 0) class TokenModel { @HiveField(0) 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/features/chat/models/chat_message_model.dart b/lib/features/chat/models/chat_message_model.dart new file mode 100644 index 00000000..220df3bf --- /dev/null +++ b/lib/features/chat/models/chat_message_model.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_message_model.freezed.dart'; + +part 'chat_message_model.g.dart'; + +@freezed +class ChatMessageModel with _$ChatMessageModel { + const factory ChatMessageModel({ + required int id, + required String data, + @JsonKey(name: 'media_data') required List mediaData, + @JsonKey(name: 'author_id') required int authorId, + @JsonKey(name: 'updated_at') required String updatedAt, + }) = _ChatMessageModel; + + factory ChatMessageModel.fromJson(Map json) => _$ChatMessageModelFromJson(json); +} diff --git a/lib/features/chat/models/chat_message_model.freezed.dart b/lib/features/chat/models/chat_message_model.freezed.dart new file mode 100644 index 00000000..61856085 --- /dev/null +++ b/lib/features/chat/models/chat_message_model.freezed.dart @@ -0,0 +1,257 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_message_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ChatMessageModel _$ChatMessageModelFromJson(Map json) { + return _ChatMessageModel.fromJson(json); +} + +/// @nodoc +mixin _$ChatMessageModel { + int get id => throw _privateConstructorUsedError; + String get data => throw _privateConstructorUsedError; + @JsonKey(name: 'media_data') + List get mediaData => throw _privateConstructorUsedError; + @JsonKey(name: 'author_id') + int get authorId => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + String get updatedAt => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ChatMessageModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatMessageModelCopyWith<$Res> { + factory $ChatMessageModelCopyWith( + ChatMessageModel value, $Res Function(ChatMessageModel) then) = + _$ChatMessageModelCopyWithImpl<$Res, ChatMessageModel>; + @useResult + $Res call( + {int id, + String data, + @JsonKey(name: 'media_data') List mediaData, + @JsonKey(name: 'author_id') int authorId, + @JsonKey(name: 'updated_at') String updatedAt}); +} + +/// @nodoc +class _$ChatMessageModelCopyWithImpl<$Res, $Val extends ChatMessageModel> + implements $ChatMessageModelCopyWith<$Res> { + _$ChatMessageModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? data = null, + Object? mediaData = null, + Object? authorId = null, + Object? updatedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as String, + mediaData: null == mediaData + ? _value.mediaData + : mediaData // ignore: cast_nullable_to_non_nullable + as List, + authorId: null == authorId + ? _value.authorId + : authorId // ignore: cast_nullable_to_non_nullable + as int, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChatMessageModelImplCopyWith<$Res> + implements $ChatMessageModelCopyWith<$Res> { + factory _$$ChatMessageModelImplCopyWith(_$ChatMessageModelImpl value, + $Res Function(_$ChatMessageModelImpl) then) = + __$$ChatMessageModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + String data, + @JsonKey(name: 'media_data') List mediaData, + @JsonKey(name: 'author_id') int authorId, + @JsonKey(name: 'updated_at') String updatedAt}); +} + +/// @nodoc +class __$$ChatMessageModelImplCopyWithImpl<$Res> + extends _$ChatMessageModelCopyWithImpl<$Res, _$ChatMessageModelImpl> + implements _$$ChatMessageModelImplCopyWith<$Res> { + __$$ChatMessageModelImplCopyWithImpl(_$ChatMessageModelImpl _value, + $Res Function(_$ChatMessageModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? data = null, + Object? mediaData = null, + Object? authorId = null, + Object? updatedAt = null, + }) { + return _then(_$ChatMessageModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as String, + mediaData: null == mediaData + ? _value._mediaData + : mediaData // ignore: cast_nullable_to_non_nullable + as List, + authorId: null == authorId + ? _value.authorId + : authorId // ignore: cast_nullable_to_non_nullable + as int, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChatMessageModelImpl implements _ChatMessageModel { + const _$ChatMessageModelImpl( + {required this.id, + required this.data, + @JsonKey(name: 'media_data') required final List mediaData, + @JsonKey(name: 'author_id') required this.authorId, + @JsonKey(name: 'updated_at') required this.updatedAt}) + : _mediaData = mediaData; + + factory _$ChatMessageModelImpl.fromJson(Map json) => + _$$ChatMessageModelImplFromJson(json); + + @override + final int id; + @override + final String data; + final List _mediaData; + @override + @JsonKey(name: 'media_data') + List get mediaData { + if (_mediaData is EqualUnmodifiableListView) return _mediaData; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_mediaData); + } + + @override + @JsonKey(name: 'author_id') + final int authorId; + @override + @JsonKey(name: 'updated_at') + final String updatedAt; + + @override + String toString() { + return 'ChatMessageModel(id: $id, data: $data, mediaData: $mediaData, authorId: $authorId, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatMessageModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.data, data) || other.data == data) && + const DeepCollectionEquality() + .equals(other._mediaData, _mediaData) && + (identical(other.authorId, authorId) || + other.authorId == authorId) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, id, data, + const DeepCollectionEquality().hash(_mediaData), authorId, updatedAt); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatMessageModelImplCopyWith<_$ChatMessageModelImpl> get copyWith => + __$$ChatMessageModelImplCopyWithImpl<_$ChatMessageModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ChatMessageModelImplToJson( + this, + ); + } +} + +abstract class _ChatMessageModel implements ChatMessageModel { + const factory _ChatMessageModel( + {required final int id, + required final String data, + @JsonKey(name: 'media_data') required final List mediaData, + @JsonKey(name: 'author_id') required final int authorId, + @JsonKey(name: 'updated_at') required final String updatedAt}) = + _$ChatMessageModelImpl; + + factory _ChatMessageModel.fromJson(Map json) = + _$ChatMessageModelImpl.fromJson; + + @override + int get id; + @override + String get data; + @override + @JsonKey(name: 'media_data') + List get mediaData; + @override + @JsonKey(name: 'author_id') + int get authorId; + @override + @JsonKey(name: 'updated_at') + String get updatedAt; + @override + @JsonKey(ignore: true) + _$$ChatMessageModelImplCopyWith<_$ChatMessageModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/models/chat_message_model.g.dart b/lib/features/chat/models/chat_message_model.g.dart new file mode 100644 index 00000000..8b28b749 --- /dev/null +++ b/lib/features/chat/models/chat_message_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_message_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ChatMessageModelImpl _$$ChatMessageModelImplFromJson( + Map json) => + _$ChatMessageModelImpl( + id: (json['id'] as num).toInt(), + data: json['data'] as String, + mediaData: json['media_data'] as List, + authorId: (json['author_id'] as num).toInt(), + updatedAt: json['updated_at'] as String, + ); + +Map _$$ChatMessageModelImplToJson( + _$ChatMessageModelImpl instance) => + { + 'id': instance.id, + 'data': instance.data, + 'media_data': instance.mediaData, + 'author_id': instance.authorId, + 'updated_at': instance.updatedAt, + }; diff --git a/lib/features/chat/models/chat_room_model.dart b/lib/features/chat/models/chat_room_model.dart new file mode 100644 index 00000000..1fa5afe7 --- /dev/null +++ b/lib/features/chat/models/chat_room_model.dart @@ -0,0 +1,20 @@ +import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/chat/models/chat_room_settings_model.dart'; +import 'package:eatvio_app/features/chat/models/chat_user_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_room_model.freezed.dart'; + +part 'chat_room_model.g.dart'; + +@freezed +class ChatRoomModel with _$ChatRoomModel { + const factory ChatRoomModel({ + required String id, + @JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings, + required List messages, + required Map users, + }) = _ChatRoomModel; + + factory ChatRoomModel.fromJson(Map json) => _$ChatRoomModelFromJson(json); +} diff --git a/lib/features/chat/models/chat_room_model.freezed.dart b/lib/features/chat/models/chat_room_model.freezed.dart new file mode 100644 index 00000000..b8b4ce46 --- /dev/null +++ b/lib/features/chat/models/chat_room_model.freezed.dart @@ -0,0 +1,255 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_room_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ChatRoomModel _$ChatRoomModelFromJson(Map json) { + return _ChatRoomModel.fromJson(json); +} + +/// @nodoc +mixin _$ChatRoomModel { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'chat_room_setting') + ChatRoomSettingsModel get chatRoomSettings => + throw _privateConstructorUsedError; + List get messages => throw _privateConstructorUsedError; + Map get users => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ChatRoomModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatRoomModelCopyWith<$Res> { + factory $ChatRoomModelCopyWith( + ChatRoomModel value, $Res Function(ChatRoomModel) then) = + _$ChatRoomModelCopyWithImpl<$Res, ChatRoomModel>; + @useResult + $Res call( + {String id, + @JsonKey(name: 'chat_room_setting') + ChatRoomSettingsModel chatRoomSettings, + List messages, + Map users}); + + $ChatRoomSettingsModelCopyWith<$Res> get chatRoomSettings; +} + +/// @nodoc +class _$ChatRoomModelCopyWithImpl<$Res, $Val extends ChatRoomModel> + implements $ChatRoomModelCopyWith<$Res> { + _$ChatRoomModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? chatRoomSettings = null, + Object? messages = null, + Object? users = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + chatRoomSettings: null == chatRoomSettings + ? _value.chatRoomSettings + : chatRoomSettings // ignore: cast_nullable_to_non_nullable + as ChatRoomSettingsModel, + messages: null == messages + ? _value.messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + users: null == users + ? _value.users + : users // ignore: cast_nullable_to_non_nullable + as Map, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $ChatRoomSettingsModelCopyWith<$Res> get chatRoomSettings { + return $ChatRoomSettingsModelCopyWith<$Res>(_value.chatRoomSettings, + (value) { + return _then(_value.copyWith(chatRoomSettings: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ChatRoomModelImplCopyWith<$Res> + implements $ChatRoomModelCopyWith<$Res> { + factory _$$ChatRoomModelImplCopyWith( + _$ChatRoomModelImpl value, $Res Function(_$ChatRoomModelImpl) then) = + __$$ChatRoomModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + @JsonKey(name: 'chat_room_setting') + ChatRoomSettingsModel chatRoomSettings, + List messages, + Map users}); + + @override + $ChatRoomSettingsModelCopyWith<$Res> get chatRoomSettings; +} + +/// @nodoc +class __$$ChatRoomModelImplCopyWithImpl<$Res> + extends _$ChatRoomModelCopyWithImpl<$Res, _$ChatRoomModelImpl> + implements _$$ChatRoomModelImplCopyWith<$Res> { + __$$ChatRoomModelImplCopyWithImpl( + _$ChatRoomModelImpl _value, $Res Function(_$ChatRoomModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? chatRoomSettings = null, + Object? messages = null, + Object? users = null, + }) { + return _then(_$ChatRoomModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + chatRoomSettings: null == chatRoomSettings + ? _value.chatRoomSettings + : chatRoomSettings // ignore: cast_nullable_to_non_nullable + as ChatRoomSettingsModel, + messages: null == messages + ? _value._messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + users: null == users + ? _value._users + : users // ignore: cast_nullable_to_non_nullable + as Map, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChatRoomModelImpl implements _ChatRoomModel { + const _$ChatRoomModelImpl( + {required this.id, + @JsonKey(name: 'chat_room_setting') required this.chatRoomSettings, + required final List messages, + required final Map users}) + : _messages = messages, + _users = users; + + factory _$ChatRoomModelImpl.fromJson(Map json) => + _$$ChatRoomModelImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'chat_room_setting') + final ChatRoomSettingsModel chatRoomSettings; + final List _messages; + @override + List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); + } + + final Map _users; + @override + Map get users { + if (_users is EqualUnmodifiableMapView) return _users; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_users); + } + + @override + String toString() { + return 'ChatRoomModel(id: $id, chatRoomSettings: $chatRoomSettings, messages: $messages, users: $users)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatRoomModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.chatRoomSettings, chatRoomSettings) || + other.chatRoomSettings == chatRoomSettings) && + const DeepCollectionEquality().equals(other._messages, _messages) && + const DeepCollectionEquality().equals(other._users, _users)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + id, + chatRoomSettings, + const DeepCollectionEquality().hash(_messages), + const DeepCollectionEquality().hash(_users)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatRoomModelImplCopyWith<_$ChatRoomModelImpl> get copyWith => + __$$ChatRoomModelImplCopyWithImpl<_$ChatRoomModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ChatRoomModelImplToJson( + this, + ); + } +} + +abstract class _ChatRoomModel implements ChatRoomModel { + const factory _ChatRoomModel( + {required final String id, + @JsonKey(name: 'chat_room_setting') + required final ChatRoomSettingsModel chatRoomSettings, + required final List messages, + required final Map users}) = _$ChatRoomModelImpl; + + factory _ChatRoomModel.fromJson(Map json) = + _$ChatRoomModelImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'chat_room_setting') + ChatRoomSettingsModel get chatRoomSettings; + @override + List get messages; + @override + Map get users; + @override + @JsonKey(ignore: true) + _$$ChatRoomModelImplCopyWith<_$ChatRoomModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/models/chat_room_model.g.dart b/lib/features/chat/models/chat_room_model.g.dart new file mode 100644 index 00000000..4ab38671 --- /dev/null +++ b/lib/features/chat/models/chat_room_model.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_room_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ChatRoomModelImpl _$$ChatRoomModelImplFromJson(Map json) => + _$ChatRoomModelImpl( + id: json['id'] as String, + chatRoomSettings: ChatRoomSettingsModel.fromJson( + json['chat_room_setting'] as Map), + messages: (json['messages'] as List) + .map((e) => ChatMessageModel.fromJson(e as Map)) + .toList(), + users: (json['users'] as Map).map( + (k, e) => + MapEntry(k, ChatUserModel.fromJson(e as Map)), + ), + ); + +Map _$$ChatRoomModelImplToJson(_$ChatRoomModelImpl instance) => + { + 'id': instance.id, + 'chat_room_setting': instance.chatRoomSettings, + 'messages': instance.messages, + 'users': instance.users, + }; diff --git a/lib/features/chat/models/chat_room_settings_model.dart b/lib/features/chat/models/chat_room_settings_model.dart new file mode 100644 index 00000000..7262c9a2 --- /dev/null +++ b/lib/features/chat/models/chat_room_settings_model.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_room_settings_model.freezed.dart'; + +part 'chat_room_settings_model.g.dart'; + +@freezed +class ChatRoomSettingsModel with _$ChatRoomSettingsModel { + const factory ChatRoomSettingsModel({ + required int id, + @JsonKey(name: 'room_name') required String roomName, + required String state, + @JsonKey(name: 'access_level') required String accessLevel, + @JsonKey(name: 'updated_at') required String updatedAt, + }) = _ChatRoomSettingsModel; + + factory ChatRoomSettingsModel.fromJson(Map json) => _$ChatRoomSettingsModelFromJson(json); +} diff --git a/lib/features/chat/models/chat_room_settings_model.freezed.dart b/lib/features/chat/models/chat_room_settings_model.freezed.dart new file mode 100644 index 00000000..4f217556 --- /dev/null +++ b/lib/features/chat/models/chat_room_settings_model.freezed.dart @@ -0,0 +1,254 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_room_settings_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ChatRoomSettingsModel _$ChatRoomSettingsModelFromJson( + Map json) { + return _ChatRoomSettingsModel.fromJson(json); +} + +/// @nodoc +mixin _$ChatRoomSettingsModel { + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'room_name') + String get roomName => throw _privateConstructorUsedError; + String get state => throw _privateConstructorUsedError; + @JsonKey(name: 'access_level') + String get accessLevel => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + String get updatedAt => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ChatRoomSettingsModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatRoomSettingsModelCopyWith<$Res> { + factory $ChatRoomSettingsModelCopyWith(ChatRoomSettingsModel value, + $Res Function(ChatRoomSettingsModel) then) = + _$ChatRoomSettingsModelCopyWithImpl<$Res, ChatRoomSettingsModel>; + @useResult + $Res call( + {int id, + @JsonKey(name: 'room_name') String roomName, + String state, + @JsonKey(name: 'access_level') String accessLevel, + @JsonKey(name: 'updated_at') String updatedAt}); +} + +/// @nodoc +class _$ChatRoomSettingsModelCopyWithImpl<$Res, + $Val extends ChatRoomSettingsModel> + implements $ChatRoomSettingsModelCopyWith<$Res> { + _$ChatRoomSettingsModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? roomName = null, + Object? state = null, + Object? accessLevel = null, + Object? updatedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + roomName: null == roomName + ? _value.roomName + : roomName // ignore: cast_nullable_to_non_nullable + as String, + state: null == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as String, + accessLevel: null == accessLevel + ? _value.accessLevel + : accessLevel // ignore: cast_nullable_to_non_nullable + as String, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChatRoomSettingsModelImplCopyWith<$Res> + implements $ChatRoomSettingsModelCopyWith<$Res> { + factory _$$ChatRoomSettingsModelImplCopyWith( + _$ChatRoomSettingsModelImpl value, + $Res Function(_$ChatRoomSettingsModelImpl) then) = + __$$ChatRoomSettingsModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + @JsonKey(name: 'room_name') String roomName, + String state, + @JsonKey(name: 'access_level') String accessLevel, + @JsonKey(name: 'updated_at') String updatedAt}); +} + +/// @nodoc +class __$$ChatRoomSettingsModelImplCopyWithImpl<$Res> + extends _$ChatRoomSettingsModelCopyWithImpl<$Res, + _$ChatRoomSettingsModelImpl> + implements _$$ChatRoomSettingsModelImplCopyWith<$Res> { + __$$ChatRoomSettingsModelImplCopyWithImpl(_$ChatRoomSettingsModelImpl _value, + $Res Function(_$ChatRoomSettingsModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? roomName = null, + Object? state = null, + Object? accessLevel = null, + Object? updatedAt = null, + }) { + return _then(_$ChatRoomSettingsModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + roomName: null == roomName + ? _value.roomName + : roomName // ignore: cast_nullable_to_non_nullable + as String, + state: null == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as String, + accessLevel: null == accessLevel + ? _value.accessLevel + : accessLevel // ignore: cast_nullable_to_non_nullable + as String, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChatRoomSettingsModelImpl implements _ChatRoomSettingsModel { + const _$ChatRoomSettingsModelImpl( + {required this.id, + @JsonKey(name: 'room_name') required this.roomName, + required this.state, + @JsonKey(name: 'access_level') required this.accessLevel, + @JsonKey(name: 'updated_at') required this.updatedAt}); + + factory _$ChatRoomSettingsModelImpl.fromJson(Map json) => + _$$ChatRoomSettingsModelImplFromJson(json); + + @override + final int id; + @override + @JsonKey(name: 'room_name') + final String roomName; + @override + final String state; + @override + @JsonKey(name: 'access_level') + final String accessLevel; + @override + @JsonKey(name: 'updated_at') + final String updatedAt; + + @override + String toString() { + return 'ChatRoomSettingsModel(id: $id, roomName: $roomName, state: $state, accessLevel: $accessLevel, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatRoomSettingsModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.roomName, roomName) || + other.roomName == roomName) && + (identical(other.state, state) || other.state == state) && + (identical(other.accessLevel, accessLevel) || + other.accessLevel == accessLevel) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, id, roomName, state, accessLevel, updatedAt); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatRoomSettingsModelImplCopyWith<_$ChatRoomSettingsModelImpl> + get copyWith => __$$ChatRoomSettingsModelImplCopyWithImpl< + _$ChatRoomSettingsModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ChatRoomSettingsModelImplToJson( + this, + ); + } +} + +abstract class _ChatRoomSettingsModel implements ChatRoomSettingsModel { + const factory _ChatRoomSettingsModel( + {required final int id, + @JsonKey(name: 'room_name') required final String roomName, + required final String state, + @JsonKey(name: 'access_level') required final String accessLevel, + @JsonKey(name: 'updated_at') required final String updatedAt}) = + _$ChatRoomSettingsModelImpl; + + factory _ChatRoomSettingsModel.fromJson(Map json) = + _$ChatRoomSettingsModelImpl.fromJson; + + @override + int get id; + @override + @JsonKey(name: 'room_name') + String get roomName; + @override + String get state; + @override + @JsonKey(name: 'access_level') + String get accessLevel; + @override + @JsonKey(name: 'updated_at') + String get updatedAt; + @override + @JsonKey(ignore: true) + _$$ChatRoomSettingsModelImplCopyWith<_$ChatRoomSettingsModelImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/models/chat_room_settings_model.g.dart b/lib/features/chat/models/chat_room_settings_model.g.dart new file mode 100644 index 00000000..7b824334 --- /dev/null +++ b/lib/features/chat/models/chat_room_settings_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_room_settings_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ChatRoomSettingsModelImpl _$$ChatRoomSettingsModelImplFromJson( + Map json) => + _$ChatRoomSettingsModelImpl( + id: (json['id'] as num).toInt(), + roomName: json['room_name'] as String, + state: json['state'] as String, + accessLevel: json['access_level'] as String, + updatedAt: json['updated_at'] as String, + ); + +Map _$$ChatRoomSettingsModelImplToJson( + _$ChatRoomSettingsModelImpl instance) => + { + 'id': instance.id, + 'room_name': instance.roomName, + 'state': instance.state, + 'access_level': instance.accessLevel, + 'updated_at': instance.updatedAt, + }; diff --git a/lib/features/chat/models/chat_user_model.dart b/lib/features/chat/models/chat_user_model.dart new file mode 100644 index 00000000..dd8bf02a --- /dev/null +++ b/lib/features/chat/models/chat_user_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_user_model.freezed.dart'; + +part 'chat_user_model.g.dart'; + +@freezed +class ChatUserModel with _$ChatUserModel { + const factory ChatUserModel({ + required int id, + required String name, + @JsonKey(name: 'profile_photo_path') + String? profilePhotoPath, + @JsonKey(name: 'profile_photo_url') + String? profilePhotoUrl, + }) = _ChatUserModel; + + factory ChatUserModel.fromJson(Map json) => _$ChatUserModelFromJson(json); +} diff --git a/lib/features/chat/models/chat_user_model.freezed.dart b/lib/features/chat/models/chat_user_model.freezed.dart new file mode 100644 index 00000000..095c09e2 --- /dev/null +++ b/lib/features/chat/models/chat_user_model.freezed.dart @@ -0,0 +1,225 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_user_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ChatUserModel _$ChatUserModelFromJson(Map json) { + return _ChatUserModel.fromJson(json); +} + +/// @nodoc +mixin _$ChatUserModel { + int get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + @JsonKey(name: 'profile_photo_path') + String? get profilePhotoPath => throw _privateConstructorUsedError; + @JsonKey(name: 'profile_photo_url') + String? get profilePhotoUrl => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ChatUserModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatUserModelCopyWith<$Res> { + factory $ChatUserModelCopyWith( + ChatUserModel value, $Res Function(ChatUserModel) then) = + _$ChatUserModelCopyWithImpl<$Res, ChatUserModel>; + @useResult + $Res call( + {int id, + String name, + @JsonKey(name: 'profile_photo_path') String? profilePhotoPath, + @JsonKey(name: 'profile_photo_url') String? profilePhotoUrl}); +} + +/// @nodoc +class _$ChatUserModelCopyWithImpl<$Res, $Val extends ChatUserModel> + implements $ChatUserModelCopyWith<$Res> { + _$ChatUserModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? profilePhotoPath = freezed, + Object? profilePhotoUrl = freezed, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + profilePhotoPath: freezed == profilePhotoPath + ? _value.profilePhotoPath + : profilePhotoPath // ignore: cast_nullable_to_non_nullable + as String?, + profilePhotoUrl: freezed == profilePhotoUrl + ? _value.profilePhotoUrl + : profilePhotoUrl // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChatUserModelImplCopyWith<$Res> + implements $ChatUserModelCopyWith<$Res> { + factory _$$ChatUserModelImplCopyWith( + _$ChatUserModelImpl value, $Res Function(_$ChatUserModelImpl) then) = + __$$ChatUserModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + String name, + @JsonKey(name: 'profile_photo_path') String? profilePhotoPath, + @JsonKey(name: 'profile_photo_url') String? profilePhotoUrl}); +} + +/// @nodoc +class __$$ChatUserModelImplCopyWithImpl<$Res> + extends _$ChatUserModelCopyWithImpl<$Res, _$ChatUserModelImpl> + implements _$$ChatUserModelImplCopyWith<$Res> { + __$$ChatUserModelImplCopyWithImpl( + _$ChatUserModelImpl _value, $Res Function(_$ChatUserModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? profilePhotoPath = freezed, + Object? profilePhotoUrl = freezed, + }) { + return _then(_$ChatUserModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + profilePhotoPath: freezed == profilePhotoPath + ? _value.profilePhotoPath + : profilePhotoPath // ignore: cast_nullable_to_non_nullable + as String?, + profilePhotoUrl: freezed == profilePhotoUrl + ? _value.profilePhotoUrl + : profilePhotoUrl // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChatUserModelImpl implements _ChatUserModel { + const _$ChatUserModelImpl( + {required this.id, + required this.name, + @JsonKey(name: 'profile_photo_path') this.profilePhotoPath, + @JsonKey(name: 'profile_photo_url') this.profilePhotoUrl}); + + factory _$ChatUserModelImpl.fromJson(Map json) => + _$$ChatUserModelImplFromJson(json); + + @override + final int id; + @override + final String name; + @override + @JsonKey(name: 'profile_photo_path') + final String? profilePhotoPath; + @override + @JsonKey(name: 'profile_photo_url') + final String? profilePhotoUrl; + + @override + String toString() { + return 'ChatUserModel(id: $id, name: $name, profilePhotoPath: $profilePhotoPath, profilePhotoUrl: $profilePhotoUrl)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatUserModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.profilePhotoPath, profilePhotoPath) || + other.profilePhotoPath == profilePhotoPath) && + (identical(other.profilePhotoUrl, profilePhotoUrl) || + other.profilePhotoUrl == profilePhotoUrl)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, id, name, profilePhotoPath, profilePhotoUrl); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatUserModelImplCopyWith<_$ChatUserModelImpl> get copyWith => + __$$ChatUserModelImplCopyWithImpl<_$ChatUserModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ChatUserModelImplToJson( + this, + ); + } +} + +abstract class _ChatUserModel implements ChatUserModel { + const factory _ChatUserModel( + {required final int id, + required final String name, + @JsonKey(name: 'profile_photo_path') final String? profilePhotoPath, + @JsonKey(name: 'profile_photo_url') final String? profilePhotoUrl}) = + _$ChatUserModelImpl; + + factory _ChatUserModel.fromJson(Map json) = + _$ChatUserModelImpl.fromJson; + + @override + int get id; + @override + String get name; + @override + @JsonKey(name: 'profile_photo_path') + String? get profilePhotoPath; + @override + @JsonKey(name: 'profile_photo_url') + String? get profilePhotoUrl; + @override + @JsonKey(ignore: true) + _$$ChatUserModelImplCopyWith<_$ChatUserModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/models/chat_user_model.g.dart b/lib/features/chat/models/chat_user_model.g.dart new file mode 100644 index 00000000..78e0f981 --- /dev/null +++ b/lib/features/chat/models/chat_user_model.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_user_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ChatUserModelImpl _$$ChatUserModelImplFromJson(Map json) => + _$ChatUserModelImpl( + id: (json['id'] as num).toInt(), + name: json['name'] as String, + profilePhotoPath: json['profile_photo_path'] as String?, + profilePhotoUrl: json['profile_photo_url'] as String?, + ); + +Map _$$ChatUserModelImplToJson(_$ChatUserModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'profile_photo_path': instance.profilePhotoPath, + 'profile_photo_url': instance.profilePhotoUrl, + }; diff --git a/lib/features/chat/models/websocket_info_model.dart b/lib/features/chat/models/websocket_info_model.dart new file mode 100644 index 00000000..9fe8abc4 --- /dev/null +++ b/lib/features/chat/models/websocket_info_model.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'websocket_info_model.freezed.dart'; + +part 'websocket_info_model.g.dart'; + +@freezed +class WebsocketInfoModel with _$WebsocketInfoModel { + const factory WebsocketInfoModel() = _WebsocketInfoModel; + + factory WebsocketInfoModel.fromJson(Map json) => _$WebsocketInfoModelFromJson(json); +} diff --git a/lib/features/chat/models/websocket_info_model.freezed.dart b/lib/features/chat/models/websocket_info_model.freezed.dart new file mode 100644 index 00000000..fd9ef4b9 --- /dev/null +++ b/lib/features/chat/models/websocket_info_model.freezed.dart @@ -0,0 +1,96 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'websocket_info_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +WebsocketInfoModel _$WebsocketInfoModelFromJson(Map json) { + return _WebsocketInfoModel.fromJson(json); +} + +/// @nodoc +mixin _$WebsocketInfoModel { + Map toJson() => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $WebsocketInfoModelCopyWith<$Res> { + factory $WebsocketInfoModelCopyWith( + WebsocketInfoModel value, $Res Function(WebsocketInfoModel) then) = + _$WebsocketInfoModelCopyWithImpl<$Res, WebsocketInfoModel>; +} + +/// @nodoc +class _$WebsocketInfoModelCopyWithImpl<$Res, $Val extends WebsocketInfoModel> + implements $WebsocketInfoModelCopyWith<$Res> { + _$WebsocketInfoModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$WebsocketInfoModelImplCopyWith<$Res> { + factory _$$WebsocketInfoModelImplCopyWith(_$WebsocketInfoModelImpl value, + $Res Function(_$WebsocketInfoModelImpl) then) = + __$$WebsocketInfoModelImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$WebsocketInfoModelImplCopyWithImpl<$Res> + extends _$WebsocketInfoModelCopyWithImpl<$Res, _$WebsocketInfoModelImpl> + implements _$$WebsocketInfoModelImplCopyWith<$Res> { + __$$WebsocketInfoModelImplCopyWithImpl(_$WebsocketInfoModelImpl _value, + $Res Function(_$WebsocketInfoModelImpl) _then) + : super(_value, _then); +} + +/// @nodoc +@JsonSerializable() +class _$WebsocketInfoModelImpl implements _WebsocketInfoModel { + const _$WebsocketInfoModelImpl(); + + factory _$WebsocketInfoModelImpl.fromJson(Map json) => + _$$WebsocketInfoModelImplFromJson(json); + + @override + String toString() { + return 'WebsocketInfoModel()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$WebsocketInfoModelImpl); + } + + @JsonKey(ignore: true) + @override + int get hashCode => runtimeType.hashCode; + + @override + Map toJson() { + return _$$WebsocketInfoModelImplToJson( + this, + ); + } +} + +abstract class _WebsocketInfoModel implements WebsocketInfoModel { + const factory _WebsocketInfoModel() = _$WebsocketInfoModelImpl; + + factory _WebsocketInfoModel.fromJson(Map json) = + _$WebsocketInfoModelImpl.fromJson; +} diff --git a/lib/features/chat/models/websocket_info_model.g.dart b/lib/features/chat/models/websocket_info_model.g.dart new file mode 100644 index 00000000..6573c3c7 --- /dev/null +++ b/lib/features/chat/models/websocket_info_model.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'websocket_info_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$WebsocketInfoModelImpl _$$WebsocketInfoModelImplFromJson( + Map json) => + _$WebsocketInfoModelImpl(); + +Map _$$WebsocketInfoModelImplToJson( + _$WebsocketInfoModelImpl instance) => + {}; diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart new file mode 100644 index 00000000..f66069e2 --- /dev/null +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -0,0 +1,20 @@ +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChatRoomCubit extends Cubit { + final ChatRepository chatRepository; + + ChatRoomCubit({ + required this.chatRepository, + }) : super(ChatRoomState.loading()); + + initChat(String chatId) async { + try { + final chatRoomModel = await chatRepository.getChatRoom(chatId); + emit(ChatRoomState.loaded(chatRoomModel: chatRoomModel)); + } catch (e) { + emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); + } + } +} diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart new file mode 100644 index 00000000..89184a63 --- /dev/null +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart @@ -0,0 +1,19 @@ +import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_room_state.freezed.dart'; + +@freezed +abstract class ChatRoomState with _$ChatRoomState { + const factory ChatRoomState() = _ChatRoomState; + + const factory ChatRoomState.loading() = ChatRoomStateLoading; + + const factory ChatRoomState.loaded({ + required ChatRoomModel chatRoomModel, + }) = ChatRoomStateLoaded; + + const factory ChatRoomState.error({ + required String errorMessage, + }) = ChatRoomStateError; +} diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart new file mode 100644 index 00000000..5bd9ae89 --- /dev/null +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart @@ -0,0 +1,622 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_room_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ChatRoomState { + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() loading, + required TResult Function(ChatRoomModel chatRoomModel) loaded, + required TResult Function(String errorMessage) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? loading, + TResult? Function(ChatRoomModel chatRoomModel)? loaded, + TResult? Function(String errorMessage)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? loading, + TResult Function(ChatRoomModel chatRoomModel)? loaded, + TResult Function(String errorMessage)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(_ChatRoomState value) $default, { + required TResult Function(ChatRoomStateLoading value) loading, + required TResult Function(ChatRoomStateLoaded value) loaded, + required TResult Function(ChatRoomStateError value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ChatRoomState value)? $default, { + TResult? Function(ChatRoomStateLoading value)? loading, + TResult? Function(ChatRoomStateLoaded value)? loaded, + TResult? Function(ChatRoomStateError value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ChatRoomState value)? $default, { + TResult Function(ChatRoomStateLoading value)? loading, + TResult Function(ChatRoomStateLoaded value)? loaded, + TResult Function(ChatRoomStateError value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatRoomStateCopyWith<$Res> { + factory $ChatRoomStateCopyWith( + ChatRoomState value, $Res Function(ChatRoomState) then) = + _$ChatRoomStateCopyWithImpl<$Res, ChatRoomState>; +} + +/// @nodoc +class _$ChatRoomStateCopyWithImpl<$Res, $Val extends ChatRoomState> + implements $ChatRoomStateCopyWith<$Res> { + _$ChatRoomStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$ChatRoomStateImplCopyWith<$Res> { + factory _$$ChatRoomStateImplCopyWith( + _$ChatRoomStateImpl value, $Res Function(_$ChatRoomStateImpl) then) = + __$$ChatRoomStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$ChatRoomStateImplCopyWithImpl<$Res> + extends _$ChatRoomStateCopyWithImpl<$Res, _$ChatRoomStateImpl> + implements _$$ChatRoomStateImplCopyWith<$Res> { + __$$ChatRoomStateImplCopyWithImpl( + _$ChatRoomStateImpl _value, $Res Function(_$ChatRoomStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$ChatRoomStateImpl implements _ChatRoomState { + const _$ChatRoomStateImpl(); + + @override + String toString() { + return 'ChatRoomState()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$ChatRoomStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() loading, + required TResult Function(ChatRoomModel chatRoomModel) loaded, + required TResult Function(String errorMessage) error, + }) { + return $default(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? loading, + TResult? Function(ChatRoomModel chatRoomModel)? loaded, + TResult? Function(String errorMessage)? error, + }) { + return $default?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? loading, + TResult Function(ChatRoomModel chatRoomModel)? loaded, + TResult Function(String errorMessage)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_ChatRoomState value) $default, { + required TResult Function(ChatRoomStateLoading value) loading, + required TResult Function(ChatRoomStateLoaded value) loaded, + required TResult Function(ChatRoomStateError value) error, + }) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ChatRoomState value)? $default, { + TResult? Function(ChatRoomStateLoading value)? loading, + TResult? Function(ChatRoomStateLoaded value)? loaded, + TResult? Function(ChatRoomStateError value)? error, + }) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ChatRoomState value)? $default, { + TResult Function(ChatRoomStateLoading value)? loading, + TResult Function(ChatRoomStateLoaded value)? loaded, + TResult Function(ChatRoomStateError value)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class _ChatRoomState implements ChatRoomState { + const factory _ChatRoomState() = _$ChatRoomStateImpl; +} + +/// @nodoc +abstract class _$$ChatRoomStateLoadingImplCopyWith<$Res> { + factory _$$ChatRoomStateLoadingImplCopyWith(_$ChatRoomStateLoadingImpl value, + $Res Function(_$ChatRoomStateLoadingImpl) then) = + __$$ChatRoomStateLoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$ChatRoomStateLoadingImplCopyWithImpl<$Res> + extends _$ChatRoomStateCopyWithImpl<$Res, _$ChatRoomStateLoadingImpl> + implements _$$ChatRoomStateLoadingImplCopyWith<$Res> { + __$$ChatRoomStateLoadingImplCopyWithImpl(_$ChatRoomStateLoadingImpl _value, + $Res Function(_$ChatRoomStateLoadingImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading { + const _$ChatRoomStateLoadingImpl(); + + @override + String toString() { + return 'ChatRoomState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatRoomStateLoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() loading, + required TResult Function(ChatRoomModel chatRoomModel) loaded, + required TResult Function(String errorMessage) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? loading, + TResult? Function(ChatRoomModel chatRoomModel)? loaded, + TResult? Function(String errorMessage)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? loading, + TResult Function(ChatRoomModel chatRoomModel)? loaded, + TResult Function(String errorMessage)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_ChatRoomState value) $default, { + required TResult Function(ChatRoomStateLoading value) loading, + required TResult Function(ChatRoomStateLoaded value) loaded, + required TResult Function(ChatRoomStateError value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ChatRoomState value)? $default, { + TResult? Function(ChatRoomStateLoading value)? loading, + TResult? Function(ChatRoomStateLoaded value)? loaded, + TResult? Function(ChatRoomStateError value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ChatRoomState value)? $default, { + TResult Function(ChatRoomStateLoading value)? loading, + TResult Function(ChatRoomStateLoaded value)? loaded, + TResult Function(ChatRoomStateError value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class ChatRoomStateLoading implements ChatRoomState { + const factory ChatRoomStateLoading() = _$ChatRoomStateLoadingImpl; +} + +/// @nodoc +abstract class _$$ChatRoomStateLoadedImplCopyWith<$Res> { + factory _$$ChatRoomStateLoadedImplCopyWith(_$ChatRoomStateLoadedImpl value, + $Res Function(_$ChatRoomStateLoadedImpl) then) = + __$$ChatRoomStateLoadedImplCopyWithImpl<$Res>; + @useResult + $Res call({ChatRoomModel chatRoomModel}); + + $ChatRoomModelCopyWith<$Res> get chatRoomModel; +} + +/// @nodoc +class __$$ChatRoomStateLoadedImplCopyWithImpl<$Res> + extends _$ChatRoomStateCopyWithImpl<$Res, _$ChatRoomStateLoadedImpl> + implements _$$ChatRoomStateLoadedImplCopyWith<$Res> { + __$$ChatRoomStateLoadedImplCopyWithImpl(_$ChatRoomStateLoadedImpl _value, + $Res Function(_$ChatRoomStateLoadedImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? chatRoomModel = null, + }) { + return _then(_$ChatRoomStateLoadedImpl( + chatRoomModel: null == chatRoomModel + ? _value.chatRoomModel + : chatRoomModel // ignore: cast_nullable_to_non_nullable + as ChatRoomModel, + )); + } + + @override + @pragma('vm:prefer-inline') + $ChatRoomModelCopyWith<$Res> get chatRoomModel { + return $ChatRoomModelCopyWith<$Res>(_value.chatRoomModel, (value) { + return _then(_value.copyWith(chatRoomModel: value)); + }); + } +} + +/// @nodoc + +class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { + const _$ChatRoomStateLoadedImpl({required this.chatRoomModel}); + + @override + final ChatRoomModel chatRoomModel; + + @override + String toString() { + return 'ChatRoomState.loaded(chatRoomModel: $chatRoomModel)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatRoomStateLoadedImpl && + (identical(other.chatRoomModel, chatRoomModel) || + other.chatRoomModel == chatRoomModel)); + } + + @override + int get hashCode => Object.hash(runtimeType, chatRoomModel); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatRoomStateLoadedImplCopyWith<_$ChatRoomStateLoadedImpl> get copyWith => + __$$ChatRoomStateLoadedImplCopyWithImpl<_$ChatRoomStateLoadedImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() loading, + required TResult Function(ChatRoomModel chatRoomModel) loaded, + required TResult Function(String errorMessage) error, + }) { + return loaded(chatRoomModel); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? loading, + TResult? Function(ChatRoomModel chatRoomModel)? loaded, + TResult? Function(String errorMessage)? error, + }) { + return loaded?.call(chatRoomModel); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? loading, + TResult Function(ChatRoomModel chatRoomModel)? loaded, + TResult Function(String errorMessage)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(chatRoomModel); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_ChatRoomState value) $default, { + required TResult Function(ChatRoomStateLoading value) loading, + required TResult Function(ChatRoomStateLoaded value) loaded, + required TResult Function(ChatRoomStateError value) error, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ChatRoomState value)? $default, { + TResult? Function(ChatRoomStateLoading value)? loading, + TResult? Function(ChatRoomStateLoaded value)? loaded, + TResult? Function(ChatRoomStateError value)? error, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ChatRoomState value)? $default, { + TResult Function(ChatRoomStateLoading value)? loading, + TResult Function(ChatRoomStateLoaded value)? loaded, + TResult Function(ChatRoomStateError value)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class ChatRoomStateLoaded implements ChatRoomState { + const factory ChatRoomStateLoaded( + {required final ChatRoomModel chatRoomModel}) = _$ChatRoomStateLoadedImpl; + + ChatRoomModel get chatRoomModel; + @JsonKey(ignore: true) + _$$ChatRoomStateLoadedImplCopyWith<_$ChatRoomStateLoadedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ChatRoomStateErrorImplCopyWith<$Res> { + factory _$$ChatRoomStateErrorImplCopyWith(_$ChatRoomStateErrorImpl value, + $Res Function(_$ChatRoomStateErrorImpl) then) = + __$$ChatRoomStateErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({String errorMessage}); +} + +/// @nodoc +class __$$ChatRoomStateErrorImplCopyWithImpl<$Res> + extends _$ChatRoomStateCopyWithImpl<$Res, _$ChatRoomStateErrorImpl> + implements _$$ChatRoomStateErrorImplCopyWith<$Res> { + __$$ChatRoomStateErrorImplCopyWithImpl(_$ChatRoomStateErrorImpl _value, + $Res Function(_$ChatRoomStateErrorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? errorMessage = null, + }) { + return _then(_$ChatRoomStateErrorImpl( + errorMessage: null == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ChatRoomStateErrorImpl implements ChatRoomStateError { + const _$ChatRoomStateErrorImpl({required this.errorMessage}); + + @override + final String errorMessage; + + @override + String toString() { + return 'ChatRoomState.error(errorMessage: $errorMessage)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatRoomStateErrorImpl && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage)); + } + + @override + int get hashCode => Object.hash(runtimeType, errorMessage); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatRoomStateErrorImplCopyWith<_$ChatRoomStateErrorImpl> get copyWith => + __$$ChatRoomStateErrorImplCopyWithImpl<_$ChatRoomStateErrorImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function() $default, { + required TResult Function() loading, + required TResult Function(ChatRoomModel chatRoomModel) loaded, + required TResult Function(String errorMessage) error, + }) { + return error(errorMessage); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function()? $default, { + TResult? Function()? loading, + TResult? Function(ChatRoomModel chatRoomModel)? loaded, + TResult? Function(String errorMessage)? error, + }) { + return error?.call(errorMessage); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function()? $default, { + TResult Function()? loading, + TResult Function(ChatRoomModel chatRoomModel)? loaded, + TResult Function(String errorMessage)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(errorMessage); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_ChatRoomState value) $default, { + required TResult Function(ChatRoomStateLoading value) loading, + required TResult Function(ChatRoomStateLoaded value) loaded, + required TResult Function(ChatRoomStateError value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ChatRoomState value)? $default, { + TResult? Function(ChatRoomStateLoading value)? loading, + TResult? Function(ChatRoomStateLoaded value)? loaded, + TResult? Function(ChatRoomStateError value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ChatRoomState value)? $default, { + TResult Function(ChatRoomStateLoading value)? loading, + TResult Function(ChatRoomStateLoaded value)? loaded, + TResult Function(ChatRoomStateError value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class ChatRoomStateError implements ChatRoomState { + const factory ChatRoomStateError({required final String errorMessage}) = + _$ChatRoomStateErrorImpl; + + String get errorMessage; + @JsonKey(ignore: true) + _$$ChatRoomStateErrorImplCopyWith<_$ChatRoomStateErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart new file mode 100644 index 00000000..c6ad9beb --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -0,0 +1,200 @@ +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; +import 'package:eatvio_app/locator_service.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; + +class ChatScreen extends StatelessWidget { + final String chatId; + + const ChatScreen({ + required this.chatId, + super.key, + }); + + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => sl()..initChat(chatId), + child: BlocBuilder( + builder: (context, state) { + if (state is ChatRoomStateLoading) { + return Scaffold( + body: Center( + child: CupertinoActivityIndicator(), + ), + ); + } else if (state is ChatRoomStateLoaded) { + return _buildChat(context, state); + } else if (state is ChatRoomStateError) { + return Center(child: Text(state.errorMessage)); + } else { + return Scaffold( + body: Center( + child: CupertinoActivityIndicator(), + ), + ); + } + }, + ), + ); + } + + _buildChat(BuildContext context, ChatRoomStateLoaded state) { + mediaQueryData = MediaQuery.of(context); + return Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: PrimaryColors().gray50, + appBar: AppBar( + titleSpacing: 0, + centerTitle: false, + backgroundColor: Colors.white, + forceMaterialTransparency: true, + elevation: 0, + title: Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFFEBF4FF), + shape: BoxShape.circle, + ), + child: Text( + state.chatRoomModel.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoomModel.users['to_chat_user']!.name.split(' ')[1][0], + style: TextStyle( + color: Color(0xFF7F9CF5), + fontWeight: FontWeight.w400, + fontSize: 20, + ), + ), + ), + SizedBox(width: 16), + Text( + state.chatRoomModel.users['to_chat_user']!.name, + style: CustomTextStyles.titleSmallBold.copyWith( + color: PrimaryColors().blueGray800, + ), + ), + ], + ), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Container( + color: Color(0xFFF0F2F5), + width: double.maxFinite, + child: CustomScrollView( + reverse: true, + slivers: [ + SliverPadding( + padding: EdgeInsets.symmetric(vertical: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => ChatBubbleWidget( + text: state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].data, + isCurrentUser: state.chatRoomModel.users['to_chat_user']!.id != + state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].authorId, + mediaData: state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].mediaData, + ), + childCount: state.chatRoomModel.messages.length, + ), + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 8), + InkWell( + onTap: () {}, + borderRadius: BorderRadiusStyle.circleBorder17, + child: Ink( + height: 32, + width: 32, + child: Icon( + Icons.attach_file_rounded, + size: 28, + color: Color(0xFF858E99), + ), + ), + ), + SizedBox(width: 8), + SizedBox( + width: mediaQueryData.size.width - 96, + child: TextFormField( + maxLines: 5, + minLines: 1, + keyboardType: TextInputType.multiline, + style: TextStyle( + fontSize: 16, + fontFamily: 'Mulish', + fontWeight: FontWeight.w400, + color: Colors.black, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), + hintText: 'Сообщение', + hintStyle: TextStyle( + fontSize: 16, + fontFamily: 'Mulish', + fontWeight: FontWeight.w400, + color: Color(0xFFAEAEB2), + ), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadiusStyle.circleBorder17, + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadiusStyle.circleBorder17, + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadiusStyle.circleBorder17, + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + ), + ), + ), + SizedBox(width: 8), + InkWell( + borderRadius: BorderRadiusStyle.circleBorder17, + onTap: () {}, + child: Ink( + height: 32, + width: 32, + child: Icon( + Icons.send_outlined, + size: 28, + color: Color(0xFF858E99), + ), + ), + ), + SizedBox(width: 8), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart b/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart new file mode 100644 index 00000000..ba44ade1 --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart @@ -0,0 +1,90 @@ +import 'package:eatvio_app/app_router.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_select_model.dart'; +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:go_router/go_router.dart'; + +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) { + AppRouter.navigatorKey.currentContext?.goNamed('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/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_event.dart b/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_event.dart new file mode 100644 index 00000000..681736b1 --- /dev/null +++ b/lib/features/chat/presentation/pages/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/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_state.dart b/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_state.dart new file mode 100644 index 00000000..e5f1262a --- /dev/null +++ b/lib/features/chat/presentation/pages/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/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart b/lib/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart new file mode 100644 index 00000000..939bb42d --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart @@ -0,0 +1,147 @@ +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 ?? + Padding( + padding: EdgeInsets.only(left: 10), + child: Icon( + Icons.search, + color: Color(0xFF7B8794), + ), + ), + suffixIcon: suffix ?? + IconButton( + onPressed: () => controller!.clear(), + icon: Icon( + Icons.clear, + color: Colors.grey.shade600, + ), + ), + suffixIconConstraints: suffixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + isDense: true, + contentPadding: EdgeInsets.all(16), + 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/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart b/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart new file mode 100644 index 00000000..fe468f3b --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart @@ -0,0 +1,98 @@ +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart'; +import 'package:flutter/cupertino.dart'; + +import 'bloc/chat_select_bloc.dart'; +import 'models/chat_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'chat_search_view.dart'; +import 'package:flutter/material.dart'; + +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 Scaffold( + body: Center( + child: CupertinoActivityIndicator(), + ), + ); + } else if (state is ChatSelectLoadedState) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + forceMaterialTransparency: true, + toolbarHeight: 0, + ), + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + backgroundColor: Colors.white, + stretchTriggerOffset: 1, + forceMaterialTransparency: true, + floating: true, + snap: true, + title: CustomSearchView( + controller: state.searchController, + fillColor: Color(0xFFF0F2F5), + hintText: "Поиск", + hintStyle: TextStyle( + color: Color(0xFF7B8794), + ), + ), + ), + ]; + }, + body: _buildChatList(context, state), + ), + ); + } else if (state is ChatSelectErrorState) { + return Center(child: Text(state.errorMessage)); + } else { + return Scaffold( + body: Center( + child: CupertinoActivityIndicator(), + ), + ); + } + }, + ); + } +} + +_buildChatList(BuildContext context, ChatSelectLoadedState state) { + return ListView.separated( + padding: EdgeInsets.all(16), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) { + return SizedBox( + height: 10.v, + ); + }, + itemCount: state.chatRooms?.length ?? 0, + itemBuilder: (context, index) { + ChatItemModel model = state.chatRooms?[index]; + return ChatEntryWidget( + chatEntryObj: model, + ); + }, + ); +} diff --git a/lib/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart b/lib/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart new file mode 100644 index 00000000..6738afb8 --- /dev/null +++ b/lib/features/chat/presentation/pages/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/features/chat/presentation/pages/chat_select_screen/models/chat_select_model.dart b/lib/features/chat/presentation/pages/chat_select_screen/models/chat_select_model.dart new file mode 100644 index 00000000..c95b52ee --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_select_screen/models/chat_select_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart'; +import 'package:equatable/equatable.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/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart new file mode 100644 index 00000000..fbd6b1b0 --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart @@ -0,0 +1,93 @@ +// ignore_for_file: must_be_immutable + +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:go_router/go_router.dart'; + +class ChatEntryWidget extends StatelessWidget { + ChatEntryWidget({ + required this.chatEntryObj, + super.key, + }); + + ChatItemModel chatEntryObj; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => GoRouter.of(context).goNamed( + 'chatScreen', + extra: chatEntryObj.id.toString(), + ), + child: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadiusStyle.circleBorder8, + color: Color(0xFFF6FAFF), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFFEBF4FF), + shape: BoxShape.circle, + ), + child: Text( + chatEntryObj.username!.split(' ').first[0] + chatEntryObj.username!.split(' ')[1][0], + style: TextStyle( + color: Color(0xFF7F9CF5), + fontWeight: FontWeight.w400, + fontSize: 20, + ), + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart new file mode 100644 index 00000000..229ced53 --- /dev/null +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -0,0 +1,124 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PhotoViewScreen extends StatefulWidget { + final List imageUrls; + final int index; + + const PhotoViewScreen({ + super.key, + required this.imageUrls, + required this.index, + }); + + @override + State createState() => _PhotoViewScreenState(); +} + +class _PhotoViewScreenState extends State { + late final pageController; + int imageIndex = 0; + + @override + void initState() { + imageIndex = widget.index; + pageController = PageController(initialPage: imageIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + centerTitle: true, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.black, + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + ), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + title: Text( + (imageIndex + 1).toString() + ' из ' + widget.imageUrls.length.toString(), + ), + ), + body: Dismissible( + direction: DismissDirection.vertical, + key: const Key('key'), + onDismissed: (_) => GoRouter.of(context).pop(), + resizeDuration: Duration(microseconds: 1), + child: Container( + child: PhotoViewGallery.builder( + scrollPhysics: const BouncingScrollPhysics(), + builder: (BuildContext context, int index) { + return PhotoViewGalleryPageOptions( + imageProvider: NetworkImage(widget.imageUrls[index]), + initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes(tag: widget.imageUrls[index]), + errorBuilder: (context, object, stackTrace) { + return Column( + children: [ + SizedBox(height: 32), + Text('Файл'), + SizedBox(height: 8), + ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadiusStyle.circleBorder17, + ), + contentPadding: EdgeInsets.all(16), + onTap: () async { + if (!await launchUrl(Uri.parse(widget.imageUrls[index]))) { + throw Exception('Could not launch ${widget.imageUrls[index]}'); + } + }, + leading: Icon( + Icons.file_copy_outlined, + color: Colors.white, + ), + trailing: Icon( + Icons.download_outlined, + color: Colors.white, + ), + title: Text( + widget.imageUrls[index].replaceAll(dotenv.env['STORAGE_URL']! + 'chat_files/', ''), + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + ); + }, + ); + }, + itemCount: widget.imageUrls.length, + loadingBuilder: (context, event) => Center( + child: Container( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + value: event == null ? 0 : event.cumulativeBytesLoaded / event.expectedTotalBytes!, + ), + ), + ), + pageController: pageController, + onPageChanged: (page) { + setState(() { + imageIndex = page; + }); + }, + ), + ), + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart new file mode 100644 index 00000000..127795cc --- /dev/null +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -0,0 +1,110 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/theme/app_decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ChatBubbleWidget extends StatelessWidget { + const ChatBubbleWidget({ + super.key, + required this.text, + required this.isCurrentUser, + this.mediaData, + }); + + final String text; + final bool isCurrentUser; + final List? mediaData; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(isCurrentUser ? 64 : 16, 4, isCurrentUser ? 16 : 64, 4), + child: Align( + alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft, + child: DecoratedBox( + decoration: BoxDecoration( + color: isCurrentUser ? Color(0xFFF6FAFF) : Colors.white, + borderRadius: BorderRadiusStyle.circleBorder17, + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + if (mediaData != null) + CustomScrollView( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + slivers: [ + SliverList.separated( + itemBuilder: (BuildContext context, int index) { + if (!mediaData![index].toString().contains('.png') && + !mediaData![index].toString().contains('.jpg') && + !mediaData![index].toString().contains('.jpeg')) { + return ListTile( + contentPadding: EdgeInsets.zero, + horizontalTitleGap: 8, + onTap: () async { + if (!await launchUrl(Uri.parse(dotenv.env['STORAGE_URL']! + mediaData![index].toString()))) { + throw Exception('Could not launch ${mediaData![index].toString()}'); + } + }, + leading: Icon( + Icons.file_copy_outlined, + color: Color(0xFF2F80ED), + ), + trailing: Icon( + Icons.download_outlined, + color: Color(0xFF2F80ED), + ), + title: Text( + mediaData![index].toString().replaceAll('chat_files/', ''), + maxLines: 2, + style: TextStyle( + color: Color(0xFF323F4B), + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + return GestureDetector( + onTap: () { + GoRouter.of(context).pushNamed( + 'photoViewScreen', + extra: mediaData!.map((e) => dotenv.env['STORAGE_URL']! + e.toString()).toList(), + pathParameters: { + 'index': index.toString(), + }, + ); + }, + child: ClipRRect( + borderRadius: BorderRadiusStyle.roundedBorder8, + child: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + '${mediaData![index]}', + ), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 8), + itemCount: mediaData!.length, + ), + ], + ), + if (mediaData!.length > 0 && text.isNotEmpty) SizedBox(height: 8), + if (text.isNotEmpty) + Text( + text, + style: TextStyle( + color: Color(0xFF323F4B), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart new file mode 100644 index 00000000..137aa878 --- /dev/null +++ b/lib/features/chat/repositories/chat_repository.dart @@ -0,0 +1,31 @@ +import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; +import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; +import 'package:eatvio_app/features/chat/services/chat_service.dart'; + +class ChatRepository { + final ChatService chatService; + + ChatRepository({ + required this.chatService, + }); + + /// TODO use "dartz" package because need Either for error handling + Future getChatRoom(String chatId) async { + try { + return (await chatService.getChatRoomModel(chatId))['chatRoom']!; + } catch (e) { + print(e); + throw (e); + } + } + + /// TODO use "dartz" package because need Either for error handling + Future getWebsocket() async { + try { + return (await chatService.getWebsocket()); + } catch (e) { + print(e); + throw (e); + } + } +} diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart new file mode 100644 index 00000000..5b27ccf2 --- /dev/null +++ b/lib/features/chat/services/chat_service.dart @@ -0,0 +1,17 @@ +import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'chat_service.g.dart'; + +@RestApi() +abstract class ChatService { + factory ChatService(Dio dio, {String baseUrl}) = _ChatService; + + @GET('/chat/show/{id}') + Future> getChatRoomModel(@Path() String id); + + @GET('/profile/ws/params') + Future getWebsocket(); +} diff --git a/lib/features/chat/services/chat_service.g.dart b/lib/features/chat/services/chat_service.g.dart new file mode 100644 index 00000000..db9fdea8 --- /dev/null +++ b/lib/features/chat/services/chat_service.g.dart @@ -0,0 +1,105 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _ChatService implements ChatService { + _ChatService( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future> getChatRoomModel(String id) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/chat/show/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + var value = _result.data!.map((k, dynamic v) => + MapEntry(k, ChatRoomModel.fromJson(v as Map))); + return value; + } + + @override + Future getWebsocket() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/profile/ws/params', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = WebsocketInfoModel.fromJson(_result.data!); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/localization/app_localization.dart b/lib/localization/app_localization.dart index 51b4b1aa..2fc106da 100644 --- a/lib/localization/app_localization.dart +++ b/lib/localization/app_localization.dart @@ -1,8 +1,9 @@ +import 'package:eatvio_app/app_router.dart'; + import 'ru/ru_translations.dart'; //import 'en_us/en_us_translations.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/cupertino.dart'; -import '../core/app_export.dart'; class AppLocalization { AppLocalization(this.locale); @@ -17,8 +18,16 @@ class AppLocalization { };*/ static AppLocalization of() { - return Localizations.of( - NavigatorService.navigatorKey.currentContext!, AppLocalization)!; + final context = AppRouter.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/locator_service.dart b/lib/locator_service.dart new file mode 100644 index 00000000..68233789 --- /dev/null +++ b/lib/locator_service.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/chat/services/chat_service.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get_it/get_it.dart'; + +final sl = GetIt.instance; + +Future init() async { + // BLoC + sl.registerFactory(() => ChatRoomCubit(chatRepository: sl())); + + // Repository + sl.registerLazySingleton(() => ChatRepository(chatService: sl())); + + // Services + sl.registerLazySingleton(() => ChatService(sl())); + + // External + sl.registerLazySingleton( + () => Dio( + BaseOptions( + baseUrl: dotenv.env['SERVER_ROOT']!, + ), + ), + ); +} diff --git a/lib/main.dart b/lib/main.dart index 7b5326dc..8e0c1b94 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,12 @@ +import 'package:eatvio_app/app_router.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_select_model.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'; +import 'package:eatvio_app/presentation/feed/feed_screen/bloc/feed_bloc.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/models/feed_model.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'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -5,15 +14,21 @@ import 'core/app_export.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:keyboard_dismisser/keyboard_dismisser.dart'; +import 'package:eatvio_app/locator_service.dart' as di; var globalMessengerKey = GlobalKey(); + void main() async { WidgetsFlutterBinding.ensureInitialized(); + await di.init(); final appDocumentDir = await getApplicationDocumentsDirectory(); final path = appDocumentDir.path; await dotenv.load(fileName: ".env"); await Hive.initFlutter(path); SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, DeviceOrientation.portraitUp, ]); await PrefUtils().init(); @@ -23,33 +38,64 @@ void main() async { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ThemeBloc( - ThemeState( - themeType: PrefUtils().getThemeData(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ThemeBloc( + ThemeState(themeType: PrefUtils().getThemeData()), + ), ), - ), + BlocProvider( + create: (context) => MyProfileBloc( + MyProfileState( + myProfileModelObj: MyProfileModel(), + ), + )..add(MyProfileInitialEvent()), + ), + BlocProvider( + create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel())) + ..add( + FeedInitialEvent(), + ), + ), + BlocProvider( + create: (context) => DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())) + ..add( + DiaryEditInitialEvent(), + ), + ), + BlocProvider( + create: (context) => ChatSelectBloc(ChatSelectState(chatSelectModelObj: ChatSelectModel())) + ..add( + ChatSelectInitialEvent(), + ), + ), + ], child: BlocBuilder( builder: (context, state) { - return MaterialApp( - theme: theme, - title: 'Eatvio', - navigatorKey: NavigatorService.navigatorKey, - debugShowCheckedModeBanner: false, - localizationsDelegates: [ - AppLocalizationDelegate(), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, + return KeyboardDismisser( + gestures: [ + GestureType.onTap, + GestureType.onPanUpdateDownDirection, ], - supportedLocales: [ - Locale( - 'ru', - '', - ), - ], - initialRoute: AppRoutes.initialRoute, - routes: AppRoutes.routes, + child: MaterialApp.router( + theme: theme, + title: 'Eatvio', + routerConfig: AppRouter.appRouter, + debugShowCheckedModeBanner: false, + localizationsDelegates: [ + AppLocalizationDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + Locale( + 'ru', + '', + ), + ], + ), ); }, ), diff --git a/lib/presentation/auth/auth_screen/auth_screen.dart b/lib/presentation/auth/auth_screen/auth_screen.dart index 19bcac26..9694a2c9 100644 --- a/lib/presentation/auth/auth_screen/auth_screen.dart +++ b/lib/presentation/auth/auth_screen/auth_screen.dart @@ -1,3 +1,6 @@ +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:go_router/go_router.dart'; + import 'bloc/auth_bloc.dart'; import 'models/auth_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -19,7 +22,9 @@ class AuthScreen extends StatelessWidget { AuthState( authModelObj: AuthModel(), ), - )..add(AuthInitialEvent()), + )..add( + AuthInitialEvent(), + ), child: AuthScreen(), ); } @@ -30,124 +35,152 @@ class AuthScreen extends StatelessWidget { return Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppBar(context), - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), - child: Column(children: [ - SizedBox(height: 39.v), - Text("user_authorization".tr, - style: theme.textTheme.headlineSmall), - SizedBox(height: 60.v), - Text("email".tr, style: theme.textTheme.bodyLarge), - SizedBox(height: 3.v), - BlocSelector( - selector: (state) => state.emailController, - builder: (context, emailController) { - return CustomTextFormField( - controller: emailController, - textInputAction: TextInputAction.done); - }), - SizedBox(height: 22.v), - Text("password".tr, style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), - BlocSelector( - selector: (state) => state.passwordController, - builder: (context, passwordController) { - return CustomTextFormField( - obscureText: true, - controller: passwordController, - ); - }), - Spacer(), - NextStepButton( - text: "no_account".tr.toUpperCase(), + 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: 36.v), + Text("user_authorization".tr, style: theme.textTheme.headlineSmall), + SizedBox(height: 60.v), + Text("email".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 3.v), + BlocSelector( + selector: (state) => state.emailController, + builder: (context, emailController) { + return CustomTextFormField(controller: emailController, textInputAction: TextInputAction.done); + }), + SizedBox(height: 22.v), + Text("password".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 5.v), + BlocSelector( + selector: (state) => state.passwordController, + builder: (context, passwordController) { + return CustomTextFormField( + obscureText: true, + controller: passwordController, + ); + }), + 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: 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.languageSelectScreen); + onPressed: onTapDiaryTest(context)), + NextStepButton( + text: "authentificate".tr.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: onTapAuth(state, 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.diaryEdit, - ); - } 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'; - } + 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); + TokenStorage.saveTokenInStorage(token); + GoRouter.of(context).go('/my-profile-screen'); + } 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 = ''; - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); - }), - ]))); + 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 () { + GoRouter.of(context).pop(context); + }; + } + + onTapDiaryTest(BuildContext context) { + return () { + GoRouter.of(context).goNamed('diaryScreen'); + }; + } + + onTapProfileTest(BuildContext context) { + return () { + GoRouter.of(context).goNamed('mySettingsScreen'); + }; + } + + onTapNoAccount(BuildContext context) { + return () { + GoRouter.of(context).goNamed('languageSelectScreen'); + }; } /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { + _buildAppBar(BuildContext context) { return CustomAppBar( - leadingWidth: 29.h, - centerTitle: true, - title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); + leadingWidth: 29.h, + centerTitle: true, + title: AppbarTitleImage( + imagePath: ImageConstant.imgLogoEatvio, + ), + ); } List validateFields(String email, String password) { diff --git a/lib/presentation/auth/auth_screen/bloc/auth_bloc.dart b/lib/presentation/auth/auth_screen/bloc/auth_bloc.dart index cd0203ac..6622981a 100644 --- a/lib/presentation/auth/auth_screen/bloc/auth_bloc.dart +++ b/lib/presentation/auth/auth_screen/bloc/auth_bloc.dart @@ -2,7 +2,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '/core/app_export.dart'; import 'package:eatvio_app/presentation/auth/auth_screen/models/auth_model.dart'; + part 'auth_event.dart'; + part 'auth_state.dart'; /// A bloc that manages the state of a Auth according to the event that is dispatched to it. @@ -15,8 +17,11 @@ class AuthBloc extends Bloc { AuthInitialEvent event, Emitter emit, ) async { - emit(state.copyWith( + emit( + state.copyWith( emailController: TextEditingController(), - passwordController: TextEditingController())); + passwordController: TextEditingController(), + ), + ); } } 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 7254d633..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,6 +1,9 @@ +import 'dart:async'; +import 'package:intl/intl.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '/core/app_export.dart'; +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'; @@ -14,6 +17,28 @@ part 'diary_edit_state.dart'; class DiaryEditBloc extends Bloc { DiaryEditBloc(DiaryEditState initialState) : super(initialState) { on(_onInitialize); + on(_changeSatiety); // пользователь нажимает на шкалу сытости + on(_changeMood); // пользователь нажимает на шкалу настроения + on(_changeMealTime); // пользовательяет времяа пиши + on(_changeMeal); + on(_changePortion); + on(_changeWakeup); // пользователь выбирает время пробуждения + on(_changeRetire); // пользователь выбирает время отхода ко сну + on(_toggleMood); + on(_toggleWakeup); + on(_addPortion); //пользователь нажимает "Добавить продукт" + on(_removePortion); + on( + _removeImage); // пользователь нажимает на крестик около продукта + on(_uploadImages); //пользователь нажимает "Добавить фото" + on(_addComment); //пользователь нажимает "Добавить комментарий" + on( + _inputProduct); //пользователь нажимает "Добавить комментарий" + on(_finishDay); //пользователь нажимает "Добавить комментарий" + on(_sendModeration); + on( + reloadScreen); //пользователь нажимает "Добавить комментарий" + on(_clearInput); } Future _onInitialize( @@ -21,42 +46,926 @@ class DiaryEditBloc extends Bloc { Emitter emit, ) async { try { + // 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', }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); - // Проверка наличия ключевых полей в JSON if (data is Map && data.containsKey('id')) { - // Все ключевые поля присутствуют, продолжаем обработку - if (state is DiaryEditLoadedState) { - // Если текущее состояние уже загружено, обновляем его - var currentState = state as DiaryEditLoadedState; - // Обновляем значения контроллеров только если они уже были созданы + final int diaryId = data['id']; - emit((currentState).copyWith()); + 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', + }, + ); + 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 { + emit(DiaryEditErrorState('Ошибка при декодировании данных КБЖУ')); + } } else { - // Если текущее состояние - начальное, создаем новое загруженное состояние - emit(DiaryEditLoadedState()); + emit(DiaryEditErrorState( + 'Ошибка при декодировании данных дневника')); } } else { - // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку emit( DiaryEditErrorState('Некоторые необходимые данные отсутствуют.')); } } else { - // Статус ответа не равен 200, обрабатываем ошибку emit(DiaryEditErrorState('Ошибка сервера: ${response.statusCode}')); } } catch (e) { - // Ошибка при декодировании JSON или другая ошибка выполнения - emit(DiaryEditErrorState('Ошибка при получении данных профиля: $e')); + emit(DiaryEditErrorState('Ошибка при получении данных дневника: $e')); } } + + Future _changeSatiety( + ChangeSatiety event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var updatedState = + currentState.copyWith(selectedSatiety: event.selectedElement); + + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await sendSatietyData( + currentState.token, + event.selectedElement, + currentState.currentMeal?.id, + currentState.diaryId); + + if (response.statusCode == 200) { + updatedState.selectedSatiety = event.selectedElement; + emit(updatedState); // Передаем обновленное состояние в emit + } + } + } + + Future sendSatietyData( + String? token, + int selectedSatiety, + int? mealID, + int diaryID, + ) async { + String url = dotenv.env['SERVER_ROOT']! + + '/diary/$diaryID/meal/$mealID/satiety/fill'; + final Map credentials = { + 'satiety': selectedSatiety, + }; + 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 _changeMood( + ChangeMood event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var updatedState = + currentState.copyWith(selectedMood: event.selectedElement); + + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await sendMoodData( + currentState.token, + event.selectedElement, + currentState.currentMeal?.id, + currentState.diaryId); + + if (response.statusCode == 200) { + print( + 'Mood change success! meal_id: ${currentState.currentMeal?.id}, mood: ${event.selectedElement}'); + updatedState.selectedMood = event.selectedElement; + emit(updatedState); // Передаем обновленное состояние в emit + } + } + } + + Future sendMoodData( + String? token, + int selectedMood, + int? mealID, + int diaryID, + ) async { + String url = + dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/meal/$mealID/mood/fill'; + final Map credentials = { + 'mood': selectedMood, + }; + + var response = await http.post(Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8); + + return response; + } + + FutureOr _changeWakeup( + WakeupEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var response = await sendWakeupData( + currentState.token, event.selectedTime, currentState.diaryId); + if (response.statusCode == 200) { + 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"); + } + } + + Future sendWakeupData( + String? token, + TimeOfDay wakeup, + int diaryID, + ) async { + 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), + encoding: utf8); + + return response; + } + + Future sendRetireData( + 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), + encoding: utf8); + + return response; + } + + FutureOr _changeMealTime( + ChangeMealTime event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var updatedState = currentState.copyWith(mealTimeController: event.time); + + emit(updatedState); + } + } + + Future sendMealTimeData( + String? token, + TimeOfDay mealTime, + int? mealID, + int diaryID, + ) async { + String url = dotenv.env['SERVER_ROOT']! + '/diary/???'; + final Map credentials = { + 'time': mealTime, + '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; + } + + 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; + print(meal['photos']); + DiaryEditLoadedState updatedState = currentState.copyWith( + 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); + } + } + + FutureOr _toggleMood( + ToggleMoodExpandedEvent event, Emitter emit) { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + emit(currentState.copyWith(moodExpanded: !currentState.moodExpanded)); + currentState.moodExpanded = !currentState.moodExpanded; + } + } + + FutureOr _toggleWakeup( + ToggleWakeupExpandedEvent event, Emitter emit) { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + emit(currentState.copyWith(wakeupExpanded: !currentState.wakeupExpanded)); + currentState.wakeupExpanded = !currentState.wakeupExpanded; + } + } + + FutureOr _addPortion( + AddPortionEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + var response = await sendPortionData( + 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.copyWith(isAdded: true)); + await reloadScreen(event, emit); + } else { + print( + 'Failed to add portion. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendPortionData(String? token, String? name, + String portion, int? quantity, int? mealID, int diaryID) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/portions/create'; + + final Map credentials = { + 'product_name': name, + 'portion': portion, + 'quantity': quantity, + }; + + 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), + encoding: utf8); + + return response; + } + + 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(List meals) { + meals.sort(compareTypes); + return meals.map((meal) { + String title = "Прием пищи ${meal['type']}"; + return SelectionPopupModel(id: meal['id'], title: title); + }).toList(); + } + + int compareTypes(a, b) { + return a['type'].compareTo(b['type']); + } + + List fillPortions() { + return [ + SelectionPopupModel(id: 0, title: "порций", isSelected: true), + SelectionPopupModel(id: 1, title: "грамм"), + ]; + } } 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 ea52a649..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 @@ -14,3 +14,158 @@ class DiaryEditInitialEvent extends DiaryEditEvent { @override List get props => []; } + +class ChangeSatiety extends DiaryEditEvent { + ChangeSatiety( + this.selectedElement, + ); + final int selectedElement; + + @override + List get props => [selectedElement]; +} + +class ChangeMood extends DiaryEditEvent { + ChangeMood( + this.selectedElement, + ); + final int selectedElement; + + @override + List get props => [selectedElement]; +} + +class ChangeMealTime extends DiaryEditEvent { + ChangeMealTime(this.time); + final TimeOfDay time; + + @override + List get props => [time]; +} + +class AddComment extends DiaryEditEvent { + AddComment(this.diaryId, this.comment); + final String comment; + final int diaryId; + + @override + List get props => [comment, diaryId]; +} + +class WakeupEvent extends DiaryEditEvent { + WakeupEvent(this.selectedTime); + final TimeOfDay selectedTime; + + @override + List get props => [selectedTime]; +} + +class RetireEvent extends DiaryEditEvent { + RetireEvent(this.selectedTime); + final TimeOfDay selectedTime; + + @override + List get props => [selectedTime]; +} + +class ToggleMoodExpandedEvent extends DiaryEditEvent { + @override + List get props => []; +} + +class ToggleWakeupExpandedEvent extends DiaryEditEvent { + @override + List get props => []; +} + +class AddPortionEvent extends DiaryEditEvent { + AddPortionEvent(); + @override + List get props => []; +} + +class RemovePortionEvent extends DiaryEditEvent { + RemovePortionEvent(this.portionId); + final int portionId; + @override + List get props => [portionId]; +} + +class UploadImages extends DiaryEditEvent { + UploadImages(this.image); + final XFile image; + @override + 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}); + + SelectionPopupModel value; + List? meals; + @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 7b9da4ab..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 @@ -18,20 +18,163 @@ class DiaryEditInitialState extends DiaryEditState { /// Represents the state of DiaryEdit in the application. class DiaryEditLoadedState extends DiaryEditState { - DiaryEditLoadedState({this.diaryEditModelObj}) + DiaryEditLoadedState( + {this.diaryEditModelObj, + this.diaryDate, + 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); DiaryEditModel? diaryEditModelObj; + String? diaryDate; + 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, - ); + 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: + productQuantityController ?? this.productQuantityController, + selectedMood: selectedMood ?? this.selectedMood, + 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, + 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 7d19300e..e2be9487 100644 --- a/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart +++ b/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart @@ -1,53 +1,27 @@ +// 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_text_form_field.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_icon_button.dart'; +import 'package:eatvio_app/widgets/custom_scale_element.dart'; +import 'package:carousel_slider/carousel_slider.dart'; // ignore: must_be_immutable class DiaryEditScreen extends StatelessWidget { - DiaryEditScreen({Key? key}) - : super( - key: key, - ); - - List dropdownItemList = [ - "Item One", - "Item Two", - "Item Three", - ]; - - List dropdownItemList1 = [ - "Item One", - "Item Two", - "Item Three", - ]; - - TextEditingController enterProductNameController = TextEditingController(); - - TextEditingController quantityController = TextEditingController(); - - List dropdownItemList2 = [ - "Item One", - "Item Two", - "Item Three", - ]; - - List dropdownItemList3 = [ - "Item One", - "Item Two", - "Item Three", - ]; + DiaryEditScreen({Key? key}) : super(key: key); static Widget builder(BuildContext context) { return BlocProvider( - create: (context) => - DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())) - ..add(DiaryEditInitialEvent()), - child: DiaryEditScreen()); + create: (context) => DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel()))..add(DiaryEditInitialEvent()), child: DiaryEditScreen()); } Widget build(BuildContext context) { @@ -55,9 +29,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) { // Данные загружены, можно строить экран. @@ -73,101 +46,104 @@ class DiaryEditScreen extends StatelessWidget { ); } - Widget _buildDiaryEditScreen( - BuildContext context, DiaryEditLoadedState state) { - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - body: SizedBox( - width: double.maxFinite, - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only(bottom: 5.v), - child: Column( - children: [ - _buildHeaderRow(context), - SizedBox(height: 30.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h), - child: CustomDropDown( - icon: Container( - margin: - EdgeInsets.fromLTRB(30.h, 24.v, 20.h, 22.v), - child: CustomImageView( - imagePath: ImageConstant.imgArrowLeft, - height: 10.v, - width: 18.h, - ), - ), - hintText: "Подъём \\ уход ко сну", - items: dropdownItemList, - onChanged: (value) {}, + Widget _buildDiaryEditScreen(BuildContext context, DiaryEditLoadedState state) { + return RefreshIndicator( + onRefresh: refreshPage(context), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + FormBuilder( + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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), + ], ), ), - SizedBox(height: 30.v), - _buildImageStack(context), - SizedBox(height: 20.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h), - child: CustomDropDown( - icon: Container( - margin: - EdgeInsets.fromLTRB(30.h, 30.v, 20.h, 30.v), - child: CustomImageView( - imagePath: ImageConstant.imgArrowLeft, - height: 10.v, - width: 18.h, - ), - ), - hintText: "Ваше настроение после приема пищи", - items: dropdownItemList3, - onChanged: (value) {}, - ), - ), - SizedBox(height: 30.v), - _buildHeaderColumn(context), - SizedBox(height: 30.v), - _buildHeaderStack(context), - ], + ), ), - ), + ], ), ), - ], + ), ), - ), - bottomNavigationBar: _buildEndDay(context), + _buildEndDay(context), + ], ), ); } + _buildEndDay(BuildContext context) { + return CustomOutlinedButton( + text: "Закончить день", + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 16.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) { + _buildHeaderRow(BuildContext context, DiaryEditLoadedState state) { + final day = state.diaryId; return Container( - margin: EdgeInsets.symmetric(horizontal: 16.h), + margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), padding: EdgeInsets.symmetric( horizontal: 24.h, vertical: 12.v, ), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder12, + 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, + mainAxisAlignment: MainAxisAlignment.center, children: [ - CustomImageView( - imagePath: ImageConstant.imgArrowLeft, - height: 2.v, - width: 20.h, - margin: EdgeInsets.only( - top: 19.v, - bottom: 17.v, - ), - ), Padding( padding: EdgeInsets.only(top: 1.v), child: Column( @@ -175,13 +151,13 @@ class DiaryEditScreen extends StatelessWidget { Align( alignment: Alignment.centerRight, child: Text( - "10 день", + "$day день", style: CustomTextStyles.titleMediumBlack, ), ), SizedBox(height: 2.v), Text( - "03-08-2023", + state.diaryDate.toString(), style: CustomTextStyles.bodyMediumBluegray800, ), ], @@ -193,309 +169,576 @@ class DiaryEditScreen extends StatelessWidget { } /// Section Widget - Widget _buildHeaderButton1(BuildContext context) { - return CustomOutlinedButton( - width: 73.h, - text: "14:52", - ); - } - - /// Section Widget - Widget _buildEnterProductName(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), - child: CustomTextFormField( - controller: enterProductNameController, - hintText: "Введите названия продукта", - ), - ); - } - - /// Section Widget - Widget _buildQuantity(BuildContext context) { - return CustomTextFormField( - width: 146.h, - controller: quantityController, - hintText: "Кол-во", - textInputAction: TextInputAction.done, - ); - } - - /// 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), ); } - /// Section Widget - Widget _buildAddPhoto(BuildContext context) { - return CustomOutlinedButton( - width: 195.h, - text: "Добавить фото", - rightIcon: Container( - margin: EdgeInsets.only(left: 14.h), - child: CustomImageView( - imagePath: ImageConstant.imgVector, - height: 30.v, - width: 34.h, - ), - ), - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - buttonTextStyle: theme.textTheme.bodyMedium!, - ); + addProduct(BuildContext context) { + return () { + context.read().add(AddPortionEvent()); + }; } /// Section Widget - Widget _buildImageStack(BuildContext context) { - return SizedBox( - height: 790.v, - width: 374.h, + _buildAddPhoto(BuildContext context, DiaryEditLoadedState state) { + bool isLoading = state.isLoading; + return GestureDetector( + onTap: addImage(context, isLoading), child: Stack( - alignment: Alignment.bottomCenter, children: [ - Align( - alignment: Alignment.center, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 15.h), + if (!isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), padding: EdgeInsets.symmetric( - horizontal: 10.h, - vertical: 13.v, + horizontal: 40.h, + vertical: 80.v, ), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder12, + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, ), - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ + // Показывайте прелоадер, если загрузка активна + Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.only( - top: 11.v, - bottom: 7.v, - ), - child: Text( - "Время приема пищи", - style: CustomTextStyles.bodyMediumBlack900, - ), - ), - _buildHeaderButton1(context), - ], + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + "Добавить фото", + selectionColor: appTheme.blueA400, ), ), - SizedBox(height: 20.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), - child: CustomDropDown( - hintText: "Прием пищи", - items: dropdownItemList1, - contentPadding: EdgeInsets.symmetric( - horizontal: 15.h, - vertical: 9.v, - ), - onChanged: (value) {}, - ), - ), - SizedBox(height: 20.v), - _buildEnterProductName(context), - SizedBox(height: 20.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildQuantity(context), - Padding( - padding: EdgeInsets.only(left: 10.h), - child: CustomDropDown( - width: 146.h, - hintText: "Порция", - items: dropdownItemList2, - contentPadding: EdgeInsets.symmetric( - horizontal: 15.h, - vertical: 9.v, - ), - onChanged: (value) {}, - ), - ), - ], - ), - ), - SizedBox(height: 30.v), - Container( - margin: EdgeInsets.symmetric(horizontal: 10.h), - padding: EdgeInsets.symmetric( - horizontal: 8.h, - vertical: 6.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( - "Сырный суп", - style: CustomTextStyles.titleMedium16, - ), - ), - Padding( - padding: EdgeInsets.only( - left: 20.h, - top: 3.v, - bottom: 3.v, - ), - child: Text( - "400мл", - style: CustomTextStyles.titleMedium16, - ), - ), - Spacer(), - CustomImageView( - imagePath: ImageConstant.imgSearch, - height: 24.adaptSize, - width: 24.adaptSize, - ), - CustomImageView( - imagePath: ImageConstant.imgClose, - height: 28.adaptSize, - width: 28.adaptSize, - margin: EdgeInsets.only(left: 10.h), - ), - ], - ), - ), - SizedBox(height: 30.v), - _buildAddProduct(context), - SizedBox(height: 249.v), - _buildAddPhoto(context), - SizedBox(height: 29.v), - Text( - "Ваша сытость после приёма пищи", - style: CustomTextStyles.bodyMediumBlack900, - ), - SizedBox(height: 15.v), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomImageView( - imagePath: ImageConstant.imgEdit, - height: 30.adaptSize, - width: 30.adaptSize, - ), - CustomImageView( - height: 13.v, - width: 243.h, - margin: EdgeInsets.only( - left: 10.h, - top: 9.v, - bottom: 8.v, - ), - ), - CustomImageView( - height: 30.adaptSize, - width: 30.adaptSize, - margin: EdgeInsets.only(left: 10.h), - ), - ], - ), - SizedBox(height: 7.v), + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ) ], ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.only(bottom: 342.v), - child: SizedBox( - width: 374.h, - child: Divider(), + 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), // Замените на свой прелоадер при необходимости ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 203.v, - width: 303.h, - margin: EdgeInsets.only(bottom: 178.v), - child: Stack( - alignment: Alignment.topRight, - children: [ - CustomImageView( - height: 203.v, - width: 303.h, - radius: BorderRadius.circular( - 5.h, - ), - alignment: Alignment.center, - ), - Align( - alignment: Alignment.topRight, - child: Padding( - padding: EdgeInsets.only( - left: 265.h, - top: 10.v, - right: 10.h, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CustomIconButton( - height: 18.adaptSize, - width: 18.adaptSize, - padding: EdgeInsets.all(4.h), - alignment: Alignment.centerRight, - child: CustomImageView( - imagePath: ImageConstant.imgCloseBlueGray900, - ), - ), - SizedBox(height: 60.v), - CustomIconButton( - height: 28.adaptSize, - width: 28.adaptSize, - padding: EdgeInsets.all(9.h), - decoration: IconButtonStyleHelper.fillBlueA, - child: CustomImageView( - imagePath: ImageConstant.imageNotFound, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), ], ), ); } - /// Section Widget - Widget _buildHeaderColumn(BuildContext context) { + addImage(BuildContext context, bool isLoading) { + if (!isLoading) { + return () async { + final picker = ImagePicker(); + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + for (var image in images) { + context.read().add(UploadImages(image)); + } + } + }; + } + } + + _buildMeal(BuildContext context, DiaryEditLoadedState state) { + final digitsOnly = FilteringTextInputFormatter.digitsOnly; + 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( + maxWidth: MediaQuery.of(context).size.width - 260, + ), + child: GestureDetector( + onTap: () => _selectMealTime(context, state), + child: AbsorbPointer( + child: CustomTextFormField( + hintText: state.mealTimeController?.format(context).toString(), + autofocus: false, + ), + ), + ), + ), + ), + ), + ], + ), + ), + 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), + ), + ); + }, + ), + ), + ), + ); + }), + ), + SizedBox(height: 20.v), + if (state.moderationExpanded) + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: GestureDetector( + onTap: () => openCaloriesDialog(context, state), + child: Text("Отправить на модерацию"), + ), + ), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomTextFormField( + width: 150.h, + hintText: "Кол-во", + textInputAction: TextInputAction.done, + // focusNode: focusNodeQuantity, + controller: state.productQuantityController, + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly], + ), + Padding( + 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, 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, + ), + ], + ), + 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) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 8.h, + vertical: 6.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( + portion['product_name'].length > 8 ? portion['product_name'].substring(0, 8) : portion['product_name'], + style: CustomTextStyles.titleMedium16, + )), + Padding( + padding: EdgeInsets.only( + left: 20.h, + top: 3.v, + bottom: 3.v, + ), + child: Text( + portion['quantity'].toString() + ' ' + portion['portion'].toString().tr, + ), + ), + Spacer(), + GestureDetector( + onTap: removePortion(context, portion), + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: Icon( + Icons.close, + color: appTheme.blueGray300, + ), + )), + ], + ), + ); + } + + _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( horizontal: 10.h, vertical: 16.v, ), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder12, + 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, @@ -519,142 +762,96 @@ 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, ), ), ], @@ -671,7 +868,7 @@ class DiaryEditScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "255 из 250Ккал", + "${consumedValues[0].toInt()} из ${recommendedValues[0].toInt()}", style: theme.textTheme.bodySmall, ), Padding( @@ -680,7 +877,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "50 из 250 гр", + "${consumedValues[1].toInt()} из ${recommendedValues[1].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -690,17 +887,17 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "270 из 250 гр", + "${consumedValues[2].toInt()} из ${recommendedValues[2].toInt()}", style: theme.textTheme.bodySmall, ), ), Padding( padding: EdgeInsets.only( - left: 16.h, + left: 14.h, bottom: 11.v, ), child: Text( - "400 из 250 гр", + "${consumedValues[3].toInt()} из ${recommendedValues[3].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -708,110 +905,304 @@ 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 _buildHeaderColumn1(BuildContext context) { - return Align( - alignment: Alignment.center, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 15.h), - padding: EdgeInsets.symmetric( - horizontal: 50.h, - vertical: 28.v, - ), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder12, - ), - child: SizedBox( - width: 240.h, - child: Text( - "Дополнительная информация для консультанта", - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: CustomTextStyles.bodyMediumBlack900, + _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), ), - ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Дополнительная информация для консультанта", + style: theme.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + SizedBox(height: 20.v), + Container( + width: 320.h, + child: CustomTextFormField( + autofocus: false, + hintText: "Введите информацию для консультанта", + maxLines: 7, + borderDecoration: InputBorder.none, + controller: state.commentController, + ), + ), + ], ), ); } - /// Section Widget - Widget _buildHeaderStack(BuildContext context) { - return SizedBox( - height: 235.v, - width: 374.h, - child: Stack( - alignment: Alignment.bottomCenter, + _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), + ), + ], + ), + child: Column( children: [ - _buildHeaderColumn1(context), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.only(bottom: 103.v), - child: SizedBox( - width: 374.h, - child: Divider(), - ), + 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), + ], ), ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - margin: EdgeInsets.only( - left: 35.h, - right: 35.h, - bottom: 20.v, - ), - padding: EdgeInsets.symmetric( - horizontal: 14.h, - vertical: 9.v, - ), - decoration: AppDecoration.outlineBlueGray.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder5, - ), + if (state.wakeupExpanded) + Padding( + padding: const EdgeInsets.only(left: 16.0), child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 63.v), - Container( - width: 245.h, - margin: EdgeInsets.only(right: 27.h), - child: Text( - "Введите дополнитльеную информацию для консультанта", - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge, + 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, + ), ), ), + 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, + + 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), + ), + ], + ); + }, + ); + }; + } + + 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), + ), + ], + ), + 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), + ], + ), + ), + if (state.moodExpanded) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("😭"), + ScaleElement( + color: Colors.red, + isSelected: state.selectedMood == 0, + onTap: changeMood(context, 0), + ), + 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(BuildContext context, DiaryEditLoadedState state) async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: state.wakeupController ?? TimeOfDay.now(), ); + if (pickedTime != null && pickedTime != state.wakeupController) { + state.wakeupController = pickedTime; + context.read().add(WakeupEvent(pickedTime)); + } + } + + Future _selectRetireTime(BuildContext context, DiaryEditLoadedState state) async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: state.retireController ?? TimeOfDay.now(), + ); + if (pickedTime != null && pickedTime != state.retireController) { + state.retireController = pickedTime; + context.read().add(RetireEvent(pickedTime)); + } + } + + Future _selectMealTime(BuildContext context, DiaryEditLoadedState state) async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: state.mealTimeController ?? TimeOfDay.now(), + ); + if (pickedTime != null && pickedTime != state.mealTimeController) { + state.mealTimeController = pickedTime; + context.read().add(ChangeMealTime(pickedTime)); + } } } + +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 b2aaf181..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 @@ -1,16 +1,25 @@ // ignore_for_file: must_be_immutable import 'package:equatable/equatable.dart'; +import 'package:eatvio_app/data/models/selectionPopupModel/selection_popup_model.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 DiaryEditModel extends Equatable { - DiaryEditModel(); + DiaryEditModel({this.mealList = const [], this.portionList = const []}); - DiaryEditModel copyWith() { - return DiaryEditModel(); + List mealList; + List? portionList; + DiaryEditModel copyWith({ + List? mealList, + List? portionList, + }) { + return DiaryEditModel( + mealList: mealList ?? this.mealList, + portionList: portionList ?? this.portionList, + ); } @override - List get props => []; + 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..e4e0cbc8 --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart @@ -0,0 +1,94 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..516a9312 --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/diet_create_screen.dart @@ -0,0 +1,91 @@ +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'; + +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('Создать'), + ) + ]), + ) + ]), + ), + ), + ); + } +} 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..e69101c0 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed( + 'dietScreen', + extra: 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..a0f988d1 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart @@ -0,0 +1,111 @@ +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: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('Сохранить'), + ) + ], + ), + ) + ], + ), + ), + ), + ); + } +} 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..dbc622fa --- /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:eatvio_app/app_router.dart'; +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'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..68834d09 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/diet_select_screen.dart @@ -0,0 +1,178 @@ +import 'package:go_router/go_router.dart'; + +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: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 Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (state is DietSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, DietSelectLoadedState state) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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: () => GoRouter.of(context).pop(), + ), + 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: () => GoRouter.of(context).goNamed('dietCreateScreen'), + child: Icon(Icons.create_outlined), + ), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildDietList(context, state) + ], + ), + ), + ), + ), + ); + } + + _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: () => GoRouter.of(context).goNamed( + 'dietScreen', + extra: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => GoRouter.of(context).goNamed( + 'dietEditScreen', + extra: post?['id'].toString(), + ), + child: Icon(Icons.edit), + ), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..5fcd3a1d --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_show_screen/models/diet_show_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..0c06f5d9 --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/diet_show_screen.dart @@ -0,0 +1,134 @@ +import 'package:go_router/go_router.dart'; + +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: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: () => GoRouter.of(context).goNamed( + 'dietEditScreen', + extra: state.diet?['id'].toString(), + ), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context.read().add(DeleteEntry(state.diet?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + ), + ); + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case '/diet': + return DietScreen.builder(context); + default: + return DietScreen(); + } + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..77f0f1a1 --- /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:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..be78f7e3 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart @@ -0,0 +1,154 @@ +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_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) { + return 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: 'Создать', + ), + ], + ), + ], + ), + ) + ], + ), + ), + ); + } +} + +_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); +} 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..b3a30278 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('blogPostScreen', extra: 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..5c38ffb7 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart @@ -0,0 +1,118 @@ +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: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, blog_postID) { + 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 Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (state is BlogPostEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPostEdit(context, state); + } else if (state is BlogPostEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ); + } + + _buildBlogPostEdit(BuildContext context, BlogPostEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return 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('Сохранить'), + ) + ], + ), + ) + ], + ), + ), + ); + } +} 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..9f5f11f2 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.dart'; +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'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..37a2ce24 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -0,0 +1,394 @@ +import 'package:go_router/go_router.dart'; + +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_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, String blog_postID) { + 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 Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (state is BlogPostLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPost(context, state); + } else if (state is BlogPostErrorState) { + // Произошла ошибка при загрузке данных. + return Center( + child: Text(state.errorMessage), + ); + } else { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + // Другие состояния, если они есть. + } + }, + ); + } + + _buildBlogPost(BuildContext context, BlogPostLoadedState state) { + 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), + ], + ), + ), + ), + ); + } + + _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: () => GoRouter.of(context).pop(), + ), + 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') { + GoRouter.of(context).goNamed( + 'blogPostEditScreen', + extra: 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'), + ); + } + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..1eda1feb --- /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:eatvio_app/app_router.dart'; +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'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..1af8a84e --- /dev/null +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -0,0 +1,303 @@ +import 'package:go_router/go_router.dart'; + +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_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) + ], + ), + ), + ), + ), + ), + ); + } + + _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: () => GoRouter.of(context).goNamed('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: () => GoRouter.of(context).goNamed( + 'blogPostScreen', + extra: 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); + }; + } + + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); + } + + _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..5f63f293 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/models/glossary_screen_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..ada1e86c --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/glossary_screen.dart @@ -0,0 +1,317 @@ +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_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 Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (state is GlossaryLoadedState) { + // Данные загружены, можно строить экран. + return _buildGlossary(context, state); + } else if (state is GlossaryErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ); + } + + _buildGlossary(BuildContext context, GlossaryLoadedState state) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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) + ], + ), + ), + ), + ), + ); + } + + _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 page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case '/glossary': + return GlossaryScreen.builder(context); + default: + return GlossaryScreen(); + } + } + + _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..51e08b9c --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_create_screen/models/plan_create_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..bd354af0 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/plan_create_screen.dart @@ -0,0 +1,93 @@ +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'; + +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('Создать'), + ) + ]), + ) + ], + ), + ), + ), + ); + } +} 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..1e7e4f27 --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_edit_screen/models/plan_edit_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed( + 'planScreen', + extra: 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..7baa33d2 --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart @@ -0,0 +1,111 @@ +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: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('Сохранить'), + ) + ], + ), + ) + ], + ), + ), + ), + ); + } +} 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..b74c0ede --- /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:eatvio_app/app_router.dart'; +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'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..810bd3ae --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/plan_select_screen.dart @@ -0,0 +1,179 @@ +import 'package:go_router/go_router.dart'; + +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: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 Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (state is PlanSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, PlanSelectLoadedState state) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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: () => GoRouter.of(context).pop(), + ), + 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: () => GoRouter.of(context).goNamed('planCreateScreen'), + child: Icon(Icons.create_outlined), + ), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildPlanList(context, state) + ], + ), + ), + ), + ), + ); + } + + _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: () => GoRouter.of(context).goNamed( + 'planScreen', + extra: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => GoRouter.of(context).goNamed( + 'planEditScreen', + extra: post?['id'].toString(), + ), + child: Icon(Icons.edit), + ), + ), + ); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..e920db89 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_show_screen/models/plan_show_model.dart'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..a19f6dac --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/plan_show_screen.dart @@ -0,0 +1,134 @@ + +import 'package:go_router/go_router.dart'; + +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: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('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + ), + ); + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case '/plan': + return PlanScreen.builder(context); + default: + return PlanScreen(); + } + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..db799d25 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,7 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:eatvio_app/app_router.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '/core/app_export.dart'; @@ -6,6 +10,8 @@ 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'; +import 'package:go_router/go_router.dart'; part 'my_profile_event.dart'; part 'my_profile_state.dart'; @@ -14,6 +20,7 @@ part 'my_profile_state.dart'; class MyProfileBloc extends Bloc { MyProfileBloc(MyProfileState initialState) : super(initialState) { on(_onInitialize); + on(_uploadAvatar); } Future _onInitialize( @@ -23,24 +30,29 @@ class MyProfileBloc extends Bloc { try { final box = await Hive.openBox('token'); final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('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')) { - // Все ключевые поля присутствуют, продолжаем обработку + final avatarBox = await Hive.openBox('avatar'); + final avatar = data['user']['profile_photo_path']; + print(avatar); + avatarBox.put('avatar', avatar); if (state is MyProfileLoadedState) { - // Если текущее состояние уже загружено, обновляем его var currentState = state as MyProfileLoadedState; var plans = [] ..addAll(data["finishedPlans"]) @@ -49,9 +61,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 +98,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 +131,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 +166,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 +219,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 +237,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 3b65469e..e6f97062 100644 --- a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart @@ -1,20 +1,16 @@ -import 'dart:ffi'; +import 'package:go_router/go_router.dart'; 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'; @@ -24,10 +20,13 @@ import 'dart:convert'; class MyProfileScreen extends StatelessWidget { static Widget builder(BuildContext context) { return BlocProvider( - create: (context) => - MyProfileBloc(MyProfileState(myProfileModelObj: MyProfileModel())) - ..add(MyProfileInitialEvent()), - child: MyProfileScreen()); + create: (context) => MyProfileBloc( + MyProfileState( + myProfileModelObj: MyProfileModel(), + ), + )..add(MyProfileInitialEvent()), + child: MyProfileScreen(), + ); } Widget build(BuildContext context) { @@ -35,9 +34,10 @@ class MyProfileScreen extends StatelessWidget { builder: (context, state) { if (state is MyProfileInitialState) { // Это начальное состояние, данные ещё не загружены. - return CircularProgressIndicator( - color: Colors.white, - backgroundColor: Colors.white, + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), ); } else if (state is MyProfileLoadedState) { // Данные загружены, можно строить экран. @@ -47,139 +47,159 @@ class MyProfileScreen extends StatelessWidget { return Center(child: Text(state.errorMessage)); } else { // Другие состояния, если они есть. - return Container(); // Пустой контейнер для необработанных состояний. + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); } }, ); } - 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(), + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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, ), - SizedBox(height: 5.v), - ])), - bottomNavigationBar: _buildBottomBar(context))); + 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), + ], + ), + ), + ), + ), + ); } /// 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) 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 () { + GoRouter.of(context).goNamed('mySettingsScreen'); + }; } - Widget _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, @@ -190,12 +210,7 @@ class MyProfileScreen extends StatelessWidget { SizedBox(height: 50.v), _buildChart( context, - state.chartPoints?.values - .map((e) => e['weight']) - .toList() - .map((i) => i.toDouble()) - .toList() ?? - [0.0, 0.0, 0.0, 0.0], + state.chartPoints?.values.map((e) => e['weight']).toList().map((i) => i.toDouble()).toList() ?? [0.0, 0.0, 0.0, 0.0], state.chartDates, ) ])); @@ -210,21 +225,13 @@ class MyProfileScreen extends StatelessWidget { SizedBox(height: 50.v), _buildChart( context, - _bodyMassIndex( - state.chartPoints?.values - .map((e) => e['weight']) - .toList() - .map((i) => i.toDouble()) - .toList() ?? - [0.0, 0.0, 0.0, 0.0], + _bodyMassIndex(state.chartPoints?.values.map((e) => e['weight']).toList().map((i) => i.toDouble()).toList() ?? [0.0, 0.0, 0.0, 0.0], state.userDetails?['height'].toInt() ?? 160), state.chartDates), ]); case 3: //График обхватов return GestureDetector( - onTap: () { - _editCircumference(context, state); - }, + onTap: () => _editCircumference(context, state), child: Column(children: [ Text( "circumferences_chart".tr, @@ -233,8 +240,7 @@ class MyProfileScreen extends StatelessWidget { ), ), SizedBox(height: 30.v), - _buildCircumferenceChart( - context, state.circumferences, state.chartDates), + _buildCircumferenceChart(context, state.circumferences, state.chartDates), _buildChartLegend() ])); default: @@ -246,7 +252,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( @@ -278,10 +284,7 @@ class MyProfileScreen extends StatelessWidget { ); } - /// Section Widget - Widget _buildChart( - BuildContext context, List chartPoints, List? dates) { -// Определение осей графика + _buildChart(BuildContext context, List chartPoints, List? dates) { List spots = []; for (int i = 0; i < 4; i++) { spots.add( @@ -289,8 +292,7 @@ class MyProfileScreen extends StatelessWidget { ); } - Widget bottomTitleWidgets( - double value, TitleMeta meta, double chartWidth, List? dates) { + Widget bottomTitleWidgets(double value, TitleMeta meta, double chartWidth, List? dates) { if (value % 1 != 0) { return Container(); } @@ -302,8 +304,7 @@ class MyProfileScreen extends StatelessWidget { int index = value.toInt(); if (index >= 0 && index < dates!.length) { // Проверка на допустимый индекс в массиве dates - String formattedDate = - DateFormat('dd.MM').format(DateTime.parse(dates[index])); + String formattedDate = DateFormat('dd.MM').format(DateTime.parse(dates[index])); return SideTitleWidget( axisSide: meta.axisSide, space: 16, @@ -342,20 +343,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, @@ -373,18 +361,15 @@ class MyProfileScreen extends StatelessWidget { dotData: const FlDotData(show: false), ), ], - minY: chartPoints.reduce( - (value, element) => value < element ? value : element), - maxY: chartPoints.reduce( - (value, element) => value > element ? value : element), + 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), + getTitlesWidget: (value, meta) => leftTitleWidgets(value, meta, constraints.maxWidth), reservedSize: 56, ), drawBelowEverything: true, @@ -395,8 +380,7 @@ class MyProfileScreen extends StatelessWidget { bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - getTitlesWidget: (value, meta) => bottomTitleWidgets( - value, meta, constraints.maxWidth, dates), + getTitlesWidget: (value, meta) => bottomTitleWidgets(value, meta, constraints.maxWidth, dates), reservedSize: 36, interval: 1, ), @@ -412,9 +396,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], @@ -425,9 +407,7 @@ class MyProfileScreen extends StatelessWidget { dashArray: [8, 2], strokeWidth: 0.8, ), - checkToShowVerticalLine: (value) { - return value.toInt() == 0; - }, + checkToShowVerticalLine: valuteToInt(), ), borderData: FlBorderData(show: false), ), @@ -438,11 +418,31 @@ class MyProfileScreen extends StatelessWidget { ); } - Widget _buildCircumferenceChart( - BuildContext context, chartPoints, List? dates) { + 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 = - chartPoints.values.map((e) => e['chest_circumference']).toList(); + final chestCircumferences = chartPoints.values.map((e) => e['chest_circumference']).toList(); List spotsChest = []; for (int i = 0; i < 4; i++) { chestCircumferences[i] = chestCircumferences[i] * 10 / 10; @@ -450,8 +450,7 @@ class MyProfileScreen extends StatelessWidget { FlSpot((i * 10 / 10) + 1, chestCircumferences[i] * 10 / 10), ); } - final waistCircumferences = - chartPoints.values.map((e) => e['waist_circumference']).toList(); + final waistCircumferences = chartPoints.values.map((e) => e['waist_circumference']).toList(); List spotsWaist = []; for (int i = 0; i < 4; i++) { waistCircumferences[i] = waistCircumferences[i] * 10 / 10; @@ -459,8 +458,7 @@ class MyProfileScreen extends StatelessWidget { FlSpot((i * 10 / 10) + 1, waistCircumferences[i] * 10 / 10), ); } - final hipCircumferences = - chartPoints.values.map((e) => e['hip_circumference']).toList(); + final hipCircumferences = chartPoints.values.map((e) => e['hip_circumference']).toList(); List spotsHip = []; for (int i = 0; i < 4; i++) { hipCircumferences[i] = hipCircumferences[i] * 10 / 10; @@ -469,14 +467,9 @@ class MyProfileScreen extends StatelessWidget { ); } - final allCircumferences = [ - ...chestCircumferences, - ...hipCircumferences, - ...waistCircumferences - ]; + final allCircumferences = [...chestCircumferences, ...hipCircumferences, ...waistCircumferences]; - Widget bottomTitleWidgets( - double value, TitleMeta meta, double chartWidth, List? dates) { + Widget bottomTitleWidgets(double value, TitleMeta meta, double chartWidth, List? dates) { if (value % 1 != 0) { return Container(); } @@ -488,8 +481,7 @@ class MyProfileScreen extends StatelessWidget { int index = value.toInt(); if (index >= 0 && index < dates!.length) { // Проверка на допустимый индекс в массиве dates - String formattedDate = - DateFormat('dd.MM').format(DateTime.parse(dates[index])); + String formattedDate = DateFormat('dd.MM').format(DateTime.parse(dates[index])); return SideTitleWidget( axisSide: meta.axisSide, space: 16, @@ -528,20 +520,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, @@ -581,18 +560,15 @@ class MyProfileScreen extends StatelessWidget { dotData: const FlDotData(show: false), ), ], - minY: allCircumferences.reduce( - (value, element) => value < element ? value : element), - maxY: allCircumferences.reduce( - (value, element) => value > element ? value : element), + minY: allCircumferences.reduce((value, element) => value < element ? value : element), + maxY: allCircumferences.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), + getTitlesWidget: (value, meta) => leftTitleWidgets(value, meta, constraints.maxWidth), reservedSize: 56, ), drawBelowEverything: true, @@ -603,8 +579,7 @@ class MyProfileScreen extends StatelessWidget { bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - getTitlesWidget: (value, meta) => bottomTitleWidgets( - value, meta, constraints.maxWidth, dates), + getTitlesWidget: (value, meta) => bottomTitleWidgets(value, meta, constraints.maxWidth, dates), reservedSize: 36, interval: 1, ), @@ -620,9 +595,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], @@ -633,9 +606,7 @@ class MyProfileScreen extends StatelessWidget { dashArray: [8, 2], strokeWidth: 0.8, ), - checkToShowVerticalLine: (value) { - return value.toInt() == 0; - }, + checkToShowVerticalLine: valuteToInt(), ), borderData: FlBorderData(show: false), ), @@ -646,127 +617,97 @@ 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, - List? imagePaths, List? states) { + _buildPlans(BuildContext context, List? plans, List? imagePaths, List? states) { return Padding( padding: EdgeInsets.only(left: 7.h), child: Column(children: [ Padding( padding: EdgeInsets.only(left: 15.h), - child: Text("plans_complete".tr, - style: CustomTextStyles.titleMediumBlack - .copyWith(color: appTheme.black900))), + child: Text("plans_complete".tr, style: CustomTextStyles.titleMediumBlack.copyWith(color: appTheme.black900))), SizedBox(height: 5.v), Container( - decoration: AppDecoration.outlineBlueGray - .copyWith(borderRadius: BorderRadiusStyle.roundedBorder5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 105.v, - width: 343.h, - child: - Stack(alignment: Alignment.bottomCenter, children: [ - Align( - alignment: Alignment.topCenter, - child: ListView.builder( - shrinkWrap: true, - itemCount: plans?.length ?? 0, - itemBuilder: (context, i) { - String state = states?[i]; - var boxColor = appTheme.greenA700; - switch (states?[i]) { - case 'active': - boxColor = appTheme.greenA700; - break; - case 'archived': // График ИМТ - boxColor = appTheme.gray400; - break; - case 'draft': - boxColor = appTheme.deepOrange900; - break; - case 'in_progress': //График обхватов - boxColor = appTheme.lightGreen300; - break; - } - return SizedBox( - height: 40.v, - width: 343.h, - child: Stack( - alignment: Alignment.bottomRight, - children: [ - Align( - alignment: - Alignment.topCenter, - child: Container( - margin: EdgeInsets.only( - bottom: 11.v), - padding: - EdgeInsets.symmetric( - horizontal: 6.h, - vertical: 4.v), - decoration: AppDecoration - .fillGray - .copyWith( - borderRadius: - BorderRadiusStyle - .roundedBorder5), - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Padding( - padding: EdgeInsets - .only( - bottom: 1 - .v), - child: Text( - plans![i], - style: CustomTextStyles - .titleSmallBluegray800Bold)), - Container( - height: 19.v, - width: 100.h, - decoration: BoxDecoration( - color: - boxColor, - borderRadius: - BorderRadius.circular( - 5.h))) - ]))), - Align( - alignment: Alignment.center, - child: Container( - margin: EdgeInsets.only( - bottom: 12.h), - child: Image.network( - dotenv.env[ - 'STORAGE_URL']! + - imagePaths?[i], - height: 25.adaptSize, - width: 25.adaptSize, - ))), - Align( - alignment: - Alignment.bottomRight, - child: Container( - width: 70.h, - margin: EdgeInsets.only( - bottom: 14.h, - right: 22.h), - child: Text("$state".tr, - maxLines: 3, - overflow: TextOverflow - .ellipsis, - style: theme.textTheme - .titleSmall))) - ])); - })), - ])) - ])) + decoration: AppDecoration.outlineBlueGray.copyWith(borderRadius: BorderRadiusStyle.roundedBorder5), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 105.v, + width: 343.h, + child: Stack(alignment: Alignment.bottomCenter, children: [ + Align( + alignment: Alignment.topCenter, + child: ListView.builder( + itemCount: plans?.length ?? 0, + itemBuilder: (context, i) { + String state = states?[i]; + var boxColor = appTheme.greenA700; + switch (states?[i]) { + case 'active': + boxColor = appTheme.greenA700; + break; + case 'archived': // График ИМТ + boxColor = appTheme.gray400; + break; + case 'draft': + boxColor = appTheme.deepOrange900; + break; + case 'in_progress': //График обхватов + boxColor = appTheme.lightGreen300; + break; + } + return SizedBox( + height: 40.v, + width: 343.h, + child: Stack(alignment: Alignment.bottomRight, children: [ + Align( + alignment: Alignment.topCenter, + child: Container( + margin: EdgeInsets.only(bottom: 11.v), + padding: EdgeInsets.symmetric(horizontal: 6.h, vertical: 4.v), + decoration: AppDecoration.fillGray.copyWith(borderRadius: BorderRadiusStyle.roundedBorder5), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Padding( + padding: EdgeInsets.only(bottom: 1.v), + child: Text(plans![i], style: CustomTextStyles.titleSmallBluegray800Bold)), + Container( + height: 19.v, + width: 100.h, + decoration: BoxDecoration(color: boxColor, borderRadius: BorderRadius.circular(5.h))) + ]))), + Align( + alignment: Alignment.center, + child: Container( + margin: EdgeInsets.only(bottom: 12.h), + child: Image.network( + dotenv.env['STORAGE_URL']! + imagePaths?[i], + height: 25.adaptSize, + width: 25.adaptSize, + ))), + Align( + alignment: Alignment.bottomRight, + child: Container( + width: 70.h, + margin: EdgeInsets.only(bottom: 14.h, right: 22.h), + child: Text("$state".tr, maxLines: 3, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleSmall))) + ])); + })), + ])) + ])) ])); } @@ -783,94 +724,60 @@ 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, @@ -878,30 +785,68 @@ class MyProfileScreen extends StatelessWidget { ), ], backgroundColor: Colors.white, - titlePadding: EdgeInsets.all( - 20.0), // Можете настроить отступы по вашему желанию + titlePadding: EdgeInsets.all(20.0), // Можете настроить отступы по вашему желанию ); }, ); } + 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) { + GoRouter.of(context).go('/my-profile-screen'); + } 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, ], ), ], @@ -909,47 +854,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, @@ -957,10 +866,53 @@ 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) { + GoRouter.of(context).go('/my-profile-screen'); + } 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..a5be7c00 100644 --- a/lib/presentation/profile/my_profile_screen/my_settings_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_settings_screen.dart @@ -1,26 +1,22 @@ +import 'package:go_router/go_router.dart'; + 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_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) { return BlocProvider( - create: (context) => - MyProfileBloc(MyProfileState(myProfileModelObj: MyProfileModel())) - ..add(MyProfileInitialEvent()), - child: MySettingsScreen()); + create: (context) => MyProfileBloc(MyProfileState(myProfileModelObj: MyProfileModel()))..add(MyProfileInitialEvent()), child: MySettingsScreen()); } Widget build(BuildContext context) { @@ -28,640 +24,162 @@ class MySettingsScreen extends StatelessWidget { builder: (context, state) { if (state is MyProfileInitialState) { // Это начальное состояние, данные ещё не загружены. - return CircularProgressIndicator( - color: Colors.white, - backgroundColor: Colors.white, + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), ); } else if (state is MyProfileLoadedState) { // Данные загружены, можно строить экран. return _buildSetingsScreen(context, state); } else { // Другие состояния, если они есть. - return Container(); // Пустой контейнер для необработанных состояний. + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); } }, ); } - Widget _buildSetingsScreen(BuildContext context, MyProfileLoadedState state) { - mediaQueryData = MediaQuery.of(context); + _buildSetingsScreen(BuildContext context, MyProfileLoadedState state) { - 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-н.в.']), - ], + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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: () => GoRouter.of(context).pop(), + ), + 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)) - ])), + ), + ), + ), ); } - Widget _buildBottomBar(BuildContext context) { - return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; } onTapArrowLeft(BuildContext context) { - 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(), - ], - ); + GoRouter.of(context).pop(); } } 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..881e14a8 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart'; +import 'package:go_router/go_router.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); + on(_addStep); + on(_removeStep); + on(_reloadScreen); + } + + _onInitialize( + RecipeCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + title: currentState.title, + token: currentState.token, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); + } else { + final Map step = { + 'description': '', + 'image': '', + }; + emit(RecipeCreateLoadedState( + token: token, + title: TextEditingController(), + description: TextEditingController(), + video_url: TextEditingController(), + tags: TextEditingController(), + stepDescriptions: [TextEditingController()], + steps: [step])); + } + } + + FutureOr _addStep( + AddStep event, Emitter emit) async { + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = + currentState.stepDescriptions; + + updatedSteps!.add({ + 'description': '', + 'image': '', + }); + updatedStepDescriptions!.add(TextEditingController()); + emit(currentState.copyWith( + steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + + FutureOr _removeStep( + RemoveStep event, Emitter emit) async { + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = + currentState.stepDescriptions; + + updatedSteps!.removeAt(event.id); + updatedStepDescriptions!.removeAt(event.id); + emit(currentState.copyWith( + steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + + 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, + currentState.steps, + ); + + if (response.statusCode == 200) { + AppRouter.navigatorKey.currentContext?.goNamed('recipeSelectScreen'); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendRecipe(String? title, String? video_url, + String? description, List? steps) async { + String url = '${dotenv.env['SERVER_ROOT']!}/recipe/create'; + final Map credentials = { + 'title': title, + 'description': description, + 'video_url': video_url, + 'steps': steps, + 'status': 'active', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } + + FutureOr _reloadScreen( + RecipeCreateEvent? event, Emitter emit) async { + print('Reloading..'); + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + title: currentState.title, + token: currentState.token, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); + } + } +} 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..085045d4 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart @@ -0,0 +1,40 @@ +// 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 => []; +} + +class AddStep extends RecipeCreateEvent { + AddStep(); + @override + List get props => []; +} + +class RemoveStep extends RecipeCreateEvent { + RemoveStep(this.id); + final int id; + @override + List get props => [id]; +} + +class ReloadScreen extends RecipeCreateEvent { + ReloadScreen(); + @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..8545b837 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart @@ -0,0 +1,88 @@ +// 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.token, + this.title, + this.description, + this.video_url, + this.tags, + this.stepDescriptions, + this.steps}); + + RecipeCreateModel? recipeModelObj; + String? token; + TextEditingController? title; + TextEditingController? description; + TextEditingController? video_url; + TextEditingController? tags; + List? steps; + List? stepDescriptions; + @override + List get props => [ + recipeModelObj, + title, + token, + description, + video_url, + steps, + tags, + stepDescriptions + ]; + RecipeCreateLoadedState copyWith({ + RecipeCreateModel? recipeModelObj, + String? token, + TextEditingController? title, + TextEditingController? description, + TextEditingController? video_url, + TextEditingController? tags, + List? steps, + List? stepDescriptions, + }) { + return RecipeCreateLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + token: token ?? this.token, + steps: steps ?? this.steps, + tags: tags ?? this.tags, + stepDescriptions: stepDescriptions ?? this.stepDescriptions); + } +} + +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..3a81754a --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart @@ -0,0 +1,391 @@ +import 'package:go_router/go_router.dart'; + +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_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 RefreshIndicator( + onRefresh: refreshPage(context), + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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) + ]), + ))); + } + + _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), + ]), + ); + } + + refreshPage(BuildContext context) { + return () async { + context.read().add(ReloadScreen()); + }; + } + + _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.stepDescriptions?[i], + 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), + SizedBox(height: 16.0), + if (isFirstStep && isLastStep) + CustomOutlinedButton( + onPressed: () => context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + if (isLastStep && !isFirstStep) // Добавляем кнопку "Добавить шаг" только для последнего шага + Column( + children: [ + SizedBox(height: 16.0), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + onPressed: () => context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "х", + onPressed: () => context.read().add(RemoveStep(i)), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ), + ], + ), + if (!isLastStep && !isFirstStep) + CustomOutlinedButton( + onPressed: () => context.read().add(RemoveStep(i)), + text: "x", + 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.tags, + 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.video_url, + 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, + ), + ]), + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..9c982dd4 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart @@ -0,0 +1,228 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart'; +import 'package:go_router/go_router.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); + on(_addStep); + on(_removeStep); + on(_reloadScreen); + } + + _onInitialize( + RecipeEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('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, + title: currentState.title, + token: currentState.token, + recipe: currentState.recipe, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); + } else { + final Map defaultStep = { + 'description': '', + 'image': '', + }; + + List steps = data['steps']; + List stepDescriptions = []; + + if (data['steps'] == null) { + stepDescriptions.add(TextEditingController()); + data['steps'].add(defaultStep); + } else { + for (var step in steps) { + stepDescriptions.add(TextEditingController(text: step['description'])); + } + } + + emit(RecipeEditLoadedState( + token: token, + recipe: data, + title: TextEditingController(text: data['title']), + description: TextEditingController(text: data['description']), + video_url: TextEditingController(text: data['video_url']), + tagsController: TextEditingController(), + tags: await _fetchTags(data['id']), + stepDescriptions: stepDescriptions, + steps: steps, + )); + } + } 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) { + AppRouter.navigatorKey.currentContext?.goNamed( + 'recipeScreen', + extra: currentState.recipe?['id'].toString(), + ); + } else { + print('Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr _addStep(AddStep event, Emitter emit) async { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = currentState.stepDescriptions; + + updatedSteps!.add({ + 'description': '', + 'image': '', + }); + updatedStepDescriptions!.add(TextEditingController()); + emit(currentState.copyWith(steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + + FutureOr _removeStep(RemoveStep event, Emitter emit) async { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = currentState.stepDescriptions; + + updatedSteps!.removeAt(event.id); + updatedStepDescriptions!.removeAt(event.id); + emit(currentState.copyWith(steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + + 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; + } + + _fetchTags(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getTags/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + FutureOr _reloadScreen(RecipeEditEvent? event, Emitter emit) async { + print('Reloading..'); + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + recipe: currentState.recipe, + title: currentState.title, + token: currentState.token, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); + } + } +} 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..c6d49d38 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart @@ -0,0 +1,40 @@ +// 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 => []; +} + +class AddStep extends RecipeEditEvent { + AddStep(); + @override + List get props => []; +} + +class RemoveStep extends RecipeEditEvent { + RemoveStep(this.id); + final int id; + @override + List get props => [id]; +} + +class ReloadScreen extends RecipeEditEvent { + ReloadScreen(); + @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..af03fcfe --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart @@ -0,0 +1,89 @@ +// 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.token, + this.recipe, + this.title, + this.description, + this.video_url, + this.tagsController, + this.stepDescriptions, + this.tags, + this.steps}); + + RecipeEditModel? recipeModelObj; + Map? recipe; + String? token; + TextEditingController? title; + TextEditingController? description; + TextEditingController? video_url; + TextEditingController? tagsController; + List? steps; + List? tags; + List? stepDescriptions; + @override + List get props => + [recipeModelObj, recipe, title, description, video_url]; + RecipeEditLoadedState copyWith({ + RecipeEditModel? recipeModelObj, + Map? recipe, + String? token, + TextEditingController? title, + TextEditingController? description, + TextEditingController? video_url, + TextEditingController? tagsController, + List? steps, + List? tags, + List? stepDescriptions, + }) { + return RecipeEditLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + token: token ?? this.token, + recipe: recipe ?? this.recipe, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + tagsController: tagsController ?? this.tagsController, + steps: steps ?? this.steps, + tags: tags ?? this.tags, + stepDescriptions: stepDescriptions ?? this.stepDescriptions, + ); + } +} + +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..c9ce2669 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart @@ -0,0 +1,433 @@ +import 'package:go_router/go_router.dart'; + +import 'bloc/recipe_edit_bloc.dart'; +import 'models/recipe_edit_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.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_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.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); + List steps = []; + if (state.steps != null) { + for (int i = 0; i < state.steps!.length; i++) { + steps.add(_buildStep(context, state, i)); + } + } + + return RefreshIndicator( + onRefresh: refreshPage(context), + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + resizeToAvoidBottomInset: false, + body: SafeArea( + child: 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) + ], + ), + ), + ), + ), + ); + } + + _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(RecipeEditLoadedState 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("Изображение")), + if (state.recipe?['image'] != null) + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.recipe?['image']), + width: double.infinity, + // Установите ширину равной ширине родительского элемента + height: 200.0, + fit: BoxFit.cover, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + ) + else + _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), + ]), + ); + } + + refreshPage(BuildContext context) { + return () async { + context.read().add(ReloadScreen()); + }; + } + + _buildAddPhoto(BuildContext context, RecipeEditLoadedState 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, RecipeEditLoadedState 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.stepDescriptions?[i], + hintText: "Введите описание шага", + maxLines: 4, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + if (state.steps?[i]['image'] != null) + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.steps?[i]['image']), + width: double.infinity, + // Установите ширину равной ширине родительского элемента + height: 200.0, + // Задайте постоянную высоту элемента + fit: BoxFit.cover, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + ) + else + _buildAddPhoto(context, state), + SizedBox(height: 16.0), + if (isFirstStep && isLastStep) + CustomOutlinedButton( + onPressed: () => context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + if (isLastStep && !isFirstStep) // Добавляем кнопку "Добавить шаг" только для последнего шага + Column( + children: [ + SizedBox(height: 16.0), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + onPressed: () => context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "х", + onPressed: () => context.read().add(RemoveStep(i)), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ), + ], + ), + if (!isLastStep && !isFirstStep) + CustomOutlinedButton( + onPressed: () => context.read().add(RemoveStep(i)), + text: "x", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); + } + + _buildAdditional(BuildContext context, RecipeEditLoadedState 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("Теги")), + _buildEditableTags(state.tags), + SizedBox(height: 8.0), + Align(alignment: Alignment.centerLeft, child: Text("Ссылка на видео (необязательно)")), + CustomTextFormField( + controller: state.video_url, + 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, + ), + ]), + ); + } + + _buildEditableTags(List? tags) { + String initialText = tags!.map((tag) => tag['name']).join(', '); + + return CustomTextFormField( + initialValue: initialText, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + onChanged: _trimTags, + ); + } + + void _trimTags(newValue) { + List newTagNames = newValue!.split(',').map((tag) => tag.trim()).toList(); + List> newTags = newTagNames.map((tagName) => {'name': tagName}).toList(); + print(newTags); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..a7b0d375 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.dart'; +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'; +import 'package:go_router/go_router.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) { + AppRouter.navigatorKey.currentContext?.goNamed('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..2a3a108e --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart @@ -0,0 +1,177 @@ +import 'package:go_router/go_router.dart'; + +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: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 Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (state is RecipeSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeScreen(context, state); + } else { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ); + } + + _buildRecipeScreen(BuildContext context, RecipeSelectLoadedState state) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + 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: () => GoRouter.of(context).pop(), + ), + 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: () => GoRouter.of(context).goNamed('recipeCreateScreen'), + child: Icon(Icons.create_outlined), + ), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildRecipeList(context, state), + ], + ), + ), + ), + ), + ); + } + + _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: () => GoRouter.of(context).goNamed( + 'recipeScreen', + extra: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => GoRouter.of(context).goNamed( + 'recipeEditScreen', + extra: post?['id'].toString(), + ), + child: Icon(Icons.edit), + ), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); + } +} 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..a8b5e192 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart @@ -0,0 +1,201 @@ +import 'dart:async'; + +import 'package:eatvio_app/app_router.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/recipe/recipe_show_screen/models/recipe_show_model.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; +import 'package:go_router/go_router.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); + on(_createComment); + } + + _onInitialize( + RecipeInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('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, + token: currentState.token, + commentController: currentState.commentController, + comments: currentState.comments, + steps: currentState.steps, + author: currentState.author)); + } else { + emit(RecipeLoadedState( + token: token, + recipe: data, + steps: data['steps'], + author: await _fetchAuthor(data['id']), + comments: fillCommentList(await _fetchComments(data['id'])), + tags: await _fetchTags(data['id']), + commentController: TextEditingController())); + } + } 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) { + AppRouter.navigatorKey.currentContext?.goNamed('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) { + AppRouter.navigatorKey.currentContext?.goNamed('recipeSelectScreen'); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeErrorState('Ошибка: ${response.statusCode}')); + } + } + + FutureOr _createComment(CreateComment event, Emitter emit) async { + if (state is RecipeLoadedState) { + var currentState = state as RecipeLoadedState; + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await _sendCommentData(currentState.token, event.body, currentState.recipe?['id']); + if (response.statusCode == 200) { + var updatedComments = fillCommentList(await _fetchComments(currentState.recipe?['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 + } + } + } + + _fetchAuthor(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/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) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getComments/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + _fetchTags(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + AppRouter.navigatorKey.currentContext?.goNamed('authScreen'); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getTags/$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']! + '/recipe/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/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..7510c3a1 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart @@ -0,0 +1,30 @@ +// 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]; +} + +class CreateComment extends RecipeEvent { + CreateComment(this.body); + final String body; + @override + List get props => [body]; +} 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..fc3a9f49 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart @@ -0,0 +1,87 @@ +// 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, + this.author, + this.comments, + this.token, + this.commentController, + this.tags, + this.steps}); + + RecipeModel? recipeModelObj; + Map? recipe; + Map? author; + List? comments; + TextEditingController? commentController; + List? steps; + List? tags; + String? token; + @override + List get props => [ + recipeModelObj, + recipe, + author, + comments, + token, + commentController, + steps + ]; + RecipeLoadedState copyWith( + {RecipeModel? recipeModelObj, + Map? recipe, + Map? author, + TextEditingController? commentController, + String? token, + List? tags, + List? steps, + List? comments}) { + return RecipeLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + recipe: recipe ?? this.recipe, + author: author ?? this.author, + tags: tags ?? this.tags, + token: token ?? this.token, + commentController: commentController ?? this.commentController, + steps: steps ?? this.steps, + comments: comments ?? this.comments, + ); + } +} + +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..f1835f8e --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart @@ -0,0 +1,458 @@ +import 'package:go_router/go_router.dart'; + +import 'bloc/recipe_show_bloc.dart'; +import 'models/recipe_show_model.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/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_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 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); + List steps = []; + if (state.steps != null) { + for (int i = 0; i < state.steps!.length; i++) { + steps.add(_buildStep(context, state, i)); + } + } + return Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 15.v), + _buildHeaderRow(context, state), + SizedBox(height: 15.v), + _buildRecipeData(context, state), + SizedBox(height: 15.v), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: steps, + ), + SizedBox(height: 15.v), + _buildTextField(context, state), + SizedBox(height: 15.v), + _buildCommentSection(context, state), + ], + ), + )); + } + + _buildHeaderRow(BuildContext context, RecipeLoadedState 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: () => GoRouter.of(context).pop(), + ), + 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, + ), + ), + ], + ), + ), + ], + ), + ); + } + + _buildRecipeData(BuildContext context, RecipeLoadedState 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.recipe?['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.recipe?['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + Align( + alignment: Alignment.topLeft, + child: Text( + DateFormat('dd.MM.yyyy').format(DateTime.parse(state.recipe?['updated_at'])), + style: theme.textTheme.bodyMedium, + )), + SizedBox(height: 16.0), + _buildTags(state.tags), + 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, + ) + ])) + ]), + ]), + ); + } + + _buildTags(List? tags) { + return Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: tags!.map((tag) { + return Chip( + label: Text(tag['name']), + ); + }).toList(), + ); + } + + _buildImage(RecipeLoadedState state) { + return CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.recipe?['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, RecipeLoadedState state) { + if (value == 'edit') { + GoRouter.of(context).goNamed( + 'recipeEditScreen', + extra: state.recipe?['id'].toString(), + ); + } else if (value == 'delete') { + context.read().add(DeleteEntry(state.recipe?['id'])); + } + } + + _buildTextField(BuildContext context, RecipeLoadedState 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, RecipeLoadedState 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 Column( + children: [ + Center( + child: Text('No comments available'), + ), + SizedBox( + height: 20, + ) + ], + ); + } + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + GoRouter.of(context).pop(); + } + + Widget _buildStep(BuildContext context, RecipeLoadedState state, int i) { + 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: 16.0), + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.steps?[i]['image']), + width: double.infinity, + // Установите ширину равной ширине родительского элемента + height: 200.0, + // Задайте постоянную высоту элемента + fit: BoxFit.cover, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + ), + SizedBox(height: 16.0), + Text(state.steps?[i]['description']) + ]), + ); + } +} diff --git a/lib/presentation/recipe/recipe_show_screen/widgets/comment_widget.dart b/lib/presentation/recipe/recipe_show_screen/widgets/comment_widget.dart new file mode 100644 index 00000000..c224966c --- /dev/null +++ b/lib/presentation/recipe/recipe_show_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/registration/additional_information_screen/additional_information_screen.dart b/lib/presentation/registration/additional_information_screen/additional_information_screen.dart index 0f6e122f..c855fb4b 100644 --- a/lib/presentation/registration/additional_information_screen/additional_information_screen.dart +++ b/lib/presentation/registration/additional_information_screen/additional_information_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/additional_information_bloc.dart'; import 'models/additional_information_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -16,97 +18,89 @@ import 'package:http/http.dart' as http; class AdditionalInformationScreen extends StatelessWidget { const AdditionalInformationScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => AdditionalInformationBloc( - AdditionalInformationState( - additionalInformationModelObj: - AdditionalInformationModel(userRegistrationSheet: sheet))) - ..add(AdditionalInformationInitialEvent()), + create: (context) => + AdditionalInformationBloc(AdditionalInformationState(additionalInformationModelObj: AdditionalInformationModel(userRegistrationSheet: sheet))) + ..add(AdditionalInformationInitialEvent()), child: AdditionalInformationScreen()); } @override Widget build(BuildContext context) { - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - 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: 1, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 20.v), - SizedBox(height: 73.v), - _buildTextFormField(context), - Spacer(), - BlocBuilder(builder: (context, state) { - return NextStepButton( - text: "finish".tr.toUpperCase(), - 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, - ); - }); - }), - ])))); + return Scaffold( + resizeToAvoidBottomInset: false, + 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: 1, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 20.v), + SizedBox(height: 73.v), + _buildTextFormField(context), + Spacer(), + BlocBuilder(builder: (context, state) { + return NextStepButton( + text: "finish".tr.toUpperCase(), + margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + 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); + GoRouter.of(context).goNamed('myProfileScreen'); + } 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), 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( @@ -116,18 +110,16 @@ class AdditionalInformationScreen extends StatelessWidget { width: 294.h, margin: EdgeInsets.only(left: 24.h, right: 23.h), child: Text("enter_additional_information".tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: CustomTextStyles.titleMediumSegoeUI)), + maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: CustomTextStyles.titleMediumSegoeUI)), SizedBox(height: 10.v), - BlocSelector( + BlocSelector( selector: (state) => state.editTextController, builder: (context, editTextController) { return CustomTextFormField( controller: editTextController, - textInputAction: TextInputAction.done); + hintText: "У меня крепкое здоровье, но последнее время часто утомляюсь, однако, это мне не мешает радоваться жизни", + textInputAction: TextInputAction.done, + maxLines: 8); }) ])) ])); @@ -135,24 +127,24 @@ class AdditionalInformationScreen extends StatelessWidget { /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } /// 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'), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Ok')) - ], + title: const Text('Ошибка'), + content: Text(text), + actions: [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..50d056b3 100644 --- a/lib/presentation/registration/allergies_screen/allergies_screen.dart +++ b/lib/presentation/registration/allergies_screen/allergies_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/allergies_bloc.dart'; import 'models/allergies_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -12,13 +14,9 @@ import 'package:flutter/material.dart'; class AllergiesScreen extends StatefulWidget { const AllergiesScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => AllergiesBloc(AllergiesState( - allergiesModelObj: AllergiesModel(userRegistrationSheet: sheet))) - ..add(AllergiesInitialEvent()), + create: (context) => AllergiesBloc(AllergiesState(allergiesModelObj: AllergiesModel(userRegistrationSheet: sheet)))..add(AllergiesInitialEvent()), child: AllergiesScreen()); } @@ -29,108 +27,93 @@ class AllergiesScreen extends StatefulWidget { class _AllergiesScreenState extends State { @override Widget build(BuildContext context) { - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - 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.76, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 22.v), - Text("allergies_presence".tr, - style: theme.textTheme.headlineSmall), - Spacer(flex: 36), - _buildTextFormField(context), - Spacer(flex: 54), - 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.allergiesModelObj!.userRegistrationSheet; - sheet.aboutAllergy = - state.enterInformationController!.text; - Navigator.pushNamed( - context, - AppRoutes.medicineIntakeScreen, - arguments: sheet, - ); - }); - }), - BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.allergiesModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.medicineIntakeScreen, - arguments: sheet, - ); - }, - child: Text("skip".tr, style: theme.textTheme.bodyLarge), - ); - }), - ])))); + return Scaffold( + resizeToAvoidBottomInset: false, + 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.76, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 22.v), + Text("allergies_presence".tr, style: theme.textTheme.headlineSmall), + Spacer(flex: 36), + _buildTextFormField(context), + Spacer(flex: 54), + 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: onTapContinue(state, context)); + }), + BlocBuilder(builder: (context, state) { + return GestureDetector( + onTap: onTapSkip(state, context), + child: Text("skip".tr, style: theme.textTheme.bodyLarge), + ); + }), + ]))); + } + + onTapSkip(AllergiesState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.allergiesModelObj!.userRegistrationSheet; + GoRouter.of(context).goNamed('medicineIntakeScreen', extra: sheet); + }; + } + + onTapContinue(AllergiesState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.allergiesModelObj!.userRegistrationSheet; + sheet.aboutAllergy = state.enterInformationController!.text; + GoRouter.of(context).goNamed('medicineIntakeScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), 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), + Text("fill_in_information".tr, style: CustomTextStyles.titleMediumSegoeUI), SizedBox(height: 2.v), BlocSelector( selector: (state) => state.enterInformationController, builder: (context, enterInformationController) { return CustomTextFormField( - controller: enterInformationController, - hintText: "enter_information".tr, - textInputAction: TextInputAction.done, - maxLines: 8); + controller: enterInformationController, hintText: "enter_information".tr, textInputAction: TextInputAction.done, maxLines: 8); }) ]); } /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } } 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..781552ef 100644 --- a/lib/presentation/registration/app_navigation_screen/app_navigation_screen.dart +++ b/lib/presentation/registration/app_navigation_screen/app_navigation_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/app_navigation_bloc.dart'; import 'models/app_navigation_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -49,92 +51,77 @@ class _AppNavigationScreenState extends State { _buildScreenTitle( context, screenTitle: "date_of_birth".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.dateOfBirthScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('dateOfBirthScreen'), ), _buildScreenTitle( context, screenTitle: "city_of_living".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.cityOfLivingScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('cityOfLivingScreen'), ), _buildScreenTitle( context, screenTitle: "body_circumferences".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.bodyCircumferencesScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('bodyCircumferencesScreen'), ), _buildScreenTitle( context, screenTitle: "weight".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.weightScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('weightScreen'), ), _buildScreenTitle( context, screenTitle: "allergies".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.allergiesScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('allergiesScreen'), ), _buildScreenTitle( context, screenTitle: "medicine_intake".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.medicineIntakeScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('medicineIntakeScreen'), ), _buildScreenTitle( context, screenTitle: "splash_screen".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.splashScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('splashScreen'), ), _buildScreenTitle( context, screenTitle: "gender".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.genderScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('genderScreen'), ), _buildScreenTitle( context, screenTitle: "blood_pressure".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.bloodPressureScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('bloodPressureScreen'), ), _buildScreenTitle( context, screenTitle: "favorite_sports".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.favoriteSportsScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('favoriteSportsScreen'), ), _buildScreenTitle( context, screenTitle: "favorite_foods".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.favoriteFoodsScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('favoriteFoodsScreen'), ), _buildScreenTitle( context, screenTitle: "schedule".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.scheduleScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('scheduleScreen'), ), _buildScreenTitle( context, screenTitle: "additional_information".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.additionalInformationScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('additionalInformationScreen'), ), _buildScreenTitle( context, screenTitle: "language_select".tr, - onTapScreenTitle: () => onTapScreenTitle( - AppRoutes.languageSelectScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('languageSelectScreen'), ), _buildScreenTitle( context, screenTitle: "auth".tr, - onTapScreenTitle: () => - onTapScreenTitle(AppRoutes.authScreen), + onTapScreenTitle: () => GoRouter.of(context).goNamed('authScreen'), ), ], ), @@ -151,7 +138,7 @@ class _AppNavigationScreenState extends State { } /// Section Widget - Widget _buildAppNavigation(BuildContext context) { + _buildAppNavigation(BuildContext context) { return Container( decoration: BoxDecoration( color: Color(0XFFFFFFFF), @@ -181,8 +168,7 @@ class _AppNavigationScreenState extends State { child: Padding( padding: EdgeInsets.only(left: 20.h), child: Text( - "Check your app's UI from the below demo screens of your app." - .tr, + "Check your app's UI from the below demo screens of your app.".tr, textAlign: TextAlign.center, style: TextStyle( color: Color(0XFF888888), @@ -205,15 +191,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), @@ -249,9 +233,4 @@ class _AppNavigationScreenState extends State { ), ); } - - /// Common click event - void onTapScreenTitle(String routeName) { - NavigatorService.pushNamed(routeName); - } } 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..5d4d8cc4 100644 --- a/lib/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart +++ b/lib/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart @@ -1,3 +1,6 @@ +import 'package:eatvio_app/app_router.dart'; +import 'package:go_router/go_router.dart'; + import 'bloc/blood_pressure_bloc.dart'; import 'package:flutter/services.dart'; import 'models/blood_pressure_model.dart'; @@ -14,14 +17,10 @@ import 'package:flutter/material.dart'; class BloodPressureScreen extends StatefulWidget { const BloodPressureScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => BloodPressureBloc(BloodPressureState( - bloodPressureModelObj: - BloodPressureModel(userRegistrationSheet: sheet))) - ..add(BloodPressureInitialEvent()), + create: (context) => + BloodPressureBloc(BloodPressureState(bloodPressureModelObj: BloodPressureModel(userRegistrationSheet: sheet)))..add(BloodPressureInitialEvent()), child: BloodPressureScreen()); } @@ -33,223 +32,194 @@ class _BloodPressureScreenState extends State { @override Widget build(BuildContext context) { final digitsOnly = FilteringTextInputFormatter.digitsOnly; - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - appBar: _buildAppBar(context), - body: Container( - width: double.maxFinite, - 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))))), - SizedBox(height: 21.v), - SizedBox( - width: 224.h, - child: Text("your_blood_pressure".tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.textTheme.headlineSmall)), - SizedBox(height: 21.v), - Container( - margin: EdgeInsets.only(right: 1.h), - padding: EdgeInsets.symmetric(horizontal: 3.h), - child: Column(children: [ - Text("up_pressure".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 2.v), - BlocSelector( - selector: (state) => state.upPressureController, - builder: (context, upPressureController) { - return CustomTextFormField( - inputFormatters: [digitsOnly], - controller: upPressureController, - hintText: "90".tr); - }) - ])), - SizedBox(height: 21.v), - Container( - margin: EdgeInsets.only(right: 1.h), - padding: EdgeInsets.symmetric(horizontal: 3.h), - child: Column(children: [ - Text("down_pressure".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 2.v), - BlocSelector( - selector: (state) => state.downPressureController, - builder: (context, downPressureController) { - return CustomTextFormField( - inputFormatters: [digitsOnly], - controller: downPressureController, - hintText: "90".tr); - }) - ])), - SizedBox(height: 22.v), - Text("do_you_have_hypotension_tendency".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 2.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 4.h), - child: BlocSelector( - selector: (state) => state.bloodPressureModelObj, - builder: (context, bloodPressureModelObj) { - return CustomDropDown( - icon: Container( - margin: EdgeInsets.fromLTRB( - 30.h, 11.v, 13.h, 11.v), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(1.h)), - child: CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize)), - hintText: "Не выбрано".tr, - items: - bloodPressureModelObj?.dropdownItemList ?? - [], - onChanged: (value) { - context - .read() - .add(ChangeHypoDownEvent(value: value)); - }); - })), - SizedBox(height: 22.v), - Text("do_you_have_hypertension_tendency".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 2.v), - Padding( - padding: EdgeInsets.symmetric(horizontal: 4.h), - child: BlocSelector( - selector: (state) => state.bloodPressureModelObj, - builder: (context, bloodPressureModelObj) { - return CustomDropDown( - icon: Container( - margin: EdgeInsets.fromLTRB( - 30.h, 11.v, 13.h, 11.v), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(1.h)), - child: CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize)), - hintText: "Не выбрано".tr, - items: - bloodPressureModelObj?.dropdownItemList ?? - [], - onChanged: (value) { - context.read().add( - ChangeHyperDropDownEvent(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: () { - 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 = ''; + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: _buildAppBar(context), + body: Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric(horizontal: 12.h, vertical: 9.v), + child: Column(children: [ + SizedBox(height: 7.v), + 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, + child: Text("your_blood_pressure".tr, + maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.headlineSmall)), + SizedBox(height: 21.v), + Container( + margin: EdgeInsets.only(right: 1.h), + padding: EdgeInsets.symmetric(horizontal: 3.h), + child: Column(children: [ + Text("up_pressure".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 2.v), + BlocSelector( + selector: (state) => state.upPressureController, + builder: (context, upPressureController) { + return CustomTextFormField(inputFormatters: [digitsOnly], controller: upPressureController, hintText: "90".tr); + }) + ])), + SizedBox(height: 21.v), + Container( + margin: EdgeInsets.only(right: 1.h), + padding: EdgeInsets.symmetric(horizontal: 3.h), + child: Column(children: [ + Text("down_pressure".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 2.v), + BlocSelector( + selector: (state) => state.downPressureController, + builder: (context, downPressureController) { + return CustomTextFormField(inputFormatters: [digitsOnly], controller: downPressureController, hintText: "90".tr); + }) + ])), + SizedBox(height: 22.v), + Text("do_you_have_hypotension_tendency".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 2.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.h), + child: BlocSelector( + selector: (state) => state.bloodPressureModelObj, + builder: (context, bloodPressureModelObj) { + return CustomDropDown( + icon: Container( + margin: EdgeInsets.fromLTRB(30.h, 11.v, 13.h, 11.v), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(1.h)), + child: CustomImageView(imagePath: ImageConstant.imgFavorite, height: 12.adaptSize, width: 12.adaptSize)), + hintText: "Не выбрано".tr, + items: bloodPressureModelObj?.dropdownItemList ?? [], + onChanged: onChangeHyper(context)); + })), + SizedBox(height: 22.v), + Text("do_you_have_hypertension_tendency".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 2.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.h), + child: BlocSelector( + selector: (state) => state.bloodPressureModelObj, + builder: (context, bloodPressureModelObj) { + return CustomDropDown( + icon: Container( + margin: EdgeInsets.fromLTRB(30.h, 11.v, 13.h, 11.v), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(1.h)), + child: CustomImageView(imagePath: ImageConstant.imgFavorite, height: 12.adaptSize, width: 12.adaptSize)), + hintText: "Не выбрано".tr, + items: bloodPressureModelObj?.dropdownItemList ?? [], + onChanged: onChangeHypo(context)); + })), + 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: onTapContinue(state, context)); + }), + BlocBuilder(builder: (context, state) { + return GestureDetector( + onTap: onTapSkip(state, context), + child: Text("skip".tr, style: theme.textTheme.bodyLarge), + ); + }), + ]))); + } - for (var error in errors) { - errorMessage += error + '\n\n'; - } + onTapSkip(BloodPressureState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.bloodPressureModelObj!.userRegistrationSheet; + AppRouter.navigatorKey.currentContext?.goNamed('allergiesScreen', extra: sheet); + }; + } - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Ошибка'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('OK'), - ), - ], - ), - ); - } - }); - }), - BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.bloodPressureModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.allergiesScreen, - arguments: sheet, - ); - }, - child: Text("skip".tr, style: theme.textTheme.bodyLarge), - ); - }), - ])))); + 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; + AppRouter.navigatorKey.currentContext?.goNamed('favoriteSportsScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), 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(); + GoRouter.of(context).pop(); } List validateFields( @@ -262,8 +232,7 @@ class _BloodPressureScreenState extends State { errors.add('pressure_empty'.tr); } - if (![bloodPressureUp, bloodPressureDown] - .every((pressure) => int.tryParse(pressure) != null)) { + if (![bloodPressureUp, bloodPressureDown].every((pressure) => int.tryParse(pressure) != null)) { errors.add('pressure_is_not_a_number'.tr); } 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..3610697e 100644 --- a/lib/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart +++ b/lib/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/body_circumferences_bloc.dart'; import 'package:flutter/services.dart'; import 'models/body_circumferences_model.dart'; @@ -13,171 +15,140 @@ import 'package:flutter/material.dart'; class BodyCircumferencesScreen extends StatelessWidget { const BodyCircumferencesScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => BodyCircumferencesBloc(BodyCircumferencesState( - bodyCircumferencesModelObj: - BodyCircumferencesModel(userRegistrationSheet: sheet))) - ..add(BodyCircumferencesInitialEvent()), - child: BodyCircumferencesScreen()); + create: (context) => BodyCircumferencesBloc(BodyCircumferencesState(bodyCircumferencesModelObj: BodyCircumferencesModel(userRegistrationSheet: sheet))) + ..add(BodyCircumferencesInitialEvent()), + child: BodyCircumferencesScreen(), + ); } @override Widget build(BuildContext context) { final digitsOnly = FilteringTextInputFormatter.digitsOnly; mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - 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.40, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 39.v), - Text("initial_measurements".tr, - style: theme.textTheme.headlineSmall), - SizedBox(height: 93.v), - Column(children: [ - Text("chest_circumference".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 3.v), - BlocSelector( - selector: (state) => state.chestCircumferenceController, - builder: (context, chestCircumferenceController) { - return CustomTextFormField( - inputFormatters: [digitsOnly], - controller: chestCircumferenceController, - hintText: "90".tr); - }) - ]), - SizedBox(height: 19.v), - Text("waist_circumference".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), - BlocSelector( - selector: (state) => state.waistCircumferenceController, - builder: (context, waistCircumferenceController) { - return CustomTextFormField( - inputFormatters: [digitsOnly], - controller: waistCircumferenceController, - hintText: "60".tr); - }), - SizedBox(height: 21.v), - Text("hip_circumference".tr, - style: theme.textTheme.bodyLarge), - SizedBox(height: 3.v), - BlocSelector( - selector: (state) => state.hipCircumferenceController, - builder: (context, hipCircumferenceController) { - return CustomTextFormField( - inputFormatters: [digitsOnly], - controller: hipCircumferenceController, - hintText: "90".tr, - textInputAction: TextInputAction.done); - }), - 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.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'), - ), - ], - ), - ); - } - }); + return Scaffold( + resizeToAvoidBottomInset: false, + 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.40, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 39.v), + Text("initial_measurements".tr, style: theme.textTheme.headlineSmall), + SizedBox(height: 93.v), + Column(children: [ + Text("chest_circumference".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 3.v), + BlocSelector( + selector: (state) => state.chestCircumferenceController, + builder: (context, chestCircumferenceController) { + return CustomTextFormField(inputFormatters: [digitsOnly], controller: chestCircumferenceController, hintText: "90".tr); + }) + ]), + SizedBox(height: 19.v), + Text("waist_circumference".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 5.v), + BlocSelector( + selector: (state) => state.waistCircumferenceController, + builder: (context, waistCircumferenceController) { + return CustomTextFormField(inputFormatters: [digitsOnly], controller: waistCircumferenceController, hintText: "60".tr); }), - ])))); + SizedBox(height: 21.v), + Text("hip_circumference".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 3.v), + BlocSelector( + selector: (state) => state.hipCircumferenceController, + builder: (context, hipCircumferenceController) { + return CustomTextFormField( + inputFormatters: [digitsOnly], controller: hipCircumferenceController, hintText: "90".tr, textInputAction: TextInputAction.done); + }), + 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: 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; + GoRouter.of(context).goNamed('weightScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), 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(); + GoRouter.of(context).pop(); } /// Navigates to the weightScreen when the action is triggered. - navigateToWeight(BuildContext context) { - NavigatorService.pushNamed( - AppRoutes.weightScreen, - ); - } + navigateToWeight(BuildContext context) => GoRouter.of(context).goNamed('weightScreen'); List validateFields( String chestCircumference, @@ -186,14 +157,11 @@ class BodyCircumferencesScreen extends StatelessWidget { ) { List errors = []; - if (chestCircumference.isEmpty | - waistCircumference.isEmpty | - hipCircumference.isEmpty) { + if (chestCircumference.isEmpty | waistCircumference.isEmpty | hipCircumference.isEmpty) { errors.add('circumference_empty'.tr); } - if (![chestCircumference, waistCircumference, hipCircumference] - .every((circumference) => int.tryParse(circumference) != null)) { + if (![chestCircumference, waistCircumference, hipCircumference].every((circumference) => int.tryParse(circumference) != null)) { errors.add('circumference_is_not_a_number'.tr); } 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..5ce91501 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 @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/city_of_living_bloc.dart'; import 'models/city_of_living_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -6,140 +8,1284 @@ 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'; class CityOfLivingScreen extends StatelessWidget { const CityOfLivingScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => CityOfLivingBloc(CityOfLivingState( - cityOfLivingModelObj: - CityOfLivingModel(userRegistrationSheet: sheet))) - ..add(CityOfLivingInitialEvent()), - child: CityOfLivingScreen()); + create: (context) => + CityOfLivingBloc(CityOfLivingState(cityOfLivingModelObj: CityOfLivingModel(userRegistrationSheet: sheet)))..add(CityOfLivingInitialEvent()), + child: CityOfLivingScreen(), + ); } @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.32, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 39.v), - Text("place_of_living".tr, - style: theme.textTheme.headlineSmall), - Spacer(flex: 34), - Text("country".tr, 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?.dropdownItemList ?? [], - onChanged: (value) { - context - .read() - .add(ChangeDropDownEvent(value: value)); - }); - }), - SizedBox(height: 22.v), - Text("city".tr, 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)); - }); - }), - Spacer(flex: 65), - 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.cityOfLivingModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.bodyCircumferencesScreen, - arguments: sheet, - ); - }); + List _options = [ + "Абаза", + "Абакан", + "Абдулино", + "Абинск", + "Агидель", + "Агрыз", + "Адыгейск", + "Азнакаево", + "Азов", + "Ак-Довурак", + "Аксай", + "Алагир", + "Алапаевск", + "Алатырь", + "Алдан", + "Алейск", + "Александров", + "Александровск", + "Александровск-Сахалинский", + "Алексеевка", + "Алексин", + "Алзамай", + "АлупкаОспаривается", + "АлуштаОспаривается", + "Альметьевск", + "Амурск", + "Анадырь", + "Анапа", + "Ангарск", + "Андреаполь", + "Анжеро-Судженск", + "Анива", + "Апатиты", + "Апрелевка", + "Апшеронск", + "Арамиль", + "Аргун", + "Ардатов", + "Ардон", + "Арзамас", + "Аркадак", + "Армавир", + "АрмянскОспаривается", + "Арсеньев", + "Арск", + "Артём", + "Артёмовск", + "Артёмовский", + "Архангельск", + "Асбест", + "Асино", + "Астрахань", + "Аткарск", + "Ахтубинск", + "Ачинск", + "Аша", + "Бабаево", + "Бабушкин", + "Бавлы", + "Багратионовск", + "Байкальск", + "Баймак", + "Бакал", + "Баксан", + "Балабаново", + "Балаково", + "Балахна", + "Балашиха", + "Балашов", + "Балей", + "Балтийск", + "Барабинск", + "Барнаул", + "Барыш", + "Батайск", + "БахчисарайОспаривается", + "Бежецк", + "Белая Калитва", + "Белая Холуница", + "Белгород", + "Белебей", + "Белёв", + "Белинский", + "Белово", + "Белогорск", + "БелогорскОспаривается", + "Белозерск", + "Белокуриха", + "Беломорск", + "Белоозёрский", + "Белорецк", + "Белореченск", + "Белоусово", + "Белоярский", + "Белый", + "Бердск", + "Березники", + "Берёзовский", + "Берёзовский", + "Беслан", + "Бийск", + "Бикин", + "Билибино", + "Биробиджан", + "Бирск", + "Бирюсинск", + "Бирюч", + "Благовещенск", + "Благовещенск", + "Благодарный", + "Бобров", + "Богданович", + "Богородицк", + "Богородск", + "Боготол", + "Богучар", + "Бодайбо", + "Бокситогорск", + "Болгар", + "Бологое", + "Болотное", + "Болохово", + "Болхов", + "Большой Камень", + "Бор", + "Борзя", + "Борисоглебск", + "Боровичи", + "Боровск", + "Бородино", + "Братск", + "Бронницы", + "Брянск", + "Бугульма", + "Бугуруслан", + "Будённовск", + "Бузулук", + "Буинск", + "Буй", + "Буйнакск", + "Бутурлиновка", + "Валдай", + "Валуйки", + "Велиж", + "Великие Луки", + "Великий Новгород", + "Великий Устюг", + "Вельск", + "Венёв", + "Верещагино", + "Верея", + "Верхнеуральск", + "Верхний Тагил", + "Верхний Уфалей", + "Верхняя Пышма", + "Верхняя Салда", + "Верхняя Тура", + "Верхотурье", + "Верхоянск", + "Весьегонск", + "Ветлуга", + "Видное", + "Вилюйск", + "Вилючинск", + "Вихоревка", + "Вичуга", + "Владивосток", + "Владикавказ", + "Владимир", + "Волгоград", + "Волгодонск", + "Волгореченск", + "Волжск", + "Волжский", + "Вологда", + "Володарск", + "Волоколамск", + "Волосово", + "Волхов", + "Волчанск", + "Вольск", + "Воркута", + "Воронеж", + "Ворсма", + "Воскресенск", + "Воткинск", + "Всеволожск", + "Вуктыл", + "Выборг", + "Выкса", + "Высоковск", + "Высоцк", + "Вытегра", + "Вышний Волочёк", + "Вяземский", + "Вязники", + "Вязьма", + "Вятские Поляны", + "Гаврилов Посад", + "Гаврилов-Ям", + "Гагарин", + "Гаджиево", + "Гай", + "Галич", + "Гатчина", + "Гвардейск", + "Гдов", + "Геленджик", + "Георгиевск", + "Глазов", + "Голицыно", + "Горбатов", + "Горно-Алтайск", + "Горнозаводск", + "Горняк", + "Городец", + "Городище", + "Городовиковск", + "Гороховец", + "Горячий Ключ", + "Грайворон", + "Гремячинск", + "Грозный", + "Грязи", + "Грязовец", + "Губаха", + "Губкин", + "Губкинский", + "Гудермес", + "Гуково", + "Гулькевичи", + "Гурьевск", + "Гурьевск", + "Гусев", + "Гусиноозёрск", + "Гусь-Хрустальный", + "Давлеканово", + "Дагестанские Огни", + "Далматово", + "Дальнегорск", + "Дальнереченск", + "Данилов", + "Данков", + "Дегтярск", + "Дедовск", + "Демидов", + "Дербент", + "Десногорск", + "ДжанкойОспаривается", + "Дзержинск", + "Дзержинский", + "Дивногорск", + "Дигора", + "Димитровград", + "Дмитриев", + "Дмитров", + "Дмитровск", + "Дно", + "Добрянка", + "Долгопрудный", + "Долинск", + "Домодедово", + "Донецк", + "Донской", + "Дорогобуж", + "Дрезна", + "Дубна", + "Дубовка", + "Дудинка", + "Духовщина", + "Дюртюли", + "Дятьково", + "ЕвпаторияОспаривается", + "Егорьевск", + "Ейск", + "Екатеринбург", + "Елабуга", + "Елец", + "Елизово", + "Ельня", + "Еманжелинск", + "Емва", + "Енисейск", + "Ермолино", + "Ершов", + "Ессентуки", + "Ефремов", + "Железноводск", + "Железногорск", + "Железногорск", + "Железногорск-Илимский", + "Жердевка", + "Жигулёвск", + "Жиздра", + "Жирновск", + "Жуков", + "Жуковка", + "Жуковский", + "Завитинск", + "Заводоуковск", + "Заволжск", + "Заволжье", + "Задонск", + "Заинск", + "Закаменск", + "Заозёрный", + "Заозёрск", + "Западная Двина", + "Заполярный", + "Зарайск", + "Заречный", + "Заречный", + "Заринск", + "Звенигово", + "Звенигород", + "Зверево", + "Зеленогорск", + "Зеленоградск", + "Зеленодольск", + "Зеленокумск", + "Зерноград", + "Зея", + "Зима", + "Златоуст", + "Злынка", + "Змеиногорск", + "Знаменск", + "Зубцов", + "Зуевка", + "Ивангород", + "Иваново", + "Ивантеевка", + "Ивдель", + "Игарка", + "Ижевск", + "Избербаш", + "Изобильный", + "Иланский", + "Инза", + "Иннополис", + "Инсар", + "Инта", + "Ипатово", + "Ирбит", + "Иркутск", + "Исилькуль", + "Искитим", + "Истра", + "Ишим", + "Ишимбай", + "Йошкар-Ола", + "Кадников", + "Казань", + "Калач", + "Калач-на-Дону", + "Калачинск", + "Калининград", + "Калининск", + "Калтан", + "Калуга", + "Калязин", + "Камбарка", + "Каменка", + "Каменногорск", + "Каменск-Уральский", + "Каменск-Шахтинский", + "Камень-на-Оби", + "Камешково", + "Камызяк", + "Камышин", + "Камышлов", + "Канаш", + "Кандалакша", + "Канск", + "Карабаново", + "Карабаш", + "Карабулак", + "Карасук", + "Карачаевск", + "Карачев", + "Каргат", + "Каргополь", + "Карпинск", + "Карталы", + "Касимов", + "Касли", + "Каспийск", + "Катав-Ивановск", + "Катайск", + "Качканар", + "Кашин", + "Кашира", + "Кедровый", + "Кемерово", + "Кемь", + "КерчьОспаривается", + "Кизел", + "Кизилюрт", + "Кизляр", + "Кимовск", + "Кимры", + "Кингисепп", + "Кинель", + "Кинешма", + "Киреевск", + "Киренск", + "Киржач", + "Кириллов", + "Кириши", + "Киров", + "Киров", + "Кировград", + "Кирово-Чепецк", + "Кировск", + "Кировск", + "Кирс", + "Кирсанов", + "Киселёвск", + "Кисловодск", + "Клин", + "Клинцы", + "Княгинино", + "Ковдор", + "Ковров", + "Ковылкино", + "Когалым", + "Кодинск", + "Козельск", + "Козловка", + "Козьмодемьянск", + "Кола", + "Кологрив", + "Коломна", + "Колпашево", + "Кольчугино", + "Коммунар", + "Комсомольск", + "Комсомольск-на-Амуре", + "Конаково", + "Кондопога", + "Кондрово", + "Константиновск", + "Копейск", + "Кораблино", + "Кореновск", + "Коркино", + "Королёв", + "Короча", + "Корсаков", + "Коряжма", + "Костерёво", + "Костомукша", + "Кострома", + "Котельники", + "Котельниково", + "Котельнич", + "Котлас", + "Котово", + "Котовск", + "Кохма", + "Красавино", + "Красноармейск", + "Красноармейск", + "Красновишерск", + "Красногорск", + "Краснодар", + "Краснозаводск", + "Краснознаменск", + "Краснознаменск", + "Краснокаменск", + "Краснокамск", + "КрасноперекопскОспаривается", + "Краснослободск", + "Краснослободск", + "Краснотурьинск", + "Красноуральск", + "Красноуфимск", + "Красноярск", + "Красный Кут", + "Красный Сулин", + "Красный Холм", + "Кремёнки", + "Кропоткин", + "Крымск", + "Кстово", + "Кубинка", + "Кувандык", + "Кувшиново", + "Кудрово", + "Кудымкар", + "Кузнецк", + "Куйбышев", + "Кукмор", + "Кулебаки", + "Кумертау", + "Кунгур", + "Купино", + "Курган", + "Курганинск", + "Курильск", + "Курлово", + "Куровское", + "Курск", + "Куртамыш", + "Курчалой", + "Курчатов", + "Куса", + "Кушва", + "Кызыл", + "Кыштым", + "Кяхта", + "Лабинск", + "Лабытнанги", + "Лагань", + "Ладушкин", + "Лаишево", + "Лакинск", + "Лангепас", + "Лахденпохья", + "Лебедянь", + "Лениногорск", + "Ленинск", + "Ленинск-Кузнецкий", + "Ленск", + "Лермонтов", + "Лесной", + "Лесозаводск", + "Лесосибирск", + "Ливны", + "Ликино-Дулёво", + "Липецк", + "Липки", + "Лиски", + "Лихославль", + "Лобня", + "Лодейное Поле", + "Лосино-Петровский", + "Луга", + "Луза", + "Лукоянов", + "Луховицы", + "Лысково", + "Лысьва", + "Лыткарино", + "Льгов", + "Любань", + "Люберцы", + "Любим", + "Людиново", + "Лянтор", + "Магадан", + "Магас", + "Магнитогорск", + "Майкоп", + "Майский", + "Макаров", + "Макарьев", + "Макушино", + "Малая Вишера", + "Малгобек", + "Малмыж", + "Малоархангельск", + "Малоярославец", + "Мамадыш", + "Мамоново", + "Мантурово", + "Мариинск", + "Мариинский Посад", + "Маркс", + "Махачкала", + "Мглин", + "Мегион", + "Медвежьегорск", + "Медногорск", + "Медынь", + "Межгорье", + "Междуреченск", + "Мезень", + "Меленки", + "Мелеуз", + "Менделеевск", + "Мензелинск", + "Мещовск", + "Миасс", + "Микунь", + "Миллерово", + "Минеральные Воды", + "Минусинск", + "Миньяр", + "Мирный", + "Мирный", + "Михайлов", + "Михайловка", + "Михайловск", + "Михайловск", + "Мичуринск", + "Могоча", + "Можайск", + "Можга", + "Моздок", + "Мончегорск", + "Морозовск", + "Моршанск", + "Мосальск", + "Москва", + "Муравленко", + "Мураши", + "Мурино", + "Мурманск", + "Муром", + "Мценск", + "Мыски", + "Мытищи", + "Мышкин", + "Набережные Челны", + "Навашино", + "Наволоки", + "Надым", + "Назарово", + "Назрань", + "Называевск", + "Нальчик", + "Нариманов", + "Наро-Фоминск", + "Нарткала", + "Нарьян-Мар", + "Находка", + "Невель", + "Невельск", + "Невинномысск", + "Невьянск", + "Нелидово", + "Неман", + "Нерехта", + "Нерчинск", + "Нерюнгри", + "Нестеров", + "Нефтегорск", + "Нефтекамск", + "Нефтекумск", + "Нефтеюганск", + "Нея", + "Нижневартовск", + "Нижнекамск", + "Нижнеудинск", + "Нижние Серги", + "Нижний Ломов", + "Нижний Новгород", + "Нижний Тагил", + "Нижняя Салда", + "Нижняя Тура", + "Николаевск", + "Николаевск-на-Амуре", + "Никольск", + "Никольск", + "Никольское", + "Новая Ладога", + "Новая Ляля", + "Новоалександровск", + "Новоалтайск", + "Новоаннинский", + "Нововоронеж", + "Новодвинск", + "Новозыбков", + "Новокубанск", + "Новокузнецк", + "Новокуйбышевск", + "Новомичуринск", + "Новомосковск", + "Новопавловск", + "Новоржев", + "Новороссийск", + "Новосибирск", + "Новосиль", + "Новосокольники", + "Новотроицк", + "Новоузенск", + "Новоульяновск", + "Новоуральск", + "Новохопёрск", + "Новочебоксарск", + "Новочеркасск", + "Новошахтинск", + "Новый Оскол", + "Новый Уренгой", + "Ногинск", + "Нолинск", + "Норильск", + "Ноябрьск", + "Нурлат", + "Нытва", + "Нюрба", + "Нягань", + "Нязепетровск", + "Няндома", + "Облучье", + "Обнинск", + "Обоянь", + "Обь", + "Одинцово", + "Озёрск", + "Озёрск", + "Озёры", + "Октябрьск", + "Октябрьский", + "Окуловка", + "Олёкминск", + "Оленегорск", + "Олонец", + "Омск", + "Омутнинск", + "Онега", + "Опочка", + "Орёл", + "Оренбург", + "Орехово-Зуево", + "Орлов", + "Орск", + "Оса", + "Осинники", + "Осташков", + "Остров", + "Островной", + "Острогожск", + "Отрадное", + "Отрадный", + "Оха", + "Оханск", + "Очёр", + "Павлово", + "Павловск", + "Павловский Посад", + "Палласовка", + "Партизанск", + "Певек", + "Пенза", + "Первомайск", + "Первоуральск", + "Перевоз", + "Пересвет", + "Переславль-Залесский", + "Пермь", + "Пестово", + "Петров Вал", + "Петровск", + "Петровск-Забайкальский", + "Петрозаводск", + "Петропавловск-Камчатский", + "Петухово", + "Петушки", + "Печора", + "Печоры", + "Пикалёво", + "Пионерский", + "Питкяранта", + "Плавск", + "Пласт", + "Плёс", + "Поворино", + "Подольск", + "Подпорожье", + "Покачи", + "Покров", + "Покровск", + "Полевской", + "Полесск", + "Полысаево", + "Полярные Зори", + "Полярный", + "Поронайск", + "Порхов", + "Похвистнево", + "Почеп", + "Починок", + "Пошехонье", + "Правдинск", + "Приволжск", + "Приморск", + "Приморск", + "Приморско-Ахтарск", + "Приозерск", + "Прокопьевск", + "Пролетарск", + "Протвино", + "Прохладный", + "Псков", + "Пугачёв", + "Пудож", + "Пустошка", + "Пучеж", + "Пушкино", + "Пущино", + "Пыталово", + "Пыть-Ях", + "Пятигорск", + "Радужный", + "Радужный", + "Райчихинск", + "Раменское", + "Рассказово", + "Ревда", + "Реж", + "Реутов", + "Ржев", + "Родники", + "Рославль", + "Россошь", + "Ростов-на-Дону", + "Ростов", + "Рошаль", + "Ртищево", + "Рубцовск", + "Рудня", + "Руза", + "Рузаевка", + "Рыбинск", + "Рыбное", + "Рыльск", + "Ряжск", + "Рязань", + "СакиОспаривается", + "Салават", + "Салаир", + "Салехард", + "Сальск", + "Самара", + "Санкт-Петербург", + "Саранск", + "Сарапул", + "Саратов", + "Саров", + "Сасово", + "Сатка", + "Сафоново", + "Саяногорск", + "Саянск", + "Светлогорск", + "Светлоград", + "Светлый", + "Светогорск", + "Свирск", + "Свободный", + "Себеж", + "СевастопольОспаривается", + "Северо-Курильск", + "Северобайкальск", + "Северодвинск", + "Североморск", + "Североуральск", + "Северск", + "Севск", + "Сегежа", + "Сельцо", + "Семёнов", + "Семикаракорск", + "Семилуки", + "Сенгилей", + "Серафимович", + "Сергач", + "Сергиев Посад", + "Сердобск", + "Серов", + "Серпухов", + "Сертолово", + "Сибай", + "Сим", + "СимферопольОспаривается", + "Сковородино", + "Скопин", + "Славгород", + "Славск", + "Славянск-на-Кубани", + "Сланцы", + "Слободской", + "Слюдянка", + "Смоленск", + "Снежинск", + "Снежногорск", + "Собинка", + "Советск", + "Советск", + "Советск", + "Советская Гавань", + "Советский", + "Сокол", + "Солигалич", + "Соликамск", + "Солнечногорск", + "Соль-Илецк", + "Сольвычегодск", + "Сольцы", + "Сорочинск", + "Сорск", + "Сортавала", + "Сосенский", + "Сосновка", + "Сосновоборск", + "Сосновый Бор", + "Сосногорск", + "Сочи", + "Спас-Деменск", + "Спас-Клепики", + "Спасск", + "Спасск-Дальний", + "Спасск-Рязанский", + "Среднеколымск", + "Среднеуральск", + "Сретенск", + "Ставрополь", + "Старая Купавна", + "Старая Русса", + "Старица", + "Стародуб", + "Старый КрымОспаривается", + "Старый Оскол", + "Стерлитамак", + "Стрежевой", + "Строитель", + "Струнино", + "Ступино", + "Суворов", + "СудакОспаривается", + "Суджа", + "Судогда", + "Суздаль", + "Сунжа", + "Суоярви", + "Сураж", + "Сургут", + "Суровикино", + "Сурск", + "Сусуман", + "Сухиничи", + "Сухой Лог", + "Сызрань", + "Сыктывкар", + "Сысерть", + "Сычёвка", + "Сясьстрой", + "Тавда", + "Таганрог", + "Тайга", + "Тайшет", + "Талдом", + "Талица", + "Тамбов", + "Тара", + "Тарко-Сале", + "Таруса", + "Татарск", + "Таштагол", + "Тверь", + "Теберда", + "Тейково", + "Темников", + "Темрюк", + "Терек", + "Тетюши", + "Тимашёвск", + "Тихвин", + "Тихорецк", + "Тобольск", + "Тогучин", + "Тольятти", + "Томари", + "Томмот", + "Томск", + "Топки", + "Торжок", + "Торопец", + "Тосно", + "Тотьма", + "Трёхгорный", + "Троицк", + "Трубчевск", + "Туапсе", + "Туймазы", + "Тула", + "Тулун", + "Туран", + "Туринск", + "Тутаев", + "Тында", + "Тырныауз", + "Тюкалинск", + "Тюмень", + "Уварово", + "Углегорск", + "Углич", + "Удачный", + "Удомля", + "Ужур", + "Узловая", + "Улан-Удэ", + "Ульяновск", + "Унеча", + "Урай", + "Урень", + "Уржум", + "Урус-Мартан", + "Урюпинск", + "Усинск", + "Усмань", + "Усолье-Сибирское", + "Усолье", + "Уссурийск", + "Усть-Джегута", + "Усть-Илимск", + "Усть-Катав", + "Усть-Кут", + "Усть-Лабинск", + "Устюжна", + "Уфа", + "Ухта", + "Учалы", + "Уяр", + "Фатеж", + "ФеодосияОспаривается", + "Фокино", + "Фокино", + "Фролово", + "Фрязино", + "Фурманов", + "Хабаровск", + "Хадыженск", + "Ханты-Мансийск", + "Харабали", + "Харовск", + "Хасавюрт", + "Хвалынск", + "Хилок", + "Химки", + "Холм", + "Холмск", + "Хотьково", + "Цивильск", + "Цимлянск", + "Циолковский", + "Чадан", + "Чайковский", + "Чапаевск", + "Чаплыгин", + "Чебаркуль", + "Чебоксары", + "Чегем", + "Чекалин", + "Челябинск", + "Чердынь", + "Черемхово", + "Черепаново", + "Череповец", + "Черкесск", + "Чёрмоз", + "Черноголовка", + "Черногорск", + "Чернушка", + "Черняховск", + "Чехов", + "Чистополь", + "Чита", + "Чкаловск", + "Чудово", + "Чулым", + "Чусовой", + "Чухлома", + "Шагонар", + "Шадринск", + "Шали", + "Шарыпово", + "Шарья", + "Шатура", + "Шахты", + "Шахунья", + "Шацк", + "Шебекино", + "Шелехов", + "Шенкурск", + "Шилка", + "Шимановск", + "Шиханы", + "Шлиссельбург", + "Шумерля", + "Шумиха", + "Шуя", + "Щёкино", + "ЩёлкиноОспаривается", + "Щёлково", + "Щигры", + "Щучье", + "Электрогорск", + "Электросталь", + "Электроугли", + "Элиста", + "Энгельс", + "Эртиль", + "Югорск", + "Южа", + "Южно-Сахалинск", + "Южно-Сухокумск", + "Южноуральск", + "Юрга", + "Юрьев-Польский", + "Юрьевец", + "Юрюзань", + "Юхнов", + "Ядрин", + "Якутск", + "ЯлтаОспаривается", + "Ялуторовск", + "Янаул", + "Яранск", + "Яровое", + "Ярославль", + "Ярцево", + "Ясногорск", + "Ясный", + "Яхрома" + ]; + return 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.32, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 39.v), + Text("place_of_living".tr, style: theme.textTheme.headlineSmall), + Spacer(flex: 34), + Text("country".tr, 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?.dropdownItemList ?? [], + onChanged: onDropDownChange(context)); }), - ])))); + SizedBox(height: 22.v), + Text("Населенный пункт", style: theme.textTheme.bodyLarge), + SizedBox(height: 2.v), + 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) { + return NextStepButton( + text: "continue".tr.toUpperCase(), + margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: onTapContinue(state, context)); + }), + ]))); + } + + onTapContinue(CityOfLivingState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.cityOfLivingModelObj!.userRegistrationSheet; + GoRouter.of(context).goNamed('bodyCircumferencesScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), 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(); + GoRouter.of(context).pop(); } } + +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..90723f60 100644 --- a/lib/presentation/registration/credentials_screen/credentials_screen.dart +++ b/lib/presentation/registration/credentials_screen/credentials_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/credentials_bloc.dart'; import 'models/credentials_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -10,9 +12,7 @@ import 'package:eatvio_app/widgets/custom_text_form_field.dart'; import 'package:flutter/material.dart'; class CredentialsScreen extends StatelessWidget { - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( create: (context) => CredentialsBloc( CredentialsState( @@ -40,45 +40,33 @@ class CredentialsScreen extends StatelessWidget { decoration: BoxDecoration( color: theme.colorScheme.onPrimary, borderRadius: BorderRadius.circular(8.h), - border: Border.all( - color: theme.colorScheme.primary, width: 1.h)), + border: Border.all(color: theme.colorScheme.primary, width: 1.h)), child: ClipRRect( borderRadius: BorderRadius.circular(8.h), child: LinearProgressIndicator( - value: 0.05, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), + value: 0.05, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), SizedBox(height: 39.v), - Text("user_registration".tr, - style: theme.textTheme.headlineSmall), + Text("user_registration".tr, style: theme.textTheme.headlineSmall), SizedBox(height: 16.v), Text("email".tr, style: theme.textTheme.bodyLarge), SizedBox(height: 3.v), - BlocSelector( + BlocSelector( selector: (state) => state.emailController, builder: (context, emailController) { - return CustomTextFormField( - controller: emailController, - textInputAction: TextInputAction.done); + return CustomTextFormField(controller: emailController, textInputAction: TextInputAction.done); }), SizedBox(height: 22.v), Text("username".tr, style: theme.textTheme.bodyLarge), SizedBox(height: 3.v), - BlocSelector( + BlocSelector( selector: (state) => state.nameController, builder: (context, nameController) { - return CustomTextFormField( - controller: nameController, - textInputAction: TextInputAction.done); + return CustomTextFormField(controller: nameController, textInputAction: TextInputAction.done); }), SizedBox(height: 19.v), Text("password".tr, style: theme.textTheme.bodyLarge), SizedBox(height: 5.v), - BlocSelector( + BlocSelector( selector: (state) => state.passwordController, builder: (context, passwordController) { return CustomTextFormField( @@ -87,83 +75,71 @@ class CredentialsScreen extends StatelessWidget { ); }), SizedBox(height: 21.v), - Text("password_confirmation".tr, - style: theme.textTheme.bodyLarge), + Text("password_confirmation".tr, style: theme.textTheme.bodyLarge), SizedBox(height: 3.v), - BlocSelector( + BlocSelector( selector: (state) => state.passwordConfirmationController, builder: (context, passwordConfirmationController) { - return CustomTextFormField( - obscureText: true, - controller: passwordConfirmationController, - textInputAction: TextInputAction.done); + return CustomTextFormField(obscureText: true, controller: passwordConfirmationController, textInputAction: TextInputAction.done); }), Spacer(), - BlocBuilder( - builder: (context, state) { + BlocBuilder(builder: (context, state) { return NextStepButton( text: "continue".tr.toUpperCase(), - margin: - EdgeInsets.only(left: 16.h, right: 16.h, bottom: 50.v), + 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)); }), ]))); } - /// Section Widget - PreferredSizeWidget _buildAppBar(BuildContext context) { - return CustomAppBar( - leadingWidth: 29.h, - centerTitle: true, - title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); + 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; + GoRouter.of(context).goNamed('nameScreen', extra: 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'), + ), + ], + ), + ); + } + }; } - List validateFields( - String email, String name, String password, String passwordConfirmation) { + onTapOk(BuildContext context) { + return () { + Navigator.pop(context); + }; + } + + /// Section Widget + _buildAppBar(BuildContext context) { + return CustomAppBar(leadingWidth: 29.h, centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); + } + + List validateFields(String email, String name, String password, String passwordConfirmation) { List errors = []; if (!isEmail(email)) { 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..5f68a3d2 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 @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/date_of_birth_bloc.dart'; import 'models/date_of_birth_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -11,15 +13,17 @@ import 'package:flutter/material.dart'; class DateOfBirthScreen extends StatefulWidget { const DateOfBirthScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => DateOfBirthBloc(DateOfBirthState( - dateOfBirthModelObj: - DateOfBirthModel(userRegistrationSheet: sheet))) - ..add(DateOfBirthInitialEvent()), - child: DateOfBirthScreen()); + create: (context) => DateOfBirthBloc( + DateOfBirthState( + dateOfBirthModelObj: DateOfBirthModel( + userRegistrationSheet: sheet, + ), + ), + )..add(DateOfBirthInitialEvent()), + child: DateOfBirthScreen(), + ); } @override @@ -28,110 +32,95 @@ class DateOfBirthScreen extends StatefulWidget { class _DateOfBirthScreenState extends State { DateTime selectedDate = DateTime.now(); + @override Widget build(BuildContext context) { mediaQueryData = MediaQuery.of(context); - return BlocBuilder( - builder: (context, state) { - return SafeArea( - child: Scaffold( - appBar: _buildAppBar(context), - body: Container( - width: double.maxFinite, - padding: - EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), - child: Column(children: [ - 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.24, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 38.v), - Text("date_of_birth".tr, - style: theme.textTheme.headlineSmall), - Spacer(flex: 39), - _buildDateInput(context), - Spacer(flex: 60), - 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 - .dateOfBirthModelObj!.userRegistrationSheet; - sheet.birthdate = selectedDate.toString(); - Navigator.pushNamed( - context, - AppRoutes.cityOfLivingScreen, - arguments: sheet, - ); - }); - }), - ])))); + return BlocBuilder(builder: (context, state) { + return Scaffold( + appBar: _buildAppBar(context), + body: Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 9.v), + child: Column(children: [ + 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.24, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 38.v), + Text("date_of_birth".tr, style: theme.textTheme.headlineSmall), + Spacer(flex: 39), + _buildDateInput(context), + Spacer(flex: 60), + 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: onTapContinue(state, context)); + }), + ]))); }); } + onTapContinue(DateOfBirthState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.dateOfBirthModelObj!.userRegistrationSheet; + sheet.birthdate = selectedDate.toString(); + GoRouter.of(context).goNamed('cityOfLivingScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), 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), - decoration: AppDecoration.outlineBlueA - .copyWith(borderRadius: BorderRadiusStyle.circleBorder8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomImageView( - imagePath: ImageConstant.imgFavorite, - height: 12.adaptSize, - width: 12.adaptSize, - radius: BorderRadius.circular(1.h)), - SizedBox(height: 6.v), - Align( - alignment: Alignment.center, - child: GestureDetector( - onTap: () { - openDatePickerDialog(context); - }, - child: Text( - '${selectedDate.day}.${selectedDate.month}.${selectedDate.year}', - style: theme.textTheme.titleMedium))), - SizedBox(height: 6.v) - ])); + decoration: AppDecoration.outlineBlueA.copyWith(borderRadius: BorderRadiusStyle.circleBorder8), + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ + CustomImageView(imagePath: ImageConstant.imgFavorite, height: 12.adaptSize, width: 12.adaptSize, radius: BorderRadius.circular(1.h)), + SizedBox(height: 6.v), + Align( + alignment: Alignment.center, + child: GestureDetector( + onTap: onTapCalendar(context), + child: Text('${selectedDate.day}.${selectedDate.month}.${selectedDate.year}', style: theme.textTheme.titleMedium))), + SizedBox(height: 6.v) + ])); + } + + onTapCalendar(BuildContext context) { + return () { + openDatePickerDialog(context); + }; } /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } /// Displays a date picker dialog to update the selected date @@ -139,15 +128,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..94771a17 100644 --- a/lib/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart +++ b/lib/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/favorite_foods_bloc.dart'; import 'models/favorite_foods_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -12,14 +14,10 @@ import 'package:flutter/material.dart'; class FavoriteFoodsScreen extends StatefulWidget { const FavoriteFoodsScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => FavoriteFoodsBloc(FavoriteFoodsState( - favoriteFoodsModelObj: - FavoriteFoodsModel(userRegistrationSheet: sheet))) - ..add(FavoriteFoodsInitialEvent()), + create: (context) => + FavoriteFoodsBloc(FavoriteFoodsState(favoriteFoodsModelObj: FavoriteFoodsModel(userRegistrationSheet: sheet)))..add(FavoriteFoodsInitialEvent()), child: FavoriteFoodsScreen()); } @@ -30,142 +28,118 @@ class FavoriteFoodsScreen extends StatefulWidget { class _FavoriteFoodsScreenState extends State { @override Widget build(BuildContext context) { - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - 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.90, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 19.v), - Container( - width: 327.h, - margin: EdgeInsets.only(left: 8.h, right: 7.h), - child: Text("favorite_unfavorite_foods".tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.textTheme.headlineSmall)), - SizedBox(height: 13.v), - _buildTextFormFieldFav(context), - SizedBox(height: 12.v), - _buildTextFormFieldUnfav(context), - SizedBox(height: 17.v), - BlocBuilder( - builder: (context, state) { - return NextStepButton( - text: "continue".tr.toUpperCase(), - 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, - ); - }); - }), - BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.favoriteFoodsModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.additionalInformationScreen, - arguments: sheet, - ); - }, - child: Text("skip".tr, style: theme.textTheme.bodyLarge), - ); - }), - ])))); + return Scaffold( + resizeToAvoidBottomInset: false, + 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.90, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 19.v), + Container( + width: 327.h, + margin: EdgeInsets.only(left: 8.h, right: 7.h), + child: Text("favorite_unfavorite_foods".tr, + maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.headlineSmall)), + SizedBox(height: 13.v), + _buildTextFormFieldFav(context), + SizedBox(height: 12.v), + _buildTextFormFieldUnfav(context), + Spacer(), + BlocBuilder(builder: (context, state) { + return NextStepButton( + text: "continue".tr.toUpperCase(), + margin: EdgeInsets.only(left: 16.h, right: 16.h, bottom: 40.v), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: onTapContinue(state, context)); + }), + BlocBuilder(builder: (context, state) { + return GestureDetector( + onTap: onTapSkip(state, context), + child: Text("skip".tr, style: theme.textTheme.bodyLarge), + ); + }), + ]))); + } + + onTapSkip(FavoriteFoodsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.favoriteFoodsModelObj!.userRegistrationSheet; + GoRouter.of(context).goNamed('additionalInformationScreen', extra: sheet); + }; + } + + onTapContinue(FavoriteFoodsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.favoriteFoodsModelObj!.userRegistrationSheet; + sheet.favoriteFoods = state.favoriteFoodsController!.text; + sheet.unfavoriteFoods = state.unfavoriteFoodsController!.text; + GoRouter.of(context).goNamed('additionalInformationScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), 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), - BlocSelector( + BlocSelector( selector: (state) => state.favoriteFoodsController, builder: (context, favoriteFoodsController) { - return CustomTextFormField( - controller: favoriteFoodsController, - hintText: "enter_information".tr, - maxLines: 7); + return CustomTextFormField(controller: favoriteFoodsController, hintText: "enter_information".tr, maxLines: 7); }) ]); } /// Section Widget - Widget _buildTextFormFieldUnfav(BuildContext context) { + _buildTextFormFieldUnfav(BuildContext context) { return Column(children: [ Text("unfavorite_foods".tr, style: CustomTextStyles.titleMediumSegoeUI), SizedBox(height: 2.v), - BlocSelector( + BlocSelector( selector: (state) => state.unfavoriteFoodsController, builder: (context, unfavoriteFoodsController) { return CustomTextFormField( - controller: unfavoriteFoodsController, - hintText: "enter_information".tr, - textInputAction: TextInputAction.done, - maxLines: 7); + controller: unfavoriteFoodsController, hintText: "enter_information".tr, textInputAction: TextInputAction.done, maxLines: 7); }) ]); } /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } /// Navigates to the scheduleScreen when the action is triggered. navigateToAdditionalInformationScreen(BuildContext context) { - NavigatorService.pushNamed( - AppRoutes.additionalInformationScreen, - ); + GoRouter.of(context).goNamed('additionalInformationScreen'); } } 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..66f98d28 100644 --- a/lib/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart +++ b/lib/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/favorite_sports_bloc.dart'; import 'models/favorite_sports_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -12,13 +14,9 @@ import 'package:flutter/material.dart'; class FavoriteSportsScreen extends StatefulWidget { const FavoriteSportsScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => FavoriteSportsBloc(FavoriteSportsState( - favoriteSportsModelObj: - FavoriteSportsModel(userRegistrationSheet: sheet))) + create: (context) => FavoriteSportsBloc(FavoriteSportsState(favoriteSportsModelObj: FavoriteSportsModel(userRegistrationSheet: sheet))) ..add(FavoriteSportsInitialEvent()), child: FavoriteSportsScreen()); } @@ -30,105 +28,88 @@ class FavoriteSportsScreen extends StatefulWidget { class _FavoriteSportsScreenState extends State { @override Widget build(BuildContext context) { - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - appBar: _buildAppBar(context), - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 5.v), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 20.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.83, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 38.v), - Text("your_favorite_sports".tr, - style: theme.textTheme.headlineSmall), - Spacer(flex: 32), - _buildTextFormFieldSection(context), - Spacer(flex: 67), - 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 - .favoriteSportsModelObj! - .userRegistrationSheet; - sheet.aboutSportHobbies = - state.enterinformationController!.text; - Navigator.pushNamed( - context, - AppRoutes.favoriteFoodsScreen, - arguments: sheet, - ); - }); - }), - BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = state - .favoriteSportsModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.favoriteFoodsScreen, - arguments: sheet, - ); - }, - child: - Text("skip".tr, style: theme.textTheme.bodyLarge), - ); - }), - ])))); + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: _buildAppBar(context), + body: Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 5.v), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + SizedBox(height: 20.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.83, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 38.v), + Text("your_favorite_sports".tr, style: theme.textTheme.headlineSmall), + Spacer(flex: 32), + _buildTextFormFieldSection(context), + Spacer(flex: 67), + 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: onTapContinue(state, context)); + }), + BlocBuilder(builder: (context, state) { + return GestureDetector( + onTap: onTapSkip(state, context), + child: Text("skip".tr, style: theme.textTheme.bodyLarge), + ); + }), + ]))); } - PreferredSizeWidget _buildAppBar(BuildContext context) { + onTapSkip(FavoriteSportsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.favoriteSportsModelObj!.userRegistrationSheet; + GoRouter.of(context).goNamed('favoriteFoodsScreen', extra: sheet); + }; + } + + onTapContinue(FavoriteSportsState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.favoriteSportsModelObj!.userRegistrationSheet; + sheet.aboutSportHobbies = state.enterinformationController!.text; + GoRouter.of(context).goNamed('favoriteFoodsScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), 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), + Text("fill_in_information".tr, style: CustomTextStyles.titleMediumSegoeUI), SizedBox(height: 2.v), - BlocSelector( + BlocSelector( selector: (state) => state.enterinformationController, builder: (context, enterinformationController) { return CustomTextFormField( controller: enterinformationController, - hintText: "enter_information".tr, + hintText: + "Я занимаюсь плаванием , почти каждый вечер прогулки на свежем воздухе, пару раз в месяц играю в волейбол. Стараюсь не сидеть на месте!", textInputAction: TextInputAction.done, maxLines: 8); }) @@ -136,6 +117,6 @@ class _FavoriteSportsScreenState extends State { } onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } } diff --git a/lib/presentation/registration/gender_screen/gender_screen.dart b/lib/presentation/registration/gender_screen/gender_screen.dart index 34dcc859..68909d7f 100644 --- a/lib/presentation/registration/gender_screen/gender_screen.dart +++ b/lib/presentation/registration/gender_screen/gender_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/gender_bloc.dart'; import 'models/gender_model.dart'; import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; @@ -11,9 +13,7 @@ import 'package:flutter/material.dart'; class GenderScreen extends StatefulWidget { const GenderScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( create: (context) => GenderBloc( GenderState( @@ -32,102 +32,100 @@ class _GenderScreenState extends State { Widget build(BuildContext context) { mediaQueryData = MediaQuery.of(context); return BlocBuilder(builder: (context, state) { - 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.16, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 36.v), - Text("your_gender".tr, - style: theme.textTheme.headlineSmall), - RadioListTile( - title: Text('Мужской'), - value: 'male', - groupValue: state.selectedRadioValue, - onChanged: (value) { - BlocProvider.of(context) - .add(GenderSelectEvent(value: 'male')); - }, - secondary: Image.asset( - 'assets/images/img_male.png', - width: 40, - height: 40, - ), - ), - RadioListTile( - title: Text('Женский'), - value: 'female', - groupValue: state.selectedRadioValue, - onChanged: (value) { - BlocProvider.of(context) - .add(GenderSelectEvent(value: 'female')); - }, - secondary: Image.asset( - 'assets/images/img_female.png', - width: 40, - height: 40, - ), - ), - Spacer(flex: 66), - 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.genderModelObj!.userRegistrationSheet; - sheet.gender = state.selectedRadioValue!; - Navigator.pushNamed( - context, - AppRoutes.dateOfBirthScreen, - arguments: sheet, - ); - }); - }), - ])))); + return 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.16, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 36.v), + Text("your_gender".tr, style: theme.textTheme.headlineSmall), + RadioListTile( + title: Text('Мужской'), + value: 'male', + groupValue: state.selectedRadioValue, + onChanged: onMale(context), + secondary: Image.asset( + 'assets/images/img_male.png', + width: 40, + height: 40, + ), + ), + RadioListTile( + title: Text('Женский'), + value: 'female', + groupValue: state.selectedRadioValue, + onChanged: onFemale(context), + secondary: Image.asset( + 'assets/images/img_female.png', + width: 40, + height: 40, + ), + ), + Spacer(flex: 66), + 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(GenderState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.genderModelObj!.userRegistrationSheet; + sheet.gender = state.selectedRadioValue!; + GoRouter.of(context).goNamed('dateOfBirthScreen', extra: 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, - ); - }), - centerTitle: true, - title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); + leadingWidth: 29.h, + leading: AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), onTap: onTapBack()), + centerTitle: true, + title: AppbarTitleImage( + imagePath: ImageConstant.imgLogoEatvio, + ), + ); + } + + Function onTapBack() { + return () { + GoRouter.of(context).pop(); + }; } /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } } 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..071285f9 100644 --- a/lib/presentation/registration/height_screen/height_screen.dart +++ b/lib/presentation/registration/height_screen/height_screen.dart @@ -1,127 +1,135 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/height_bloc.dart'; import 'models/height_model.dart'; 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) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => HeightBloc(HeightState( - heightModelObj: HeightModel(userRegistrationSheet: sheet))) - ..add(HeightInitialEvent()), + create: (context) => HeightBloc(HeightState(heightModelObj: HeightModel(userRegistrationSheet: sheet)))..add(HeightInitialEvent()), 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), - 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, - ); - }); - }), - ])))); + return 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), + 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; + GoRouter.of(context).goNamed('bloodPressureScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), 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(); + GoRouter.of(context).pop(); } } 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..75a7c1a3 100644 --- a/lib/presentation/registration/language_select_screen/language_select_screen.dart +++ b/lib/presentation/registration/language_select_screen/language_select_screen.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/data/models/userRegistrationModel/user_registration_model.dart'; +import 'package:go_router/go_router.dart'; import 'bloc/language_select_bloc.dart'; import 'models/language_select_model.dart'; @@ -9,98 +10,112 @@ import 'package:flutter/material.dart'; class LanguageSelectScreen extends StatelessWidget { const LanguageSelectScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { return BlocProvider( - create: (context) => LanguageSelectBloc( - LanguageSelectState(languageSelectModelObj: LanguageSelectModel())) - ..add(LanguageSelectInitialEvent()), - child: LanguageSelectScreen()); + create: (context) => LanguageSelectBloc( + LanguageSelectState( + languageSelectModelObj: LanguageSelectModel(), + ), + )..add(LanguageSelectInitialEvent()), + child: LanguageSelectScreen(), + ); } @override Widget build(BuildContext context) { - mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - body: Container( - width: double.maxFinite, - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 5.v), - child: Column(children: [ - CustomImageView( - imagePath: ImageConstant.imgLogoEatvio, - height: 50.adaptSize, - width: 50.adaptSize), - SizedBox(height: 20.v), - Container( - height: 16.v, - width: 343.h, + return Scaffold( + body: Container( + width: double.maxFinite, + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 5.v), + child: Column( + children: [ + CustomImageView(imagePath: ImageConstant.imgLogoEatvio, height: 50.adaptSize, width: 50.adaptSize), + SizedBox(height: 20.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, + backgroundColor: theme.colorScheme.onPrimary, + valueColor: AlwaysStoppedAnimation(appTheme.blueA400), + ), + ), + ), + SizedBox(height: 20.v), + Container( + width: 262.h, + margin: EdgeInsets.only(left: 41.h, right: 39.h), + child: Text( + "preferred_language".tr, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: theme.textTheme.headlineSmall, + ), + ), + Spacer(flex: 36), + BlocSelector( + selector: (state) => state.languageSelectModelObj, + builder: ( + context, + languageSelectModelObj, + ) { + return CustomDropDown( + icon: Container( + margin: EdgeInsets.fromLTRB(30.h, 11.v, 17.h, 11.v), 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, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 20.v), - Container( - width: 262.h, - margin: EdgeInsets.only(left: 41.h, right: 39.h), - child: Text("preferred_language".tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.textTheme.headlineSmall)), - Spacer(flex: 36), - BlocSelector( - selector: (state) => state.languageSelectModelObj, - builder: ( - context, - 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)); - }); - }), - Spacer(flex: 63), - ])), - bottomNavigationBar: - 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.languageSelectModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.credentialsScreen, - arguments: sheet, - ); - }); - }))); + 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), + ], + ), + ), + bottomNavigationBar: 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: _saveAndPush(state, context), + ); + }, + ), + ); + } + + void Function()? _saveAndPush(LanguageSelectState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.languageSelectModelObj!.userRegistrationSheet; + GoRouter.of(context).goNamed( + 'credentialsScreen', + extra: 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..9f01f6f8 100644 --- a/lib/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart +++ b/lib/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart @@ -1,3 +1,5 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/medicine_intake_bloc.dart'; import 'models/medicine_intake_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -12,13 +14,9 @@ import 'package:flutter/material.dart'; class MedicineIntakeScreen extends StatefulWidget { const MedicineIntakeScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => MedicineIntakeBloc(MedicineIntakeState( - medicineIntakeModelObj: - MedicineIntakeModel(userRegistrationSheet: sheet))) + create: (context) => MedicineIntakeBloc(MedicineIntakeState(medicineIntakeModelObj: MedicineIntakeModel(userRegistrationSheet: sheet))) ..add(MedicineIntakeInitialEvent()), child: MedicineIntakeScreen()); } @@ -31,114 +29,103 @@ class _MedicineIntakeScreenState extends State { @override Widget build(BuildContext context) { mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - 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.69, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 20.v), - Container( - width: 271.h, - margin: EdgeInsets.only(left: 35.h, right: 36.h), - child: Text("do_you_take_pills".tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.textTheme.headlineSmall)), - SizedBox(height: 86.v), - _buildTextFormField(context), - 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 - .medicineIntakeModelObj!.userRegistrationSheet; - sheet.aboutTakingPils = - state.enterInformationController!.text; - Navigator.pushNamed( - context, - AppRoutes.bloodPressureScreen, - arguments: sheet, - ); - }); - }), - BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () { - UserRegistrationSheet sheet = - state.medicineIntakeModelObj!.userRegistrationSheet; - Navigator.pushNamed( - context, - AppRoutes.bloodPressureScreen, - arguments: sheet, - ); - }, - child: Text("skip".tr, style: theme.textTheme.bodyLarge), - ); - }), - ])))); + return Scaffold( + resizeToAvoidBottomInset: false, + 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.69, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 20.v), + Container( + width: 271.h, + margin: EdgeInsets.only(left: 35.h, right: 36.h), + child: Text("do_you_take_pills".tr, + maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.headlineSmall)), + SizedBox(height: 86.v), + _buildTextFormField(context), + 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)); + }), + BlocBuilder(builder: (context, state) { + return GestureDetector( + onTap: onTapSkip(state, context), + child: Text("skip".tr, style: theme.textTheme.bodyLarge), + ); + }), + ]))); + } + + onTapSkip(MedicineIntakeState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.medicineIntakeModelObj!.userRegistrationSheet; + GoRouter.of(context).goNamed( + 'bloodPressureScreen', + extra: sheet, + ); + }; + } + + onTapNext(MedicineIntakeState state, BuildContext context) { + return () { + UserRegistrationSheet sheet = state.medicineIntakeModelObj!.userRegistrationSheet; + sheet.aboutTakingPils = state.enterInformationController!.text; + GoRouter.of(context).goNamed( + 'bloodPressureScreen', + extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 18.h, top: 15.v, bottom: 15.v), 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), + Text("fill_in_information".tr, style: CustomTextStyles.titleMediumSegoeUI), SizedBox(height: 2.v), - BlocSelector( + BlocSelector( selector: (state) => state.enterInformationController, builder: (context, enterInformationController) { return CustomTextFormField( - controller: enterInformationController, - hintText: "enter_information".tr, - textInputAction: TextInputAction.done, - maxLines: 8); + controller: enterInformationController, hintText: "enter_information".tr, textInputAction: TextInputAction.done, maxLines: 8); }) ]); } /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } } diff --git a/lib/presentation/registration/name_screen/name_screen.dart b/lib/presentation/registration/name_screen/name_screen.dart index cc234a77..e1f03271 100644 --- a/lib/presentation/registration/name_screen/name_screen.dart +++ b/lib/presentation/registration/name_screen/name_screen.dart @@ -1,3 +1,6 @@ +import 'package:eatvio_app/app_router.dart'; +import 'package:go_router/go_router.dart'; + import 'bloc/name_bloc.dart'; import 'models/name_model.dart'; import 'package:eatvio_app/core/app_export.dart'; @@ -12,9 +15,7 @@ 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; + static Widget builder(BuildContext context, sheet) { return BlocProvider( create: (context) => NameBloc( NameState( @@ -27,142 +28,126 @@ class NameScreen extends StatelessWidget { @override Widget build(BuildContext context) { mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - 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.1, - backgroundColor: theme.colorScheme.onPrimary, - valueColor: AlwaysStoppedAnimation( - appTheme.blueA400)))), - SizedBox(height: 39.v), - Text("what_is_your_name".tr, - style: theme.textTheme.headlineSmall), - SizedBox(height: 93.v), - Text("first_name".tr, style: theme.textTheme.bodyLarge), - SizedBox(height: 3.v), - BlocSelector( - selector: (state) => state.firstNameController, - builder: (context, firstNameController) { - return CustomTextFormField( - controller: firstNameController, - textInputAction: TextInputAction.done); - }), - SizedBox(height: 19.v), - Text("last_name".tr, style: theme.textTheme.bodyLarge), - SizedBox(height: 5.v), - BlocSelector( - selector: (state) => state.lastNameController, - builder: (context, lastNameController) { - return CustomTextFormField( - 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'), - ), - ], - ), - ); - } - }); + return Scaffold( + resizeToAvoidBottomInset: false, + 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.1, backgroundColor: theme.colorScheme.onPrimary, valueColor: AlwaysStoppedAnimation(appTheme.blueA400)))), + SizedBox(height: 39.v), + Text("what_is_your_name".tr, style: theme.textTheme.headlineSmall), + SizedBox(height: 93.v), + Text("first_name".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 3.v), + BlocSelector( + selector: (state) => state.firstNameController, + builder: (context, firstNameController) { + return CustomTextFormField(controller: firstNameController, textInputAction: TextInputAction.done); }), - ])))); + SizedBox(height: 19.v), + Text("last_name".tr, style: theme.textTheme.bodyLarge), + SizedBox(height: 5.v), + BlocSelector( + selector: (state) => state.lastNameController, + builder: (context, lastNameController) { + return CustomTextFormField( + controller: lastNameController, + ); + }), + 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(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); - }), + imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), onTap: () => onTapArrowLeft(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } /// Navigates to the previous screen. onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + GoRouter.of(context).pop(); } - 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; + AppRouter.navigatorKey.currentContext?.goNamed('genderScreen', extra: 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..bdd396f0 100644 --- a/lib/presentation/registration/splash_screen/bloc/splash_bloc.dart +++ b/lib/presentation/registration/splash_screen/bloc/splash_bloc.dart @@ -1,8 +1,12 @@ +import 'package:eatvio_app/app_router.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import '/core/app_export.dart'; import 'package:eatvio_app/presentation/registration/splash_screen/models/splash_model.dart'; + part 'splash_event.dart'; + part 'splash_state.dart'; /// A bloc that manages the state of a Splash according to the event that is dispatched to it. @@ -16,9 +20,7 @@ class SplashBloc extends Bloc { Emitter emit, ) async { Future.delayed(const Duration(milliseconds: 3000), () { - NavigatorService.popAndPushNamed( - AppRoutes.authScreen, - ); + AppRouter.navigatorKey.currentContext?.goNamed('myProfileScreen'); }); } } 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..7bf28f1f 100644 --- a/lib/presentation/registration/weight_screen/weight_screen.dart +++ b/lib/presentation/registration/weight_screen/weight_screen.dart @@ -1,134 +1,134 @@ +import 'package:go_router/go_router.dart'; + import 'bloc/weight_bloc.dart'; import 'models/weight_model.dart'; 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) { - final sheet = - ModalRoute.of(context)!.settings.arguments as UserRegistrationSheet; + static Widget builder(BuildContext context, sheet) { return BlocProvider( - create: (context) => WeightBloc(WeightState( - weightModelObj: WeightModel(userRegistrationSheet: sheet))) - ..add(WeightInitialEvent()), + create: (context) => WeightBloc(WeightState(weightModelObj: WeightModel(userRegistrationSheet: sheet)))..add(WeightInitialEvent()), 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), - 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, - ); - }); - }), - ])))); + return 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), + 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; + GoRouter.of(context).goNamed('heightScreen', extra: 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); - }), + leading: + AppbarLeadingImage(imagePath: ImageConstant.imgArrowLeft, margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), onTap: onTapBack(context)), centerTitle: true, title: AppbarTitleImage(imagePath: ImageConstant.imgLogoEatvio)); } - /// Navigates to the previous screen. - onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; } - /// Navigates to the allergiesScreen when the action is triggered. - navigateToHeight(BuildContext context) { - NavigatorService.pushNamed( - AppRoutes.heightScreen, - ); + /// Navigates to the previous screen. + onTapArrowLeft(BuildContext context) { + GoRouter.of(context).pop(); } } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart deleted file mode 100644 index c754e47b..00000000 --- a/lib/routes/app_routes.dart +++ /dev/null @@ -1,99 +0,0 @@ -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'; -import 'package:eatvio_app/presentation/registration/body_circumferences_screen/body_circumferences_screen.dart'; -import 'package:eatvio_app/presentation/registration/weight_screen/weight_screen.dart'; -import 'package:eatvio_app/presentation/registration/allergies_screen/allergies_screen.dart'; -import 'package:eatvio_app/presentation/registration/medicine_intake_screen/medicine_intake_screen.dart'; -import 'package:eatvio_app/presentation/registration/splash_screen/splash_screen.dart'; -import 'package:eatvio_app/presentation/registration/gender_screen/gender_screen.dart'; -import 'package:eatvio_app/presentation/registration/blood_pressure_screen/blood_pressure_screen.dart'; -import 'package:eatvio_app/presentation/registration/favorite_sports_screen/favorite_sports_screen.dart'; -import 'package:eatvio_app/presentation/registration/favorite_foods_screen/favorite_foods_screen.dart'; -import 'package:eatvio_app/presentation/registration/additional_information_screen/additional_information_screen.dart'; -import 'package:eatvio_app/presentation/registration/language_select_screen/language_select_screen.dart'; -import 'package:eatvio_app/presentation/registration/height_screen/height_screen.dart'; -import 'package:eatvio_app/presentation/registration/credentials_screen/credentials_screen.dart'; -import 'package:eatvio_app/presentation/registration/name_screen/name_screen.dart'; -import 'package:eatvio_app/presentation/registration/app_navigation_screen/app_navigation_screen.dart'; -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'; - -class AppRoutes { - // REGISTRATION - - static const String dateOfBirthScreen = '/date_of_birth_screen'; - - static const String cityOfLivingScreen = '/city_of_living_screen'; - - static const String bodyCircumferencesScreen = '/body_circumferences_screen'; - - static const String weightScreen = '/weight_screen'; - - static const String heightScreen = '/height_screen'; - - static const String allergiesScreen = '/allergies_screen'; - - static const String medicineIntakeScreen = '/medicine_intake_screen'; - - static const String splashScreen = '/splash_screen'; - - static const String genderScreen = '/gender_screen'; - - static const String bloodPressureScreen = '/blood_pressure_screen'; - - static const String favoriteSportsScreen = '/favorite_sports_screen'; - - static const String favoriteFoodsScreen = '/favorite_foods_screen'; - - static const String scheduleScreen = '/schedule_screen'; - - static const String additionalInformationScreen = - '/additional_information_screen'; - - static const String languageSelectScreen = '/language_select_screen'; - - static const String nameScreen = '/name_screen'; - - static const String credentialsScreen = '/credentials_screen'; - - static const String appNavigationScreen = '/app_navigation_screen'; - - static const String authScreen = '/auth_screen'; - - static const String initialRoute = '/initialRoute'; - - //PROFILE - static const String myProfile = '/myProfile'; - - static const String mySettings = '/mySettings'; - - static const String diaryEdit = '/diaryEdit'; - - 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, - }; -} diff --git a/lib/theme/custom_button_style.dart b/lib/theme/custom_button_style.dart index 95a5581e..60204f5e 100644 --- a/lib/theme/custom_button_style.dart +++ b/lib/theme/custom_button_style.dart @@ -8,7 +8,7 @@ class CustomButtonStyles { backgroundColor: appTheme.blueA400, side: BorderSide( color: theme.colorScheme.primary, - width: 1, + width: 0, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15.h), @@ -16,7 +16,7 @@ class CustomButtonStyles { ); // text button style static ButtonStyle get none => ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.transparent), - elevation: MaterialStateProperty.all(0), + backgroundColor: WidgetStateProperty.all(Colors.transparent), + elevation: WidgetStateProperty.all(0), ); } diff --git a/lib/theme/custom_text_style.dart b/lib/theme/custom_text_style.dart index ff969f92..de3bee78 100644 --- a/lib/theme/custom_text_style.dart +++ b/lib/theme/custom_text_style.dart @@ -8,155 +8,171 @@ import '../core/app_export.dart'; class CustomTextStyles { // Headline style static get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish; + // Title text style - static get titleMediumSegoeUI => - theme.textTheme.titleMedium!.segoeUI.copyWith( + static get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith( fontWeight: FontWeight.w600, ); + static get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith( color: appTheme.black900, ); + static get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith( color: appTheme.black900, fontSize: 13.fSize, ); + static get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith( color: appTheme.blueGray800, ); + static get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith( color: appTheme.blueGray800, ); + static get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith( color: appTheme.blueGray800, ); + static get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith( color: appTheme.whiteA700, ); - static get bodySmallMulishBlack900 => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith( color: appTheme.black900, fontSize: 12.fSize, ); - static get bodySmallMulishBluegray800 => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith( color: appTheme.blueGray800, fontSize: 12.fSize, ); - static get bodySmallMulishBluegray80012 => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith( color: appTheme.blueGray800, fontSize: 12.fSize, ); - static get bodySmallMulishBluegray80012_1 => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith( color: appTheme.blueGray800, fontSize: 12.fSize, ); - static get bodySmallMulishBluegray900 => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith( color: appTheme.blueGray900, fontSize: 12.fSize, ); - static get bodySmallMulishGray600 => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith( color: appTheme.gray600, ); - static get bodySmallMulishPrimary => - theme.textTheme.bodySmall!.mulish.copyWith( + + static get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith( color: theme.colorScheme.primary, fontSize: 12.fSize, ); - static get bodySmallSegoeUIBlack900 => - theme.textTheme.bodySmall!.segoeUI.copyWith( + + static get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith( color: appTheme.black900, fontSize: 12.fSize, ); + // Label text style static get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith( color: appTheme.gray60003, ); + static get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith( color: appTheme.whiteA700, ); + // Title text style static get titleMedium16 => theme.textTheme.titleMedium!.copyWith( fontSize: 16.fSize, ); + static get titleMediumBlack => theme.textTheme.titleMedium!.copyWith( fontSize: 17.fSize, fontWeight: FontWeight.w900, ); + static get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith( color: appTheme.black900, ); + static get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.w600, ); + static get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith( color: appTheme.whiteA700, fontWeight: FontWeight.w600, ); + static get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith( color: appTheme.blue800, fontWeight: FontWeight.w800, ); + static get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith( color: appTheme.blueGray800, ); + static get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith( color: appTheme.blueGray800, fontWeight: FontWeight.w700, ); - static get titleSmallBluegray800ExtraBold => - theme.textTheme.titleSmall!.copyWith( + + static get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith( color: appTheme.blueGray800, fontWeight: FontWeight.w800, ); + static get titleSmallBold => theme.textTheme.titleSmall!.copyWith( fontSize: 15.fSize, fontWeight: FontWeight.w700, ); + static get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith( color: appTheme.greenA70001, ); - static get titleSmallGreenA70001ExtraBold => - theme.textTheme.titleSmall!.copyWith( + + static get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith( color: appTheme.greenA70001, fontWeight: FontWeight.w800, ); + static get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith( color: appTheme.lightGreen300, fontWeight: FontWeight.w800, ); + static get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith( color: appTheme.red200, fontWeight: FontWeight.w800, ); - static get titleSmallSecondaryContainer => - theme.textTheme.titleSmall!.copyWith( + + static get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith( color: theme.colorScheme.secondaryContainer, fontWeight: FontWeight.w800, ); - static get titleSmallSecondaryContainer_1 => - theme.textTheme.titleSmall!.copyWith( + + static get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith( color: theme.colorScheme.secondaryContainer, ); - static get titleSmallSegoeUIOnPrimary => - theme.textTheme.titleSmall!.segoeUI.copyWith( + + static get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith( color: theme.colorScheme.onPrimary, ); } extension on TextStyle { TextStyle get mulish { - return copyWith( - fontFamily: 'Mulish', - ); + return copyWith(); } TextStyle get segoeUI { - return copyWith( - fontFamily: 'Segoe UI', - ); + return copyWith(); } } diff --git a/lib/theme/theme_helper.dart b/lib/theme/theme_helper.dart index 699e2825..7226e373 100644 --- a/lib/theme/theme_helper.dart +++ b/lib/theme/theme_helper.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../../core/app_export.dart'; /// Helper class for managing themes and colors. @@ -50,6 +51,13 @@ class ThemeHelper { colorScheme: colorScheme, textTheme: TextThemes.textTheme(colorScheme), scaffoldBackgroundColor: colorScheme.onPrimary, + appBarTheme: AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarIconBrightness: Brightness.dark, + statusBarBrightness: Brightness.light, + systemNavigationBarColor: Colors.white, + ), + ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( backgroundColor: Colors.transparent, @@ -187,6 +195,8 @@ class PrimaryColors { // White Color get whiteA700 => Color(0XFFFFFFFF); + + Color get backgroundAvatar => Color(0xFFEBF4FF); } PrimaryColors get appTheme => ThemeHelper().themeColor(); 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..75784abc 100644 --- a/lib/widgets/app_bar/appbar_subtitle.dart +++ b/lib/widgets/app_bar/appbar_subtitle.dart @@ -21,9 +21,6 @@ class AppbarSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - onTap!.call(); - }, child: Padding( padding: margin ?? EdgeInsets.zero, child: Text( 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/custom_app_bar.dart b/lib/widgets/app_bar/custom_app_bar.dart index 7ffe17b3..469e9315 100644 --- a/lib/widgets/app_bar/custom_app_bar.dart +++ b/lib/widgets/app_bar/custom_app_bar.dart @@ -30,7 +30,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { return AppBar( - elevation: 0, toolbarHeight: height ?? 56.v, automaticallyImplyLeading: false, backgroundColor: Colors.transparent, 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/bottom_navigation_bar_widget.dart b/lib/widgets/bottom_navigation_bar_widget.dart new file mode 100644 index 00000000..68197c17 --- /dev/null +++ b/lib/widgets/bottom_navigation_bar_widget.dart @@ -0,0 +1,160 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_navigation_menu.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; + +int currentBottomNavBarIndex = 0; + +class BottomNavigationBarWidget extends StatefulWidget { + final Widget child; + + const BottomNavigationBarWidget({ + super.key, + required this.child, + }); + + @override + State createState() => _BottomNavigationBarWidgetState(); +} + +class _BottomNavigationBarWidgetState extends State { + void _onItemTapped(int index) { + if (currentBottomNavBarIndex != index) { + setState(() { + currentBottomNavBarIndex = index; + }); + } + switch (currentBottomNavBarIndex) { + case 0: + GoRouter.of(context).replace('/my-profile-screen'); + break; + case 1: + GoRouter.of(context).replace('/feed-screen'); + break; + case 2: + GoRouter.of(context).replace('/diary-screen'); + break; + case 3: + GoRouter.of(context).replace('/chat-select-screen'); + break; + case 4: + break; + case 5: + final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final size = overlay.size; + showMenu( + context: context, + color: Colors.white, + shadowColor: Colors.white, + surfaceTintColor: Colors.white, + items: [ + PopupMenuItem( + child: CustomNavigationMenu(), + ) + ], + position: RelativeRect.fromLTRB(size.width, size.height, 0, 0), + ); + break; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: widget.child, + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + onTap: _onItemTapped, + selectedFontSize: 12, + unselectedFontSize: 12, + selectedItemColor: appTheme.blueA400, + currentIndex: currentBottomNavBarIndex, + items: [ + BottomNavigationBarItem( + label: '', + icon: SvgPicture.asset( + ImageConstant.imgLogo, + ), + ), + BottomNavigationBarItem( + label: 'Лента', + icon: SvgPicture.asset( + width: 24, + height: 24, + ImageConstant.imgFeed, + ), + activeIcon: SvgPicture.asset( + ImageConstant.imgFeed, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ), + BottomNavigationBarItem( + label: "diary".tr, + icon: SvgPicture.asset( + ImageConstant.imgDiary, + width: 24, + height: 24, + ), + activeIcon: SvgPicture.asset( + ImageConstant.imgDiary, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ), + BottomNavigationBarItem( + label: "chat".tr, + icon: SvgPicture.asset( + ImageConstant.imgChat, + width: 24, + height: 24, + ), + activeIcon: SvgPicture.asset( + ImageConstant.imgChat, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ), + BottomNavigationBarItem( + label: '', + icon: SvgPicture.asset( + ImageConstant.imgBell, + width: 24, + height: 24, + ), + activeIcon: SvgPicture.asset( + ImageConstant.imgBell, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ), + BottomNavigationBarItem( + label: '', + icon: Image.asset( + ImageConstant.imgEllipse1, + width: 32, + height: 32, + ), + ), + ], + ), + ); + } +} 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 deleted file mode 100644 index 67ae4e3f..00000000 --- a/lib/widgets/custom_bottom_bar.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:eatvio_app/core/app_export.dart'; -import 'package:flutter/material.dart'; - -class CustomBottomBar extends StatefulWidget { - CustomBottomBar({this.onChanged}); - - Function(BottomBarEnum)? onChanged; - - @override - CustomBottomBarState createState() => CustomBottomBarState(); -} - -class CustomBottomBarState extends State { - int selectedIndex = 2; - - List bottomMenuList = [ - BottomMenuModel( - icon: ImageConstant.imgNav, - activeIcon: ImageConstant.imgNav, - title: "diary".tr, - type: BottomBarEnum.tf, - ), - BottomMenuModel( - icon: ImageConstant.imgNavBlueGray400, - activeIcon: ImageConstant.imgNavBlueGray400, - title: "my_blog".tr, - type: BottomBarEnum.tf, - ), - BottomMenuModel( - icon: ImageConstant.imgNavBlueGray40024x23, - activeIcon: ImageConstant.imgNavBlueGray40024x23, - title: "profile".tr, - type: BottomBarEnum.tf, - ), - BottomMenuModel( - icon: ImageConstant.imgNavPrimary, - activeIcon: ImageConstant.imgNavPrimary, - title: "admin".tr, - type: BottomBarEnum.tf, - ), - BottomMenuModel( - icon: ImageConstant.imgNavBlueGray40024x25, - activeIcon: ImageConstant.imgNavBlueGray40024x25, - title: "chat".tr, - type: BottomBarEnum.tf, - ) - ]; - - @override - Widget build(BuildContext context) { - return Container( - height: 80.v, - decoration: BoxDecoration( - border: Border.all( - color: appTheme.whiteA700, - width: 1.h, - ), - ), - child: BottomNavigationBar( - backgroundColor: Colors.transparent, - showSelectedLabels: false, - showUnselectedLabels: false, - selectedFontSize: 0, - 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(() {}); - }, - ), - ); - } -} - -enum BottomBarEnum { - tf, -} - -class BottomMenuModel { - BottomMenuModel({ - required this.icon, - required this.activeIcon, - this.title, - required this.type, - }); - - String icon; - - String activeIcon; - - 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, - ), - ), - ], - ), - ), - ); - } -} 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 12b98cf3..7f56bbd8 100644 --- a/lib/widgets/custom_edit_drop_down.dart +++ b/lib/widgets/custom_edit_drop_down.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; class CustomDropDown extends StatelessWidget { @@ -22,6 +23,7 @@ class CustomDropDown extends StatelessWidget { this.filled = true, this.validator, this.onChanged, + this.defaultValue, }) : super( key: key, ); @@ -38,7 +40,7 @@ class CustomDropDown extends StatelessWidget { final TextStyle? textStyle; - final List? items; + final List? items; final String? hintText; @@ -60,75 +62,84 @@ class CustomDropDown extends StatelessWidget { final bool? filled; - final FormFieldValidator? validator; + final SelectionPopupModel? defaultValue; - final Function(String)? onChanged; + final FormFieldValidator? validator; + + final Function(SelectionPopupModel)? onChanged; @override Widget build(BuildContext context) { 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!, - items: items?.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - overflow: TextOverflow.ellipsis, - ), - ); - }).toList(), - decoration: decoration, - validator: validator, - onChanged: (value) { - onChanged!(value.toString()); - }, - ), - ); - InputDecoration get decoration => InputDecoration( - hintText: hintText ?? "", - hintStyle: hintStyle, - prefixIcon: prefix, - prefixIconConstraints: prefixConstraints, - suffixIcon: suffix, - suffixIconConstraints: suffixConstraints, - isDense: true, - contentPadding: contentPadding ?? - EdgeInsets.only( - left: 17, - top: 17, - bottom: 17, + _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, - filled: filled, - border: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - enabledBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - focusedBorder: borderDecoration ?? - OutlineInputBorder( - borderRadius: BorderRadius.circular(5), - borderSide: BorderSide( - color: Colors.blue, - 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 new file mode 100644 index 00000000..0853d3e5 --- /dev/null +++ b/lib/widgets/custom_edit_text_form_field.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CustomTextFormField extends StatelessWidget { + CustomTextFormField({ + Key? key, + this.alignment, + this.width, + this.controller, + this.focusNode, + this.autofocus = true, + this.textStyle, + this.obscureText = false, + this.textInputAction = TextInputAction.next, + this.textInputType = TextInputType.text, + this.maxLines, + this.hintText, + this.hintStyle, + this.prefix, + this.prefixConstraints, + this.suffix, + this.suffixConstraints, + this.onFieldSubmitted, + this.initialValue, + this.contentPadding, + this.inputFormatters, + this.borderDecoration, + this.fillColor, + this.filled = true, + this.validator, + this.keyboardType, + this.onChanged, + 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 Function(String)? onFieldSubmitted; + + final BoxConstraints? suffixConstraints; + + final EdgeInsets? contentPadding; + + final InputBorder? borderDecoration; + + final Color? fillColor; + + final bool? filled; + + final TextInputType? keyboardType; + + final String? initialValue; + + final FormFieldValidator? validator; + + final void Function(String?)? onSaved; + + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: _buildTextFormFieldWidget(context), + ) + : _buildTextFormFieldWidget(context); + } + + _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..c3d552b7 --- /dev/null +++ b/lib/widgets/custom_expandable_list.dart @@ -0,0 +1,445 @@ +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:go_router/go_router.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() { + GoRouter.of(context).go('/my-profile-screen'); + } + + 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..03d11a98 --- /dev/null +++ b/lib/widgets/custom_navigation_menu.dart @@ -0,0 +1,81 @@ +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:go_router/go_router.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: () => GoRouter.of(context).push('/my-settings-screen'), + ), + MenuItem( + icon: ImageConstant.imgRecipes, + text: 'Мои рецепты', + onTap: () => GoRouter.of(context).push('/recipe-select-screen'), + // Действие при нажатии на "Мои рецепты" + ), + MenuItem( + icon: ImageConstant.imgDiets, + text: 'Мои диеты', + onTap: () => GoRouter.of(context).push('/diet-select-screen'), + // Действие при нажатии на "Мои рецепты" + ), + MenuItem( + icon: ImageConstant.imgPlans, + text: 'Мои планы', + onTap: () => GoRouter.of(context).push('/plan-select-screen'), + // Действие при нажатии на "Мои рецепты" + ), + MenuItem( + icon: ImageConstant.imgFinance, + text: 'Глоссарий', + onTap: () => GoRouter.of(context).push('/glossary-screen'), + ), + 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 + GoRouter.of(context).go('/auth-screen'); + } + }; +} 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_scale_element.dart b/lib/widgets/custom_scale_element.dart new file mode 100644 index 00000000..531c95ef --- /dev/null +++ b/lib/widgets/custom_scale_element.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class ScaleElement extends StatelessWidget { + final Color color; + final bool isSelected; + final VoidCallback onTap; + + const ScaleElement({ + required this.color, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: isSelected ? 40 : 20, + height: isSelected ? 40 : 20, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + ); + } +} 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..e70b2b53 100644 --- a/lib/widgets/custom_text_form_field.dart +++ b/lib/widgets/custom_text_form_field.dart @@ -28,120 +28,194 @@ class CustomTextFormField extends StatelessWidget { this.filled = true, this.validator, this.keyboardType, + this.onFieldSubmitted, this.onSaved, + this.initialValue, + this.onChanged, }) : 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 String? initialValue; + final Function(String)? onFieldSubmitted; final void Function(String?)? onSaved; + final void Function(String?)? onChanged; @override Widget build(BuildContext context) { return alignment != null ? Align( alignment: alignment ?? Alignment.center, - child: textFormFieldWidget, + child: SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + controller: controller, + focusNode: focusNode ?? null, + autofocus: autofocus!, + style: textStyle ?? theme.textTheme.titleMedium, + obscureText: obscureText!, + textInputAction: textInputAction, + maxLines: maxLines ?? 1, + onFieldSubmitted: onFieldSubmitted, + onChanged: onChanged, + 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, + initialValue: initialValue, + ), + ), ) - : textFormFieldWidget; + : SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + controller: controller, + focusNode: focusNode ?? null, + autofocus: autofocus!, + style: textStyle ?? theme.textTheme.titleMedium, + obscureText: obscureText!, + textInputAction: textInputAction, + maxLines: maxLines ?? 1, + initialValue: initialValue, + 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, + ), + ), + ); + } +}*/ 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 40b6b2f2..6000794d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,21 @@ name: eatvio_app description: A new Flutter project. -version: 1.0.0+1 -environment: - sdk: '>=2.12.0 <3.0.0' -dependencies: - flutter: +version: 0.0.0+1 + +environment: + sdk: '>=3.3.4 <4.0.0' + +dependencies: + flutter: sdk: flutter - flutter_localizations: + + flutter_localizations: sdk: flutter + flutter_bloc: ^8.0.0 connectivity_plus: ^5.0.2 shared_preferences: ^2.0.15 - cached_network_image: ^3.2.1 + cached_network_image: ^3.3.1 flutter_svg: ^2.0.9 equatable: ^2.0.0 validators: ^3.0.0 @@ -23,28 +27,52 @@ dependencies: flutter_spinkit: ^5.0.0 flutter_dotenv: ^5.1.0 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 + # navigation + go_router: ^14.1.4 + keyboard_dismisser: ^3.0.0 -dev_dependencies: + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + retrofit: ^4.1.0 + get_it: ^7.7.0 + dio: ^5.4.3+1 + + # photo viewer + photo_view: ^0.15.0 + #url launcher + url_launcher: ^6.3.0 + +dev_dependencies: dart_code_metrics: ^5.7.6 dart_code_metrics_presets: ^1.8.0 - flutter_test: + build_runner: ^2.4.11 + freezed: ^2.5.2 + json_serializable: ^6.8.0 + retrofit_generator: ^8.1.0 + + flutter_test: sdk: flutter -flutter: + +flutter: uses-material-design: true - assets: + assets: - assets/ - assets/images/ - .env - fonts: + fonts: - family: Segoe UI - fonts: + fonts: - asset: assets/fonts/SegoeUISemibold.ttf weight: 600 - family: Mulish - fonts: + fonts: - asset: assets/fonts/MulishBold.ttf weight: 700 - asset: assets/fonts/MulishRegular.ttf weight: 400 - asset: assets/fonts/MulishSemiBold.ttf - weight: 600 + weight: 600 \ No newline at end of file diff --git a/rc b/rc new file mode 100644 index 00000000..49ddfaa0 --- /dev/null +++ b/rc @@ -0,0 +1,102 @@ +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/custom_bottom_bar.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); + return Scaffold( + resizeToAvoidBottomInset: false, + body: SafeArea( + child: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + 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.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(CreateRecipe()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + ), + bottomNavigationBar: _buildBottomBar(context)); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} 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..c7570d39 --- /dev/null +++ b/test/diary_test.dart @@ -0,0 +1,71 @@ +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(), + ), + ), + ); + + 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); - }); -}