Compare commits

..

40 Commits

Author SHA1 Message Date
misha
c8284338aa feat(image_compress_utility): adde test for corupted images 2026-04-15 16:27:27 +03:00
misha
03ec91a595 feat(image_compress_utility): improved compressing logic 2026-04-15 15:50:09 +03:00
misha
e814683984 version changed 2026-04-14 09:44:12 +03:00
misha
684415ef5b feat(setting): added profile data apdating for sttings screen, added validation for general info form 2026-04-14 09:28:02 +03:00
misha
631ceb2b7a version changed 2026-04-08 12:47:07 +03:00
misha
71a405c63f feat(notifications): changed navigation flow from Cancelled diet and started diet notifications 2026-04-08 12:43:15 +03:00
misha
4ca90eb2df feat(push_navigation): fixed navigation to chat screen 2026-04-08 12:14:43 +03:00
misha
f17f1ecb57 version changed 2026-04-07 10:32:10 +03:00
misha
a71bcc752d feat(fixes): fixed analyzer issue 2026-04-07 10:06:04 +03:00
misha
aac085def4 feat(app): refactoring of sentry using, feat(become_consultant): fixed possible infinite loading, feat(push_notifications): fixed push navigation from paid plan push 2026-04-07 10:05:11 +03:00
misha
b6b13b1db5 version update 2026-04-01 13:21:19 +03:00
misha
6629286bf1 feat(CI/CD): changed gitea user token name 2026-04-01 10:59:54 +03:00
misha
966bc23f0b feat(CI/CD): removed proxy server 2026-04-01 10:49:50 +03:00
misha
a13b623ca2 feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage 2026-04-01 10:37:59 +03:00
misha
ee2dd75a94 feat(ci/cd): added test coverage verification 2026-03-31 10:20:43 +03:00
misha
c450a41715 feat(ci/cd): added token for repo 2026-03-30 10:33:46 +03:00
misha
55b6b75134 feat(ci/cd): env repo changed 2026-03-30 10:16:13 +03:00
misha
335e36dd18 feat(pushNotifications): fixed push notifications navigation 2026-03-26 18:06:20 +03:00
7106bde4ae Merge pull request 'fix 939, 927' (#940) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#940
Reviewed-by: misha <misha@noreply.localhost>
2026-03-26 08:58:17 +00:00
mso7nueh
4f241f7db6 feat: try push handling 2026-03-18 15:39:13 +03:00
mso7nueh
3d7755a9a2 feat: Introduce AppRouter for centralized application routing and deep link handling, and create push_navigation_handler for push notification navigation. 2026-03-17 16:33:54 +03:00
mso7nueh
decc8c63e6 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:32:31 +03:00
mso7nueh
ad5b48ef1b Fix merge conflicts with upstream main by retaining list refresh logic 2026-03-17 12:32:02 +03:00
mso7nueh
e8dc0138e2 Merge pull request 'feat(recipes, diets)' (#938) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#938
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-17 09:19:25 +00:00
mso7nueh
96896671b6 merge upstream 2026-03-17 09:18:48 +00:00
mso7nueh
5e2f0430d9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:18:25 +03:00
mso7nueh
2da3b10e63 feat: Implement 'My Recipes' feature, add diary moderation request modal, and introduce diary repository. 2026-03-17 12:18:17 +03:00
misha
5cb121d733 fat(recipes, diets): added list refreshing 2026-03-16 10:32:12 +03:00
mso7nueh
109013c25c Merge pull request 'feat(diary)' (#931) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#931
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-12 13:49:32 +00:00
misha
f5d14d213f Merge remote-tracking branch 'upstream/main' 2026-03-12 16:04:58 +03:00
misha
26ae28a705 feat(tests): fixed test errors 2026-03-12 14:41:16 +03:00
mso7nueh
7672477c03 Merge pull request 'Обновить pubspec.yaml' (#932) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#932
2026-03-11 12:15:49 +00:00
mso7nueh
07277d6e48 Обновить pubspec.yaml 2026-03-11 12:15:33 +00:00
misha
9d1bedff26 feat(diary): fixed consultant portion comments and day finishing errors handling 2026-03-11 11:55:10 +03:00
misha
e39638be7f feat(notifications): removed unknown notification from layoute 2026-03-05 13:44:33 +03:00
442fc0ec14 Merge pull request 'feat(consultant_registration)' (#924) from misha/eatvio_app:issue-914 into main
Reviewed-on: eatvio/eatvio_app#924
2026-03-04 07:56:21 +00:00
a8f8cc8eb0 Merge pull request 'feat(notifications)' (#921) from misha/eatvio_app:issue-911 into main
Reviewed-on: eatvio/eatvio_app#921
2026-03-04 07:55:58 +00:00
misha
a492c2d964 Merge remote-tracking branch 'upstream/main' into issue-914 2026-03-04 09:21:45 +03:00
misha
be0a99b164 fix: ci/cd 2026-02-25 13:02:28 +03:00
misha
6dab0d159b feat(consultant_registration): added new consultant registration flow 2026-02-25 07:08:05 +03:00
193 changed files with 4454 additions and 3447 deletions

View File

@@ -1,37 +1,39 @@
name: Eatvio App Android Test
on: [ pull_request ]
on: [pull_request]
env:
flutter_version: "3.41.2"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
ANDROID_SDK_ROOT: "/opt/hostedtoolcache/Android/Sdk"
flutter_version: '3.41.2'
java_version: '21.x'
flutter_channel: 'stable'
my_local_cache_dir: '/opt/hostedtoolcache/'
ANDROID_SDK_ROOT: '/opt/hostedtoolcache/Android/Sdk'
jobs:
build:
runs-on: [ ubuntu-latest ]
runs-on: [ubuntu-latest]
steps:
- uses: https://github.com/actions/checkout@v3
- name: Set proxy server
env:
PROXY_USER: ${{ secrets.PROXY_USER }}
PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
run: |
echo "Setting up proxy..."
cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
EOF
# Пока что не нужно проксирование
# - name: Set proxy server
# env:
# PROXY_USER: ${{ secrets.PROXY_USER }}
# PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
# run: |
# echo "Setting up proxy..."
# cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
# Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
# EOF
- name: Checkout env repository
uses: https://github.com/actions/checkout@v3
with:
repository: mso7nueh/eatvio_app_env
repository: eatvio/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
# Здесь может быть токен любого юзера с доступом в gitea
token: ${{ vars.READ_USER_GITEA_TOKEN }}
ref: main
- name: Copy .env file
@@ -66,7 +68,7 @@ jobs:
- name: Cache Android SDK
id: cache-sdk
uses: "https://github.com/actions/cache@v4"
uses: 'https://github.com/actions/cache@v4'
with:
path: ${{ env.my_local_cache_dir }}
key: ${{ runner.os }}-cache-sdk
@@ -74,14 +76,14 @@ jobs:
- name: Set up JDK
uses: https://github.com/actions/setup-java@v3
with:
distribution: "zulu"
distribution: 'zulu'
java-version: ${{ env.java_version }}
cache: gradle
- name: Install utils for Android SDK
run: |
apt-get update
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
- name: Install Android SDK
if: steps.cache-sdk.outputs.cache-hit != 'true'
@@ -100,15 +102,14 @@ jobs:
sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31"
sdkmanager "build-tools;34.0.0"
sdkmanager "ndk;25.1.8937393"
sdkmanager --install "cmake;3.22.1"
sdkmanager --install "cmake;3.22.1"
- name: Set up Android environment variables
id: "android-env-setup"
id: 'android-env-setup'
run: |
echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
- name: Set up Flutter
run: |
@@ -147,7 +148,7 @@ jobs:
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter"
flutter config --android-sdk="$ANDROID_HOME"
flutter doctor -v
flutter doctor -v
- name: Get dependencies
run: |
@@ -212,12 +213,11 @@ jobs:
percentage = (covered / total) * 100
printf "✅ Coverage: %.2f%%\n", percentage
print ""
if (percentage < 60) {
printf "⚠️ Warning: Coverage is below 60%%\n"
} else if (percentage < 80) {
printf "💛 Good: Coverage is above 60%%\n"
if (percentage < 70) {
printf "❌ FAIL: Coverage is below 70%%\n"
exit 1
} else {
printf "💚 Excellent: Coverage is above 80%%\n"
printf "💚 Excellent: Coverage is above 70%%\n"
}
}
}
@@ -236,7 +236,7 @@ jobs:
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
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact
uses: https://github.com/actions/upload-artifact@v3

View File

@@ -55,32 +55,19 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Main domains -->
<data android:host="eatvio.ru" />
<data android:host="www.eatvio.ru" />
<data android:host="eatvio.com" />
<data android:host="www.eatvio.com" />
<!-- Development/Subdomains -->
<data android:host="*.eatvio.ru" />
<data android:host="*.eatvio.com" />
<data android:pathPattern="/ru/recipe/.*" />
<data android:pathPattern="/en/recipe/.*" />
<data android:pathPattern="/ru/blog_post/.*" />
<data android:pathPattern="/en/blog_post/.*" />
<data android:pathPattern="/ru/diet/.*" />
<data android:pathPattern="/en/diet/.*" />
<data android:pathPattern="/ru/plan/.*" />
<data android:pathPattern="/en/plan/.*" />
<data android:pathPattern="/ru/chat/.*" />
<data android:pathPattern="/en/chat/.*" />
<data android:pathPattern="/ru/chat" />
<data android:pathPattern="/en/chat" />
<data android:pathPattern="/ru/profile/.*" />
<data android:pathPattern="/en/profile/.*" />
<data android:pathPattern="/ru/diary/.*" />
<data android:pathPattern="/en/diary/.*" />
<!-- Simplify patterns to allow navigation within the app for all matching hosts -->
<data android:pathPattern="/.*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.

View File

@@ -0,0 +1,3 @@
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/firebase_options.dart';
@@ -16,6 +17,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
/// Класс для инициализации приложения
class AppInitializer {
/// Инициализация всех сервисов приложения
static Future<void> initialize({required AppBuildType buildType}) async {
await _initializeEnvironment();
@@ -79,11 +81,12 @@ class AppInitializer {
options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0;
options.debug = false;
options.beforeSend = (event, hint) {
if (kDebugMode) return null;
if (!ErrorReportPolicy.allowSentryEvent(event, hint)) return null;
return event;
};
}, appRunner: appRunner);
FlutterError.onError = (details) async {
if (kDebugMode) return;
await Sentry.captureException(details.exception, stackTrace: details.stack);
};
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/routing/auth_routes.dart';
import 'package:eatvio_app/core/routing/content_routes.dart';
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
@@ -6,6 +7,7 @@ import 'package:eatvio_app/core/routing/other_routes.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/routing/settings_routes.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
@@ -14,8 +16,8 @@ import 'package:go_router/go_router.dart';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru', 'dev2.eatvio.com', 'www.dev2.eatvio.com'];
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com'];
static const List<String> metaSegments = ['ru', 'en', 'api', 'v2'];
static GoRouter appRouter = GoRouter(
initialLocation: RouteConstants.updateScreen,
@@ -30,11 +32,102 @@ class AppRouter {
],
);
static String? _resolveRecipeDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
return '/about-recipes-screen?slug=$contentIdentifier';
}
static String? _resolveBlogPostDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
return '/blog-post-screen?id=$contentIdentifier';
}
static String? _resolvePlanDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
final planId = int.tryParse(contentIdentifier);
if (planId != null) {
developer.log('Deep link: diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
return '/about-plan-screen?slug=$contentIdentifier';
}
static String? _resolveDietDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
return '/about-diet-screen?slug=$contentIdentifier';
}
const dietTabIndexWithHiddenPlansTab = 3;
const dietTabIndexWithoutHiddenPlansTab = 4;
final initialTabIndex = hidePlans ? dietTabIndexWithHiddenPlansTab : dietTabIndexWithoutHiddenPlansTab;
return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex';
}
static String? _resolveChatDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
} else if (effectiveSegments.length == 3 && effectiveSegments[1] == 'show') {
final contentIdentifier = effectiveSegments[2];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
}
developer.log('Deep link: chat', name: 'AppRouter');
return RouteConstants.chatSelectScreen;
}
static String? _resolveProfileDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
return '/profile-screen?slug=$contentIdentifier';
}
return RouteConstants.myProfileScreen;
}
static String? _resolveBadgeDeepLink(List<String> effectiveSegments) {
return RouteConstants.myProfileScreen;
}
static String? _resolveDiaryDeepLink(List<String> effectiveSegments) {
developer.log('Deep link: diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
static String? _resolveConsultantDiaryCommentDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
static String? _resolveConsultationsDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 3 || effectiveSegments[1] != 'status') return RouteConstants.consultationsScreen;
final consultationId = effectiveSegments[2];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
return '${InfoByOrderScreen.path}?planId=$consultationId';
}
static String? _resolveDeepLinkUri(Uri uri) {
final host = uri.host.toLowerCase();
final segments = uri.pathSegments;
final isDeepLink = _deepLinkHosts.contains(host);
final isDeepLink = deepLinkHosts.any((allowedHost) => host == allowedHost || host.endsWith('.$allowedHost'));
if (!isDeepLink) {
return null;
@@ -44,120 +137,61 @@ class AppRouter {
if (segments.isEmpty) {
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
if (segments.length >= 4 &&
segments[0] == 'api' &&
segments[1] == 'v2' &&
segments[2] == 'chat' &&
segments[3] == 'show') {
developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
if (segments[0] != 'ru' && segments[0] != 'en') {
developer.log('Invalid language segment: ${segments[0]}, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList();
if (effectiveSegments.isEmpty) {
developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter');
return RouteConstants.feedScreen;
}
if (segments.length == 4) {
final type = segments[1];
final type = effectiveSegments[0];
if (type == 'diary') {
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
if (type == 'consultations' && segments[2] == 'status') {
final consultationId = segments[3];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '${RouteConstants.consultationsScreen}?id=$consultationId';
}
developer.log('Unknown deep link type for 4 segments: $type, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
} else if (segments.length == 3) {
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) {
case 'recipe':
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet': {
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
}
case 'chat':
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
case 'profile':
// В диплинке передан slug — открываем профиль пользователя по slug.
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/profile-screen?slug=$contentIdentifier';
case 'badge':
// В badge нет slug, открываем вкладку «Мой профиль».
developer.log('Deep link: badge → my-profile tab', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.myProfileScreen;
case 'diary':
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
case 'consultant_diary_comment':
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
default:
developer.log('Unknown deep link type: $type, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else if (segments.length == 2) {
final type = segments[1];
if (type == 'chat') {
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
if (type == 'diary') {
developer.log('Deep link: diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
developer.log('Unknown deep link type for 2 segments: $type, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
switch (type) {
case 'recipe':
final target = _resolveRecipeDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'blog_post':
final target = _resolveBlogPostDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'plan':
final target = _resolvePlanDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'diet':
final target = _resolveDietDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'chat':
final target = _resolveChatDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'profile':
final target = _resolveProfileDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'badge':
final target = _resolveBadgeDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'diary':
final target = _resolveDiaryDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'consultant_diary_comment':
final target = _resolveConsultantDiaryCommentDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'consultations':
final target = _resolveConsultationsDeepLink(effectiveSegments);
if (target != null) return target;
break;
}
developer.log('Unknown deep link format: ${segments.length} segments, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
return RouteConstants.feedScreen;
}
@@ -190,20 +224,20 @@ class AppRouter {
if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) {
uri = Uri(
scheme: 'https',
host: _deepLinkHosts.first,
host: deepLinkHosts.first,
pathSegments: uri.pathSegments,
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
);
}
final target = _resolveDeepLinkUri(uri);
FlutterNativeSplash.remove();
if (target != null) {
appRouter.go(target);
} else {
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
return;
}
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
}
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {

View File

@@ -5,15 +5,13 @@ import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CommentsCubit extends Cubit<CommentsState> {
final FeedRepository feedRepository;
final CommentsRepository commentsRepository;
CommentsCubit({
required this.feedRepository,
required this.commentsRepository,
}) : super(const CommentsState());
CommentsCubit({required this.feedRepository, required this.commentsRepository}) : super(const CommentsState());
void setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
@@ -36,51 +34,34 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
break;
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.error(errorMessage: e.toString()));
}
}
Future<void> sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,
int? parentCommentId,
}) async {
Future<bool> sendComment({required String id, required String commentText, required CommentSourceType commentSourceType, int? parentCommentId}) async {
try {
switch (commentSourceType) {
case CommentSourceType.blogPost:
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendBlogPostComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
break;
case CommentSourceType.diet:
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendDietsComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
break;
case CommentSourceType.recipe:
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendRecipesComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
break;
case CommentSourceType.plan:
await feedRepository.sendPlansComment(
slug: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendPlansComment(slug: id, commentText: commentText, parentCommentId: parentCommentId);
break;
}
getComments(id, commentSourceType);
} catch (e) {
return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.error(errorMessage: e.toString()));
return false;
}
}
@@ -93,6 +74,7 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}
@@ -101,13 +83,15 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<bool> updateComment({required int id, required String body}) async {
final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel =
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList());
final updatedCommentModel = commentModel.copyWith(
data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList(),
);
try {
await commentsRepository.updateComment(id: id, body: body);
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CommentsRepository {
final CommentsService service;
@@ -8,20 +7,12 @@ class CommentsRepository {
CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return service.deleteComment(id);
}
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return service.updateComment(id, CommentUpdateModel(body: body));
}
}

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class MarketingCubit extends Cubit<MarketingCubitState> {
final Prefs prefs;
@@ -22,8 +23,9 @@ class MarketingCubit extends Cubit<MarketingCubitState> {
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
}
return needToShowMarketingBottomSheet;
} catch (e) {
rethrow;
} catch (e, st) {
await reportFrom(e, st, this);
return false;
}
}
}

View File

@@ -1,10 +1,13 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/sentry_error_reporter.dart';
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:get_it/get_it.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sentry_dio/sentry_dio.dart';
@@ -14,6 +17,7 @@ class ExternalRegistrations {
/// Регистрация всех внешних зависимостей
static void registerAll() {
sl.registerLazySingleton<ErrorReporter>(SentryErrorReporter.new);
_registerDio();
_registerPrefs();
}
@@ -30,7 +34,9 @@ class ExternalRegistrations {
sendTimeout: AppConstants.pendingDelay,
),
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
..addSentry(),
..addSentry(
failedRequestStatusCodes: const [SentryStatusCode.defaultRange()],
),
);
}

View File

@@ -72,7 +72,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi(errorReporter: sl()));
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));

View File

@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// Rules for which errors are sent to crash reporting.
///
/// [shouldReport] is used by [ErrorReporter]. [allowSentryEvent] is used in
/// [SentryFlutter.init] as a last-line filter (Dio failed-request capture, etc.).
class ErrorReportPolicy {
ErrorReportPolicy._();
/// When `false`, the error is not sent via [ErrorReporter].
static bool shouldReport(Object? error) {
if (error == null) return false;
if (error is String) return false;
if (error is SocketException) return false;
if (error is DioException) {
return _shouldReportDio(error);
}
return true;
}
/// Used in SDK [SentryOptions.beforeSend]. Return `false` to drop the event.
///
/// For [DioException], rules differ from [shouldReport]: events from `sentry_dio`
/// (e.g. 5xx) must pass through here, while [shouldReport] returns false for 5xx
/// so [SentryErrorReporter] does not duplicate them after `catch` in cubits / manual [report] calls.
static bool allowSentryEvent(SentryEvent event, Hint hint) {
final t = event.throwable;
if (t == null) return true;
if (t is DioException) {
return _allowSentrySdkDio(t);
}
return shouldReport(t);
}
/// Cancel, timeouts, socket / connection errors — do not send (noisy, often user environment).
static bool _isTransientDioFailure(DioException e) {
switch (e.type) {
case DioExceptionType.cancel:
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.connectionError:
return true;
case DioExceptionType.unknown:
case DioExceptionType.badResponse:
case DioExceptionType.badCertificate:
break;
}
return e.error is SocketException;
}
/// Noise to drop for any Dio-originated SDK event; do not use for manual [report].
static bool _allowSentrySdkDio(DioException e) {
if (_isTransientDioFailure(e)) return false;
final code = e.response?.statusCode;
if (code != null && code >= 400 && code < 500) return false;
return true;
}
static bool _shouldReportDio(DioException e) {
if (_isTransientDioFailure(e)) return false;
final code = e.response?.statusCode;
if (code != null) {
if (code >= 500 && code <= 599) {
return false;
}
if (code >= 400 && code < 500) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,9 @@
/// Application-wide error reporting (e.g. Sentry).
abstract class ErrorReporter {
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
});
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
/// Reporting sink that does nothing (dev tooling, tests, diagnostics).
class NoOpErrorReporter implements ErrorReporter {
const NoOpErrorReporter();
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
}) async {}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/no_op_error_reporter.dart';
import 'package:eatvio_app/core/locator_service.dart';
/// Отправка в [ErrorReporter] с тегом origin = [context.runtimeType] (имя кубита/виджета и т.д.).
///
/// Если [ErrorReporter] не зарегистрирован (например, после [GetIt.reset] в тестах), используется [NoOpErrorReporter].
Future<void> reportFrom(Object error, StackTrace? stackTrace, Object context) {
final reporter = sl.isRegistered<ErrorReporter>()
? sl<ErrorReporter>()
: const NoOpErrorReporter();
return reporter.report(
error,
stackTrace,
origin: context.runtimeType.toString(),
);
}

View File

@@ -0,0 +1,33 @@
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class SentryErrorReporter implements ErrorReporter {
SentryErrorReporter();
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
}) async {
if (!ErrorReportPolicy.shouldReport(error)) return;
await Sentry.captureException(
error,
stackTrace: stackTrace,
withScope: (scope) {
if (origin != null && origin.isNotEmpty) {
scope.setTag('origin', origin);
}
if (extras != null && extras.isNotEmpty) {
scope.setContexts(
'report_extras',
extras.map((k, v) => MapEntry(k, v?.toString())),
);
}
},
);
}
}

View File

@@ -11,11 +11,14 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
class FirebaseApi {
final ErrorReporter _errorReporter;
final _firebaseMessaging = FirebaseMessaging.instance;
FirebaseApi({required ErrorReporter errorReporter}) : _errorReporter = errorReporter;
Future<String?> getToken() async {
String? token;
@@ -61,7 +64,7 @@ class FirebaseApi {
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -86,7 +89,7 @@ class FirebaseApi {
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -112,12 +115,12 @@ class FirebaseApi {
} on PlatformException catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null;
} catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null;
}
}

View File

@@ -3,7 +3,6 @@ import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
abstract class ImageCompressUtility {
@@ -30,44 +29,21 @@ abstract class ImageCompressUtility {
}
static Future<File> compressFile(File file) async {
// Skip compression in test environment to avoid hangs
try {
final appConfig = sl<AppConfig>();
if (appConfig.buildType == AppBuildType.tests) {
// In tests, return the original file without compression
return file;
}
} catch (_) {
// If AppConfig is not available, proceed with compression
} catch (_) {}
final compressedPath = '${p.withoutExtension(file.path)}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(file.absolute.path, compressedPath, quality: 90, minHeight: 1080, minWidth: 1080);
if (result == null) {
return file;
}
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;
int height = image.height;
if (width > height) {
if (width > 1080) {
height = (height * 1080) ~/ width;
width = 1080;
}
} else {
if (height > 1080) {
width = (width * 1080) ~/ height;
height = 1080;
}
}
String filePathWithoutExtension = p.withoutExtension(file.path);
String compressedFilePath = '${filePathWithoutExtension}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
compressedFilePath,
quality: 90,
minHeight: height,
minWidth: width,
);
return File(result!.path);
return File(result.path);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:go_router/go_router.dart';
@@ -12,18 +13,21 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
final data = err.response?.data;
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
final String phone = await sl<ProfileRepository>().getMyPhone();
final String phone = await sl<ProfileRepository>().getMyPhone();
final String passKey = await sl<ProfileRepository>().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone});
await showErrorDialog(AppRouter.navigatorKey.currentContext!,
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона');
await showErrorDialog(
AppRouter.navigatorKey.currentContext!,
title: 'Привяжите номер телефона',
description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона',
);
final currentContext = AppRouter.navigatorKey.currentContext;
if (currentContext != null && currentContext.mounted && GoRouter.of(currentContext).state.path == RouteConstants.stepperBecomeConsultantScreen) {
GoRouter.of(currentContext).pop();
}
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
return handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: 'Phone number not verified',
type: DioExceptionType.badResponse,
),
DioException(requestOptions: err.requestOptions, response: err.response, error: 'Phone number not verified', type: DioExceptionType.badResponse),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:http/http.dart' as http;
class InternetConnectionCubit extends Cubit<InternetConnectionState> {
@@ -33,7 +34,8 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
} on PlatformException catch (e, st) {
await reportFrom(e, st, this);
log('Couldn\'t check connectivity status', error: e);
return;
}
@@ -42,9 +44,13 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
}
Future<void> _updateAccessStatus(List<ConnectivityResult> result) async {
final hasInternet = await _checkInternetAccess(result);
if (!hasInternet) showInternetConnectionToast();
emit(state.copyWith(isInternetAvailable: hasInternet));
try {
final hasInternet = await _checkInternetAccess(result);
if (!hasInternet) showInternetConnectionToast();
emit(state.copyWith(isInternetAvailable: hasInternet));
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<bool> _checkInternetAccess(List<ConnectivityResult> result) async {
@@ -57,7 +63,8 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
try {
final response = await http.get(uri).timeout(const Duration(seconds: 3));
return response.statusCode >= 100 && response.statusCode < 600;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
continue;
}
}

View File

@@ -32,5 +32,5 @@ Future<void> init({required AppBuildType buildType}) async {
CubitRegistrations.registerAll();
// Регистрация WebSocket менеджера
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl()));
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl(), errorReporter: sl()));
}

View File

@@ -5,18 +5,14 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях.
class PushNavigationHandler {
static bool _initialized = false;
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
/// Инициализация обработчиков Firebase Messaging.
/// Должна вызываться один раз после старта приложения.
static Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
// Сообщение, по которому запустили приложение из "убитого" состояния.
try {
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
@@ -25,18 +21,11 @@ class PushNavigationHandler {
});
}
} catch (e) {
developer.log(
'PushNavigationHandler: error while getting initialMessage: $e',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: error while getting initialMessage: $e', name: 'PushNavigationHandler');
}
// Тап по уведомлению, когда приложение было в фоне.
FirebaseMessaging.onMessageOpenedApp.listen(
(message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'),
);
FirebaseMessaging.onMessageOpenedApp.listen((message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'));
// Подписка на события от native-кода (HMS через MethodChannel).
_hmsChannel.setMethodCallHandler((call) async {
if (call.method == 'openActionUrl') {
final url = call.arguments as String? ?? '';
@@ -45,25 +34,17 @@ class PushNavigationHandler {
});
}
/// Входная точка для навигации из native-кода (например, HMS через MethodChannel).
static void handleActionUrlFromNative(String url) {
if (url.isEmpty) {
developer.log(
'PushNavigationHandler.handleActionUrlFromNative: empty url',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler.handleActionUrlFromNative: empty url', name: 'PushNavigationHandler');
return;
}
developer.log(
'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(url);
}
/// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё).
static void handleMessage(RemoteMessage message) {
_handleMessageInternal(message, source: 'manualHandle');
}
@@ -71,21 +52,13 @@ class PushNavigationHandler {
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
final data = message.data;
final actionUrl = data['action_url'] as String?;
if (actionUrl == null || actionUrl.isEmpty) {
developer.log(
'PushNavigationHandler: no action_url in message data, source=$source, data=$data',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: no action_url in message data, source=$source, data=$data', name: 'PushNavigationHandler');
return;
}
developer.log(
'PushNavigationHandler: navigating by action_url=$actionUrl, source=$source',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(actionUrl);
}
}

View File

@@ -10,17 +10,14 @@ import 'package:eatvio_app/features/registration/presentation/pages/verify_email
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
class AuthRoutes {
/// Создание маршрутов аутентификации
static List<RouteBase> createRoutes() {
return [
GoRoute(
path: RouteConstants.authScreen,
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
// и не должен блокировать навигацию
routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),

View File

@@ -33,10 +33,7 @@ class MainNavigationRoutes {
});
}
return _MainShellWithNavScope(
currentLocation: state.matchedLocation,
child: child,
);
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
},
routes: [
GoRoute(
@@ -49,7 +46,14 @@ class MainNavigationRoutes {
GoRoute(
path: RouteConstants.feedScreen,
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
pageBuilder: (context, state) {
final initialTabIndexFromQuery = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '');
final extra = state.extra;
if (extra is int) {
return NoTransitionPage(child: FeedScreen(initialTabIndex: extra));
}
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndexFromQuery));
},
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
@@ -85,16 +89,7 @@ class MainNavigationRoutes {
GoRoute(
path: RouteConstants.menuScreen,
name: 'menuScreen',
pageBuilder: (context, state) => NoTransitionPage(
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return MenuScreen(profileModel: state.profile);
}
return const SizedBox.shrink();
},
),
),
pageBuilder: (context, state) => const NoTransitionPage(child: MenuScreen()),
),
],
),
@@ -105,10 +100,7 @@ class MainNavigationRoutes {
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
class _MainShellWithNavScope extends StatefulWidget {
const _MainShellWithNavScope({
required this.child,
required this.currentLocation,
});
const _MainShellWithNavScope({required this.child, required this.currentLocation});
final Widget child;
final String currentLocation;
@@ -124,10 +116,7 @@ class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
Widget build(BuildContext context) {
return BottomNavScope(
controller: _navController,
child: BottomNavigationBarWidget(
currentLocation: widget.currentLocation,
child: widget.child,
),
child: BottomNavigationBarWidget(currentLocation: widget.currentLocation, child: widget.child),
);
}
}

View File

@@ -156,10 +156,7 @@ class OtherRoutes {
builder: (context, state) {
if (state.extra is Map<String, dynamic>) {
final data = state.extra as Map<String, dynamic>;
return RecipeCreateScreen(
recipe: data['recipe'] as Recipe?,
autoValidate: data['autoValidate'] as bool? ?? false,
);
return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false);
}
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
},
@@ -212,6 +209,10 @@ class OtherRoutes {
path: RouteConstants.chatScreen,
name: 'chatScreen',
builder: (context, state) {
final uuidFromQuery = state.uri.queryParameters['uuid'];
if (uuidFromQuery != null) {
return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false);
}
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
@@ -229,7 +230,12 @@ class OtherRoutes {
GoRoute(
path: RouteConstants.stepperBecomeConsultantScreen,
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled);
},
),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -262,6 +268,11 @@ class OtherRoutes {
path: InfoByOrderScreen.path,
name: InfoByOrderScreen.name,
builder: (context, state) {
final queryParams = state.uri.queryParameters;
final planIdFromQuery = int.tryParse(queryParams['planId'] ?? '');
if (planIdFromQuery != null) {
return InfoByOrderScreen(planId: planIdFromQuery, successfulPayment: null, customerSlug: null, isConsultantView: true);
}
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;

View File

@@ -1,4 +1,6 @@
import 'dart:async';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:eatvio_app/core/views/cubit/views_state.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
@@ -86,9 +88,13 @@ class ViewsCubit extends Cubit<ViewsCubitState> {
}
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
await viewsRepository.viewContent(
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
try {
await viewsRepository.viewContent(
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
void _scheduleDebouncedSend() {

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ViewsRepository {
final ViewsService viewsService;
@@ -8,11 +7,7 @@ class ViewsRepository {
ViewsRepository({required this.viewsService});
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await viewsService.viewContent(viewModel: viewModel);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await viewsService.viewContent(viewModel: viewModel);
}
}

View File

@@ -9,14 +9,16 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
class WsManager {
final WsService wsService;
final ErrorReporter _errorReporter;
WsManager({required this.wsService});
WsManager({required this.wsService, required ErrorReporter errorReporter})
: _errorReporter = errorReporter;
PusherChannelsClient? client;
int? userId;
@@ -114,7 +116,7 @@ class WsManager {
_subscribe();
await _connectCubitsToStreams();
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'WsManager');
log(e.toString(), stackTrace: st);
}
}
@@ -130,7 +132,7 @@ class WsManager {
await sl<NotificationsCubit>().connectToWebSocket();
} catch (e, st) {
log('WsManager._connectCubitsToStreams: $e', stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'WsManager');
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
class AuthCubit extends Cubit<AuthState> {
@@ -26,7 +27,8 @@ class AuthCubit extends Cubit<AuthState> {
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Неверный адрес электронной почты или пароль');
emit(AuthState.error(errorMessage: '$e'));
@@ -37,22 +39,30 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> initializeUser() async {
try {
await registerAnonymousPushToken();
FlutterNativeSplash.remove();
} catch (e) {
log('Error in initializeUser: $e');
FlutterNativeSplash.remove();
FlutterNativeSplash.remove();
} catch (e, st) {
log('Error in initializeUser: $e', stackTrace: st);
FlutterNativeSplash.remove();
}
}
Future<void> registerAnonymousPushToken() async {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
try {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> registerPushToken() async {
await firebaseApi.registerPushToken();
try {
await firebaseApi.registerPushToken();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_request_respo
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final AuthRepository authRepository;
@@ -29,7 +30,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: Got token model: ${tokenModel.token}');
TokenStorage.saveTokenInStorage(tokenModel.token);
emit(OAuthState.loaded(token: tokenModel.token));
} on DioException catch (e) {
} on DioException catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error occurred: $e');
// Проверяем, является ли ошибка ошибкой валидации email/phone
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
@@ -62,7 +64,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
} else {
emit(OAuthState.error(errorMessage: e.toString()));
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString()));
}
@@ -103,7 +106,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
TokenStorage.saveTokenInStorage(token);
cacheRegistrationDate(DateTime.now());
emit(OAuthState.loaded(token: token));
} on DioException catch (e) {
} on DioException catch (e, st) {
await reportFrom(e, st, this);
emit(OAuthState.error(errorMessage: e.toString()));
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
@@ -153,7 +157,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
// Сбрасываем состояние после успешного запроса кода
emit(const OAuthState());
return response;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error requesting verification code: $e');
emit(OAuthState.error(errorMessage: e.toString()));
return null;
@@ -200,10 +205,10 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: OAuth verification confirmed successfully, token received');
TokenStorage.saveTokenInStorage(response.token);
emit(OAuthState.loaded(token: response.token));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error confirming OAuth verification: $e');
emit(OAuthState.error(errorMessage: e.toString()));
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
}
}
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ResetPasswordCubit extends Cubit<ResetPasswordState> {
final ResetPasswordRepository resetPasswordRepository;
@@ -15,7 +16,8 @@ class ResetPasswordCubit extends Cubit<ResetPasswordState> {
} else {
emit(const ResetPasswordState.codeSendError());
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ResetPasswordState.codeSendError());
}
}
@@ -28,7 +30,8 @@ class ResetPasswordCubit extends Cubit<ResetPasswordState> {
} else {
emit(const ResetPasswordState.resetPasswordError());
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ResetPasswordState.resetPasswordError());
}
}

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_respo
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class AuthRepository {
final AuthService authService;
@@ -15,14 +14,10 @@ class AuthRepository {
AuthRepository({required this.authService});
Future<TokenModel> getTokenByCredentials(String email, String password) async {
try {
return await authService.getTokenByCredentials(
credentials: CredentialsModel(email: email, password: password),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await authService.getTokenByCredentials(
credentials: CredentialsModel(email: email, password: password),
);
}
Future<TokenModel> signInViaSocialMedia({
@@ -31,14 +26,10 @@ class AuthRepository {
required String refreshToken,
required String clientId,
}) async {
try {
return await authService.signInViaSocialMedia(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await authService.signInViaSocialMedia(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
);
}
Future<String> signUpViaSocialMedia({
@@ -56,47 +47,39 @@ class AuthRepository {
required int height,
required String clientId,
}) async {
try {
return (await authService.signUpViaSocialMedia(
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
clientId: clientId,
),
))['token']!
as String;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await authService.signUpViaSocialMedia(
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
clientId: clientId,
),
))['token']!
as String;
}
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
Map<String, dynamic>? body;
if (email != null) {
body = {'email': email};
} else if (phone != null) {
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
Map<String, dynamic>? body;
if (email != null) {
body = {'email': email};
} else if (phone != null) {
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
}
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
@@ -116,33 +99,28 @@ class AuthRepository {
required int weight,
required int height,
}) async {
try {
final request = OAuthVerificationConfirmRequest(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Логируем запрос перед отправкой
log(
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
);
return await authService.confirmOAuthVerification(body: request);
} catch (e, s) {
log('AuthRepository: Error in confirmOAuthVerification: $e');
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final request = OAuthVerificationConfirmRequest(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Логируем запрос перед отправкой
log(
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
);
return await authService.confirmOAuthVerification(body: request);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ResetPasswordRepository {
final ResetPasswordService resetPasswordService;
@@ -9,22 +8,18 @@ class ResetPasswordRepository {
});
Future<bool> sendEmailCodeForPasswordChange(String email) async {
try {
final response = await resetPasswordService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final response = await resetPasswordService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
}
Future<bool> confirmPasswordChange(
@@ -33,24 +28,20 @@ class ResetPasswordRepository {
String passwordConfirmation,
String email,
) async {
try {
final response = await resetPasswordService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final response = await resetPasswordService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
}
}

View File

@@ -8,10 +8,11 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
import 'package:flutter/material.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -43,7 +44,7 @@ class SocialAuthFacade {
}
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
return null;
}
}
@@ -161,7 +162,7 @@ class SocialAuthFacade {
);
} catch (e, s) {
log(e.toString());
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
}
return null;
}
@@ -209,7 +210,7 @@ class SocialAuthFacade {
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
return null;
}
}

View File

@@ -1,7 +1,6 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BarcodeRepository {
final BarcodeService barcodeService;
@@ -15,49 +14,46 @@ class BarcodeRepository {
/// Получить информацию о продукте по штрихкоду
/// Возвращает либо данные продукта, либо статус задачи обработки
Future<BarcodeResult> getProductByBarcode(String barcode) async {
try {
final response = await dio.get('/api/gsone/barcode/$barcode');
if (response.statusCode == 200) {
// Данные найдены в БД
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.success(product);
} else if (response.statusCode == 202) {
// Запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw Exception('Unexpected status code: ${response.statusCode}');
} on DioException catch (e, s) {
if (e.response?.statusCode == 202) {
// Если получен статус 202, значит запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
await Sentry.captureException(e, stackTrace: s);
rethrow;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final response = await dio.get<Map<String, dynamic>>(
'/api/gsone/barcode/$barcode',
options: Options(validateStatus: (_) => true),
);
final code = response.statusCode ?? 0;
final data = response.data;
if (data is! Map<String, dynamic>) {
throw DioException(
requestOptions: response.requestOptions,
response: response,
message: 'Invalid barcode response',
);
}
if (code == 200) {
final product = BarcodeProductModel.fromJson(data);
return BarcodeResult.success(product);
}
if (code == 202) {
final taskStatus = BarcodeTaskStatusModel.fromJson(data);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw DioException(
requestOptions: response.requestOptions,
response: response,
message: 'Unexpected status code: $code',
);
}
/// Получить статус задачи обработки
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
try {
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
}
}
@@ -109,4 +105,3 @@ enum BarcodeTaskStatusType {
processing,
failed,
}

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class BlogPostCubit extends Cubit<BlogPostState> {
final FeedRepository feedRepository;
@@ -16,6 +17,7 @@ class BlogPostCubit extends Cubit<BlogPostState> {
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false));
}
@@ -37,6 +39,7 @@ class BlogPostCubit extends Cubit<BlogPostState> {
await feedRepository.likeBlogPost(id: current.id);
}
} catch (e, st) {
await reportFrom(e, st, this);
// rollback on error
log(e.toString(), stackTrace: st);
emit(state.copyWith(blogPostModel: current));

View File

@@ -78,9 +78,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 8,
),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
@@ -147,14 +145,18 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
onTap: () async {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
final isSent = await commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
if (!context.mounted) return;
if (isSent) context.read<FeedCubit>().incrementCommentsState(widget.id);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
@@ -237,9 +239,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: MarkdownHtmlWidget(
markdownText: blogPostState.blogPostModel!.description,
),
child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)

View File

@@ -1,9 +1,12 @@
import 'package:eatvio_app/core/cubit/base_cubit.dart';
import 'dart:developer';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
final BlogPostRepository blogPostRepository;
BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState());
@@ -15,7 +18,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
emit(const BlogPostCreateState.loaded());
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false;
}
@@ -28,7 +32,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
emit(const BlogPostCreateState.loaded());
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false;
}
@@ -39,7 +44,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
await blogPostRepository.deleteBlogPost(id);
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
return false;
}
}

View File

@@ -1,5 +1,5 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -8,7 +8,6 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BlogPostRepository {
final BlogPostService blogPostService;
@@ -23,48 +22,30 @@ class BlogPostRepository {
String? description,
XFile? image,
) async {
FormData formData = FormData.fromMap({
final formData = FormData.fromMap({
if (title != null && title.isNotEmpty) 'title': title,
if (description != null && description.isNotEmpty) 'description_md': description,
if (videoUrl != null && videoUrl.isNotEmpty) 'video_url': videoUrl,
if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name),
});
try {
return await blogPostService.createBlogPost(
formData: formData,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return blogPostService.createBlogPost(
formData: formData,
);
}
Future<XFile?> downloadImage(String imageUrl) async {
try {
// Загрузка изображения
final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
if (response.statusCode == 200) {
// Получение временной директории
final directory = await getTemporaryDirectory();
// Создание файла в временной директории
final filePath = '${directory.path}/image.jpg'; // Вы можете изменить название файла
final file = File(filePath);
// Запись данных в файл
await file.writeAsBytes(response.bodyBytes);
if (response.statusCode == 200) {
final directory = await getTemporaryDirectory();
final filePath = '${directory.path}/image.jpg';
final file = File(filePath);
await file.writeAsBytes(response.bodyBytes);
// Возвращаем объект XFile
return XFile(file.path);
} else {
log('Ошибка загрузки изображения: ${response.statusCode}');
return null;
}
} catch (e, s) {
log('Произошла ошибка: $e');
await Sentry.captureException(e, stackTrace: s);
return null;
return XFile(file.path);
}
return null;
}
Future<void> updateBlogPost(
@@ -75,9 +56,9 @@ class BlogPostRepository {
XFile? image,
String? oldImageUrl,
) async {
FormData formData = FormData.fromMap({
final formData = FormData.fromMap({
'title': title,
'description_md': description,
'description_md': description,
'video_url': videoUrl,
'image': image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
@@ -86,23 +67,13 @@ class BlogPostRepository {
: null,
});
try {
return await blogPostService.updateBlogPost(
id: id,
formData: formData,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return blogPostService.updateBlogPost(
id: id,
formData: formData,
);
}
Future<void> deleteBlogPost(int id) async {
try {
await blogPostService.deleteBlogPost(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await blogPostService.deleteBlogPost(id: id);
}
}

View File

@@ -1,8 +1,11 @@
import 'dart:async';
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -28,7 +31,8 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
return age;
}
} catch (e) {
} catch (e, st) {
unawaited(reportFrom(e, st, this));
return 0;
}
return 0;
@@ -51,10 +55,12 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
]);
AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
// Игнорируем ошибки плагинов, профиль уже загружен
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_state.dart';
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CalendarCubit extends Cubit<CalendarState> {
final CalendarRepository calendarRepository;
@@ -12,7 +13,8 @@ class CalendarCubit extends Cubit<CalendarState> {
try {
final calendarList = await calendarRepository.getCalendar();
emit(CalendarState.loaded(calendarList: calendarList));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(CalendarState.error(errorMessage: e.toString()));
}
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/calendar/models/calendar_model.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CalendarRepository {
final CalendarService calendarService;
@@ -8,11 +7,7 @@ class CalendarRepository {
CalendarRepository({required this.calendarService});
Future<List<CalendarModel>> getCalendar() async {
try {
return await calendarService.getCalendar();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await calendarService.getCalendar();
}
}

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_s
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CancelPlanCubit extends Cubit<CancelPlanState> {
final CancelPlanRepository cancelPlanRepository;
@@ -18,6 +19,7 @@ class CancelPlanCubit extends Cubit<CancelPlanState> {
await cancelPlanRepository.cancelChosenPlan(id: id, body: body);
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
return false;

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CancelPlanRepository {
final CancelPlanService cancelService;
@@ -9,11 +8,7 @@ class CancelPlanRepository {
required int id,
CancelationModel? body
}) async {
try {
await cancelService.cancelChosenPlan(id: id, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await cancelService.cancelChosenPlan(id: id, body: body);
}
}

View File

@@ -20,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:uuid/uuid.dart';
class ChatRoomCubit extends Cubit<ChatRoomState> {
@@ -64,7 +65,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
currentPage++;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
}
}
@@ -75,6 +77,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final cards = await cardsRepository.getCards();
_setCard(cards: cards);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}
@@ -90,6 +93,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
try {
emit(const ChatRoomState.loading());
final chatRoom = await chatRepository.getChatRoom(uuid);
ChosenPlanModel? chosenPlan;
if (chatRoom.chosenPlanId != null) {
@@ -111,7 +115,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
await checkCards();
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
typingSubscription?.cancel();
messagesSubscription?.cancel();
@@ -121,13 +126,17 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
Future<void> updateChosenPlanState() async {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
final chosenPlanId = currentState.chatRoom.chosenPlanId;
if (chosenPlanId != null) {
final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId);
emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan));
try {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
final chosenPlanId = currentState.chatRoom.chosenPlanId;
if (chosenPlanId != null) {
final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId);
emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan));
}
}
} catch (e, st) {
await reportFrom(e, st, this);
}
}
@@ -142,6 +151,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: chosenPlan));
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -151,7 +161,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
await chatRepository.cancelChosenPlan(id: planId);
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: null));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
}
}
@@ -165,12 +176,12 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
final currentState = state as ChatRoomStateLoaded;
// Сообщения от текущего пользователя не приходят через WebSocket,
// они обрабатываются через ответ API в sendMessage
// Здесь обрабатываем только входящие сообщения от других пользователей
List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages];
chatRepository.emitNewMessageToChats(message);
emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
@@ -191,7 +202,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final messageUuid = uuid.v4();
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Создаем временное сообщение с состоянием loading
emit(
(state as ChatRoomStateLoaded).copyWith(
@@ -216,7 +227,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
// Отправляем сообщение и получаем ответ от сервера с правильными данными
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Заменяем локальное сообщение данными с сервера (с правильными URL файлов)
emit(
(state as ChatRoomStateLoaded).copyWith(
@@ -238,7 +249,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}).toList(),
),
);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
@@ -253,6 +265,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
sl<ChatsCubit>().getChatRooms();
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -274,6 +287,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
return false;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -286,6 +300,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final paymentModel = await chatRepository.getPaymentLink(id: planId);
return paymentModel.paymentUrl;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return null;
}
@@ -300,7 +315,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
body: PlanComplainModel(description: description),
);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
return false;
}
@@ -312,7 +328,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (planId == null) return false;
await chatRepository.finishChosenPlan(id: planId);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
return false;
}
@@ -341,7 +358,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (message != null) {
try {
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Определяем, есть ли файлы для повторной отправки
List<File>? filesToResend;
if (message.mediaData != null && message.mediaData!.isNotEmpty) {
@@ -352,7 +369,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
filesToResend = message.mediaData!.cast<File>();
}
}
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
@@ -375,12 +392,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
// Отправляем сообщение и получаем ответ от сервера
final serverMessage = await chatRepository.sendMessage(
data: message.data,
files: filesToResend,
slug: userSlug,
);
final serverMessage = await chatRepository.sendMessage(data: message.data, files: filesToResend, slug: userSlug);
// Заменяем локальное сообщение данными с сервера
emit(
(state as ChatRoomStateLoaded).copyWith(
@@ -400,7 +413,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}).toList(),
),
);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
@@ -414,6 +428,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
sl<ChatsCubit>().getChatRooms();
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_sta
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ChatsCubit extends Cubit<ChatsState> {
final ChatRepository chatRepository;
@@ -31,10 +32,14 @@ class ChatsCubit extends Cubit<ChatsState> {
late int lastPage;
Future<void> connectToWebSocket() async {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
_handleNewMessageViaSocket(event.data);
});
try {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
_handleNewMessageViaSocket(event.data);
});
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> getChatRoomsWithLoading({String? query}) async {
@@ -53,7 +58,8 @@ class ChatsCubit extends Cubit<ChatsState> {
),
);
page++;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error),
);
@@ -76,6 +82,7 @@ class ChatsCubit extends Cubit<ChatsState> {
);
page++;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error),
@@ -99,6 +106,7 @@ class ChatsCubit extends Cubit<ChatsState> {
);
page++;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(
state.copyWith(
@@ -132,33 +140,37 @@ class ChatsCubit extends Cubit<ChatsState> {
}
Future<void> _handleNewMessageViaSocket(dynamic data) async {
final json = jsonDecode(data);
final message = ChatMessageModel.fromJson(json['message']);
try {
final json = jsonDecode(data);
final message = ChatMessageModel.fromJson(json['message']);
final currentChatRooms = [...state.chatRooms];
final currentChatRooms = [...state.chatRooms];
bool wasChanged = false;
bool willIncrementUnreadChats = false;
final updatedChatRooms = currentChatRooms.map((chatRoom) {
if (chatRoom.id == message.roomId) {
wasChanged = true;
if (chatRoom.unreadMessagesCount == 0) {
willIncrementUnreadChats = true;
bool wasChanged = false;
bool willIncrementUnreadChats = false;
final updatedChatRooms = currentChatRooms.map((chatRoom) {
if (chatRoom.id == message.roomId) {
wasChanged = true;
if (chatRoom.unreadMessagesCount == 0) {
willIncrementUnreadChats = true;
}
return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1);
}
return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1);
}
return chatRoom;
}).toList();
return chatRoom;
}).toList();
if (!wasChanged && _query == null) {
getChatRooms();
} else {
emit(
state.copyWith(
chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount,
),
);
if (!wasChanged && _query == null) {
getChatRooms();
} else {
emit(
state.copyWith(
chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount,
),
);
}
} catch (e, st) {
await reportFrom(e, st, this);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -62,6 +63,14 @@ class _ChatScreenState extends State<ChatScreen> {
chatRoomCubit.setTimer();
}
@override
void didUpdateWidget(ChatScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.uuid != widget.uuid) {
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
}
}
@override
void initState() {
super.initState();
@@ -96,11 +105,34 @@ class _ChatScreenState extends State<ChatScreen> {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Scaffold(
appBar: AppBar(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
);
} else {
return const Scaffold(body: Loader());
return Scaffold(
body: const Loader(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
);
}
},
),
@@ -148,8 +180,18 @@ class _ChatScreenState extends State<ChatScreen> {
resizeToAvoidBottomInset: true,
appBar: AppBar(
titleSpacing: 0,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
backgroundColor: appTheme.gray100,
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
@@ -224,7 +266,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
@@ -235,7 +277,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
_messageDate(message.updatedAt),
@@ -284,7 +326,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
@@ -295,7 +337,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
@@ -12,7 +11,6 @@ import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:http_parser/http_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ChatRepository {
final ChatService chatService;
@@ -34,160 +32,104 @@ class ChatRepository {
String? data,
List<File>? files,
}) async {
try {
final multipartFiles = <MultipartFile>[];
final multipartFiles = <MultipartFile>[];
if (files != null) {
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(
fileBytes,
filename: file.path.split('/').last,
contentType: MediaType('application', 'octet-stream'),
);
multipartFiles.add(multipartFile);
}
if (files != null) {
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(
fileBytes,
filename: file.path.split('/').last,
contentType: MediaType('application', 'octet-stream'),
);
multipartFiles.add(multipartFile);
}
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
}
Future<ChatRoomModel> getChatRoom(String uuid) async {
try {
return (await chatService.getChatRoom(uuid: uuid));
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
return (await chatService.getChatRoom(uuid: uuid));
}
Future<ChatRoomPaginationModel> getChatRooms({String? query, int page = 1}) async {
try {
return (await chatService.getChatRooms(query: query, page: page));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await chatService.getChatRooms(query: query, page: page));
}
Future<ChatMessagesModel> getChatMessages(String id, int page) async {
try {
return (await chatService.getChatMessages(id: id, page: page));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await chatService.getChatMessages(id: id, page: page));
}
Future<ChatRoomModel> getChatRoomIdByUserSlug({required String slug}) async {
try {
return (await chatService.getChatRoomIdByUserSlug(user: slug));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await chatService.getChatRoomIdByUserSlug(user: slug));
}
Future<void> markMessagesAsRead(String id, List<int> messageIds) async {
try {
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
}
Future<void> cancelChosenPlan({
required int id,
}) async {
try {
await chatService.cancelChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.cancelChosenPlan(id: id);
}
Future<void> choseStartPlanDate({required int id, required Map<String, String> date}) async {
try {
await chatService.choseStartPlanDate(id: id, date: date);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.choseStartPlanDate(id: id, date: date);
}
Future<PaymentModel> getPaymentLink({
required int id,
}) async {
try {
return await chatService.getPaymentLink(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await chatService.getPaymentLink(id: id);
}
Future<ChosenPlanModel> getChosenPlan({
required int id,
}) async {
try {
return await chatService.getChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await chatService.getChosenPlan(id: id);
}
Future<void> finishChosenPlan({
required int id,
}) async {
try {
await chatService.finishChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.finishChosenPlan(id: id);
}
Future<void> complainAboutConsultation({
required int id,
required PlanComplainModel body,
}) async {
try {
await chatService.complainAboutConsultation(id: id, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.complainAboutConsultation(id: id, body: body);
}
Future<String> getChatByUserSlug(String slug) async {
try {
final response = await chatService.getChatByUserSlug(slug: slug);
if (response.response.statusCode == 404) {
return '404';
} else {
return response.data['id'];
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return 'error';
final response = await chatService.getChatByUserSlug(slug: slug);
if (response.response.statusCode == 404) {
return '404';
}
return response.data['id'];
}
}

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ComplainingCubit extends Cubit<ComplainingState> {
final ComplainingRepository repository;
@@ -81,6 +82,7 @@ class ComplainingCubit extends Cubit<ComplainingState> {
}
}
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
emit(state.copyWith(wasSentSuccessfully: false));

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/complaining/models/complain_model.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ComplainingRepository {
final ComplainingService service;
@@ -11,12 +10,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendPlanComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendPlanComplain(slug: slug, category: category, body: body);
}
Future<void> sendRecipeComplain({
@@ -24,12 +19,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendRecipeComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendRecipeComplain(slug: slug, category: category, body: body);
}
Future<void> sendBlogPostsComplain({
@@ -37,12 +28,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendBlogPostsComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendBlogPostsComplain(id: id, category: category, body: body);
}
Future<void> sendProfileComplain({
@@ -50,12 +37,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendProfileComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendProfileComplain(slug: slug, category: category, body: body);
}
Future<void> sendDietComplain({
@@ -63,12 +46,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendDietComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendDietComplain(id: id, category: category, body: body);
}
Future<void> sendCommentComplain({
@@ -76,11 +55,7 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendCommentComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendCommentComplain(id: id, category: category, body: body);
}
}

View File

@@ -22,6 +22,9 @@ abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
static const String visibleGroup = 'user_certs';
static const String hiddenGroup = 'hidden_user_certs';
static const String pendingGroup = 'pending_user_certs';
bool get isHidden => group == hiddenGroup;
bool get isActive => group == visibleGroup || group == hiddenGroup;
}

View File

@@ -1,10 +1,11 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
final ConsultantRepository consultantRepository;
@@ -16,82 +17,120 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true),
loading: true,
acceptOffer: false,
isUpdating: false,
),
);
Future<void> getMyRequestStatus(String slug) async {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await _fetchVisibleCertPhotos();
try {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await consultantRepository.getConsultantCertPhotos();
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
return CertificatesModel(title: course.title, date: course.date);
}).toList();
return CertificatesModel(title: course.title, date: course.date);
}).toList();
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
isUpdating: _update,
error: null,
),
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
),
);
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(loading: false, error: e.toString()));
}
}
Future<void> disableLoading() async {
final certPhotos = await _fetchVisibleCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos));
try {
final certPhotos = await consultantRepository.getConsultantCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos, error: null));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(loading: false, error: e.toString()));
}
}
/// Повторная загрузка после [DataErrorWidget], аналогично [AboutDietCubit.getDiet].
Future<void> reloadData(String? slug) async {
emit(state.copyWith(loading: true, error: null));
if (slug != null) {
await getMyRequestStatus(slug);
} else {
await disableLoading();
}
}
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false));
emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false, error: null));
}
Future<void> registerConsultant() async {
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
emit(state.copyWith(error: null));
try {
if (state.newAddedPhotos.isNotEmpty) {
await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos);
}
if (state.deletedPhotoIds.isNotEmpty) {
for (final int id in state.deletedPhotoIds) {
await consultantRepository.deletePhoto(id);
}
}
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(error: e.toString()));
}
}
Future<void> uploadCertPhotos(List<File> files) async {
Future<void> addCertPhotos(List<File> files) async {
if (files.isEmpty) {
return;
}
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
final uploaded = await consultantRepository.storeConsultantCertPhotos(files: compressedFiles);
emit(state.copyWith(certPhotos: _onlyVisible(uploaded)));
}
Future<void> removeCertPhoto(int id) async {
await consultantRepository.hideConsultantCertPhoto(id);
emit(state.copyWith(certPhotos: state.certPhotos.where((photo) => photo.id != id).toList()));
}
Future<List<ConsultantCertPhotoModel>> _fetchVisibleCertPhotos() async {
try {
final photos = await consultantRepository.getConsultantCertPhotos();
return _onlyVisible(photos);
} catch (e) {
return [];
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles], error: null));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(error: e.toString()));
}
}
List<ConsultantCertPhotoModel> _onlyVisible(List<ConsultantCertPhotoModel> photos) {
return photos.where((photo) => !photo.isHidden).toList();
Future<void> removeCertPhoto(int? id, String? path) async {
if (id != null) {
emit(state.copyWith(deletedPhotoIds: [...state.deletedPhotoIds, id]));
return;
}
if (path != null) {
emit(state.copyWith(newAddedPhotos: state.newAddedPhotos.where((file) => file.uri.path != path).toList()));
}
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
@@ -11,7 +13,11 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
required ConsultantProfileModel consultant,
required bool acceptOffer,
required bool loading,
required bool isUpdating,
ConsultationRequestModel? requestModel,
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
@Default(<File>[]) List<File> newAddedPhotos,
@Default(<int>[]) List<int> deletedPhotoIds,
String? error,
}) = _RegisterConsultantState;
}

View File

@@ -17,18 +17,13 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
title: const Text('Стать консультантом'),
automaticallyImplyLeading: false,
centerTitle: false,
leading: IconButton(
onPressed: () => GoRouter.of(context).pop(),
icon: const Icon(Icons.close),
),
leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)),
),
body: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.',
style: TextStyle(
fontSize: 16,
),
style: TextStyle(fontSize: 16),
),
),
bottomSheet: _startRegistrationButton(),
@@ -37,24 +32,16 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
InkWell _startRegistrationButton() {
return InkWell(
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}),
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Начать регистрацию',
style: TextStyle(
color: Colors.white,
height: 1,
fontWeight: FontWeight.w600,
fontSize: 18,
),
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
),
),
),

