diff --git a/.github/workflows/branch_name_validation.yaml b/.github/workflows/branch_name_validation.yaml index b297195..07ba91c 100644 --- a/.github/workflows/branch_name_validation.yaml +++ b/.github/workflows/branch_name_validation.yaml @@ -17,11 +17,11 @@ jobs: BRANCH_NAME="${{ github.head_ref }}" echo "Branch name: $BRANCH_NAME" - # Allowed types + # Allowed prefixes PREFIXES="feature|fix|hotfix|refactor|bugfix|release|docs|perf|test|chore" # Allowed special branch names - ALLOWED_BRANCHES="main|development" + ALLOWED_BRANCHES="main|developement" # Check if branch is explicitly allowed if [[ "$BRANCH_NAME" =~ ^($ALLOWED_BRANCHES)$ ]]; then @@ -29,16 +29,12 @@ jobs: exit 0 fi - # Regex pattern: type/fit-123-task-name - PATTERN="^($PREFIXES)/fit-[0-9]+(-[a-z0-9\-]+)*$" - - # Validate branch name - if [[ ! "$BRANCH_NAME" =~ $PATTERN ]]; then + # Validate branch name format + if [[ ! "$BRANCH_NAME" =~ ^($PREFIXES)/[a-z0-9\-]+$ ]]; then echo "❌ Invalid branch name: $BRANCH_NAME" - echo "Branch names must follow the pattern: type/fit-123-task-name" - echo "Examples: feature/fit-456-login-api, fix/fit-789-crash-issue" - echo "Allowed types: $PREFIXES" - echo "Allowed special branches: main, development" + echo "Branch names must follow the pattern: feature/meaningful-name, fix/issue-description, etc." + echo "Allowed special branches: main, develop" + echo "Example: feature/authentication-module, fix/user-login-bug, bugfix/api-timeout-issue" exit 1 fi diff --git a/.github/workflows/flutter-unit-tests.yaml b/.github/workflows/flutter-unit-tests.yaml index c31ecce..d39ef41 100644 --- a/.github/workflows/flutter-unit-tests.yaml +++ b/.github/workflows/flutter-unit-tests.yaml @@ -15,6 +15,7 @@ on: - closed - reopened - unlocked + jobs: test: diff --git a/.github/workflows/pull_request_title_validation.yaml b/.github/workflows/pull_request_title_validation.yaml index b38d6cf..8b031f2 100644 --- a/.github/workflows/pull_request_title_validation.yaml +++ b/.github/workflows/pull_request_title_validation.yaml @@ -23,12 +23,12 @@ jobs: PR_title="${{ github.event.pull_request.title }}" # Define regex to validate PR title format - pattern='^(Fix|Release|Hotfix|Test|Feature|Docs|Chore|Refactor|Bugfix|Perf)\/FIT-[0-9]+-.+' + pattern='^(Fix|Release|Hotfix|Test|Feature|Docs|Chore|Refactor|Bugfix|Perf)\/TA-[0-9]+-.+' # Validate PR title using regex if ! [[ "$PR_title" =~ $pattern ]]; then - echo "❌ PR title does not match the expected format: 'type/FIT-Number-TaskName'" - echo "Expected format: 'Feature/FIT-4-Initialize-Project'" + echo "❌ PR title does not match the expected format: 'type/TA-Number-TaskName'" + echo "Expected format: 'Feature/TA-4-Initialize-Project'" echo "Allowed types: fix, release, feat, hotfix, build, test, feature" exit 1 fi diff --git a/.github/workflows/sonar_qube_cloud.yaml b/.github/workflows/sonar_qube_cloud.yaml deleted file mode 100644 index 97d569b..0000000 --- a/.github/workflows/sonar_qube_cloud.yaml +++ /dev/null @@ -1,76 +0,0 @@ -name: SonarCloud Analysis -on: - push: - branches: - - development - - main - pull_request: - types: - - opened - - synchronize - - reopened - - edited - - ready_for_review - - review_requested - - review_request_removed - - labeled - - unlabeled - - closed - - reopened - - unlocked - -jobs: - sonarcloud: - name: SonarCloud Scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Required for SonarCloud analysis - - # Set up Java (required for SonarScanner) - - name: 🔧 Setup Java (for Android builds) - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "17" - - # Set up Flutter - - name: 🛠 Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - # Install lcov for coverage reporting - - name: 🔽 Install lcov - run: sudo apt-get install -y lcov - - # Install dependencies - - name: 📦 Install Dependencies - run: flutter pub get - - # Run tests with coverage - - name: 🧪 Run tests with coverage - run: | - flutter test --coverage - # Convert lcov to SonarQube format - lcov --list coverage/lcov.info # Verify coverage file exists - - # SonarCloud Scan - - name: 🔍 SonarCloud Scan - uses: sonarsource/sonarcloud-github-action@master - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - args: > - -Dsonar.organization=ahmedelazab1220 - -Dsonar.projectKey=ahmedelazab1220_fitness_app - -Dsonar.sources=lib - -Dsonar.host.url=https://sonarcloud.io - -Dsonar.dart.lcov.reportPaths=coverage/lcov.info - -Dsonar.language=dart - -Dsonar.exclusions=**/*.g.dart,**/*.freezed.dart,**/*.gen.dart,android/**,ios/**,windows/**,macos/**,assets/**,lib/data/**/api/**,lib/data/**/models/**,lib/domain/**/entities/**,core/utils/di/**,core/utils/font_responsive/**,core/utils/responsive_util/** - -Dsonar.c.file.suffixes=- - -Dsonar.cpp.file.suffixes=- - -Dsonar.objc.file.suffixes=- \ No newline at end of file diff --git a/assets/translations/ar.json b/assets/translations/ar.json index 1bd10b8..056cb35 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -1,5 +1,5 @@ { - "NoMusclesFound": "لا توجد عضلات", + "NoMusclesFound": "لا توجد عضلات", "TellUsAboutYourselfWeNeedToKnowYourGender": "أخبرنا عن نفسك!\n نحتاج إلى معرفة جنسك", "HowOldAreYouThisHelpsUsCreateYourPersonalizedPlan": "كم عمرك؟\n هذا يساعدنا في انشاء خطتك المخصصة", "WhatIsYourHight": "ما هو طولك؟", @@ -18,139 +18,141 @@ "motivational": "محبوب", "OopsSomethingWentWrongLetsGetBackToFitnessTryAskingAboutYourNextWorkout": "عذرا، حدث خطأ ما! يرجى العودة إلى الياقة البدنية—حاول سؤال عن عملية التمرين القادمة!", "InvalidCredentials": "تاكد من ان البريد الالكتروني وكلمة المرور الخاصة بك صحيحة", - "Receive_timeout": "انت تستخدم الانترنت بطريقة غير صحيحة", - "Timeout_occurred": "أوه، لقد فقدنا الاتصال للحظة. حاول مجددًا عندما تكون جاهزًا!", - "Invalid_certificate": "هناك مشكلة أمنية في شهادتنا. يرجى التواصل مع الدعم للمساعدة.", - "Unexpected_server_error": "حدث خطأ ما من جانبنا. نأسف لذلك—يرجى المحاولة لاحقًا.", - "Request_cancelled": "يبدو أن الطلب قد أُلغي. أخبرنا إذا كنت بحاجة إلى مساعدة!", - "Connection_failed": "لم نتمكن من الاتصال. هل يمكنك التحقق من شبكتك والمحاولة مجددًا؟", - "Unexpected_error": "يا إلهي، حدث شيء غير متوقع. يرجى المحاولة مرة أخرى أو إعلامنا إذا استمر الأمر!", - "Not_found": "لم نتمكن من العثور على ما تبحث عنه. تحقق مرة أخرى وحاول مجددًا؟", - "Internal_server_error": "خادومنا يمر بلحظة صعبة. يرجى المحاولة مجددًا قريبًا—نحن نعمل على ذلك!", - "EmailNotValid": "لا يبدو هذا البريد الإلكتروني صالحًا. هل يمكنك إدخاله مرة أخرى؟", - "Connection_timeout": "انتهت مهلة الاتصال. يرجى التحقق من الإنترنت والمحاولة مرة أخرى.", - "Send_timeout": "استغرق الإرسال وقتًا طويلاً هذه المرة. هل يمكنك المحاولة مجددًا عندما تكون جاهزًا؟", - "Bad_request": "هناك خطأ ما في هذا الطلب. هل يمكنك المحاولة مجددًا أو تعديل ما أدخلته؟", - "Unauthorized": "أوه، يبدو أنك لا تملك الإذن للقيام بذلك. تحقق من تفاصيل تسجيل الدخول الخاصة بك!", - "Forbidden": "عذرًا، الوصول إلى هذا مقيد. أخبرنا إذا كنت تعتقد أن هذا خطأ!", - "NoInternetConnection": "لا يوجد اتصال بالإنترنت", - "Service_unavailable": "خدمتنا متوقفة مؤقتًا للصيانة. يرجى التحقق مرة أخرى بعد قليل!", - "DataParsingException": "حدثت مشكلة أثناء معالجة البيانات. يرجى المحاولة مجددًا أو التواصل مع الدعم إذا استمرت المشكلة.", - "OtpCodeIsInvalidOrExpired": "رمز التحقق غير صالح أو منتهي الصلاحية", - "ThePriceOfExcellenceIsDiscipline": "ثمن التميز\n هو الانضباط", - "LoremIpsumDolorSitAmetConsecteturEuUrnaUtGravidaQuisIdPretiumPurusMaurIsMassa": "لوريم إيبسوم دولور سيت أميت كونسيكتتور. يو أرنا أوت جرافيدا كويز إيد بريتيوم بروس. موريس ماسا", - "FitnessHasNeverBeenSoMuchFun": "اللياقة البدنية لم تكن ممتعة\n هكذا من قبل", - "NOMOREEXCUSESDoItNow": "لا مزيد من الأعذار\n افعلها الآن", - "Back": "رجوع", - "Next": "التالي", - "Skip": "تخطي", - "HeyThere": "مرحبًا", - "WelcomeBack": "مرحبًا بعودتك", - "Login": "تسجيل الدخول", - "Email": "البريد الإلكتروني", - "Password": "كلمة المرور", - "ForgotPassword": "هل نسيت كلمة المرور؟", - "Or": "أو", - "DonotHaveAnAccountYet": "لا تملك حسابًا بعد؟", - "Register": "تسجيل", - "CreateAnAccount": "إنشاء حساب", - "FirstName": "الاسم الأول", - "LastName": "الاسم الأخير", - "AlreadyHaveAnAccount": "هل لديك حساب بالفعل؟", - "TellUsAboutYourself": "أخبرنا عن نفسك!", + "Receive_timeout": "انت تستخدم الانترنت بطريقة غير صحيحة", + "Timeout_occurred": "أوه، لقد فقدنا الاتصال للحظة. حاول مجددًا عندما تكون جاهزًا!", + "Invalid_certificate": "هناك مشكلة أمنية في شهادتنا. يرجى التواصل مع الدعم للمساعدة.", + "Unexpected_server_error": "حدث خطأ ما من جانبنا. نأسف لذلك—يرجى المحاولة لاحقًا.", + "Request_cancelled": "يبدو أن الطلب قد أُلغي. أخبرنا إذا كنت بحاجة إلى مساعدة!", + "Connection_failed": "لم نتمكن من الاتصال. هل يمكنك التحقق من شبكتك والمحاولة مجددًا؟", + "Unexpected_error": "يا إلهي، حدث شيء غير متوقع. يرجى المحاولة مرة أخرى أو إعلامنا إذا استمر الأمر!", + "Not_found": "لم نتمكن من العثور على ما تبحث عنه. تحقق مرة أخرى وحاول مجددًا؟", + "Internal_server_error": "خادومنا يمر بلحظة صعبة. يرجى المحاولة مجددًا قريبًا—نحن نعمل على ذلك!", + "EmailNotValid": "لا يبدو هذا البريد الإلكتروني صالحًا. هل يمكنك إدخاله مرة أخرى؟", + "Connection_timeout": "انتهت مهلة الاتصال. يرجى التحقق من الإنترنت والمحاولة مرة أخرى.", + "Send_timeout": "استغرق الإرسال وقتًا طويلاً هذه المرة. هل يمكنك المحاولة مجددًا عندما تكون جاهزًا؟", + "Bad_request": "هناك خطأ ما في هذا الطلب. هل يمكنك المحاولة مجددًا أو تعديل ما أدخلته؟", + "Unauthorized": "أوه، يبدو أنك لا تملك الإذن للقيام بذلك. تحقق من تفاصيل تسجيل الدخول الخاصة بك!", + "Forbidden": "عذرًا، الوصول إلى هذا مقيد. أخبرنا إذا كنت تعتقد أن هذا خطأ!", + "NoInternetConnection": "لا يوجد اتصال بالإنترنت", + "Service_unavailable": "خدمتنا متوقفة مؤقتًا للصيانة. يرجى التحقق مرة أخرى بعد قليل!", + "DataParsingException": "حدثت مشكلة أثناء معالجة البيانات. يرجى المحاولة مجددًا أو التواصل مع الدعم إذا استمرت المشكلة.", + "OtpCodeIsInvalidOrExpired": "رمز التحقق غير صالح أو منتهي الصلاحية", + "ThePriceOfExcellenceIsDiscipline": "ثمن التميز\n هو الانضباط", + "LoremIpsumDolorSitAmetConsecteturEuUrnaUtGravidaQuisIdPretiumPurusMaurIsMassa": "لوريم إيبسوم دولور سيت أميت كونسيكتتور. يو أرنا أوت جرافيدا كويز إيد بريتيوم بروس. موريس ماسا", + "FitnessHasNeverBeenSoMuchFun": "اللياقة البدنية لم تكن ممتعة\n هكذا من قبل", + "NOMOREEXCUSESDoItNow": "لا مزيد من الأعذار\n افعلها الآن", + "Back": "رجوع", + "Next": "التالي", + "Skip": "تخطي", + "HeyThere": "مرحبًا", + "WelcomeBack": "مرحبًا بعودتك", + "Login": "تسجيل الدخول", + "Email": "البريد الإلكتروني", + "Password": "كلمة المرور", + "ForgotPassword": "هل نسيت كلمة المرور؟", + "Or": "أو", + "DonotHaveAnAccountYet": "لا تملك حسابًا بعد؟", + "Register": "تسجيل", + "CreateAnAccount": "إنشاء حساب", + "FirstName": "الاسم الأول", + "LastName": "الاسم الأخير", + "AlreadyHaveAnAccount": "هل لديك حساب بالفعل؟", + "TellUsAboutYourself": "أخبرنا عن نفسك!", "WeNeedToKnowYourGender": "نحتاج إلى معرفة جنسك", - "Male": "ذكر", - "Female": "انثى", - "Year": "سنة", - "Kg": "كجم", + "Male": "ذكر", + "Female": "انثى", + "Year": "سنة", + "Kg": "كجم", "Cm": "سم", "HowOldAreYou": "كم عمرك؟", - "Done": "تم", - "WhatIsYourWeight": "ما هو وزنك؟", - "ThisHelpsUsCreateYourPersonalizedPlan": "هذا يساعدنا في إنشاء خطتك المخصصة", - "WhatIsYourHeight": "ما هو طولك؟", - "whatIsYourGoal": "ما هو هدفك؟", - "GainWeight": "زيادة الوزن", - "LoseWeight": "فقدان الوزن", - "GetFitter": "تحسين اللياقة", - "GainMoreFlexible": "زيادة المرونة", - "LearnTheBasic": "تعلم الأساسيات", - "Rookie": "مبتدئ", - "Beginner": "مبتدئ", - "Intermediate": "متوسط", - "Advanced": "متقدم", - "TrueBeast": "وحش حقيقي", - "YourRegularPhysicalActivityLevel": "مستوى نشاطك البدني المنتظم؟", - "EnterYourEmail": "أدخل بريدك الإلكتروني", - "EnterYourPassword": "أدخل كلمة المرور الخاصة بك", - "SendOTP": "إرسال رمز التحقق", - "EnterYourOTPCheckYourEmail": "أدخل رمز التحقق الخاص بك\n تحقق من بريدك الإلكتروني", - "Confirm": "تأكيد", - "ResendCode": "اعادة ارسال الكود؟", - "DidnotReceiveVerificationCode": "لم تستلم رمز التحقق؟", - "MakeSureIts8CharacterOrMore": "تأكد من أن كلمة المرور تتكون من 8 أحرف أو أكثر", - "CreateNewPassword": "إنشاء كلمة مرور جديدة", - "Category": "الفئة", - "RecommendationToDay": "توصية اليوم", - "UpcomingWorkouts": "التمارين القادمة", - "SeeAll": "عرض الكل", - "RecommendationForYou": "توصيات لك", - "Explore": "استكشاف", - "Workouts": "تمارين", - "Gym": "صالة الألعاب الرياضية", - "Hi": "مرحبًا", - "LetUsStartYourDay": "دعنا نبدأ يومك", - "HighChestExercise": "تمرين الصدر العلوي", - "BenchPress": "ضغط الصدر", - "PastaWithChicks": "باستا مع الدجاج", - "GetStarted": "ابدأ الآن", - "HowCanIAssistYouToday": "كيف يمكنني مساعدتك اليوم؟", - "IAmYourSmartCoach": "أنا مدربك الذكي", - "SmartCoach": "المدرب الذكي", - "PreviousConversations": "المحادثات السابقة", - "Profile": "الملف الشخصي", - "SelectLanguage": "اختر اللغة", - "Security": "الأمان", - "Help": "المساعدة", - "Logout": "تسجيل الخروج", - "PrivacyPolicy": "سياسة الخصوصية", - "EditProfile": "تعديل الملف الشخصي", - "AreYouSureToCloseTheApplication": "هل أنت متأكد من إغلاق التطبيق؟", - "Yes": "نعم", - "No": "لا", - "TapToEdit": "اضغط للتعديل", - "ChangePassword": "تغيير كلمة المرور", - "Dinner": "عشاء", - "Legs": "أرجل", - "FullBody": "جسم كامل", - "Jogging": "جري", - "English": "الإنجليزية", - "Arabic": "العربية", - "Carbs": "كربوهيدرات", - "FoodRecommendation": "توصيات الطعام", - "EmailCannotBeEmpty": "البريد الإلكتروني لا يمكن أن يكون فارغًا", - "PasswordCannotBeEmpty": "كلمة المرور لا يمكن أن تكون فارغة", - "InvalidPassword": "كلمة المرور غير صالحة", - "InvalidEmailFormat": "تنسيق البريد الإلكتروني غير صالح", - "PasswordMustBeAtLeast8Characters": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل", - "NameCannotBeEmpty": "الاسم لا يمكن أن يكون فارغًا", - "InvalidName": "الاسم غير صالح", - "EnterAValidEmail": "أدخل بريدًا إلكترونيًا صالحًا", - "ConfirmPasswordMustMatch": "يجب أن تتطابق كلمة المرور المؤكدة مع كلمة المرور", - "PasswordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح", - "ProfileUpdatedSuccessfully": "تم تحديث الملف الشخصي بنجاح", - "Loading": "جار التحميل...", - "Ok": "حسنا", - "NoMealsFound": "لا توجد وجبات", - "SuccessSendOTPToYourEmail": "تم إرسال رمز التحقق إلى بريدك الإلكتروني بنجاح!", - "OtpCode": "رمز التحقق", - "ResendIn": "اعادة الارسال في", - "ResetPasswordSuccessfully": "تم تغيير كلمة المرور بنجاح", - "ConfirmPassword": "تاكيد كلمة المرور", - "OtpVerificationSuccessfully": "تم التحقق من رمز التحقق بنجاح", - "SomethingWentWrongPleaseTryAgainLater": "حدث خطأ ما، يرجى المحاولة في وقت لاحق", - "Home": "الرئيسية", - "FitnessAI": "الذكاء الاصطناعي للياقة البدنية", - "DoIt": "قم بذلك", - "Hey": "مرحبا" + "Done": "تم", + "WhatIsYourWeight": "ما هو وزنك؟", + "ThisHelpsUsCreateYourPersonalizedPlan": "هذا يساعدنا في إنشاء خطتك المخصصة", + "WhatIsYourHeight": "ما هو طولك؟", + "whatIsYourGoal": "ما هو هدفك؟", + "GainWeight": "زيادة الوزن", + "LoseWeight": "فقدان الوزن", + "GetFitter": "تحسين اللياقة", + "GainMoreFlexible": "زيادة المرونة", + "LearnTheBasic": "تعلم الأساسيات", + "Rookie": "مبتدئ", + "Beginner": "مبتدئ", + "Intermediate": "متوسط", + "Advanced": "متقدم", + "TrueBeast": "وحش حقيقي", + "YourRegularPhysicalActivityLevel": "مستوى نشاطك البدني المنتظم؟", + "EnterYourEmail": "أدخل بريدك الإلكتروني", + "EnterYourPassword": "أدخل كلمة المرور الخاصة بك", + "SendOTP": "إرسال رمز التحقق", + "EnterYourOTPCheckYourEmail": "أدخل رمز التحقق الخاص بك\n تحقق من بريدك الإلكتروني", + "Confirm": "تأكيد", + "ResendCode": "اعادة ارسال الكود؟", + "DidnotReceiveVerificationCode": "لم تستلم رمز التحقق؟", + "MakeSureIts8CharacterOrMore": "تأكد من أن كلمة المرور تتكون من 8 أحرف أو أكثر", + "CreateNewPassword": "إنشاء كلمة مرور جديدة", + "Category": "الفئة", + "RecommendationToDay": "توصية اليوم", + "UpcomingWorkouts": "التمارين القادمة", + "SeeAll": "عرض الكل", + "RecommendationForYou": "توصيات لك", + "Explore": "استكشاف", + "Workouts": "تمارين", + "Gym": "صالة الألعاب الرياضية", + "Hi": "مرحبًا", + "LetUsStartYourDay": "دعنا نبدأ يومك", + "HighChestExercise": "تمرين الصدر العلوي", + "BenchPress": "ضغط الصدر", + "PastaWithChicks": "باستا مع الدجاج", + "GetStarted": "ابدأ الآن", + "HowCanIAssistYouToday": "كيف يمكنني مساعدتك اليوم؟", + "IAmYourSmartCoach": "أنا مدربك الذكي", + "SmartCoach": "المدرب الذكي", + "PreviousConversations": "المحادثات السابقة", + "Profile": "الملف الشخصي", + "SelectLanguage": "اختر اللغة", + "Security": "الأمان", + "Help": "المساعدة", + "Logout": "تسجيل الخروج", + "PrivacyPolicy": "سياسة الخصوصية", + "EditProfile": "تعديل الملف الشخصي", + "AreYouSureToCloseTheApplication": "هل أنت متأكد من إغلاق التطبيق؟", + "Yes": "نعم", + "No": "لا", + "TapToEdit": "اضغط للتعديل", + "ChangePassword": "تغيير كلمة المرور", + "Dinner": "عشاء", + "Legs": "أرجل", + "FullBody": "جسم كامل", + "Jogging": "جري", + "English": "الإنجليزية", + "Arabic": "العربية", + "Carbs": "كربوهيدرات", + "FoodRecommendation": "توصيات الطعام", + "EmailCannotBeEmpty": "البريد الإلكتروني لا يمكن أن يكون فارغًا", + "PasswordCannotBeEmpty": "كلمة المرور لا يمكن أن تكون فارغة", + "InvalidPassword": "كلمة المرور غير صالحة", + "InvalidEmailFormat": "تنسيق البريد الإلكتروني غير صالح", + "PasswordMustBeAtLeast8Characters": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل", + "NameCannotBeEmpty": "الاسم لا يمكن أن يكون فارغًا", + "InvalidName": "الاسم غير صالح", + "EnterAValidEmail": "أدخل بريدًا إلكترونيًا صالحًا", + "ConfirmPasswordMustMatch": "يجب أن تتطابق كلمة المرور المؤكدة مع كلمة المرور", + "PasswordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح", + "ProfileUpdatedSuccessfully": "تم تحديث الملف الشخصي بنجاح", + "Loading": "جار التحميل...", + "Ok": "حسنا", + "NoMealsFound": "لا توجد وجبات", + "SuccessSendOTPToYourEmail": "تم إرسال رمز التحقق إلى بريدك الإلكتروني بنجاح!", + "OtpCode": "رمز التحقق", + "ResendIn": "اعادة الارسال في", + "ResetPasswordSuccessfully": "تم تغيير كلمة المرور بنجاح", + "ConfirmPassword": "تاكيد كلمة المرور", + "OtpVerificationSuccessfully": "تم التحقق من رمز التحقق بنجاح", + "SomethingWentWrongPleaseTryAgainLater": "حدث خطأ ما، يرجى المحاولة في وقت لاحق", + "Home": "الرئيسية", + "FitnessAI": "الذكاء الاصطناعي للياقة البدنية", + "DoIt": "قم بذلك", + "Hey": "مرحبا", + "Ingredients": "المكونات", + "Recommendation": "توصيات" } \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index edb7d5e..bf52b70 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -1,5 +1,5 @@ { - "TellUsAboutYourselfWeNeedToKnowYourGender": "tell us about yourself!", + "TellUsAboutYourselfWeNeedToKnowYourGender": "tell us about yourself!", "WhatIsYourHight": "What is your height ?", "NewChat": "New chat", "Previous": "Previous", @@ -16,140 +16,142 @@ "motivational": "motivational", "OopsSomethingWentWrongLetsGetBackToFitnessTryAskingAboutYourNextWorkout": "Oops, something went wrong! Let's get back to fitness—try asking about your next workout!", "InvalidCredentials": "Sorry, the email or password you entered doesn't match. Double-check and try again!", - "Receive_timeout": "It's taking longer than usual to get a response. Please give it another try.", - "Timeout_occurred": "oops, we lost connection for a moment. Try again when you're ready!", - "Invalid_certificate": "There's a security issue with our certificate. Please reach out to support for help.", - "Unexpected_server_error": "Something went wrong on our end. We're sorry about that—please try again later.", - "Request_cancelled": "It looks like the request was cancelled. Let us know if you need assistance!", - "Connection_failed": "We couldn't connect. Could you check your network and try again?", - "Unexpected_error": "Yikes, something unexpected happened. Please try again or let us know if it keeps occurring!", - "Not_found": "we couldn't find what you're looking for. Double-check and try again?", - "Internal_server_error": "Our server's having a rough moment. Please try again soon—we're on it!", - "EmailNotValid": "That email doesn't seem valid. Could you try entering it again?", - "Connection_timeout": "The connection timed out. Please check your internet and give it another go.", - "Send_timeout": "Sending took too long this time. Could you try again when you're ready?", - "Bad_request": "Something's off with that request. Could you try again or adjust what you entered?", - "Unauthorized": "Oops, it looks like you don't have permission to do that. Check your login details!", - "Forbidden": "Sorry, access to this is restricted. Let us know if you think this is a mistake!", - "NoInternetConnection": "No internet connection", - "Service_unavailable": "Our service is temporarily down for maintenance. Please check back in a little while!", - "DataParsingException": "There was an issue processing the data. Please try again or contact support if the issue persists.", - "OtpCodeIsInvalidOrExpired": "Otp code is invalid or has expired", - "ThePriceOfExcellenceIsDiscipline": "the price of excellence\n is discipline", - "LoremIpsumDolorSitAmetConsecteturEuUrnaUtGravidaQuisIdPretiumPurusMaurIsMassa": "Lorem ipsum dolor sit amet consectetur. Eu urna ut gravida quis id pretium purus. Mauris massa", - "FitnessHasNeverBeenSoMuchFun": "Fitness has never been so\n much fun", - "NOMOREEXCUSESDoItNow": "NO MORE EXCUSES\nDo It Now", - "Back": "Back", - "Next": "Next", - "Skip": "Skip", - "HeyThere": "Hey there", - "WelcomeBack": "Welcome Back", - "Login": "Login", - "Email": "Email", - "Password": "Password", - "ForgotPassword": "Forgot Password?", - "Or": "Or", - "DonotHaveAnAccountYet": "Don't have an account yet?", - "Register": "Register", - "CreateAnAccount": "Create an account", - "FirstName": "First Name", - "LastName": "Last Name", - "AlreadyHaveAnAccount": "Already have an account?", - "TellUsAboutYourself": "TELL US ABOUT YOURSELF!", - "WeNeedToKnowYourGender": "We Need To Know Your Gender", - "Male": "Male", - "Female": "Female", - "Year": "Year", - "Kg": "KG", + "Receive_timeout": "It's taking longer than usual to get a response. Please give it another try.", + "Timeout_occurred": "oops, we lost connection for a moment. Try again when you're ready!", + "Invalid_certificate": "There's a security issue with our certificate. Please reach out to support for help.", + "Unexpected_server_error": "Something went wrong on our end. We're sorry about that—please try again later.", + "Request_cancelled": "It looks like the request was cancelled. Let us know if you need assistance!", + "Connection_failed": "We couldn't connect. Could you check your network and try again?", + "Unexpected_error": "Yikes, something unexpected happened. Please try again or let us know if it keeps occurring!", + "Not_found": "we couldn't find what you're looking for. Double-check and try again?", + "Internal_server_error": "Our server's having a rough moment. Please try again soon—we're on it!", + "EmailNotValid": "That email doesn't seem valid. Could you try entering it again?", + "Connection_timeout": "The connection timed out. Please check your internet and give it another go.", + "Send_timeout": "Sending took too long this time. Could you try again when you're ready?", + "Bad_request": "Something's off with that request. Could you try again or adjust what you entered?", + "Unauthorized": "Oops, it looks like you don't have permission to do that. Check your login details!", + "Forbidden": "Sorry, access to this is restricted. Let us know if you think this is a mistake!", + "NoInternetConnection": "No internet connection", + "Service_unavailable": "Our service is temporarily down for maintenance. Please check back in a little while!", + "DataParsingException": "There was an issue processing the data. Please try again or contact support if the issue persists.", + "OtpCodeIsInvalidOrExpired": "Otp code is invalid or has expired", + "ThePriceOfExcellenceIsDiscipline": "the price of excellence\n is discipline", + "LoremIpsumDolorSitAmetConsecteturEuUrnaUtGravidaQuisIdPretiumPurusMaurIsMassa": "Lorem ipsum dolor sit amet consectetur. Eu urna ut gravida quis id pretium purus. Mauris massa", + "FitnessHasNeverBeenSoMuchFun": "Fitness has never been so\n much fun", + "NOMOREEXCUSESDoItNow": "NO MORE EXCUSES\nDo It Now", + "Back": "Back", + "Next": "Next", + "Skip": "Skip", + "HeyThere": "Hey there", + "WelcomeBack": "Welcome Back", + "Login": "Login", + "Email": "Email", + "Password": "Password", + "ForgotPassword": "Forgot Password?", + "Or": "Or", + "DonotHaveAnAccountYet": "Don't have an account yet?", + "Register": "Register", + "CreateAnAccount": "Create an account", + "FirstName": "First Name", + "LastName": "Last Name", + "AlreadyHaveAnAccount": "Already have an account?", + "TellUsAboutYourself": "TELL US ABOUT YOURSELF!", + "WeNeedToKnowYourGender": "We Need To Know Your Gender", + "Male": "Male", + "Female": "Female", + "Year": "Year", + "Kg": "KG", "Cm": "CM", "HowOldAreYou": "HOW OLD ARE YOU ?", - "Done": "Done", - "WhatIsYourWeight": "WHAT IS YOUR WEIGHT ?", - "ThisHelpsUsCreateYourPersonalizedPlan": "This helps us create Your personalized plan", - "WhatIsYourHeight": "What is your height ?", - "whatIsYourGoal": "WHAT IS YOUR GOAL ?", - "GainWeight": "Gain Weight", - "LoseWeight": "Lose Weight", - "GetFitter": "Get Fitter", - "GainMoreFlexible": "Gain More Flexible", - "LearnTheBasic": "Learn The Basic", - "Rookie": "Rookie", - "Beginner": "Beginner", - "Intermediate": "Intermediate", - "Advanced": "Advanced", - "TrueBeast": "True Beast", - "YourRegularPhysicalActivityLevel": "YOUR REGULAR PHYSICAL\nACTIVITY LEVEL ?", - "EnterYourEmail": "Enter your email", - "ForgetPassword": "Forgot Password", - "SendOTP": "Send OTP", - "OTPCODE": "OTP CODE", - "EnterYourOTPCheckYourEmail": "Enter Your OTP Check your email", - "Confirm": "Confirm", - "ResendCode": "Resend Code", - "DidnotReceiveVerificationCode": "Didn't Receive Verification Code?", - "MakeSureIts8CharacterOrMore": "Make sure It's 8 Characters Or More", - "CreateNewPassword": "Create New Password", - "Category": "Category", - "RecommendationToDay": "Recommendation To Day", - "UpcomingWorkouts": "Upcoming Workouts", - "SeeAll": "See All", - "RecommendationForYou": "Recommendation For You", - "Explore": "Explore", - "Workouts": "Workouts", - "Gym": "Gym", - "Hi": "Hi", - "LetUsStartYourDay": "Let's Start Your Day", - "HighChestExercise": "High chest exercise", - "BenchPress": "Bench Press", - "PastaWithChicks": "Pasta with chicks", - "GetStarted": "Get Started", - "HowCanIAssistYouToday": "How Can I Assist You Today ?", - "IAmYourSmartCoach": "I Am Your Smart Coach", - "SmartCoach": "Smart Coach", - "PreviousConversations": "Previous conversations", - "Profile": "Profile", - "SelectLanguage": "Select Language", - "Security": "Security", - "Help": "Help", - "Logout": "Logout", - "PrivacyPolicy": "Privacy Policy", - "EditProfile": "Edit Profile", - "AreYouSureToCloseTheApplication": "Are you sure to close the \napplication?", - "Yes": "Yes", - "No": "No", - "TapToEdit": "tap to edit", - "ChangePassword": "Change Password", - "Dinner": "Dinner", - "Legs": "Legs", - "FullBody": "Full Body", - "Jogging": "Jogging", - "English": "English", - "Arabic": "Arabic", - "Carbs": "Carbs", - "FoodRecommendation": "Food Recommendation", - "EmailCannotBeEmpty": "Email cannot be empty", - "PasswordCannotBeEmpty": "Password cannot be empty", - "InvalidPassword": "Invalid password", - "InvalidEmailFormat": "Invalid email format", - "PasswordMustBeAtLeast8Characters": "Password must be at least 8 characters", - "NameCannotBeEmpty": "Name cannot be empty", - "InvalidName": "Invalid name", - "EnterAValidEmail": "Enter a valid email", - "ConfirmPasswordMustMatch": "Confirm password must match the password", - "PasswordChangedSuccessfully": "Password changed successfully", - "ProfileUpdatedSuccessfully": "Profile updated successfully", - "Loading": "Loading...", - "Ok": "Ok", - "NoMealsFound": "No meals found", - "SuccessSendOTPToYourEmail": "Success! We sent an OTP to your email.", - "OtpCode": "OTP CODE", - "ResendIn": "Resend In", - "ResetPasswordSuccessfully": "Reset Password Successfully", - "ConfirmPassword": "Confirm Password", - "OtpVerificationSuccessfully": "OTP Verification Successfully", - "SomethingWentWrongPleaseTryAgainLater": "something went wrong, please try again later!", - "Home": "Home", - "FitnessAI": "Fitness AI", - "DoIt": "Do It", - "Hey": "Hey" + "Done": "Done", + "WhatIsYourWeight": "WHAT IS YOUR WEIGHT ?", + "ThisHelpsUsCreateYourPersonalizedPlan": "This helps us create Your personalized plan", + "WhatIsYourHeight": "What is your height ?", + "whatIsYourGoal": "WHAT IS YOUR GOAL ?", + "GainWeight": "Gain Weight", + "LoseWeight": "Lose Weight", + "GetFitter": "Get Fitter", + "GainMoreFlexible": "Gain More Flexible", + "LearnTheBasic": "Learn The Basic", + "Rookie": "Rookie", + "Beginner": "Beginner", + "Intermediate": "Intermediate", + "Advanced": "Advanced", + "TrueBeast": "True Beast", + "YourRegularPhysicalActivityLevel": "YOUR REGULAR PHYSICAL\nACTIVITY LEVEL ?", + "EnterYourEmail": "Enter your email", + "ForgetPassword": "Forgot Password", + "SendOTP": "Send OTP", + "OTPCODE": "OTP CODE", + "EnterYourOTPCheckYourEmail": "Enter Your OTP Check your email", + "Confirm": "Confirm", + "ResendCode": "Resend Code", + "DidnotReceiveVerificationCode": "Didn't Receive Verification Code?", + "MakeSureIts8CharacterOrMore": "Make sure It's 8 Characters Or More", + "CreateNewPassword": "Create New Password", + "Category": "Category", + "RecommendationToDay": "Recommendation To Day", + "UpcomingWorkouts": "Upcoming Workouts", + "SeeAll": "See All", + "RecommendationForYou": "Recommendation For You", + "Explore": "Explore", + "Workouts": "Workouts", + "Gym": "Gym", + "Hi": "Hi", + "LetUsStartYourDay": "Let's Start Your Day", + "HighChestExercise": "High chest exercise", + "BenchPress": "Bench Press", + "PastaWithChicks": "Pasta with chicks", + "GetStarted": "Get Started", + "HowCanIAssistYouToday": "How Can I Assist You Today ?", + "IAmYourSmartCoach": "I Am Your Smart Coach", + "SmartCoach": "Smart Coach", + "PreviousConversations": "Previous conversations", + "Profile": "Profile", + "SelectLanguage": "Select Language", + "Security": "Security", + "Help": "Help", + "Logout": "Logout", + "PrivacyPolicy": "Privacy Policy", + "EditProfile": "Edit Profile", + "AreYouSureToCloseTheApplication": "Are you sure to close the \napplication?", + "Yes": "Yes", + "No": "No", + "TapToEdit": "tap to edit", + "ChangePassword": "Change Password", + "Dinner": "Dinner", + "Legs": "Legs", + "FullBody": "Full Body", + "Jogging": "Jogging", + "English": "English", + "Arabic": "Arabic", + "Carbs": "Carbs", + "FoodRecommendation": "Food Recommendation", + "EmailCannotBeEmpty": "Email cannot be empty", + "PasswordCannotBeEmpty": "Password cannot be empty", + "InvalidPassword": "Invalid password", + "InvalidEmailFormat": "Invalid email format", + "PasswordMustBeAtLeast8Characters": "Password must be at least 8 characters", + "NameCannotBeEmpty": "Name cannot be empty", + "InvalidName": "Invalid name", + "EnterAValidEmail": "Enter a valid email", + "ConfirmPasswordMustMatch": "Confirm password must match the password", + "PasswordChangedSuccessfully": "Password changed successfully", + "ProfileUpdatedSuccessfully": "Profile updated successfully", + "Loading": "Loading...", + "Ok": "Ok", + "NoMealsFound": "No meals found", + "SuccessSendOTPToYourEmail": "Success! We sent an OTP to your email.", + "OtpCode": "OTP CODE", + "ResendIn": "Resend In", + "ResetPasswordSuccessfully": "Reset Password Successfully", + "ConfirmPassword": "Confirm Password", + "OtpVerificationSuccessfully": "OTP Verification Successfully", + "SomethingWentWrongPleaseTryAgainLater": "something went wrong, please try again later!", + "Home": "Home", + "FitnessAI": "Fitness AI", + "DoIt": "Do It", + "Hey": "Hey", + "Ingredients": "Ingredients", + "Recommendation": "Recommendation" } \ No newline at end of file diff --git a/lib/core/assets/app_colors.dart b/lib/core/assets/app_colors.dart index eb98fb5..5fcfa6c 100644 --- a/lib/core/assets/app_colors.dart +++ b/lib/core/assets/app_colors.dart @@ -17,6 +17,7 @@ class AppColors { static const Color black = Color(0xFF000000); static const Color darkBlack = Color(0xFF0B0B0B); static const Color darkgrey = Color(0xFF242424); + static const Color containerBackGround = Color(0x19242424); static const MaterialColor white = MaterialColor(0xFFFFFFFF, { baseColor: Color(0xFFFFFFFF), colorCode10: Color(0xFFE9E9E9), diff --git a/lib/core/assets/app_icons.dart b/lib/core/assets/app_icons.dart index cfc3b41..df63e14 100644 --- a/lib/core/assets/app_icons.dart +++ b/lib/core/assets/app_icons.dart @@ -35,6 +35,7 @@ class AppIcons { static const String fitnessThirtyTwo = 'assets/svgs/fitness_32.svg'; static const String fitnessThirtyThree = 'assets/svgs/fitness_33.svg'; static const String fitnessThirtyFour = 'assets/svgs/fitness_34.svg'; + static const String arrowBack = 'assets/svgs/arrow_back.svg'; static const String homeIcon = 'assets/svgs/home_icon.svg'; static const String profileIcon = 'assets/svgs/profile_icon.svg'; static const String chatIcon = 'assets/svgs/chat_icon.svg'; diff --git a/lib/core/extensions/meal_details_extensions.dart b/lib/core/extensions/meal_details_extensions.dart new file mode 100644 index 0000000..f66128f --- /dev/null +++ b/lib/core/extensions/meal_details_extensions.dart @@ -0,0 +1,17 @@ +import '../../domain/meals/entity/meal_details_entity.dart'; + +extension MealDetailsEntityVideo on MealDetailsEntity { + bool get hasYoutubeVideo => + strYoutube != null && strYoutube!.contains('http'); +} + +extension MealDetailsEntityNutrients on MealDetailsEntity { + List> get nutrients { + return [ + {'label': 'Energy', 'value': '100 K'}, // أو قيمة محسوبة + {'label': 'Protein', 'value': '15 G'}, + {'label': 'Carbs', 'value': '58 G'}, + {'label': 'Fat', 'value': '20 G'}, + ]; + } +} diff --git a/lib/core/utils/constants.dart b/lib/core/utils/constants.dart index c8e4461..2a7e72d 100644 --- a/lib/core/utils/constants.dart +++ b/lib/core/utils/constants.dart @@ -42,4 +42,6 @@ class Constants { static const String sessionBox = 'sessions'; static const String delete = 'delete'; static const String muscleData = 'muscleData'; + static const String mealId = 'mealId'; + static const String mealRecommendation = 'mealRecommendation'; } diff --git a/lib/core/utils/di/di.config.dart b/lib/core/utils/di/di.config.dart index c63e84a..a7a0ef9 100644 --- a/lib/core/utils/di/di.config.dart +++ b/lib/core/utils/di/di.config.dart @@ -39,10 +39,15 @@ import '../../../data/home/data_source/remote/home_remote_data_source_impl.dart' as _i208; import '../../../data/home/repo_impl/home_repo_impl.dart' as _i779; import '../../../data/meals/api/meals_retrofit_client.dart' as _i328; +import '../../../data/meals/data_source/contract/meal_details_remote_data_source.dart' + as _i1021; import '../../../data/meals/data_source/contract/meals_remote_data_source.dart' as _i112; +import '../../../data/meals/data_source/remote/meal_details_remote_data_source_impl.dart' + as _i1069; import '../../../data/meals/data_source/remote/meals_remote_data_source_impl.dart' as _i107; +import '../../../data/meals/repo_impl/meal_details_impl.dart' as _i1059; import '../../../data/meals/repo_impl/meals_repo_impl.dart' as _i732; import '../../../data/smart_coach/api/smart_coach_ai_service.dart' as _i144; import '../../../data/smart_coach/api/smart_coach_ai_service_impl.dart' @@ -80,10 +85,12 @@ import '../../../domain/home/use_case/get_food_recommendation_use_case.dart' as _i910; import '../../../domain/home/use_case/get_muscles_by_group_use_case.dart' as _i389; +import '../../../domain/meals/repo/meal_details_repo.dart' as _i154; import '../../../domain/meals/repo/meals_repo.dart' as _i1031; import '../../../domain/meals/use_case/get_categories_use_case.dart' as _i985; import '../../../domain/meals/use_case/get_meals_by_category_use_case.dart' as _i112; +import '../../../domain/meals/use_case/meal_details_use_case.dart' as _i940; import '../../../domain/smart_coach/repo/smart_coach_repo.dart' as _i622; import '../../../domain/smart_coach/use_case/ask_smart_coach_use_case.dart' as _i332; @@ -98,6 +105,8 @@ import '../../../domain/workouts/use_case/get_all_muscles_by_muscle_group_use_ca as _i546; import '../../../features/chat_bot/presentation/view_model/cubit/smart_coach_cubit.dart' as _i603; +import '../../../features/details_food/presentation/view_model/cubit/meal_details_cubit.dart' + as _i38; import '../../../features/forget_password/presentation/view_model/cubit/forget_password_cubit.dart' as _i70; import '../../../features/home/presentation/view_model/cubit/home_cubit.dart' @@ -240,6 +249,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i112.GetMealsByCategoryUseCase>( () => _i112.GetMealsByCategoryUseCase(gh<_i1031.MealsRepo>()), ); + gh.factory<_i1021.MealsRemoteDataSource>( + () => _i1069.MealsRemoteDataSourceImpl(gh<_i328.MealsRetrofitClient>()), + ); gh.factory<_i622.SmartCoachRepo>( () => _i128.SmartCoachRepoImpl( gh<_i382.SmartCoachRemoteDataSource>(), @@ -263,6 +275,12 @@ extension GetItInjectableX on _i174.GetIt { gh<_i796.DeleteConversationUseCase>(), ), ); + gh.factory<_i154.MealsRepo>( + () => _i1059.MealsRepoImpl( + gh<_i1021.MealsRemoteDataSource>(), + gh<_i28.ApiManager>(), + ), + ); gh.factory<_i1047.AuthRepo>( () => _i15.AuthRepoImpl( gh<_i28.ApiManager>(), @@ -292,6 +310,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i872.LoginUseCase>( () => _i872.LoginUseCase(gh<_i1047.AuthRepo>()), ); + gh.factory<_i940.GetMealDetailsUseCase>( + () => _i940.GetMealDetailsUseCase(gh<_i154.MealsRepo>()), + ); gh.factory<_i728.ForgetPasswordUseCase>( () => _i728.ForgetPasswordUseCase(gh<_i1047.AuthRepo>()), ); @@ -340,6 +361,9 @@ extension GetItInjectableX on _i174.GetIt { gh<_i728.ForgetPasswordUseCase>(), ), ); + gh.factory<_i38.MealDetailsCubit>( + () => _i38.MealDetailsCubit(gh<_i940.GetMealDetailsUseCase>()), + ); gh.factory<_i131.HomeCubit>( () => _i131.HomeCubit( gh<_i360.GetDailyRecommendationExerciseUseCase>(), diff --git a/lib/core/utils/di/di.dart b/lib/core/utils/di/di.dart index ed11a72..a5e79e1 100644 --- a/lib/core/utils/di/di.dart +++ b/lib/core/utils/di/di.dart @@ -1,8 +1,9 @@ -import 'package:fitness_app/core/utils/di/di.config.dart'; import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; +import 'di.config.dart'; final getIt = GetIt.instance; + @InjectableInit( initializerName: 'init', preferRelativeImports: true, diff --git a/lib/core/utils/dialogs/app_toast.dart b/lib/core/utils/dialogs/app_toast.dart index 3b7d5be..47b37f7 100644 --- a/lib/core/utils/dialogs/app_toast.dart +++ b/lib/core/utils/dialogs/app_toast.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../../assets/app_colors.dart'; import 'package:fluttertoast/fluttertoast.dart'; -showToast({required String title, required Color color}) { +void showToast({required String title, required Color color}) { Fluttertoast.showToast( msg: title, toastLength: Toast.LENGTH_SHORT, diff --git a/lib/core/utils/l10n/locale_keys.g.dart b/lib/core/utils/l10n/locale_keys.g.dart index 3e3fa9a..b710dab 100644 --- a/lib/core/utils/l10n/locale_keys.g.dart +++ b/lib/core/utils/l10n/locale_keys.g.dart @@ -169,4 +169,6 @@ abstract class LocaleKeys { static const FitnessAI = 'FitnessAI'; static const DoIt = 'DoIt'; static const Hey = 'Hey'; + static const Ingredients = 'Ingredients'; + static const Recommendation = 'Recommendation'; } diff --git a/lib/core/utils/routes/app_routes.dart b/lib/core/utils/routes/app_routes.dart index db095ca..5dd9f2d 100644 --- a/lib/core/utils/routes/app_routes.dart +++ b/lib/core/utils/routes/app_routes.dart @@ -1,5 +1,8 @@ +import 'package:fitness_app/core/utils/constants.dart'; import 'package:flutter/material.dart'; +import '../../../features/details_food/presentation/view/meal_details_screen.dart'; + import '../../../features/meals/presentation/view/screens/meals_screen.dart'; import '../../../features/login/presentation/view/login_screen.dart'; @@ -9,7 +12,6 @@ import '../../../features/otp_verification/presentation/view/otp_verification_sc import '../../../features/register/presentation/view/register_screen.dart'; import '../../../features/reset_password/presentation/view/reset_password_screen.dart'; import '../../../features/workouts/presentation/view/workouts_screen.dart'; -import '../constants.dart'; import '../../../features/home/presentation/view/home_screen.dart'; import '../../../features/chat_bot/presentation/view/smart_coach_screen.dart'; @@ -26,15 +28,14 @@ class AppRoutes { static const String profileRoute = '/profile'; static const String editProfileRoute = '/edit-profile'; static const String onBoardingRoute = '/on-boarding'; + static const String mealDetailsRoute = '/meals-details'; static const String workoutsRoute = '/workouts'; static const String mealsRoute = '/meals'; static const String exerciseDetailsRoute = '/exercise-details'; static const String otpVerificationRoute = '/otp-verification'; static const String resetPasswordRoute = '/reset-password'; - static const String chatBotRoute = 'chat-bot'; static const String completeRegisterRoute = '/complete-register'; static const String forgetPasswordRoute = '/forget-password'; - static const String workoutRoute = '/workout'; static const String smartCoachRoute = '/smart-coach'; static Map routes = { @@ -58,5 +59,13 @@ class AppRoutes { ModalRoute.of(context)!.settings.arguments as Map; return ResetPasswordScreen(email: args[Constants.email]); }, + mealDetailsRoute: (context) { + var args = + ModalRoute.of(context)!.settings.arguments as Map; + return MealDetailsScreen( + mealId: args[Constants.mealId], + meals: args[Constants.mealRecommendation], + ); + }, }; } diff --git a/lib/data/meals/api/meals_retrofit_client.dart b/lib/data/meals/api/meals_retrofit_client.dart index 5588e14..81ac3ce 100644 --- a/lib/data/meals/api/meals_retrofit_client.dart +++ b/lib/data/meals/api/meals_retrofit_client.dart @@ -5,6 +5,7 @@ import 'package:injectable/injectable.dart'; import 'package:retrofit/retrofit.dart'; import '../../../core/utils/datasource_excution/api_constants.dart'; +import '../models/meal_details_response_dto.dart'; part 'meals_retrofit_client.g.dart'; @@ -21,4 +22,7 @@ abstract class MealsRetrofitClient { Future getMealsByCategory({ @Query("c") required String category, }); + + @GET(ApiConstants.mealDetails) + Future getMealDetails(@Query("i") String id); } diff --git a/lib/data/meals/api/meals_retrofit_client.g.dart b/lib/data/meals/api/meals_retrofit_client.g.dart index e63a317..371f975 100644 --- a/lib/data/meals/api/meals_retrofit_client.g.dart +++ b/lib/data/meals/api/meals_retrofit_client.g.dart @@ -75,6 +75,33 @@ class _MealsRetrofitClient implements MealsRetrofitClient { return _value; } + @override + Future getMealDetails(String id) async { + final _extra = {}; + final queryParameters = {r'i': id}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '1/lookup.php', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late MealDetailsResponseDto _value; + try { + _value = MealDetailsResponseDto.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/data/meals/data_source/contract/meal_details_remote_data_source.dart b/lib/data/meals/data_source/contract/meal_details_remote_data_source.dart new file mode 100644 index 0000000..b08ef95 --- /dev/null +++ b/lib/data/meals/data_source/contract/meal_details_remote_data_source.dart @@ -0,0 +1,5 @@ +import '../../models/meal_details_response_dto.dart'; + +abstract interface class MealsRemoteDataSource { + Future getMealDetails(String id); +} diff --git a/lib/data/meals/data_source/remote/meal_details_remote_data_source_impl.dart b/lib/data/meals/data_source/remote/meal_details_remote_data_source_impl.dart new file mode 100644 index 0000000..06a50c3 --- /dev/null +++ b/lib/data/meals/data_source/remote/meal_details_remote_data_source_impl.dart @@ -0,0 +1,17 @@ +import 'package:injectable/injectable.dart'; + +import '../../api/meals_retrofit_client.dart'; +import '../../models/meal_details_response_dto.dart'; +import '../contract/meal_details_remote_data_source.dart'; + +@Injectable(as: MealsRemoteDataSource) +class MealsRemoteDataSourceImpl implements MealsRemoteDataSource { + final MealsRetrofitClient _client; + + MealsRemoteDataSourceImpl(this._client); + + @override + Future getMealDetails(String id) { + return _client.getMealDetails(id); + } +} diff --git a/lib/data/meals/models/meal_details_dto.dart b/lib/data/meals/models/meal_details_dto.dart new file mode 100644 index 0000000..bb1a6db --- /dev/null +++ b/lib/data/meals/models/meal_details_dto.dart @@ -0,0 +1,178 @@ +import 'package:fitness_app/domain/meals/entity/meal_details_entity.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../../domain/meals/entity/ingredient_entity.dart'; + +part 'meal_details_dto.g.dart'; + +@JsonSerializable() +class MealDetailsDto { + final String idMeal; + final String strMeal; + final String strCategory; + final String strArea; + final String strInstructions; + final String strMealThumb; + final String? strYoutube; + + final String? strIngredient1; + final String? strIngredient2; + final String? strIngredient3; + final String? strIngredient4; + final String? strIngredient5; + final String? strIngredient6; + final String? strIngredient7; + final String? strIngredient8; + final String? strIngredient9; + final String? strIngredient10; + final String? strIngredient11; + final String? strIngredient12; + final String? strIngredient13; + final String? strIngredient14; + final String? strIngredient15; + final String? strIngredient16; + final String? strIngredient17; + final String? strIngredient18; + final String? strIngredient19; + final String? strIngredient20; + + final String? strMeasure1; + final String? strMeasure2; + final String? strMeasure3; + final String? strMeasure4; + final String? strMeasure5; + final String? strMeasure6; + final String? strMeasure7; + final String? strMeasure8; + final String? strMeasure9; + final String? strMeasure10; + final String? strMeasure11; + final String? strMeasure12; + final String? strMeasure13; + final String? strMeasure14; + final String? strMeasure15; + final String? strMeasure16; + final String? strMeasure17; + final String? strMeasure18; + final String? strMeasure19; + final String? strMeasure20; + + MealDetailsDto({ + required this.idMeal, + required this.strMeal, + required this.strCategory, + required this.strArea, + required this.strInstructions, + required this.strMealThumb, + this.strYoutube, + this.strIngredient1, + this.strIngredient2, + this.strIngredient3, + this.strIngredient4, + this.strIngredient5, + this.strIngredient6, + this.strIngredient7, + this.strIngredient8, + this.strIngredient9, + this.strIngredient10, + this.strIngredient11, + this.strIngredient12, + this.strIngredient13, + this.strIngredient14, + this.strIngredient15, + this.strIngredient16, + this.strIngredient17, + this.strIngredient18, + this.strIngredient19, + this.strIngredient20, + this.strMeasure1, + this.strMeasure2, + this.strMeasure3, + this.strMeasure4, + this.strMeasure5, + this.strMeasure6, + this.strMeasure7, + this.strMeasure8, + this.strMeasure9, + this.strMeasure10, + this.strMeasure11, + this.strMeasure12, + this.strMeasure13, + this.strMeasure14, + this.strMeasure15, + this.strMeasure16, + this.strMeasure17, + this.strMeasure18, + this.strMeasure19, + this.strMeasure20, + }); + + factory MealDetailsDto.fromJson(Map json) => + _$MealDetailsDtoFromJson(json); + + Map toJson() => _$MealDetailsDtoToJson(this); + + MealDetailsEntity toEntity() { + final names = [ + strIngredient1, + strIngredient2, + strIngredient3, + strIngredient4, + strIngredient5, + strIngredient6, + strIngredient7, + strIngredient8, + strIngredient9, + strIngredient10, + strIngredient11, + strIngredient12, + strIngredient13, + strIngredient14, + strIngredient15, + strIngredient16, + strIngredient17, + strIngredient18, + strIngredient19, + strIngredient20, + ]; + final measures = [ + strMeasure1, + strMeasure2, + strMeasure3, + strMeasure4, + strMeasure5, + strMeasure6, + strMeasure7, + strMeasure8, + strMeasure9, + strMeasure10, + strMeasure11, + strMeasure12, + strMeasure13, + strMeasure14, + strMeasure15, + strMeasure16, + strMeasure17, + strMeasure18, + strMeasure19, + strMeasure20, + ]; + final ingredients = []; + for (int i = 0; i < names.length; i++) { + if (names[i] != null && names[i]!.isNotEmpty) { + ingredients.add( + IngredientEntity(name: names[i]!, measure: measures[i]), + ); + } + } + return MealDetailsEntity( + idMeal: idMeal, + strMeal: strMeal, + strCategory: strCategory, + strArea: strArea, + strInstructions: strInstructions, + strMealThumb: strMealThumb, + ingredients: ingredients, + ); + } +} diff --git a/lib/data/meals/models/meal_details_dto.g.dart b/lib/data/meals/models/meal_details_dto.g.dart new file mode 100644 index 0000000..994cff0 --- /dev/null +++ b/lib/data/meals/models/meal_details_dto.g.dart @@ -0,0 +1,109 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'meal_details_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MealDetailsDto _$MealDetailsDtoFromJson(Map json) => + MealDetailsDto( + idMeal: json['idMeal'] as String, + strMeal: json['strMeal'] as String, + strCategory: json['strCategory'] as String, + strArea: json['strArea'] as String, + strInstructions: json['strInstructions'] as String, + strMealThumb: json['strMealThumb'] as String, + strYoutube: json['strYoutube'] as String?, + strIngredient1: json['strIngredient1'] as String?, + strIngredient2: json['strIngredient2'] as String?, + strIngredient3: json['strIngredient3'] as String?, + strIngredient4: json['strIngredient4'] as String?, + strIngredient5: json['strIngredient5'] as String?, + strIngredient6: json['strIngredient6'] as String?, + strIngredient7: json['strIngredient7'] as String?, + strIngredient8: json['strIngredient8'] as String?, + strIngredient9: json['strIngredient9'] as String?, + strIngredient10: json['strIngredient10'] as String?, + strIngredient11: json['strIngredient11'] as String?, + strIngredient12: json['strIngredient12'] as String?, + strIngredient13: json['strIngredient13'] as String?, + strIngredient14: json['strIngredient14'] as String?, + strIngredient15: json['strIngredient15'] as String?, + strIngredient16: json['strIngredient16'] as String?, + strIngredient17: json['strIngredient17'] as String?, + strIngredient18: json['strIngredient18'] as String?, + strIngredient19: json['strIngredient19'] as String?, + strIngredient20: json['strIngredient20'] as String?, + strMeasure1: json['strMeasure1'] as String?, + strMeasure2: json['strMeasure2'] as String?, + strMeasure3: json['strMeasure3'] as String?, + strMeasure4: json['strMeasure4'] as String?, + strMeasure5: json['strMeasure5'] as String?, + strMeasure6: json['strMeasure6'] as String?, + strMeasure7: json['strMeasure7'] as String?, + strMeasure8: json['strMeasure8'] as String?, + strMeasure9: json['strMeasure9'] as String?, + strMeasure10: json['strMeasure10'] as String?, + strMeasure11: json['strMeasure11'] as String?, + strMeasure12: json['strMeasure12'] as String?, + strMeasure13: json['strMeasure13'] as String?, + strMeasure14: json['strMeasure14'] as String?, + strMeasure15: json['strMeasure15'] as String?, + strMeasure16: json['strMeasure16'] as String?, + strMeasure17: json['strMeasure17'] as String?, + strMeasure18: json['strMeasure18'] as String?, + strMeasure19: json['strMeasure19'] as String?, + strMeasure20: json['strMeasure20'] as String?, + ); + +Map _$MealDetailsDtoToJson(MealDetailsDto instance) => + { + 'idMeal': instance.idMeal, + 'strMeal': instance.strMeal, + 'strCategory': instance.strCategory, + 'strArea': instance.strArea, + 'strInstructions': instance.strInstructions, + 'strMealThumb': instance.strMealThumb, + 'strYoutube': instance.strYoutube, + 'strIngredient1': instance.strIngredient1, + 'strIngredient2': instance.strIngredient2, + 'strIngredient3': instance.strIngredient3, + 'strIngredient4': instance.strIngredient4, + 'strIngredient5': instance.strIngredient5, + 'strIngredient6': instance.strIngredient6, + 'strIngredient7': instance.strIngredient7, + 'strIngredient8': instance.strIngredient8, + 'strIngredient9': instance.strIngredient9, + 'strIngredient10': instance.strIngredient10, + 'strIngredient11': instance.strIngredient11, + 'strIngredient12': instance.strIngredient12, + 'strIngredient13': instance.strIngredient13, + 'strIngredient14': instance.strIngredient14, + 'strIngredient15': instance.strIngredient15, + 'strIngredient16': instance.strIngredient16, + 'strIngredient17': instance.strIngredient17, + 'strIngredient18': instance.strIngredient18, + 'strIngredient19': instance.strIngredient19, + 'strIngredient20': instance.strIngredient20, + 'strMeasure1': instance.strMeasure1, + 'strMeasure2': instance.strMeasure2, + 'strMeasure3': instance.strMeasure3, + 'strMeasure4': instance.strMeasure4, + 'strMeasure5': instance.strMeasure5, + 'strMeasure6': instance.strMeasure6, + 'strMeasure7': instance.strMeasure7, + 'strMeasure8': instance.strMeasure8, + 'strMeasure9': instance.strMeasure9, + 'strMeasure10': instance.strMeasure10, + 'strMeasure11': instance.strMeasure11, + 'strMeasure12': instance.strMeasure12, + 'strMeasure13': instance.strMeasure13, + 'strMeasure14': instance.strMeasure14, + 'strMeasure15': instance.strMeasure15, + 'strMeasure16': instance.strMeasure16, + 'strMeasure17': instance.strMeasure17, + 'strMeasure18': instance.strMeasure18, + 'strMeasure19': instance.strMeasure19, + 'strMeasure20': instance.strMeasure20, + }; diff --git a/lib/data/meals/models/meal_details_response_dto.dart b/lib/data/meals/models/meal_details_response_dto.dart new file mode 100644 index 0000000..e31791f --- /dev/null +++ b/lib/data/meals/models/meal_details_response_dto.dart @@ -0,0 +1,16 @@ +import 'package:fitness_app/data/meals/models/meal_details_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'meal_details_response_dto.g.dart'; + +@JsonSerializable() +class MealDetailsResponseDto { + final List meals; + + MealDetailsResponseDto({required this.meals}); + + factory MealDetailsResponseDto.fromJson(Map json) => + _$MealDetailsResponseDtoFromJson(json); + + Map toJson() => _$MealDetailsResponseDtoToJson(this); +} diff --git a/lib/data/meals/models/meal_details_response_dto.g.dart b/lib/data/meals/models/meal_details_response_dto.g.dart new file mode 100644 index 0000000..af22b6b --- /dev/null +++ b/lib/data/meals/models/meal_details_response_dto.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'meal_details_response_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MealDetailsResponseDto _$MealDetailsResponseDtoFromJson( + Map json, +) => MealDetailsResponseDto( + meals: (json['meals'] as List) + .map((e) => MealDetailsDto.fromJson(e as Map)) + .toList(), +); + +Map _$MealDetailsResponseDtoToJson( + MealDetailsResponseDto instance, +) => {'meals': instance.meals}; diff --git a/lib/data/meals/repo_impl/meal_details_impl.dart b/lib/data/meals/repo_impl/meal_details_impl.dart new file mode 100644 index 0000000..b5e7110 --- /dev/null +++ b/lib/data/meals/repo_impl/meal_details_impl.dart @@ -0,0 +1,23 @@ +import 'package:fitness_app/core/utils/datasource_excution/api_manager.dart'; +import 'package:fitness_app/core/utils/datasource_excution/api_result.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/meals/entity/meal_details_entity.dart'; +import '../../../domain/meals/repo/meal_details_repo.dart'; +import '../data_source/contract/meal_details_remote_data_source.dart'; + +@Injectable(as: MealsRepo) +class MealsRepoImpl implements MealsRepo { + final MealsRemoteDataSource _mealsRemoteDataSource; + final ApiManager _apiManager; + + MealsRepoImpl(this._mealsRemoteDataSource, this._apiManager); + + @override + Future> getMealDetails(String id) async { + return _apiManager.execute(() async { + final response = await _mealsRemoteDataSource.getMealDetails(id); + return response.meals.first.toEntity(); + }); + } +} diff --git a/lib/domain/meals/entity/ingredient_entity.dart b/lib/domain/meals/entity/ingredient_entity.dart new file mode 100644 index 0000000..951b9a1 --- /dev/null +++ b/lib/domain/meals/entity/ingredient_entity.dart @@ -0,0 +1,6 @@ +class IngredientEntity { + final String? name; + final String? measure; + + IngredientEntity({required this.name, required this.measure}); +} diff --git a/lib/domain/meals/entity/meal_details_entity.dart b/lib/domain/meals/entity/meal_details_entity.dart new file mode 100644 index 0000000..3a801d1 --- /dev/null +++ b/lib/domain/meals/entity/meal_details_entity.dart @@ -0,0 +1,39 @@ +import 'ingredient_entity.dart'; + +class MealDetailsEntity { + final String idMeal; + final String strMeal; + final String strCategory; + final String strArea; + final String strInstructions; + final String strMealThumb; + final String? strYoutube; + final List ingredients; + + MealDetailsEntity({ + required this.idMeal, + required this.strMeal, + required this.strCategory, + required this.strArea, + required this.strInstructions, + required this.strMealThumb, + this.strYoutube, + required this.ingredients, + }); +} + +extension MealDetailsEntityVideo on MealDetailsEntity { + bool get hasYoutubeVideo => + strYoutube != null && strYoutube!.contains('http'); +} + +extension MealDetailsEntityNutrients on MealDetailsEntity { + List> get nutrients { + return [ + {'label': 'Energy', 'value': '100 K'}, + {'label': 'Protein', 'value': '15 G'}, + {'label': 'Carbs', 'value': '58 G'}, + {'label': 'Fat', 'value': '20 G'}, + ]; + } +} diff --git a/lib/domain/meals/repo/meal_details_repo.dart b/lib/domain/meals/repo/meal_details_repo.dart new file mode 100644 index 0000000..3baeb6f --- /dev/null +++ b/lib/domain/meals/repo/meal_details_repo.dart @@ -0,0 +1,6 @@ +import '../../../core/utils/datasource_excution/api_result.dart'; +import '../entity/meal_details_entity.dart'; + +abstract interface class MealsRepo { + Future> getMealDetails(String id); +} diff --git a/lib/domain/meals/use_case/meal_details_use_case.dart b/lib/domain/meals/use_case/meal_details_use_case.dart new file mode 100644 index 0000000..42633c1 --- /dev/null +++ b/lib/domain/meals/use_case/meal_details_use_case.dart @@ -0,0 +1,15 @@ +import 'package:injectable/injectable.dart'; +import '../../../core/utils/datasource_excution/api_result.dart'; +import '../entity/meal_details_entity.dart'; +import '../repo/meal_details_repo.dart'; + +@injectable +class GetMealDetailsUseCase { + final MealsRepo _mealsRepo; + + GetMealDetailsUseCase(this._mealsRepo); + + Future> call(String id) async { + return await _mealsRepo.getMealDetails(id); + } +} diff --git a/lib/features/details_food/presentation/view/meal_details_screen.dart b/lib/features/details_food/presentation/view/meal_details_screen.dart new file mode 100644 index 0000000..004dd42 --- /dev/null +++ b/lib/features/details_food/presentation/view/meal_details_screen.dart @@ -0,0 +1,70 @@ +import 'package:fitness_app/domain/meals/entity/meal_entity.dart'; +import 'package:fitness_app/features/details_food/presentation/view/widgets/meal_details_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../core/assets/app_images.dart'; +import '../../../../core/base/base_state.dart'; +import '../../../../core/utils/di/di.dart'; +import '../../../../domain/meals/entity/meal_details_entity.dart'; +import '../view_model/cubit/meal_details_state.dart'; +import '../view_model/cubit/meal_details_cubit.dart'; + +class MealDetailsScreen extends StatefulWidget { + final String mealId; + final List meals; + + const MealDetailsScreen({ + super.key, + required this.mealId, + required this.meals, + }); + + @override + State createState() => _MealDetailsScreenState(); +} + +class _MealDetailsScreenState extends State { + late final MealDetailsCubit viewModel; + + @override + void initState() { + viewModel = getIt(); + viewModel.doIntent(GetMealDetailsAction(widget.mealId)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => viewModel, + child: BlocBuilder( + builder: (context, state) { + final status = state.mealDetailsStatus; + if (status is BaseLoadingState) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } else if (status is BaseErrorState) { + return Scaffold(body: Center(child: Text(status.errorMessage))); + } else if (status is BaseSuccessState) { + final meal = status.data!; + return Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage(AppImages.backgroundThree), + fit: BoxFit.fill, + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + body: MealDetailsBody(meal: meal, meals: widget.meals), + ), + ); + } + return const SizedBox(); + }, + ), + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/ingredient_body.dart b/lib/features/details_food/presentation/view/widgets/ingredient_body.dart new file mode 100644 index 0000000..bf9562f --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/ingredient_body.dart @@ -0,0 +1,44 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../../../../../core/utils/l10n/locale_keys.g.dart'; +import '../../../../../core/utils/shared_widgets/shared_blured_container.dart'; +import '../../../../../domain/meals/entity/meal_details_entity.dart'; +import 'ingredient_item.dart'; + +class IngredientBody extends StatelessWidget { + const IngredientBody({super.key, required this.meal}); + + final MealDetailsEntity meal; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.Ingredients.tr(), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + SharedBluredContainer( + padding: const EdgeInsets.all(8), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: meal.ingredients.length, + itemBuilder: (_, index) { + final item = meal.ingredients[index]; + return IngredientItem(name: item.name!, amount: item.measure!); + }, + separatorBuilder: (_, _) => const SizedBox(height: 8), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/ingredient_item.dart b/lib/features/details_food/presentation/view/widgets/ingredient_item.dart new file mode 100644 index 0000000..8529bc0 --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/ingredient_item.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../../../core/assets/app_colors.dart'; + +class IngredientItem extends StatelessWidget { + final String name; + final String amount; + + const IngredientItem({super.key, required this.name, required this.amount}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: Theme.of(context).textTheme.bodyMedium), + Text( + amount, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: AppColors.orange), + ), + ], + ), + const Divider(color: AppColors.darkgrey, thickness: 1), + ], + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/meal_details_body.dart b/lib/features/details_food/presentation/view/widgets/meal_details_body.dart new file mode 100644 index 0000000..6a86e05 --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/meal_details_body.dart @@ -0,0 +1,40 @@ +import 'package:fitness_app/core/utils/shared_widgets/shared_blured_container.dart'; +import 'package:fitness_app/domain/meals/entity/meal_details_entity.dart'; +import 'package:fitness_app/domain/meals/entity/meal_entity.dart'; +import 'package:fitness_app/features/details_food/presentation/view/widgets/meal_image.dart'; +import 'package:fitness_app/features/details_food/presentation/view/widgets/recommendations_meal_body.dart'; +import 'package:flutter/material.dart'; +import 'ingredient_body.dart'; + +class MealDetailsBody extends StatelessWidget { + final MealDetailsEntity meal; + final List meals; + + const MealDetailsBody({super.key, required this.meal, required this.meals}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SharedBluredContainer( + padding: EdgeInsets.zero, + child: MealImage(meal: meal), + ), + const SizedBox(height: 16), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + IngredientBody(meal: meal), + RecommendationsMealBody( + meals: meals, + currentMealId: meal.idMeal, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/meal_header_video.dart b/lib/features/details_food/presentation/view/widgets/meal_header_video.dart new file mode 100644 index 0000000..0653c81 --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/meal_header_video.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class MealHeaderVideo extends StatefulWidget { + final String videoUrl; + final String fallbackImage; + + const MealHeaderVideo({ + super.key, + required this.videoUrl, + required this.fallbackImage, + }); + + @override + State createState() => _MealHeaderVideoState(); +} + +class _MealHeaderVideoState extends State { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + // ignore: deprecated_member_use + _controller = VideoPlayerController.network(widget.videoUrl) + ..initialize().then((_) { + setState(() {}); + _controller.setLooping(true); + _controller.setVolume(0); + _controller.play(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isVideoAvailable = + widget.videoUrl.isNotEmpty && _controller.value.isInitialized; + + return Stack( + children: [ + AspectRatio( + aspectRatio: isVideoAvailable + ? _controller.value.aspectRatio + : 16 / 9, + child: isVideoAvailable + ? VideoPlayer(_controller) + : Image.network( + widget.fallbackImage, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: Colors.black12, + child: const Center(child: CircularProgressIndicator()), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey.shade300, + alignment: Alignment.center, + child: const Icon( + Icons.broken_image, + size: 48, + color: Colors.grey, + ), + ); + }, + ), + ), + Positioned( + top: 16, + left: 16, + child: SafeArea( + child: CircleAvatar( + backgroundColor: Colors.black45, + child: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/meal_image.dart b/lib/features/details_food/presentation/view/widgets/meal_image.dart new file mode 100644 index 0000000..83b6c28 --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/meal_image.dart @@ -0,0 +1,80 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:fitness_app/domain/meals/entity/meal_details_entity.dart'; +import 'package:flutter/material.dart'; + +import '../../../../../core/assets/app_colors.dart'; + +class MealImage extends StatelessWidget { + final MealDetailsEntity meal; + + const MealImage({super.key, required this.meal}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + CachedNetworkImage( + imageUrl: meal.strMealThumb, + width: double.infinity, + height: 350, + fit: BoxFit.fill, + color: AppColors.darkgrey.withAlpha(150), + colorBlendMode: BlendMode.darken, + placeholder: (context, url) => + const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [AppColors.darkgrey, AppColors.darkBlack], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + ), + Positioned( + top: 48, + left: 16, + child: CircleAvatar( + backgroundColor: AppColors.orange, + radius: 14, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + size: 28, + Icons.arrow_back_rounded, + color: AppColors.white, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + meal.strMeal, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontSize: 24, + fontWeight: FontWeight.w500, + color: AppColors.white, + ), + ), + const SizedBox(height: 8), + Text( + meal.strInstructions, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 2, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/nutrient_body.dart b/lib/features/details_food/presentation/view/widgets/nutrient_body.dart new file mode 100644 index 0000000..42d2669 --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/nutrient_body.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'nutrient_info.dart'; + +class NutrientBody extends StatelessWidget { + const NutrientBody({super.key, required this.nutrients}); + + final List> nutrients; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 90, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: nutrients.length, + separatorBuilder: (context, index) => const SizedBox(width: 8), + itemBuilder: (context, index) { + final nutrient = nutrients[index]; + return NutrientInfo( + label: nutrient['label']!, + value: nutrient['value']!, + ); + }, + ), + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/nutrient_info.dart b/lib/features/details_food/presentation/view/widgets/nutrient_info.dart new file mode 100644 index 0000000..aa7fdb6 --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/nutrient_info.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import '../../../../../core/assets/app_colors.dart'; + +class NutrientInfo extends StatelessWidget { + final String value; + final String label; + + const NutrientInfo({super.key, required this.value, required this.label}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: Colors.white70), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Text( + value, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Colors.white), + ), + Text( + label, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: AppColors.orange), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/details_food/presentation/view/widgets/recommendations_meal_body.dart b/lib/features/details_food/presentation/view/widgets/recommendations_meal_body.dart new file mode 100644 index 0000000..1001d1c --- /dev/null +++ b/lib/features/details_food/presentation/view/widgets/recommendations_meal_body.dart @@ -0,0 +1,72 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:fitness_app/domain/meals/entity/meal_entity.dart'; +import 'package:flutter/material.dart'; +import '../../../../../core/utils/constants.dart'; +import '../../../../../core/utils/l10n/locale_keys.g.dart'; +import '../../../../../core/utils/routes/app_routes.dart'; +import '../../../../../core/utils/shared_widgets/grid_item.dart'; + +class RecommendationsMealBody extends StatelessWidget { + final List meals; + final String currentMealId; + + const RecommendationsMealBody({ + super.key, + required this.meals, + required this.currentMealId, + }); + + @override + Widget build(BuildContext context) { + final filteredMeals = meals + .where((meal) => meal.idMeal != currentMealId) + .toList(); + filteredMeals.shuffle(); + final recommendations = filteredMeals.take(4).toList(); + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.Recommendation.tr(), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + GridView.builder( + itemCount: recommendations.length, + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 17, + mainAxisSpacing: 17, + childAspectRatio: 1, + ), + itemBuilder: (context, index) { + final meal = recommendations[index]; + return GridItem( + title: meal.strMeal, + imageUrl: meal.strMealThumb, + onTap: () { + Navigator.pushReplacementNamed( + context, + AppRoutes.mealDetailsRoute, + arguments: { + Constants.mealId: meal.idMeal, + Constants.mealRecommendation: meals, + }, + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/details_food/presentation/view_model/cubit/meal_details_cubit.dart b/lib/features/details_food/presentation/view_model/cubit/meal_details_cubit.dart new file mode 100644 index 0000000..c023bc9 --- /dev/null +++ b/lib/features/details_food/presentation/view_model/cubit/meal_details_cubit.dart @@ -0,0 +1,44 @@ +import 'package:fitness_app/core/base/base_state.dart'; +import 'package:fitness_app/core/utils/datasource_excution/api_result.dart'; +import 'package:fitness_app/domain/meals/entity/meal_details_entity.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../../../domain/meals/use_case/meal_details_use_case.dart'; +import 'meal_details_state.dart'; + +@injectable +class MealDetailsCubit extends Cubit { + final GetMealDetailsUseCase _getMealDetailsUseCase; + + MealDetailsCubit(this._getMealDetailsUseCase) + : super(MealDetailsState(mealDetailsStatus: BaseInitialState())); + + void doIntent(MealDetailsAction action) { + switch (action) { + case GetMealDetailsAction(): + _getMealDetails(action.mealId); + } + } + + Future _getMealDetails(String id) async { + emit(state.copyWith(mealDetailsStatus: BaseLoadingState())); + final result = await _getMealDetailsUseCase(id); + switch (result) { + case SuccessResult(): + emit( + state.copyWith( + mealDetailsStatus: BaseSuccessState(data: result.data), + ), + ); + case FailureResult(): + emit( + state.copyWith( + mealDetailsStatus: BaseErrorState( + errorMessage: result.exception.toString(), + ), + ), + ); + } + } +} diff --git a/lib/features/details_food/presentation/view_model/cubit/meal_details_state.dart b/lib/features/details_food/presentation/view_model/cubit/meal_details_state.dart new file mode 100644 index 0000000..08a03f0 --- /dev/null +++ b/lib/features/details_food/presentation/view_model/cubit/meal_details_state.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; +import 'package:fitness_app/core/base/base_state.dart'; + +class MealDetailsState extends Equatable { + final BaseState mealDetailsStatus; + + const MealDetailsState({required this.mealDetailsStatus}); + + MealDetailsState copyWith({BaseState? mealDetailsStatus}) { + return MealDetailsState( + mealDetailsStatus: mealDetailsStatus ?? this.mealDetailsStatus, + ); + } + + @override + List get props => [mealDetailsStatus]; +} + +sealed class MealDetailsAction {} + +class GetMealDetailsAction extends MealDetailsAction { + final String mealId; + + GetMealDetailsAction(this.mealId); +} diff --git a/lib/features/meals/presentation/view/screens/meals_screen.dart b/lib/features/meals/presentation/view/screens/meals_screen.dart index 7e54f3b..3b03858 100644 --- a/lib/features/meals/presentation/view/screens/meals_screen.dart +++ b/lib/features/meals/presentation/view/screens/meals_screen.dart @@ -30,29 +30,28 @@ class _MealsScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (context) => viewModel, - child: Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - centerTitle: true, - title: Text( - LocaleKeys.FoodRecommendation.tr(), - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - fontSize: 24, - ), + child: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage(AppImages.backgroundThree), + fit: BoxFit.fill, ), ), - extendBodyBehindAppBar: true, - body: Container( - width: double.infinity, - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage(AppImages.backgroundThree), - fit: BoxFit.fill, + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + centerTitle: true, + title: Text( + LocaleKeys.FoodRecommendation.tr(), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 24, + ), ), ), - child: const Padding( - padding: EdgeInsets.only(top: 115, left: 16, right: 16, bottom: 16), + body: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), child: MealsBody(), ), ), diff --git a/lib/features/meals/presentation/view/widgets/meals_grid.dart b/lib/features/meals/presentation/view/widgets/meals_grid.dart index c1645a8..383abe2 100644 --- a/lib/features/meals/presentation/view/widgets/meals_grid.dart +++ b/lib/features/meals/presentation/view/widgets/meals_grid.dart @@ -1,4 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:fitness_app/core/utils/constants.dart'; +import 'package:fitness_app/core/utils/routes/app_routes.dart'; import 'package:fitness_app/domain/meals/entity/meal_entity.dart'; import 'package:flutter/material.dart'; @@ -77,7 +79,16 @@ class MealsGrid extends StatelessWidget { child: GridItem( title: meal.strMeal, imageUrl: meal.strMealThumb, - onTap: () {}, + onTap: () { + Navigator.pushNamed( + context, + AppRoutes.mealDetailsRoute, + arguments: { + Constants.mealId: meal.idMeal, + Constants.mealRecommendation: meals, + }, + ); + }, ), ); }, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 11f67c2..0f153c1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation import sqflite_darwin +import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) @@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index ee90578..fb42038 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -626,6 +626,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + http_mock_adapter: + dependency: "direct dev" + description: + name: http_mock_adapter + sha256: "00f48d69f31d79e389f7a8544b266a214020a8b79d9f376f54e84ae13d3e2965" + url: "https://pub.dev" + source: hosted + version: "0.4.4" http_multi_server: dependency: transitive description: @@ -875,7 +883,7 @@ packages: source: hosted version: "5.4.5" mocktail: - dependency: transitive + dependency: "direct dev" description: name: mocktail sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" @@ -1431,6 +1439,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73" + url: "https://pub.dev" + source: hosted + version: "2.8.7" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "9fedd55023249f3a02738c195c906b4e530956191febf0838e37d0dac912f953" + url: "https://pub.dev" + source: hosted + version: "2.8.0" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a + url: "https://pub.dev" + source: hosted + version: "6.4.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5fdb978..761f3af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: skeletonizer: ^2.0.1 uuid: ^4.5.1 wheel_slider: ^1.2.2 + video_player: dev_dependencies: build_runner: ^2.4.6 @@ -74,6 +75,8 @@ dev_dependencies: injectable_generator: ^2.6.2 json_serializable: ^6.9.0 retrofit_generator: ">=8.0.0 <10.0.0" + http_mock_adapter: ^0.4.3 + mocktail: ^1.0.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/data/auth/repo_impl/auth_repo_impl_test.dart b/test/data/auth/repo_impl/auth_repo_impl_test.dart index 041e567..5b12eb8 100644 --- a/test/data/auth/repo_impl/auth_repo_impl_test.dart +++ b/test/data/auth/repo_impl/auth_repo_impl_test.dart @@ -101,6 +101,7 @@ void main() { group("Auth Repo Test", () { test("should return SuccessResult when register is successful", () async { + // Arrange provideDummy>( SuccessResult(registerResponseDto), ); @@ -123,7 +124,7 @@ void main() { final result = await authRepoImpl.register(registerRequestDto); // Assert - expect(result, isA>()); + expect(result, isA>()); }); test( diff --git a/test/data/meals/data_source/remote/meals_remote_data_source_impl_test.mocks.dart b/test/data/meals/data_source/remote/meals_remote_data_source_impl_test.mocks.dart index fc17a65..a2d9690 100644 --- a/test/data/meals/data_source/remote/meals_remote_data_source_impl_test.mocks.dart +++ b/test/data/meals/data_source/remote/meals_remote_data_source_impl_test.mocks.dart @@ -3,11 +3,13 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i6; -import 'package:fitness_app/data/meals/api/meals_retrofit_client.dart' as _i4; +import 'package:fitness_app/data/meals/api/meals_retrofit_client.dart' as _i5; import 'package:fitness_app/data/meals/models/categories_response_dto.dart' as _i2; +import 'package:fitness_app/data/meals/models/meal_details_response_dto.dart' + as _i4; import 'package:fitness_app/data/meals/models/meals_response_dto.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; @@ -37,35 +39,41 @@ class _FakeMealsResponseDto_1 extends _i1.SmartFake : super(parent, parentInvocation); } +class _FakeMealDetailsResponseDto_2 extends _i1.SmartFake + implements _i4.MealDetailsResponseDto { + _FakeMealDetailsResponseDto_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [MealsRetrofitClient]. /// /// See the documentation for Mockito's code generation for more information. class MockMealsRetrofitClient extends _i1.Mock - implements _i4.MealsRetrofitClient { + implements _i5.MealsRetrofitClient { MockMealsRetrofitClient() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.CategoriesResponseDto> getCategories() => + _i6.Future<_i2.CategoriesResponseDto> getCategories() => (super.noSuchMethod( Invocation.method(#getCategories, []), - returnValue: _i5.Future<_i2.CategoriesResponseDto>.value( + returnValue: _i6.Future<_i2.CategoriesResponseDto>.value( _FakeCategoriesResponseDto_0( this, Invocation.method(#getCategories, []), ), ), ) - as _i5.Future<_i2.CategoriesResponseDto>); + as _i6.Future<_i2.CategoriesResponseDto>); @override - _i5.Future<_i3.MealsResponseDto> getMealsByCategory({ + _i6.Future<_i3.MealsResponseDto> getMealsByCategory({ required String? category, }) => (super.noSuchMethod( Invocation.method(#getMealsByCategory, [], {#category: category}), - returnValue: _i5.Future<_i3.MealsResponseDto>.value( + returnValue: _i6.Future<_i3.MealsResponseDto>.value( _FakeMealsResponseDto_1( this, Invocation.method(#getMealsByCategory, [], { @@ -74,5 +82,18 @@ class MockMealsRetrofitClient extends _i1.Mock ), ), ) - as _i5.Future<_i3.MealsResponseDto>); + as _i6.Future<_i3.MealsResponseDto>); + + @override + _i6.Future<_i4.MealDetailsResponseDto> getMealDetails(String? id) => + (super.noSuchMethod( + Invocation.method(#getMealDetails, [id]), + returnValue: _i6.Future<_i4.MealDetailsResponseDto>.value( + _FakeMealDetailsResponseDto_2( + this, + Invocation.method(#getMealDetails, [id]), + ), + ), + ) + as _i6.Future<_i4.MealDetailsResponseDto>); }