View File

@@ -33,7 +33,9 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
const SizedBox(height: 16),
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) {
final photos = state.certPhotos;
final photos = state.certPhotos.where((photo) => !state.deletedPhotoIds.contains(photo.id)).toList();
final addedPhotos = state.newAddedPhotos;
final totalPhotosCount = photos.length + addedPhotos.length;
return Column(
children: [
if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)],
@@ -47,7 +49,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
mainAxisSpacing: 8,
),
itemBuilder: (BuildContext context, int index) {
if (index == photos.length) {
if (index == totalPhotosCount) {
return InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _pickAndUploadPhotos(),
@@ -57,10 +59,25 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
),
);
} else {
return _CertPhotoCard(photo: photos[index], allPhotos: photos, onRemove: () => _removePhoto(photos[index].id));
if (index < photos.length) {
final photo = photos[index];
return _CertPhotoCard(
photo: photo,
allPhotos: photos,
onRemove: () => _removePhoto(id: photo.id),
);
} else {
final localIndex = index - photos.length;
final localPhoto = addedPhotos[localIndex];
return _LocalCertPhotoCard(
photo: localPhoto,
allPhotos: addedPhotos,
onRemove: () => _removePhoto(path: localPhoto.uri.path),
);
}
}
},
itemCount: photos.length + 1,
itemCount: totalPhotosCount + 1,
),
],
);
@@ -82,7 +99,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
});
try {
// ignore: use_build_context_synchronously
await context.read<RegisterConsultantCubit>().uploadCertPhotos(newImages.map((file) => File(file.path)).toList());
await context.read<RegisterConsultantCubit>().addCertPhotos(newImages.map((file) => File(file.path)).toList());
} finally {
if (mounted) {
setState(() {
@@ -92,8 +109,8 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
}
}
Future<void> _removePhoto(int id) async {
await context.read<RegisterConsultantCubit>().removeCertPhoto(id);
Future<void> _removePhoto({int? id, String? path}) async {
await context.read<RegisterConsultantCubit>().removeCertPhoto(id, path);
}
}
@@ -147,3 +164,61 @@ class _CertPhotoCard extends StatelessWidget {
);
}
}
class _LocalCertPhotoCard extends StatelessWidget {
final File photo;
final List<File> allPhotos;
final VoidCallback onRemove;
const _LocalCertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
@override
Widget build(BuildContext context) {
return Hero(
tag: photo.path,
child: Material(
borderRadius: BorderRadius.circular(20),
child: InkWell(
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: allPhotos,
isLocal: true,
index: allPhotos.indexOf(photo),
),
);
},
borderRadius: BorderRadius.circular(20),
child: Stack(
clipBehavior: Clip.none,
children: [
Ink(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
image: DecorationImage(
fit: BoxFit.cover,
image: FileImage(photo),
),
),
),
Positioned(
right: -12,
top: -12,
child: InkWell(
onTap: onRemove,
customBorder: const CircleBorder(),
child: Ink(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
child: const Icon(size: 20, Icons.close, color: Colors.black),
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/additional_settings_screen.dart';
@@ -10,6 +10,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/social_networks_screen.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -19,8 +20,8 @@ import 'package:lottie/lottie.dart';
class StepperBecomeConsultantScreen extends StatefulWidget {
final String? slug;
const StepperBecomeConsultantScreen({super.key, this.slug});
final bool? isCanceled;
const StepperBecomeConsultantScreen({super.key, this.slug, this.isCanceled});
@override
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
@@ -49,8 +50,37 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
create: (context) => registrationConsultantCubit,
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) {
if (state.loading == false) {
if (state.loading) {
return const Scaffold(body: Loader());
}
if (state.error != null) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
'',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: DataErrorWidget(
onReload: () => context.read<RegisterConsultantCubit>().reloadData(widget.slug),
),
);
}
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
@@ -63,25 +93,17 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
),
),
if (state.requestModel?.educationModel.requestState == ModerationStatus.canceled)
if (widget.isCanceled == true)
InkWell(
onTap: () {
showDialog(
@@ -94,15 +116,10 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
children: [
for (int i = 0; i < state.requestModel!.canceledModeration!.additional!.length; i++) ...[
Text('• ${state.requestModel!.canceledModeration!.additional![i]}'),
]
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
},
@@ -110,26 +127,18 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: Ink(
height: 48,
width: MediaQuery.of(context).size.width * 0.6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
child: const Center(
child: Text(
'Комментарий',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
height: 1,
),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF323F4B), height: 1),
),
),
),
),
Container(
height: 48,
width: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
width: widget.isCanceled == true
? MediaQuery.of(context).size.width * 0.2
: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
@@ -138,18 +147,19 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: isLoading
? Colors.grey
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
? const Color(0xFFEAF2FD)
: appTheme.blueA400,
: widget.isCanceled == true
? const Color(0xFFEAF2FD)
: appTheme.blueA400,
onPressed: isLoading
? null
: () async {
if (step == 6) {
if (widget.isCanceled == true) {
return;
}
if (registrationConsultantCubit.state.acceptOffer) {
setState(() {
isLoading = true;
@@ -157,6 +167,9 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
try {
await registrationConsultantCubit.registerConsultant();
if (!context.mounted) return;
if (registrationConsultantCubit.state.error != null) {
return;
}
await showModalBottomSheet(
useRootNavigator: true,
showDragHandle: true,
@@ -170,27 +183,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
const Text(
'Спасибо!\nВаши данные отправлены на модерацию',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
Lottie.asset(
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
const SizedBox(height: 16),
const Text(
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
),
const SizedBox(height: 16),
InkWell(
@@ -201,18 +202,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Закрыть',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
),
),
@@ -223,7 +217,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
);
} catch (e) {
if (!context.mounted) return;
showDialog(
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
@@ -231,9 +225,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
actions: [
TextButton(
child: const Text('Ок'),
onPressed: () {
Navigator.of(context).pop();
},
onPressed: () => Navigator.of(context).pop(),
),
],
),
@@ -263,24 +255,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
} else {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
},
label: isLoading
? const CircularProgressIndicator()
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
? Icon(
Icons.arrow_forward_ios_outlined,
color: appTheme.blueA400,
)
: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
: widget.isCanceled == true
? Icon(Icons.arrow_forward_ios_outlined, color: appTheme.blueA400)
: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
],
@@ -288,23 +271,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 6'),
),
],
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 6'))],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
@@ -312,10 +283,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 6,
end: progressValue,
),
tween: Tween<double>(begin: 1 / 6, end: progressValue),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
@@ -346,11 +314,6 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
],
),
);
} else {
return const Scaffold(
body: Loader(),
);
}
},
),
);

View File

@@ -5,7 +5,6 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consulta
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:http_parser/http_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ConsultantRepository {
final ConsultantService consultantService;
@@ -13,81 +12,58 @@ class ConsultantRepository {
const ConsultantRepository({required this.consultantService});
Future<void> registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async {
try {
if (update) {
return await consultantService.consultantUpdate(consultantModel: consultant);
} else {
return await consultantService.consultantRegistration(consultantModel: consultant);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
if (update) {
return await consultantService.consultantUpdate(consultantModel: consultant);
} else {
return await consultantService.consultantRegistration(consultantModel: consultant);
}
}
Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async {
try {
return await consultantService.getConsultantRequestStatus(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await consultantService.getConsultantRequestStatus(slug: slug);
}
Future<ConsultationRequestModel> getMyRequestStatus(String slug) async {
try {
return await consultantService.getMyRequestStatus(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await consultantService.getMyRequestStatus(slug: slug);
}
Future<List<ConsultantCertPhotoModel>> getConsultantCertPhotos() async {
try {
return await consultantService.getConsultantCertPhotos();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await consultantService.getConsultantCertPhotos();
}
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({required List<File> files}) async {
try {
final multipartFiles = <MultipartFile>[];
final multipartFiles = <MultipartFile>[];
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
multipartFiles.add(multipartFile);
}
if (multipartFiles.isEmpty) {
return [];
}
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
multipartFiles.add(multipartFile);
}
if (multipartFiles.isEmpty) {
return [];
}
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
}
Future<void> hideConsultantCertPhoto(int id) async {
try {
await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
}
Future<void> unHideConsultantCertPhoto(int id) async {
try {
await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
}
Future<void> deletePhoto(int id) async {
await consultantService.deletePhoto(id: id);
}
}

View File

@@ -40,4 +40,7 @@ abstract class ConsultantService {
@POST('/api/v2/consultant_request/cert_photos/un_hide')
Future<void> unHideConsultantCertPhoto({@Body() required Map<String, dynamic> payload});
@POST('/api/v2/consultant_request/cert_photos/{id}/queue-for-deletion')
Future<void> deletePhoto({@Path() required int id});
}

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final ConsultantRatingRepository repository;
@@ -20,6 +21,7 @@ class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name);
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores));
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}
@@ -31,6 +33,7 @@ class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final scoreLogs = await repository.getScoreLogs(date: date);
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}

View File

@@ -1,7 +1,6 @@
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ConsultantRatingRepository {
final ConsultantRatingService service;
@@ -9,21 +8,13 @@ class ConsultantRatingRepository {
ConsultantRatingRepository(this.service);
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
try {
return await service.getConsultantStatistics(date: date);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await service.getConsultantStatistics(date: date);
}
Future<List<RatingModel>> getScoreLogs({String? date}) async {
try {
final data = (await service.getScoreLogs(date: date)).data;
return data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final data = (await service.getScoreLogs(date: date)).data;
return data;
}
}

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultationsCubit extends Cubit<ConsultationsState> {
final ConsultationsRepository consultationsRepository;
@@ -14,6 +15,7 @@ class ConsultationsCubit extends Cubit<ConsultationsState> {
final consultations = await consultationsRepository.getConsultations();
emit(state.copyWith(consultations: consultations, state: BaseState.success));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(state: BaseState.error));
}

View File

@@ -1,7 +1,6 @@
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
@@ -11,13 +10,8 @@ class ConsultationsRepository {
ConsultationsRepository({required this.consultationService});
Future<List<ConsultationModel>> getConsultations() async {
try {
final consultations = await consultationService.getConsultations();
return consultations.data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final consultations = await consultationService.getConsultations();
return consultations.data;
}
Future<List<ConsultationModel>> getFilteredConsultations({
@@ -26,44 +20,24 @@ class ConsultationsRepository {
FiltersComparisonEnum? stateComparison,
FiltersComparisonEnum? dateComparison,
}) async {
try {
final model = await consultationService.getFilteredConsultations(
state: state,
date: date,
stateComparison: stateComparison,
dateComparison: dateComparison,
);
return model.data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final model = await consultationService.getFilteredConsultations(
state: state,
date: date,
stateComparison: stateComparison,
dateComparison: dateComparison,
);
return model.data;
}
Future<StatisticModel> getStatistic() async {
try {
return await consultationService.getStatistics();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return consultationService.getStatistics();
}
Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getConsultantFinances(date, dateComparison);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return consultationService.getConsultantFinances(date, dateComparison);
}
Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getUserPurchases(date, dateComparison);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return consultationService.getUserPurchases(date, dateComparison);
}
}

View File

@@ -103,7 +103,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
if (state.published) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
@@ -113,7 +113,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else if (state.drafted) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
@@ -231,7 +231,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
if (dietState == DietState.draft) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
@@ -241,7 +241,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
@@ -304,7 +304,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,

View File

@@ -13,9 +13,9 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:path_provider/path_provider.dart';
class CreateDietCubit extends Cubit<CreateDietState> {
@@ -29,9 +29,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<void> setInitialData(Diet? diet) async {
if (diet == null) return;
emit(state.copyWith(isLoading: true, isDietBeingEdited: true));
dietId = diet.id;
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id);
try {
emit(state.copyWith(isLoading: true, isDietBeingEdited: true));
dietId = diet.id;
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id);
final countOfDays = diet.duration;
final List<List<List<DailySchedule>>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]);
@@ -66,7 +67,11 @@ class CreateDietCubit extends Cubit<CreateDietState> {
imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl,
),
);
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isLoading: false, isDietBeingEdited: false));
}
}
void setDetails(String text) {
@@ -112,7 +117,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
void setImage(XFile? image, {String? imageUrl}) {
@@ -204,7 +214,8 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<Diet?> getDietForPreview() async {
if (!_isFormValidForDraft()) return null;
final authorProfile = await profileRepository.getMyProfile();
try {
final authorProfile = await profileRepository.getMyProfile();
final imageBytes = (await state.dietData.image?.readAsBytes()) ?? <int>[];
String image = '';
if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) {
@@ -297,19 +308,26 @@ class CreateDietCubit extends Cubit<CreateDietState> {
isFavorite: false,
dailyNutrition: dailyNutrition,
);
return diet;
return diet;
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
}
Future<File> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
Future<File?> downloadImageAsFile(String imageUrl) async {
try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode != 200) {
throw Exception('Не удалось загрузить изображение');
}
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes);
} else {
throw Exception('Не удалось загрузить изображение');
return file.writeAsBytes(response.bodyBytes);
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
}
@@ -353,7 +371,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft));
return null;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietPublishing: false));
}
@@ -391,7 +409,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
emit(state.copyWith(isDietBeingEdited: false));
return null;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietBeingEdited: false));
return 'error';

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CreateDietRepository {
final CreateDietService createDietService;
@@ -62,48 +61,40 @@ class CreateDietRepository {
dietMap.addAll({'name': name, 'duration': duration});
final formData = FormData.fromMap(dietMap);
try {
switch (state) {
case DietState.active:
return await createDietService.createPublishedDiet(formData: formData);
case DietState.draft:
return await createDietService.createDraftedDiet(formData: formData);
case DietState.personal:
return await createDietService.createPersonalDiet(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
switch (state) {
case DietState.active:
return await createDietService.createPublishedDiet(formData: formData);
case DietState.draft:
return await createDietService.createDraftedDiet(formData: formData);
case DietState.personal:
return await createDietService.createPersonalDiet(formData: formData);
}
}
Future<Map<int, Map<int, Map<int, DailySchedule>>>> getDietPortionsNutrition(int dietId) async {
try {
final response = await createDietService.getDietPortionsNutrition(diet: dietId);
final data = response.data;
final dailySchedule = (data as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>);
final pnvid = portionMap['product_nutritional_value_id'];
if (pnvid is String) {
portionMap['product_nutritional_value_id'] = int.tryParse(pnvid);
}
return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap));
}),
);
}),
);
});
return dailySchedule;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final response = await createDietService.getDietPortionsNutrition(diet: dietId);
final data = response.data;
final dailySchedule = (data as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>);
final pnvid = portionMap['product_nutritional_value_id'];
if (pnvid is String) {
portionMap['product_nutritional_value_id'] = int.tryParse(pnvid);
}
return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap));
}),
);
}),
);
});
return dailySchedule;
}
Future<int> updateDiet({
@@ -161,23 +152,16 @@ class CreateDietRepository {
}
final formData = FormData.fromMap(dietMap);
try {
switch (state) {
case DietState.active:
final response = await createDietService.updatePublished(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.draft:
final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.personal:
final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
}
} on DioException catch (e) {
return e.response?.statusCode ?? 0;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
switch (state) {
case DietState.active:
final response = await createDietService.updatePublished(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.draft:
final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.personal:
final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
class CreatePlanCubit extends Cubit<CreatePlanState> {
@@ -457,7 +458,8 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
await createPlanRepository.createPlan(model: planMap);
emit(state.copyWith(isLoading: false, planCreated: true));
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(isLoading: false, planCreated: true));
}

View File

@@ -1,6 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CreatePlanRepository {
final CreatePlanService createPlanService;
@@ -10,13 +9,9 @@ class CreatePlanRepository {
Future<void> createPlan({
required Map<String, dynamic> model,
}) async {
try {
final formData = FormData.fromMap(model);
return await createPlanService.createPlan(formData: formData);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final formData = FormData.fromMap(model);
return await createPlanService.createPlan(formData: formData);
}
}

View File

@@ -11,7 +11,7 @@ import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
@@ -166,7 +166,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
await recipeRepository.publishRecipe(model, update, slug);
return true;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -179,7 +179,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith(drafted: true));
return true;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -204,16 +204,19 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
));
}
Future<File> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
Future<File?> downloadImageAsFile(String imageUrl) async {
try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode != 200) {
throw Exception('Не удалось загрузить изображение');
}
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes);
} else {
throw Exception('Не удалось загрузить изображение');
return file.writeAsBytes(response.bodyBytes);
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
}
@@ -222,44 +225,53 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
Future<void> recipeToEntity(Recipe recipe) async {
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}');
try {
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}');
if (file == null) {
disableLoading();
return;
}
final List<StepEntity> steps = await Future.wait(
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'),
final List<StepEntity> steps = await Future.wait(
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'),
),
),
),
);
emit(
CreateRecipeState(
entity: RecipeCreateEntity(
title: recipe.title,
typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!),
countOfPortions: recipe.servings,
energyInOnePortion: recipe.calories,
totalCookHours: recipe.cookingTime ~/ 60,
totalCookMinutes: recipe.cookingTime % 60,
activeCookHours: recipe.activeCookingTime ~/ 60,
activeCookMinutes: recipe.activeCookingTime % 60,
description: recipe.description,
image: file,
videoUrl: recipe.videoUrl,
ingredients: recipe.ingredients!
.map((e) => i.Ingredient(
name: e.name,
unit: e.unit!,
portion: e.portion,
productId: e.productId,
))
.toList(),
steps: steps,
tags: recipe.tags.map((e) => (e.name)).toList(),
);
emit(
CreateRecipeState(
entity: RecipeCreateEntity(
title: recipe.title,
typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!),
countOfPortions: recipe.servings,
energyInOnePortion: recipe.calories,
totalCookHours: recipe.cookingTime ~/ 60,
totalCookMinutes: recipe.cookingTime % 60,
activeCookHours: recipe.activeCookingTime ~/ 60,
activeCookMinutes: recipe.activeCookingTime % 60,
description: recipe.description,
image: file,
videoUrl: recipe.videoUrl,
ingredients: recipe.ingredients!
.map((e) => i.Ingredient(
name: e.name,
unit: e.unit!,
portion: e.portion,
productId: e.productId,
))
.toList(),
steps: steps,
tags: recipe.tags.map((e) => (e.name)).toList(),
),
loading: false,
),
loading: false,
),
);
);
} catch (e, st) {
await reportFrom(e, st, this);
disableLoading();
}
}
bool validateAll() {
@@ -376,13 +388,18 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
await recipeRepository.deleteRecipe(slug);
return true;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await reportFrom(e, s, this);
return false;
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
bool validateSteps() {

View File

@@ -109,7 +109,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop('active');
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
@@ -157,7 +157,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
});
if (isSuccess) {
if (!context.mounted) return;
context.pop();
context.pop('drafted');
} else {
if (!context.mounted) return;
showErrorDialog(context, title: 'Не удалось сохранить черновик');

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/create_recipe/models/product.dart';
class RecipeRepository {
@@ -16,12 +15,8 @@ class RecipeRepository {
RecipeRepository({required this.recipeService});
Future<Map<String, List<Product>>> inputProduct(String name) async {
try {
return await recipeService.getInputProduct(name);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await recipeService.getInputProduct(name);
}
Future<List> _buildStepsMap(List<Step?>? steps) async {
@@ -110,67 +105,43 @@ class RecipeRepository {
}
Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async {
try {
final formData = await _buildPublishFormData(recipe);
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.publishRecipe(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final formData = await _buildPublishFormData(recipe);
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.publishRecipe(formData: formData);
}
}
Future<void> saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async {
try {
final formData = await _buildDraftFormData(recipe);
log(formData.fields.toString());
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.createDraftedRecipe(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final formData = await _buildDraftFormData(recipe);
log(formData.fields.toString());
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.createDraftedRecipe(formData: formData);
}
}
Future<void> draftExistingRecipe(int id) async {
try {
await recipeService.draftRecipe(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await recipeService.draftRecipe(id: id);
}
Future<RecipeModel> getRecipe(String recipeID) async {
try {
return await recipeService.getRecipe(recipeID);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await recipeService.getRecipe(recipeID);
}
Future<List<RecipeTag>> getRecipesTags() async {
try {
return (await recipeService.getTags()).data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await recipeService.getTags()).data;
}
Future<void> deleteRecipe(String slug) async {
try {
return await recipeService.deleteRecipe(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await recipeService.deleteRecipe(slug: slug);
}
}

View File

@@ -240,7 +240,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
} else {
platform = 'ios';
}
await dio.post('/api/profile/notification/token/register', data: {'token': await FirebaseApi().getToken(), 'platform': platform});
await dio.post('/api/profile/notification/token/register', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен добавлен');
} catch (e) {
showInfoToast('Не удалось добавть токен');
@@ -253,7 +253,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
buttonStyle: const ButtonStyle(),
onPressed: () async {
try {
final token = await FirebaseApi().getToken();
final token = await sl<FirebaseApi>().getToken();
if (token != null) {
Clipboard.setData(ClipboardData(text: token));
showInfoToast('Токен скопирован');
@@ -278,7 +278,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
} else {
platform = 'ios';
}
await dio.post('/api/profile/notification/token/unregister', data: {'token': await FirebaseApi().getToken(), 'platform': platform});
await dio.post('/api/profile/notification/token/unregister', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен удалён');
} catch (e) {
showInfoToast('Не удалось удалить токен');

View File

@@ -36,7 +36,6 @@ import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/core/env_config.dart';
class TestResult {
final bool success;
final String? error;
@@ -57,7 +56,9 @@ class CreateRecipeTask implements TestCases {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
final recipeRepository = RecipeRepository(
recipeService: CreateRecipeService(dio, baseUrl: baseUrl),
);
await recipeRepository.publishRecipe(
const RecipeModel(
title: 'Title',

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> {
final DiaryRepository diaryRepository;
@@ -74,6 +75,7 @@ class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> {
}
await diaryRepository.commentUserDay(diaryId: diaryId, commentModel: state.commentsModel);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}

View File

@@ -19,8 +19,15 @@ import 'package:eatvio_app/features/glossary/repositories/glossary_repository.da
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class DiaryCubit extends Cubit<DiaryState> {
static const NutritionModel _zeroNutritionModel = NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0);
static const NutritionDataModel _fallbackNutrition = NutritionDataModel(
dailyNutritionIntake: _zeroNutritionModel,
dailyNutritionRecommendation: _zeroNutritionModel,
);
late final DateTime today;
late DateTime selectedDay;
int selectedMeal = 1;
@@ -47,6 +54,7 @@ class DiaryCubit extends Cubit<DiaryState> {
}
String comment = '';
bool commentNeedsToBeFilled = false;
void setInitialDate(DateTime? date) {
if (date == null) return;
@@ -68,7 +76,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return canSelect;
}
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId, DateTime? date}) async {
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
final currentState = state;
try {
if (needLoading) {
@@ -101,6 +109,7 @@ class DiaryCubit extends Cubit<DiaryState> {
emit(currentState);
return false;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
@@ -114,7 +123,8 @@ class DiaryCubit extends Cubit<DiaryState> {
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
}
}
@@ -125,6 +135,7 @@ class DiaryCubit extends Cubit<DiaryState> {
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
return diaryDay;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
if (e is DioException) {
if (e.response?.statusCode == 422) {
@@ -151,8 +162,9 @@ class DiaryCubit extends Cubit<DiaryState> {
return false;
}
emit(const DiaryState.loading());
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
try {
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
final meals = diet.dailySchedule[diaryDay.dayOfChosenDiet];
final firstMealPortions = meals?[1]?.values.map((elem) {
return PortionModel(
@@ -220,9 +232,14 @@ class DiaryCubit extends Cubit<DiaryState> {
selectedMeal: selectedMeal,
),
);
return true;
return true;
}
return false;
} catch (e, st) {
await reportFrom(e, st, this);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
}
return false;
}
Future<void> setDiary({int? diaryId, required DiaryDayEntity diaryDay}) async {
@@ -278,18 +295,31 @@ class DiaryCubit extends Cubit<DiaryState> {
),
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
}
}
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await updateCurrentDiary();
Future<bool> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
try {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await updateCurrentDiary();
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
}
Future<NutritionDataModel> getNutrition(int index) async {
return (await diaryRepository.getNutritionData(index));
try {
return (await diaryRepository.getNutritionData(index));
} catch (e, st) {
await reportFrom(e, st, this);
return _fallbackNutrition;
}
}
Future<void> addProductToMeal({
@@ -301,15 +331,23 @@ class DiaryCubit extends Cubit<DiaryState> {
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await updateCurrentDiary();
try {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
final s = state;
if (s is DiaryStateLoaded) {
emit(s.copyWith(isLoadingAddingProductInMeal: false));
}
}
}
Future<void> updateProductInMeal({
@@ -322,62 +360,95 @@ class DiaryCubit extends Cubit<DiaryState> {
int? productNutritionalValueId,
required PortionState portionState,
}) async {
if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} else {
await diaryRepository.updateProductInMeal(
diaryId: diaryId,
mealId: mealId,
portionId: portionId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
portionState: portionState,
);
try {
if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} else {
await diaryRepository.updateProductInMeal(
diaryId: diaryId,
mealId: mealId,
portionId: portionId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
portionState: portionState,
);
}
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
await updateCurrentDiary();
}
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
try {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
try {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<GlossaryModerationModel?> sendProductToModeration({
@@ -390,38 +461,60 @@ class DiaryCubit extends Cubit<DiaryState> {
try {
return await diaryRepository.sendProductToModeration(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates);
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log(e.toString(), stackTrace: stackTrace);
}
return null;
}
Future<void> fillTime({required int diaryId, required String time}) async {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await updateCurrentDiary();
try {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillWakeup({required int diaryId, required String time}) async {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await updateCurrentDiary();
try {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillRetire({required int diaryId, required String time}) async {
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await updateCurrentDiary();
try {
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await updateCurrentDiary();
try {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillMood({required int diaryId, required int mood}) async {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await updateCurrentDiary();
try {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
void fillComment(String comment) {
this.comment = comment;
commentNeedsToBeFilled = comment.isNotEmpty && comment.length < 3;
}
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
@@ -39,12 +40,12 @@ class UserDiaryScreen extends StatelessWidget {
final int diaryId;
@override
Widget build(BuildContext context) {
return DiaryScreenContent(diaryId: diaryId,);
return DiaryScreenContent(diaryId: diaryId);
}
}
class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId,this.date});
const DiaryScreenContent({super.key, this.diaryId, this.date});
final int? diaryId;
final DateTime? date;
@override
@@ -134,11 +135,14 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
setState(() {
_isDiaryFinishing = true;
});
await context.read<DiaryCubit>().finishDiaryDay(
final isFinished = await context.read<DiaryCubit>().finishDiaryDay(
diaryId: state.diary.id,
wakeUp: state.diary.wakeup.toString(),
retire: state.diary.retire.toString(),
);
if (!isFinished && context.mounted) {
showErrorDialog(context);
}
setState(() {
_isDiaryFinishing = false;
});

View File

@@ -2,6 +2,7 @@ import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -15,10 +16,14 @@ class CommentForConsultantWidget extends StatefulWidget {
class _CommentForConsultantWidgetState extends State<CommentForConsultantWidget> {
final controller = TextEditingController();
bool _commentNeedsToBeFilled = false;
@override
void initState() {
super.initState();
if (widget.comment != null) controller.text = widget.comment ?? '';
if (widget.comment != null) {
controller.text = widget.comment ?? '';
}
}
@override
@@ -41,13 +46,27 @@ class _CommentForConsultantWidgetState extends State<CommentForConsultantWidget>
CustomTextFormField(
controller: controller,
onChanged: (value) {
EasyDebounce.debounce('client-typing', const Duration(milliseconds: 500), () => context.read<DiaryCubit>().fillComment(value));
EasyDebounce.debounce(
'client-typing',
const Duration(milliseconds: 500),
() => context.read<DiaryCubit>().fillComment(value),
);
if (widget.comment == null) {
setState(() {
_commentNeedsToBeFilled = value.isNotEmpty && value.length < 3;
});
}
},
hasError: _commentNeedsToBeFilled,
hintText: 'Укажите дополнительную информацию',
minLines: 4,
maxLines: 4,
readOnly: widget.comment != null,
),
if (_commentNeedsToBeFilled) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Минимум 3 символа'),
],
],
),
),

View File

@@ -4,7 +4,9 @@ import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class FinishDayButtonWidget extends StatefulWidget {
final Future<void> Function() onPressed;
@@ -18,6 +20,14 @@ class FinishDayButtonWidget extends StatefulWidget {
class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
void _onTap() {
if (context.read<DiaryCubit>().commentNeedsToBeFilled) {
showErrorDialog(
context,
title: 'Комментарий слишком короткий',
description: 'Минимум 3 символа в комментарии для консультанта.',
);
return;
}
if (!widget.hasProducts) {
showErrorDialog(
context,

View File

@@ -257,12 +257,17 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
const SizedBox(height: 12),
InkWell(
onTap: () async {
final energy = double.tryParse(energyController.text.replaceAll(',', '.')) ?? 0;
final proteins = double.tryParse(proteinsController.text.replaceAll(',', '.')) ?? 0;
final fats = double.tryParse(fatsController.text.replaceAll(',', '.')) ?? 0;
final carbohydrates = double.tryParse(carbohydratesController.text.replaceAll(',', '.')) ?? 0;
glossary = (await widget.diaryCubit.sendProductToModeration(
name: productNameController.text,
energy: double.parse(energyController.text.replaceAll(',', '.')),
proteins: double.parse(proteinsController.text.replaceAll(',', '.')),
fats: double.parse(fatsController.text.replaceAll(',', '.')),
carbohydrates: double.parse(carbohydratesController.text.replaceAll(',', '.')),
energy: energy,
proteins: proteins,
fats: fats,
carbohydrates: carbohydrates,
))?.glossary;
setState(() {
glossaryState = GlossaryState.success;

View File

@@ -22,6 +22,11 @@ class PortionCommentWidget extends StatefulWidget {
class _PortionCommentWidgetState extends State<PortionCommentWidget> {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(PortionCommentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.portionId != oldWidget.portionId) {
@@ -57,17 +62,19 @@ class _PortionCommentWidgetState extends State<PortionCommentWidget> {
hintText: s.typePortionComment,
hasError: hasError,
onChanged: (value) {
final cubit = context.read<ConsultantCommentsCubit>();
EasyDebounce.debounce(
'client-typing',
'portion-comment-${widget.portionId}',
const Duration(milliseconds: 500),
() {
final hasPortionComment = state.commentsModel.portions.any((portion) => portion.id == widget.portionId);
final currentState = cubit.state;
final hasPortionComment = currentState.commentsModel.portions.any((portion) => portion.id == widget.portionId);
if (hasPortionComment) {
final portion = state.commentsModel.portions.firstWhere((portion) => portion.id == widget.portionId);
context.read<ConsultantCommentsCubit>().updatePortionComment(portion.copyWith(comment: value));
final portion = currentState.commentsModel.portions.firstWhere((portion) => portion.id == widget.portionId);
cubit.updatePortionComment(portion.copyWith(comment: value));
return;
}
context.read<ConsultantCommentsCubit>().addPortionComment(PortionCommentModel(id: widget.portionId, comment: value));
cubit.addPortionComment(PortionCommentModel(id: widget.portionId, comment: value));
},
);
},

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/create_day_model.dart';
@@ -9,7 +11,6 @@ import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/models/product_moderation_model.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class DiaryRepository {
final DiaryService diaryService;
@@ -19,134 +20,82 @@ class DiaryRepository {
DiaryRepository({required this.diaryService});
Future<DiaryDayModel> getDiaryByDate(String date) async {
try {
Set<int> ids = {};
final diaryDays = await getDiaryIndexByDate(date);
if (diaryDays.isEmpty) {
throw Exception('No diary entry found for $date');
}
final DiaryDayModel diaryDay = diaryDays.last;
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
PortionModel _portionWithGlossary(PortionModel portion, List<GlossaryModel> glossaries) {
final id = portion.productNutritionalValueId;
if (id == null) return portion;
final glossaryItem = glossaries.firstWhereOrNull((g) => g.id == id);
if (glossaryItem == null) return portion;
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
}
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
Future<DiaryDayModel> getDiaryByDate(String date) async {
final ids = <int>{};
final diaryDays = await getDiaryIndexByDate(date);
if (diaryDays.isEmpty) {
throw Exception('No diary entry found for $date');
}
final DiaryDayModel diaryDay = diaryDays.last;
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isEmpty) {
return diaryDay;
}
final glossary = await getGlossariesIndex(ids: ids.toList());
final glossaries = glossary.glossariesList;
final updatedMeals = diaryDay.meals.map((meal) {
final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList();
return meal.copyWith(portions: updatedPortions);
}).toList();
return diaryDay.copyWith(meals: updatedMeals);
}
Future<List<DiaryDayModel>> getDiaryIndexByDate(String date) async {
try {
return (await diaryService.getDiaryIndexByDate(date: date));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getDiaryIndexByDate(date: date);
}
Future<DiaryDayModel> getDiaryDay(int diaryId) async {
try {
// Align with getDiaryByDate: enrich portions with glossary nutritional info
final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
final DiaryDayModel diaryDay = diaryWrapper.diary;
Set<int> ids = {};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
final DiaryDayModel diaryDay = diaryWrapper.diary;
final ids = <int>{};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
if (ids.isEmpty) {
return diaryDay;
}
final glossary = await getGlossariesIndex(ids: ids.toList());
final glossaries = glossary.glossariesList;
final updatedMeals = diaryDay.meals.map((meal) {
final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList();
return meal.copyWith(portions: updatedPortions);
}).toList();
return diaryDay.copyWith(meals: updatedMeals);
}
Future<NutritionDataModel> getNutritionData(int index) async {
try {
return await diaryService.getNutrition(diary: index);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getNutrition(diary: index);
}
Future<void> addPortionToMeal({
@@ -157,19 +106,14 @@ class DiaryRepository {
required int quantity,
int? productNutritionalValueId,
}) async {
try {
return await diaryService.addProductToMeal(
diary: diaryId,
typeNo: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.addProductToMeal(
diary: diaryId,
typeNo: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
}
Future<void> updateProductInMeal({
@@ -182,83 +126,43 @@ class DiaryRepository {
int? productNutritionalValueId,
required PortionState portionState,
}) async {
try {
Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId;
data['state'] = portionState.name;
return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId;
data['state'] = portionState.name;
return diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
}
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> uploadPhotos({required int diaryId, required int mealId, required List<File> files}) async {
try {
for (int i = 0; i < files.length; i++) {
await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
for (var i = 0; i < files.length; i++) {
await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
}
}
Future<void> deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async {
try {
await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
}
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async {
try {
return await diaryService.finishDiaryDay(
diary: diaryId,
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.finishDiaryDay(
diary: diaryId,
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
);
}
Future<GlossaryModerationModel> sendProductToModeration({
@@ -268,126 +172,66 @@ class DiaryRepository {
required double fats,
required double carbohydrates,
}) async {
try {
return await diaryService.sendProductToModeration(
data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.sendProductToModeration(
data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
);
}
Future<void> fillSatiety({required int diaryId, required int mealId, required int satiety}) async {
try {
return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
}
Future<void> fillMood({required int diaryId, required int mealId, required int mood}) async {
try {
return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
}
Future<void> fillTime({required int diaryId, required int mealId, required String time}) async {
try {
return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
}
Future<void> fillWakeup({required int diaryId, required String time}) async {
try {
return await diaryService.fillWakeup(diary: diaryId, wakeUp: time);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillWakeup(diary: diaryId, wakeUp: time);
}
Future<void> fillRetire({required int diaryId, required String time}) async {
try {
return await diaryService.fillRetire(diary: diaryId, retire: time);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillRetire(diary: diaryId, retire: time);
}
Future<void> commentUserDay({required int diaryId, required ConsultantCommentModel commentModel}) async {
try {
await diaryService.commentUserDay(id: diaryId, body: commentModel);
_diaryForceFetchController.add('force');
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.commentUserDay(id: diaryId, body: commentModel);
_diaryForceFetchController.add('force');
}
Future<ConsultantReviewModel> getDiaryComments({required int planId, required int diaryId}) async {
try {
return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
}
Future<void> viewCommentOfDiaryDayByConsultant({required int diaryId}) async {
try {
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
}
Future<GlossaryPaginationModel> getGlossariesIndex({required List<int> ids}) async {
Map<String, dynamic> filters = {};
final filters = <String, dynamic>{};
if (ids.isNotEmpty) {
for (int i = 0; i < ids.length; i++) {
for (var i = 0; i < ids.length; i++) {
filters['filters[id][\$in][$i]'] = ids[i];
}
}
try {
return await diaryService.getGlossariesIndex(filters: filters);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getGlossariesIndex(filters: filters);
}
Future<DiaryDayModel> getDiaryDayV2({required String day}) async {
try {
final diaryDay = await diaryService.getDiaryDayV2(day: day);
return setCPFCToDiary(diaryDay);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final diaryDay = await diaryService.getDiaryDayV2(day: day);
return setCPFCToDiary(diaryDay);
}
Future<void> createDiaryDay({required String day}) async {
try {
await diaryService.createDay(body: CreateDayModel(day: day));
await getDiaryByDate(day);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.createDay(body: CreateDayModel(day: day));
await getDiaryByDate(day);
}
Future<DiaryDayModel> setCPFCToDiary(DiaryDayModel diaryDay) async {
Set<int> ids = {};
final ids = <int>{};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
@@ -395,41 +239,15 @@ class DiaryRepository {
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
if (ids.isEmpty) {
return diaryDay;
}
return diaryDay;
final glossary = await getGlossariesIndex(ids: ids.toList());
final glossaries = glossary.glossariesList;
final updatedMeals = diaryDay.meals.map((meal) {
final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList();
return meal.copyWith(portions: updatedPortions);
}).toList();
return diaryDay.copyWith(meals: updatedMeals);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.da
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class AboutDietCubit extends Cubit<AboutDietState> {
AboutDietCubit({required this.dietsRepository, required this.profileRepository}) : super(const AboutDietState.initial());
@@ -31,7 +32,8 @@ class AboutDietCubit extends Cubit<AboutDietState> {
dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments));
}
emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(AboutDietState.error(message: e.toString()));
}
@@ -62,7 +64,8 @@ class AboutDietCubit extends Cubit<AboutDietState> {
}
_selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0);
emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(AboutDietState.error(message: e.toString()));
}
@@ -90,7 +93,8 @@ class AboutDietCubit extends Cubit<AboutDietState> {
EasyDebounce.debounce('like_diet', const Duration(seconds: 1), () async {
try {
await dietsRepository.likeDiet(id);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
@@ -114,7 +118,8 @@ class AboutDietCubit extends Cubit<AboutDietState> {
EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async {
try {
await dietsRepository.addToFavorite(id);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (dietIndex != null) {
@@ -139,7 +144,8 @@ class AboutDietCubit extends Cubit<AboutDietState> {
EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async {
try {
await dietsRepository.removeFromFavorite(id);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (dietIndex != null) {
@@ -159,6 +165,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
await dietsRepository.startDiet(id, stringDate);
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -170,10 +177,12 @@ class AboutDietCubit extends Cubit<AboutDietState> {
final id = (state as DietStateLoaded).diet.id;
await dietsRepository.cancelDiet(id);
return true;
} on DioException catch (e) {
} on DioException catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
return false;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
return false;
}
@@ -194,7 +203,8 @@ class AboutDietCubit extends Cubit<AboutDietState> {
Future<Diet?> getDietBySlug(String slug) async {
try {
return await dietsRepository.getDietBySlug(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
}
return null;

View File

@@ -4,7 +4,6 @@ import 'package:eatvio_app/features/diets/models/diet_likes_model.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:equatable/equatable.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class DietRepository {
DietRepository({required this.dietsService});
@@ -18,76 +17,44 @@ class DietRepository {
Stream get dietCommentsState => _dietCommentsStateStreamController.stream;
Future<DietLikesModel> likeDiet(int id) async {
try {
return await dietsService.likeDiet(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await dietsService.likeDiet(id: id);
}
Future<Diet> getDiet(int id) async {
try {
final diet = await dietsService.getDiet(id: id);
return diet;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final diet = await dietsService.getDiet(id: id);
return diet;
}
Future<Diet> getDietBySlug(String slug) async {
try {
final diet = await dietsService.getDietBySlug(slug: slug);
return diet;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final diet = await dietsService.getDietBySlug(slug: slug);
return diet;
}
Future<Diet> addToFavorite(int id) async {
try {
return await dietsService.addToFavorite(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await dietsService.addToFavorite(id: id);
}
Future<Diet> removeFromFavorite(int id) async {
try {
return await dietsService.removeFromFavorite(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await dietsService.removeFromFavorite(id: id);
}
Future<void> startDiet(int id, String date) async {
try {
return await dietsService.startDiet(id: id, body: {'date': date});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await dietsService.startDiet(id: id, body: {'date': date});
}
Future<void> cancelDiet(int id) async{
try {
return await dietsService.cancelDiet(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await dietsService.cancelDiet(id: id);
}
Future<void> deleteDiet(int id) async {
try {
return await dietsService.deleteDiet(id: id);
} catch (e) {
rethrow;
}
return dietsService.deleteDiet(id: id);
}
void emitLikesStateToFeed(LikesState state) => _dietLikeStateStreamController.add(state);

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantsCubit extends Cubit<ConsultantsState> {
final FeedRepository feedRepository;
@@ -21,6 +22,7 @@ class ConsultantsCubit extends Cubit<ConsultantsState> {
),
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(ConsultantsState.error(errorMessage: e.toString()));
}

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class DietsCubit extends Cubit<DietsState> {
final FeedRepository feedRepository;
@@ -109,6 +110,7 @@ class DietsCubit extends Cubit<DietsState> {
),
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(
state.copyWith(isLoading: false, errorMessage: e.toString()),
@@ -144,7 +146,8 @@ class DietsCubit extends Cubit<DietsState> {
),
);
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
state.copyWith(isLoading: false, errorMessage: e.toString()),
);
@@ -191,7 +194,8 @@ class DietsCubit extends Cubit<DietsState> {
() async {
try {
await dietsRepository.likeDiet(id);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(
state.copyWith(
@@ -287,7 +291,8 @@ class DietsCubit extends Cubit<DietsState> {
() async {
try {
await dietsRepository.addToFavorite(id);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
@@ -327,7 +332,8 @@ class DietsCubit extends Cubit<DietsState> {
() async {
try {
await dietsRepository.removeFromFavorite(id);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
@@ -348,7 +354,8 @@ class DietsCubit extends Cubit<DietsState> {
try {
await dietsRepository.deleteDiet(dietId);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
return false;
}
}

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class FeedCubit extends Cubit<FeedState> {
final FeedRepository feedRepository;
@@ -26,6 +27,17 @@ class FeedCubit extends Cubit<FeedState> {
}
}
void incrementCommentsState(int id) {
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexWhere((e) => e.id == id);
if (index == -1) return;
final current = feedBlogPostList[index];
feedBlogPostList[index] = current.copyWith(totalComments: current.totalComments + 1);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
}
}
Future<void> loadFeedBlogPostList({String? query}) async {
try {
_query = query;
@@ -34,7 +46,8 @@ class FeedCubit extends Cubit<FeedState> {
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false));
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
}
@@ -58,7 +71,8 @@ class FeedCubit extends Cubit<FeedState> {
emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading));
}
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
}
@@ -70,14 +84,48 @@ class FeedCubit extends Cubit<FeedState> {
try {
await feedRepository.likeBlogPost(id: id);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}
Future<void> unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id: id);
Future<void> unlikedPost(int id) async {
try {
await feedRepository.unlikedBlogPost(id: id);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> viewPost(int id) async => await feedRepository.viewPost(id);
Future<void> viewRecipe(String slug) async => await feedRepository.viewRecipe(slug);
Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id);
Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug);
Future<void> viewPost(int id) async {
try {
await feedRepository.viewPost(id);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> viewRecipe(String slug) async {
try {
await feedRepository.viewRecipe(slug);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> viewDiet(int id) async {
try {
await feedRepository.viewDiet(id);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> viewPlan(String slug) async {
try {
await feedRepository.viewPlan(slug);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
}

View File

@@ -10,6 +10,7 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class PlansCubit extends Cubit<PlansState> {
final FeedRepository feedRepository;
@@ -41,6 +42,17 @@ class PlansCubit extends Cubit<PlansState> {
});
}
@override
Future<void> close() async {
EasyDebounce.cancel('loadPlans');
EasyDebounce.cancel('add_or_remove_plan_from_favorite');
EasyDebounce.cancel('like_plan');
likesStateSubscription.cancel();
favoriteStateSubscription.cancel();
commentsStateSubscription.cancel();
await super.close();
}
void changeDurationFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null));
EasyDebounce.debounce(
@@ -168,6 +180,7 @@ class PlansCubit extends Cubit<PlansState> {
),
);
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log(e.toString());
log(stackTrace.toString());
emit(state.copyWith(isLoading: isLoading, errorMessage: e.toString()));
@@ -199,7 +212,8 @@ class PlansCubit extends Cubit<PlansState> {
),
);
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(errorMessage: e.toString()));
}
}
@@ -228,7 +242,8 @@ class PlansCubit extends Cubit<PlansState> {
() async {
try {
await planRepository.addToFavorite(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
@@ -268,7 +283,8 @@ class PlansCubit extends Cubit<PlansState> {
() async {
try {
await planRepository.removeFromFavorite(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
@@ -324,7 +340,8 @@ class PlansCubit extends Cubit<PlansState> {
() async {
try {
await planRepository.likePlan(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(
state.copyWith(

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class FeedProfileCubit extends Cubit<ProfileState> {
final ProfileRepository profileRepository;
@@ -13,6 +14,7 @@ class FeedProfileCubit extends Cubit<ProfileState> {
final profile = await profileRepository.getMyProfile();
emit(state.copyWith(profile: profile));
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log(e.toString(), stackTrace: stackTrace);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'dart:developer';
class RecipesCubit extends Cubit<RecipesState> {
@@ -62,6 +63,7 @@ class RecipesCubit extends Cubit<RecipesState> {
);
lastPage = currentPage;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(errorMessage: e.toString(), isLoading: false));
}
@@ -93,7 +95,8 @@ class RecipesCubit extends Cubit<RecipesState> {
);
}
lastPage = currentPage;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(errorMessage: e.toString(), isLoading: false));
}
}
@@ -132,7 +135,8 @@ class RecipesCubit extends Cubit<RecipesState> {
() async {
try {
await aboutRecipeRepository.addToFavorite(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
@@ -172,7 +176,8 @@ class RecipesCubit extends Cubit<RecipesState> {
() async {
try {
await aboutRecipeRepository.removeFromFavorite(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
@@ -228,7 +233,8 @@ class RecipesCubit extends Cubit<RecipesState> {
() async {
try {
await aboutRecipeRepository.likeRecipe(slug);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(
state.copyWith(
@@ -305,7 +311,8 @@ class RecipesCubit extends Cubit<RecipesState> {
try {
await aboutRecipeRepository.deleteRecipe(slug);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
return false;
}
}

View File

@@ -32,8 +32,8 @@ import 'package:go_router/go_router.dart';
import '../widgets/recipes.dart';
class FeedScreen extends StatelessWidget {
const FeedScreen({super.key});
const FeedScreen({super.key, this.initialTabIndex = 0});
final int? initialTabIndex;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -44,14 +44,14 @@ class FeedScreen extends StatelessWidget {
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
],
child: const FeedScreenContent(),
child: FeedScreenContent(initialTabIndex: initialTabIndex),
);
}
}
class FeedScreenContent extends StatefulWidget {
const FeedScreenContent({super.key});
const FeedScreenContent({super.key, required this.initialTabIndex});
final int? initialTabIndex;
@override
State<FeedScreenContent> createState() => _FeedScreenContentState();
}
@@ -64,7 +64,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsController = TextEditingController();
late final List<String> chipsLabelList;
int _selectedChipIndex = 0;
late int _selectedChipIndex;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
final dietsScrollController = ScrollController();
@@ -72,6 +72,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsScrollController = ScrollController();
@override
void initState() {
_selectedChipIndex = widget.initialTabIndex ?? 0;
chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты'];
consultantsScrollController.addListener(_onScrollConsultants);
_checkRegistrationDate(context);
@@ -88,6 +89,17 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
super.initState();
}
@override
void didUpdateWidget(FeedScreenContent oldWidget) {
super.didUpdateWidget(oldWidget);
final initialTabIndex = widget.initialTabIndex;
setState(() {
if (initialTabIndex != null) {
_selectedChipIndex = initialTabIndex;
}
});
}
void _onScrollConsultants() {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
}

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class FeedRepository {
final FeedService feedService;
@@ -15,191 +14,107 @@ class FeedRepository {
FeedRepository({required this.feedService});
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({String? query, int page = 1}) async {
try {
return (await feedService.getBlogPostList(page: page, query: query));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getBlogPostList(page: page, query: query));
}
Future<FeedBlogPostModel> getBlogPost({required int id}) async {
try {
return await feedService.getBlogPost(postId: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await feedService.getBlogPost(postId: id);
}
Future<List<Recipe>> getRecipes({String? query, List<int>? tags, int page = 1}) async {
try {
return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList;
}
Future<DietsPagination> getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async {
try {
return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting));
}
Future<PlanPaginationModel> getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async {
try {
return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice));
}
Future<CommentModel> getBlogPostComments({required String id}) async {
try {
return (await feedService.getBlogPostComments(id: id));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getBlogPostComments(id: id));
}
Future<void> sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId);
}
Future<CommentModel> getDietsComments({required String id}) async {
try {
return (await feedService.getDietsComments(id: id));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getDietsComments(id: id));
}
Future<void> sendDietsComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId);
}
Future<CommentModel> getRecipesComments({required String id}) async {
try {
return (await feedService.getRecipesComments(id: id));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getRecipesComments(id: id));
}
Future<void> sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId);
}
Future<CommentModel> getPlansComments({required String slug}) async {
try {
return (await feedService.getPlansComments(slug: slug));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getPlansComments(slug: slug));
}
Future<void> sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId);
}
Future<void> likeBlogPost({required int id}) async {
try {
await feedService.likeBlogPost(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.likeBlogPost(id: id);
}
Future<void> unlikedBlogPost({required int id}) async {
try {
await feedService.unlikedBlogPost(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.unlikedBlogPost(id: id);
}
Future<List<FeedConsultantModel>> getConsultants([String? name]) async {
try {
return (await feedService.getConsultants(name: name)).data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.getConsultants(name: name)).data;
}
Future<void> viewPost(int id) async {
try {
return (await feedService.viewPost(id: id));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.viewPost(id: id));
}
Future<void> viewRecipe(String slug) async {
try {
return (await feedService.viewRecipe(slug: slug));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.viewRecipe(slug: slug));
}
Future<void> viewDiet(int id) async {
try {
return (await feedService.viewDiet(id: id));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.viewDiet(id: id));
}
Future<void> viewPlan(String slug) async {
try {
return (await feedService.viewPlan(slug: slug));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await feedService.viewPlan(slug: slug));
}
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await feedService.viewContent(viewModel: viewModel);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await feedService.viewContent(viewModel: viewModel);
}
}

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/arc
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
final ConsultationsRepository consultationsRepository;
@@ -34,7 +35,8 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
: FiltersComparisonEnum.equal,
);
emit(state.copyWith(consultations: consultations, isLoading: false));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isLoading: false));
log(e.toString());
}
@@ -55,7 +57,8 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
emit(state.copyWith(isLoading: true));
final statistic = await consultationsRepository.getStatistic();
emit(state.copyWith(statisticModel: statistic, isLoading: false));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isLoading: false));
log(e.toString());
}

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantFinancesCubit extends Cubit<ConsultantFinancesState> {
final ConsultationsRepository consultationsRepository;
@@ -22,7 +23,8 @@ class ConsultantFinancesCubit extends Cubit<ConsultantFinancesState> {
state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
);
emit(state.copyWith(financesModels: consultantFinances, isLoading: false));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(financesModels: [], isLoading: false));
log(e.toString());
}

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class UserPurchasesCubit extends Cubit<UserPurchasesState> {
final ConsultationsRepository consultationsRepository;
@@ -23,7 +24,8 @@ class UserPurchasesCubit extends Cubit<UserPurchasesState> {
);
emit(state.copyWith(purchasesModels: purchasesModels, isLoading: false));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(purchasesModels: [], isLoading: false));
log(e.toString());
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class GlossaryRepository {
final GlossaryService glossaryService;
@@ -10,11 +9,7 @@ class GlossaryRepository {
});
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
try {
return (await glossaryService.getGlossaryByQuery(query: query)).glossariesList;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await glossaryService.getGlossaryByQuery(query: query)).glossariesList;
}
}

View File

@@ -3,16 +3,30 @@ import 'dart:developer';
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class MenuCubit extends Cubit<MenuState> {
final ProfileRepository profileRepository;
MenuCubit(this.profileRepository) : super(MenuState());
MenuCubit(this.profileRepository) : super(const MenuState.initial());
Future<void> getMyProfile() async {
emit(const MenuState.loading());
try {
final profile = await profileRepository.getMyProfile();
emit(MenuState.loaded(profileModel: profile));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(const MenuState.error());
}
}
Future<String?> makePassKey() async {
try {
final passKey = await profileRepository.getPassKey(routeName: 'product_nutritional_value.index', routeParams: {});
return passKey;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
}
return null;

View File

@@ -1 +1,17 @@
class MenuState {}
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'menu_state.freezed.dart';
@freezed
abstract class MenuState with _$MenuState {
const factory MenuState.initial() = MenuStateInitial;
const factory MenuState.loading() = MenuStateLoading;
const factory MenuState.loaded({
required ProfileModel profileModel,
}) = MenuStateLoaded;
const factory MenuState.error() = MenuStateError;
}

View File

@@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
@@ -16,6 +17,7 @@ class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
final notificationSettings = await _notificationSettingsRepository.getNotificationSettings();
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log('Error getting notification settings: $e', stackTrace: stackTrace);
emit(NotificationSettingsStateError(errorMessage: e.toString()));
}
@@ -26,6 +28,7 @@ class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data);
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log('Error updating notification settings: $e', stackTrace: stackTrace);
emit(NotificationSettingsStateError(errorMessage: e.toString()));
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class BlockedUsersCubit extends Cubit<BlockedUsersState> {
final BlockedUsersRepository blockedUsersRepository;
@@ -13,17 +14,26 @@ class BlockedUsersCubit extends Cubit<BlockedUsersState> {
try {
emit(BlockedUsersState.loaded(profilesList: await blockedUsersRepository.getBlockedUsers()));
} catch (e, st) {
await reportFrom(e, st, this);
emit(BlockedUsersState.error(errorMessage: '$e $st'));
}
}
Future<void> blockUser(String slug) async {
await blockedUsersRepository.blockUserBySlug(slug);
await getBlockedUsers();
try {
await blockedUsersRepository.blockUserBySlug(slug);
await getBlockedUsers();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> unblockUser(String slug) async {
await blockedUsersRepository.unblockUserBySlug(slug);
await getBlockedUsers();
try {
await blockedUsersRepository.unblockUserBySlug(slug);
await getBlockedUsers();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
}

View File

@@ -1,11 +1,13 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
@@ -13,9 +15,9 @@ import 'package:eatvio_app/features/consultant_rating/presentation/consultant_ra
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -25,90 +27,107 @@ import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
class MenuScreen extends StatelessWidget {
final ProfileModel profileModel;
const MenuScreen({super.key, required this.profileModel});
const MenuScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<MenuCubit>(),
create: (context) => sl<MenuCubit>()..getMyProfile(),
child: Builder(
builder: (context) {
return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('Меню')),
body: SingleChildScrollView(
child: Column(
children: [
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
if (!hidePlans) ...[
_divider(context),
_tile(
leading: 'assets/icons/finance.svg',
title: 'Мои финансы',
onTap: () {
if (profileModel.roles.contains('consultant')) {
GoRouter.of(context).push(ConsultantFinancesScreen.path);
return;
}
GoRouter.of(context).push(UserPurchasesScreen.path);
},
return BlocBuilder<MenuCubit, MenuState>(
builder: (BuildContext context, MenuState state) {
if (state is MenuStateLoaded) {
final profileModel = state.profileModel;
return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('Меню')),
body: SingleChildScrollView(
child: Column(
children: [
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
if (!hidePlans) ...[
_divider(context),
_tile(
leading: 'assets/icons/finance.svg',
title: 'Мои финансы',
onTap: () {
if (profileModel.roles.contains('consultant')) {
GoRouter.of(context).push(ConsultantFinancesScreen.path);
return;
}
GoRouter.of(context).push(UserPurchasesScreen.path);
},
),
],
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: 'assets/icons/consultant_rating_icon.svg',
title: 'Мой рейтинг',
onTap: () {
GoRouter.of(context).push(ConsultantRatingScreen.path);
},
),
_divider(context),
_tile(
leading: ImageConstant.icnGlossary,
title: 'Глоссарий',
onTap: () async {
final passKey = await context.read<MenuCubit>().makePassKey();
if (passKey != null && context.mounted) {
GoRouter.of(context).push('/glossary-screen', extra: passKey);
}
},
),
_divider(context),
_tile(
leading: 'assets/icons/notifications.svg',
title: 'Уведомления',
onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen'),
),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context),
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
// _divider(context),
// _tile(
// leading: ImageConstant.icnDevTools,
// title: 'Для разработчиков',
// onTap: () {
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
// },
// ),
_divider(context),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
const SizedBox(height: 10),
const AppInfo(),
],
),
],
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: 'assets/icons/consultant_rating_icon.svg',
title: 'Мой рейтинг',
onTap: () {
GoRouter.of(context).push(ConsultantRatingScreen.path);
},
),
_divider(context),
_tile(
leading: ImageConstant.icnGlossary,
title: 'Глоссарий',
onTap: () async {
final passKey = await context.read<MenuCubit>().makePassKey();
if (passKey != null && context.mounted) {
GoRouter.of(context).push('/glossary-screen', extra: passKey);
}
},
),
_divider(context),
_tile(
leading: 'assets/icons/notifications.svg',
title: 'Уведомления',
onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen'),
),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context),
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
// _divider(context),
// _tile(
// leading: ImageConstant.icnDevTools,
// title: 'Для разработчиков',
// onTap: () {
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
// },
// ),
_divider(context),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
const SizedBox(height: 10),
const AppInfo(),
],
),
),
);
}
if (state is MenuStateError) {
return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('Меню')),
body: DataErrorWidget(onReload: () async => context.read<MenuCubit>().getMyProfile()),
);
}
return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('Меню')),
body: const Loader(),
);
},
);
},
),
@@ -144,7 +163,7 @@ class MenuScreen extends StatelessWidget {
try {
await sl<Dio>().post('${sl<AppConfig>().baseUrl}/api/profile/logout');
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'MenuScreen');
} finally {
await sl<WsManager>().clear();
await TokenStorage.removeTokenInStorage();

View File

@@ -1,7 +1,5 @@
import 'dart:developer';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BlockedUsersRepository {
final BlockedUserService blockedUserService;
@@ -11,40 +9,25 @@ class BlockedUsersRepository {
});
Future<List<ProfileModel>> getBlockedUsers() async {
try {
return (await blockedUserService.getBlockedUsers()).profilesList;
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
return (await blockedUserService.getBlockedUsers()).profilesList;
}
Future<void> blockUserBySlug(String slug) async {
try {
return await blockedUserService.blockUser(
data: {
'slug': slug,
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
return await blockedUserService.blockUser(
data: {
'slug': slug,
},
);
}
Future<void> unblockUserBySlug(String slug) async {
try {
return await blockedUserService.unblockUser(
data: {
'slug': slug,
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
return await blockedUserService.unblockUser(
data: {
'slug': slug,
},
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class NotificationSettingsRepository {
final NotificationSettingsService _notificationSettingsService;
@@ -8,20 +7,12 @@ class NotificationSettingsRepository {
NotificationSettingsRepository(this._notificationSettingsService);
Future<NotificationSettingsModel> getNotificationSettings() async {
try {
return await _notificationSettingsService.getNotificationSettings();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await _notificationSettingsService.getNotificationSettings();
}
Future<NotificationSettingsModel> updateNotificationSettings(Map<String, dynamic> data) async {
try {
return await _notificationSettingsService.updateNotificationSettings(data: data);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await _notificationSettingsService.updateNotificationSettings(data: data);
}
}

Some files were not shown because too many files have changed in this diff Show More