Compare commits

..

62 Commits

Author SHA1 Message Date
Username1548
26ff548752 changed base url 2025-07-07 19:55:16 +07:00
Username1548
43e9098d8d added date parsing for reminder 2025-07-07 14:16:23 +07:00
mso7nueh
ad538472f0 Merge pull request 'feat(qrcode): Issue-515' (#544) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#544
2025-07-02 13:42:10 +00:00
mso7nueh
49827e979e feat(qrcode): Issue-515 2025-07-02 16:41:49 +03:00
mso7nueh
24cff8b4e9 Merge pull request 'feat(deepLinks)' (#518) from misha/eatvio_app:deepLinks into main
Reviewed-on: eatvio/eatvio_app#518
2025-07-02 12:23:16 +00:00
Username1548
be5a858c73 Merge branch 'main' into deepLinks 2025-07-02 19:06:04 +07:00
Username1548
e80b660210 fix: fixed analyzer issues and merge conflicts 2025-07-02 19:01:37 +07:00
Username1548
d42604dc87 Merge branch 'main' into deepLinks 2025-07-02 19:00:49 +07:00
mso7nueh
4bed103cec Merge pull request 'fix: analyzer' (#543) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#543
2025-07-02 11:47:19 +00:00
mso7nueh
8d429862dd fix: analyzer 2025-07-02 14:46:54 +03:00
mso7nueh
dd90f62286 Merge pull request 'fix: go router ver' (#542) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#542
2025-07-02 11:39:21 +00:00
mso7nueh
7809624186 fix: go router ver 2025-07-02 14:39:08 +03:00
mso7nueh
7c131f8970 Merge pull request 'fix(descriptions): Issue-540' (#541) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#541
2025-07-02 11:33:53 +00:00
mso7nueh
0926b50537 fix(descriptions): Issue-540 2025-07-02 14:33:12 +03:00
Username1548
3e96fdcfc2 fix(deepLinks): fixed gray screen problem on android 2025-07-02 17:46:41 +07:00
mso7nueh
3ce6caa0af Merge pull request 'feat(test): Issue-165' (#539) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#539
2025-07-02 07:54:04 +00:00
mso7nueh
58855b7f11 feat(test): Issue-165 2025-07-01 16:53:11 +03:00
mso7nueh
f0d5dacc00 Merge pull request 'feat(blackList): Issue-523' (#537) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#537
2025-06-30 12:34:49 +00:00
mso7nueh
86199d08f0 fix: build error sentry 2025-06-30 15:00:43 +03:00
mso7nueh
1801324fb5 fix: build error 2025-06-30 14:45:06 +03:00
mso7nueh
d4a05f47d1 feat(blackList): Issue-523 2025-06-30 14:27:10 +03:00
mso7nueh
300a708dfc Merge pull request 'fix(chat): multi images' (#535) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#535
2025-06-27 13:48:59 +00:00
mso7nueh
693260afb6 fix(chat): multi images 2025-06-27 16:48:12 +03:00
mso7nueh
9b4328a2ee Merge pull request 'feat(chat): add chat images/files preview' (#533) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#533
2025-06-27 12:07:41 +00:00
mso7nueh
591a8a597d feat(chat): add chat images/files preview 2025-06-27 15:06:46 +03:00
Username1548
bc660e2aff removed processing from info plist 2025-06-27 16:18:50 +07:00
Username1548
d129687fc6 changed build version and number 2025-06-27 15:58:27 +07:00
mso7nueh
2e5e1b3af4 Merge pull request 'Issue-440: chat(s) screen' (#530) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#530
2025-06-26 13:12:38 +00:00
mso7nueh
c103d214c4 feat(testflight): new version 2025-06-26 16:11:31 +03:00
mso7nueh
9946abc0be feat(chat): add refresh when new message 2025-06-26 16:10:34 +03:00
mso7nueh
87bf277248 feat(chat): add refresh in chats screen 2025-06-26 15:49:06 +03:00
Username1548
23a606bfd4 merged main into deepLinks 2025-06-25 22:55:58 +07:00
Username1548
5f57bded51 added deeplink for diets 2025-06-25 22:43:29 +07:00
mso7nueh
535dd595b7 Merge pull request 'fix: black screen Issue-514' (#528) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#528
2025-06-25 12:38:06 +00:00
mso7nueh
80afcd8949 upd ver to 32 2025-06-25 15:36:27 +03:00
mso7nueh
8c7c7f51cf fix: auth 2025-06-25 15:32:37 +03:00
mso7nueh
9c168d37ce fix: black screen 2025-06-25 14:56:59 +03:00
mso7nueh
55bc361b05 fix(qr+menu): text 2025-06-25 14:47:49 +03:00
mso7nueh
d14fbbb8d3 Merge pull request 'feat(chat+qr+devtools): fix chat first message, add pick images from gallery, qrcode, devtools' (#526) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#526
2025-06-25 09:24:28 +00:00
mso7nueh
7530270a2e feat(qr+devtools): scan qr code and dev tools 2025-06-25 12:22:14 +03:00
Username1548
afc36e6b63 feat(deepLinks): added domains for ios 2025-06-25 14:15:34 +07:00
mso7nueh
f7e40e654d merge upstream 2025-06-24 17:28:49 +00:00
Username1548
aa78d385a2 feat(deeplinks): added backwards novigation for contant that was oppened via deeplinks, added routing to plans 2025-06-24 03:18:03 +07:00
mso7nueh
75fa9ebc5c feat(chat): fix chat first message, add pick images from gallery 2025-06-23 19:46:23 +03:00
Username1548
105c7cb462 added deep link for blog posts 2025-06-23 13:30:32 +07:00
mso7nueh
c3647288a6 Merge pull request 'fix(chat): change sendChatMessage method' (#524) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#524
2025-06-22 15:29:47 +00:00
mso7nueh
85d67ba9c4 fix(chat): change sendChatMessage method 2025-06-22 18:27:25 +03:00
mso7nueh
15ffb4a3ec Merge pull request 'feat(testflight): update version' (#521) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#521
2025-06-20 12:46:11 +00:00
mso7nueh
2a705fdb66 feat(testflight): update version 2025-06-20 15:45:50 +03:00
mso7nueh
41ca97d030 Merge pull request 'feat(auth): reset password' (#520) from mso7nueh/eatvio_app:Issue-497 into main
Reviewed-on: eatvio/eatvio_app#520
2025-06-20 11:14:49 +00:00
mso7nueh
84b88282ad feat(auth): reset password 2025-06-20 14:12:54 +03:00
mso7nueh
84d52de935 Merge pull request 'fix(chat): Issue-440 chat v2' (#519) from mso7nueh/eatvio_app:Issue-440 into main
Reviewed-on: eatvio/eatvio_app#519
2025-06-20 08:48:07 +00:00
mso7nueh
fee92801b8 Merge pull request 'Issue-514 and Issue-497' (#516) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#516
2025-06-20 08:41:24 +00:00
mso7nueh
3b25e30b41 fix(chat): Issue-440 chat v2 2025-06-20 11:38:31 +03:00
Username1548
6aef2a9a08 feat(deepLinks): fixed gitignore 2025-06-20 06:41:39 +07:00
Username1548
6a2525dbfd feat(deepLinks): Remove Xcode build cache from repo and add to .gitignore 2025-06-20 06:41:16 +07:00
Username1548
21ce0fc9f8 feat(deepLinks): added deep link for android and implemented recipes links handling 2025-06-20 06:36:47 +07:00
mso7nueh
b0c29345a2 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/main.dart
2025-06-18 16:57:22 +03:00
mso7nueh
7682859d32 fix: Issue-514 BlackScreens + info for apple 2025-06-18 16:56:42 +03:00
mso7nueh
d0426f6e1e merge upstream 2025-06-18 06:56:09 +00:00
mso7nueh
ca296040f2 fix: Issue-514 and Issue-497 2025-06-18 09:55:33 +03:00
mso7nueh
0581da038a feat(chat): Issue-440 add photo/file picker, fix black screen 2025-06-16 17:33:17 +03:00
153 changed files with 3988 additions and 1846 deletions

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ] on: [ pull_request ]
env: env:
flutter_version: "3.32.0" flutter_version: "3.32.5"
java_version: "21.x" java_version: "21.x"
flutter_channel: "stable" flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/" my_local_cache_dir: "/opt/hostedtoolcache/"

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@
/android/app/.cxx /android/app/.cxx
*.g.dart *.g.dart
*.freezed.dart *.freezed.dart
android/build/
ios/XCBuildData/

View File

@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
namespace = "com.eatvioapp.app" namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973" ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_21 sourceCompatibility = JavaVersion.VERSION_21

View File

@@ -5,6 +5,7 @@
<application <application
android:label="Eatvio" android:label="Eatvio"
android:usesCleartextTraffic="true"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
@@ -25,8 +26,30 @@
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link intent -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="eatvio.ru" />
<data android:host="www.eatvio.ru" />
<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/.*" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="19.633818mm"
height="19.633816mm"
viewBox="0 0 19.633818 19.633816"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-64.066064,-72.435122)">
<g
id="g2">
<path
id="path1"
style="fill-opacity:0;stroke:#2f80ed;stroke-width:1.065"
d="m 83.167381,82.252022 a 9.2844086,9.2844086 0 0 1 -9.284408,9.284408 9.2844086,9.2844086 0 0 1 -9.284409,-9.284408 9.2844086,9.2844086 0 0 1 9.284409,-9.284409 9.2844086,9.2844086 0 0 1 9.284408,9.284409 z" />
<path
style="fill:#000000;fill-opacity:0;stroke:#2f80ed;stroke-width:1.065;stroke-dasharray:none;stroke-opacity:1"
d="M 68.517124,89.22945 79.520546,75.54452"
id="path2" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 27; CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>27</string> <string>39</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
@@ -31,6 +31,8 @@
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSAppleMusicUsageDescription</key>
<string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string> <string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>

View File

@@ -4,5 +4,10 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:eatvio.ru</string>
<string>applinks:www.eatvio.ru</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -7,6 +7,8 @@ import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart';
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
@@ -15,6 +17,7 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/b
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
@@ -22,6 +25,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; 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/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -42,7 +46,6 @@ import 'package:eatvio_app/features/finances_and_consultations_archive/presentat
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
@@ -67,10 +70,12 @@ import 'package:eatvio_app/features/user_details/presentation/user_details_scree
import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'dart:developer';
class AppRouter { class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@@ -78,6 +83,30 @@ class AppRouter {
static GoRouter appRouter = GoRouter( static GoRouter appRouter = GoRouter(
initialLocation: '/update-screen', initialLocation: '/update-screen',
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
redirect: (context, state) {
final segments = state.uri.pathSegments;
if (segments.length >= 3) {
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) {
case 'recipe':
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet':
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
}
}
return null;
},
routes: [ routes: [
GoRoute( GoRoute(
path: '/auth-screen', path: '/auth-screen',
@@ -89,6 +118,16 @@ class AppRouter {
name: 'registrationScreen', name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(), builder: (context, state) => const RegistrationScreen(),
), ),
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => const ForgetPasswordScreen(),
),
GoRoute(
path: 'scan-qr-code-screen',
name: 'scanQRCodeScreen',
builder: (context, state) => const ScanQRCodeScreen(),
),
], ],
), ),
ShellRoute( ShellRoute(
@@ -117,10 +156,10 @@ class AppRouter {
redirect: (context, state) async { redirect: (context, state) async {
try { try {
await sl<WsManager>().initialize(); await sl<WsManager>().initialize();
} catch (e) { } catch (e, st) {
rethrow; log(e.toString(), stackTrace: st);
} }
return '/feed-screen'; return null;
}, },
), ),
GoRoute( GoRoute(
@@ -155,6 +194,11 @@ class AppRouter {
), ),
], ],
), ),
GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute( GoRoute(
path: '/welcome-screen', path: '/welcome-screen',
name: 'welcomeScreen', name: 'welcomeScreen',
@@ -168,22 +212,24 @@ class AppRouter {
await sl<Dio>().post( await sl<Dio>().post(
'/api/profile/notification/token/register', '/api/profile/notification/token/register',
data: { data: {
'platform': Platform.isIOS ? 'ios': 'android', 'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken, 'token': fbToken,
}, },
); );
} }
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return '/feed-screen';
} else {
return '/auth-screen';
}
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
return '/feed-screen';
} else {
return null;
}
}, },
), ),
GoRoute( GoRoute(
@@ -191,14 +237,18 @@ class AppRouter {
name: 'updateScreen', name: 'updateScreen',
builder: (context, state) => const UpdateScreen(), builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async { redirect: (context, state) async {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data; try {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
String version = packageInfo.version; PackageInfo packageInfo = await PackageInfo.fromPlatform();
Version currentVersion = Version.parse(version); String version = packageInfo.version;
Version latestVersion = Version.parse(versions['min_version']); Version currentVersion = Version.parse(version);
if (latestVersion > currentVersion) { Version latestVersion = Version.parse(versions['min_version']);
return null; if (latestVersion > currentVersion) {
} else { return null;
} else {
return '/welcome-screen';
}
} catch (e) {
return '/welcome-screen'; return '/welcome-screen';
} }
}, },
@@ -235,8 +285,17 @@ class AppRouter {
path: '/about-plan-screen', path: '/about-plan-screen',
name: 'aboutPlanScreen', name: 'aboutPlanScreen',
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutPlanScreen(
slug: slug,
navigateToComments: false,
index: null,
);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen(plan: data['plan'] as PlanModel, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
}, },
), ),
GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
@@ -244,9 +303,18 @@ class AppRouter {
path: '/about-recipes-screen', path: '/about-recipes-screen',
name: 'aboutRecipesScreen', name: 'aboutRecipesScreen',
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutRecipeScreen(
recipeSlug: slug,
navigateToComments: false,
index: null,
);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen( return AboutRecipeScreen(
recipe: data['recipe'] as Recipe, recipeSlug: data['recipe'] as String,
index: data['index'] as int?, index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool, navigateToComments: data['navigateToComments'] as bool,
); );
@@ -269,8 +337,16 @@ class AppRouter {
path: '/about-diet-screen', path: '/about-diet-screen',
name: 'aboutDietScreen', name: 'aboutDietScreen',
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutDietScreen(
slug: slug,
index: null,
navigateToComments: false,
);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(diet: data['diet'] as Diet, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
}, },
), ),
GoRoute( GoRoute(
@@ -278,7 +354,20 @@ class AppRouter {
name: 'blogPostCreateScreen', name: 'blogPostCreateScreen',
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
), ),
GoRoute(path: '/blog-post-screen', name: 'blogPostScreen', builder: (context, state) => BlogPostScreen(blogPost: state.extra as FeedBlogPostModel)), GoRoute(
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(
id: parsedId,
);
}
return BlogPostScreen(id: state.extra as int);
}),
GoRoute( GoRoute(
path: '/chat-screen', path: '/chat-screen',
name: 'chatScreen', name: 'chatScreen',
@@ -292,6 +381,11 @@ class AppRouter {
); );
}, },
), ),
GoRoute(
path: '/empty-chat-screen',
name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
),
GoRoute( GoRoute(
path: '/settings-screen', path: '/settings-screen',
name: 'settingsScreen', name: 'settingsScreen',

View File

@@ -20,12 +20,12 @@ abstract class CredentialsStorage {
}; };
} }
static saveCredentials(String email, String password) async { static Future<void> saveCredentials(String email, String password) async {
await const FlutterSecureStorage().write(key: 'email', value: email); await const FlutterSecureStorage().write(key: 'email', value: email);
await const FlutterSecureStorage().write(key: 'password', value: password); await const FlutterSecureStorage().write(key: 'password', value: password);
} }
static removeCredentials() async { static Future<void> removeCredentials() async {
await const FlutterSecureStorage().delete(key: 'email'); await const FlutterSecureStorage().delete(key: 'email');
await const FlutterSecureStorage().delete(key: 'password'); await const FlutterSecureStorage().delete(key: 'password');
} }

View File

@@ -7,11 +7,25 @@ abstract class ImageCompressUtility {
static Future<List<File>> compressFilesList(List<File> files) async { static Future<List<File>> compressFilesList(List<File> files) async {
final List<File> compressedFilesList = []; final List<File> compressedFilesList = [];
for (int i = 0; i < files.length; i++) { for (int i = 0; i < files.length; i++) {
compressedFilesList.add(await compressFile(files[i])); if (_isImageFile(files[i].path)) {
compressedFilesList.add(await compressFile(files[i]));
} else {
compressedFilesList.add(files[i]);
}
} }
return compressedFilesList; return compressedFilesList;
} }
static bool _isImageFile(String path) {
final lower = path.toLowerCase();
return lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.gif') ||
lower.endsWith('.bmp') ||
lower.endsWith('.webp');
}
static Future<File> compressFile(File file) async { static Future<File> compressFile(File file) async {
final image = img.decodeImage(file.readAsBytesSync())!; final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width; int width = image.width;

View File

@@ -5,18 +5,19 @@ import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
@@ -36,6 +37,9 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -161,7 +165,9 @@ Future<void> init() async {
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl())); sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl())); sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl())); sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl())); sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
// Repository // Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl())); sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -190,7 +196,8 @@ Future<void> init() async {
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl())); sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl())); sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl())); sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl())); sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
// Services // Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl())); sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -219,9 +226,10 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl())); sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl())); sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl())); sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl())); sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl())); sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
// External // External
sl.registerLazySingleton( sl.registerLazySingleton(
@@ -232,6 +240,7 @@ Future<void> init() async {
'accept': 'application/json', 'accept': 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
}, },
validateStatus: (status) => status != null && status < 500,
), ),
)..interceptors.addAll( )..interceptors.addAll(
[ [

View File

@@ -7,162 +7,162 @@ import 'package:flutter/material.dart';
class CustomTextStyles { class CustomTextStyles {
// Headline style // Headline style
static get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish; static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
// Title text style // Title text style
static get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith( static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
static get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith( static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900, color: appTheme.black900,
); );
static get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith( static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900, color: appTheme.black900,
fontSize: 13, fontSize: 13,
); );
static get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith( static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black, color: appTheme.black,
); );
static get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith( static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black, color: appTheme.black,
); );
static get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith( static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black, color: appTheme.black,
); );
static get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith( static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.whiteA700, color: appTheme.whiteA700,
); );
static get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black900, color: appTheme.black900,
fontSize: 12, fontSize: 12,
); );
static get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black, color: appTheme.black,
fontSize: 12, fontSize: 12,
); );
static get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black, color: appTheme.black,
fontSize: 12, fontSize: 12,
); );
static get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black, color: appTheme.black,
fontSize: 12, fontSize: 12,
); );
static get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.blueGray900, color: appTheme.blueGray900,
fontSize: 12, fontSize: 12,
); );
static get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.gray600, color: appTheme.gray600,
); );
static get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith( static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
fontSize: 12, fontSize: 12,
); );
static get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith( static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
color: appTheme.black900, color: appTheme.black900,
fontSize: 12, fontSize: 12,
); );
// Label text style // Label text style
static get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith( static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.gray60003, color: appTheme.gray60003,
); );
static get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith( static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700, color: appTheme.whiteA700,
); );
// Title text style // Title text style
static get titleMedium16 => theme.textTheme.titleMedium!.copyWith( static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
fontSize: 16, fontSize: 16,
); );
static get titleMediumBlack => theme.textTheme.titleMedium!.copyWith( static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
fontSize: 17, fontSize: 17,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
); );
static get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith( static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.black900, color: appTheme.black900,
); );
static get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith( static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
static get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith( static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.whiteA700, color: appTheme.whiteA700,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
static get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.blue800, color: appTheme.blue800,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
); );
static get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black, color: appTheme.black,
); );
static get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black, color: appTheme.black,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
static get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black, color: appTheme.black,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
); );
static get titleSmallBold => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
static get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001, color: appTheme.greenA70001,
); );
static get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001, color: appTheme.greenA70001,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
); );
static get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.lightGreen300, color: appTheme.lightGreen300,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
); );
static get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.red200, color: appTheme.red200,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
); );
static get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer, color: theme.colorScheme.secondaryContainer,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
); );
static get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith( static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer, color: theme.colorScheme.secondaryContainer,
); );
static get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith( static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
); );
} }

View File

@@ -15,7 +15,7 @@ abstract class TokenStorage {
static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token'); static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
static saveTokenInStorage(String token) async { static Future<void> saveTokenInStorage(String token) async {
await const FlutterSecureStorage().write(key: 'token', value: token); await const FlutterSecureStorage().write(key: 'token', value: token);
} }

View File

@@ -33,7 +33,7 @@ extension DateTimeExtension on DateTime {
} }
} }
String calculateDifference(DateTime dateTime) { String calculateDifference(DateTime dateTime, [String? format]) {
final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime; final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime;
if (localDateTime.isToday()) { if (localDateTime.isToday()) {
@@ -41,6 +41,6 @@ String calculateDifference(DateTime dateTime) {
} else if (localDateTime.isYesterday()) { } else if (localDateTime.isYesterday()) {
return 'Вчера'; return 'Вчера';
} else { } else {
return localDateTime.format('d MMMM', 'ru'); return localDateTime.format(format ?? 'd MMMM', 'ru');
} }
} }

View File

@@ -1,9 +1,12 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart'; 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/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
class WsManager { class WsManager {
final WsService wsService; final WsService wsService;
@@ -39,6 +42,7 @@ class WsManager {
metadata: const PusherChannelsOptionsMetadata.byDefault(), metadata: const PusherChannelsOptionsMetadata.byDefault(),
port: 443, port: 443,
); );
client = PusherChannelsClient.websocket( client = PusherChannelsClient.websocket(
options: options, options: options,
connectionErrorHandler: (exception, trace, refresh) { connectionErrorHandler: (exception, trace, refresh) {
@@ -70,6 +74,11 @@ class WsManager {
} }
}); });
client?.eventStream.listen((event) { client?.eventStream.listen((event) {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
log(event.rootObject.toString()); log(event.rootObject.toString());
}); });
_subscribe(); _subscribe();
@@ -136,7 +145,7 @@ class WsManager {
_privateDirectChatChannel?.unsubscribe(); _privateDirectChatChannel?.unsubscribe();
} }
Future<void> clear() async{ Future<void> clear() async {
await client?.disconnect(); await client?.disconnect();
client = null; client = null;
userId = null; userId = null;

View File

@@ -56,7 +56,7 @@ class CustomDropDown extends StatelessWidget {
: _buildDropDownWidget(context); : _buildDropDownWidget(context);
} }
_buildDropDownWidget(BuildContext context) { DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
return DropdownButtonFormField<SelectionPopupModel>( return DropdownButtonFormField<SelectionPopupModel>(
focusNode: focusNode ?? FocusNode(), focusNode: focusNode ?? FocusNode(),
icon: const Icon(Icons.keyboard_arrow_down_rounded), icon: const Icon(Icons.keyboard_arrow_down_rounded),
@@ -84,7 +84,7 @@ class CustomDropDown extends StatelessWidget {
); );
} }
void onDropDownChanged(value) { void onDropDownChanged(dynamic value) {
onChanged!(value!); onChanged!(value!);
} }

View File

@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -28,6 +29,7 @@ class CustomPopup extends StatelessWidget {
final FeedBlogPostModel? feedBlogPostModel; final FeedBlogPostModel? feedBlogPostModel;
final Recipe? recipe; final Recipe? recipe;
final Diet? diet; final Diet? diet;
final String userSlug;
const CustomPopup({ const CustomPopup({
super.key, super.key,
@@ -39,6 +41,7 @@ class CustomPopup extends StatelessWidget {
this.feedBlogPostModel, this.feedBlogPostModel,
this.recipe, this.recipe,
this.diet, this.diet,
required this.userSlug
}); });
@override @override
@@ -73,7 +76,7 @@ class CustomPopup extends StatelessWidget {
); );
} }
List<PopupMenuEntry> _buildMenuItems(s, context) { List<PopupMenuEntry> _buildMenuItems(S s,BuildContext context) {
final entries = <PopupMenuEntry>[]; final entries = <PopupMenuEntry>[];
void addItem(String icon, String text, Function() onTap) { void addItem(String icon, String text, Function() onTap) {
@@ -107,28 +110,28 @@ class CustomPopup extends StatelessWidget {
addItem('assets/icons/red_trash.svg', 'Удалить', () async { addItem('assets/icons/red_trash.svg', 'Удалить', () async {
if (complainedContentType == ComplainedContent.posts) { if (complainedContentType == ComplainedContent.posts) {
final result = await sl<BlogPostCreateCubit>().deleteBlogPost(id!); final result = await sl<BlogPostCreateCubit>().deleteBlogPost(id!);
if (!result) { if (!result && context.mounted) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post); showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return; return;
} }
showSuccessToast('Пост удален, обновите ленту'); showSuccessToast('Пост удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.recipe) { } else if (complainedContentType == ComplainedContent.recipe) {
final result = await sl<RecipesCubit>().deleteRecipe(slug!); final result = await sl<RecipesCubit>().deleteRecipe(slug!);
if (!result) { if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить рецепт'); showErrorDialog(context, title: 'Не удалось удалить рецепт');
return; return;
} }
showSuccessToast('Рецепт удален, обновите ленту'); showSuccessToast('Рецепт удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.diet) { } else if (complainedContentType == ComplainedContent.diet) {
final result = await sl<DietsCubit>().deleteDiet(id!); final result = await sl<DietsCubit>().deleteDiet(id!);
if (!result) { if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить диету'); showErrorDialog(context, title: 'Не удалось удалить диету');
return; return;
} }
showSuccessToast('Диета удалена, обновите ленту'); showSuccessToast('Диета удалена, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) { } else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().deletePlan(slug!); final result = await sl<MyPlansCubit>().deletePlan(slug!);
if (!result) { if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить план'); showErrorDialog(context, title: 'Не удалось удалить план');
return; return;
} }
@@ -164,6 +167,13 @@ class CustomPopup extends StatelessWidget {
); );
} }
}); });
addItem(
'assets/icons/complain.svg',
'Заблокировать автора',
() {
sl<BlockedUsersCubit>().blockUser(userSlug);
},
);
} }
if (entries.isNotEmpty && entries.last is PopupMenuItem) { if (entries.isNotEmpty && entries.last is PopupMenuItem) {

View File

@@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';

View File

@@ -0,0 +1,35 @@
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';
class ResetPasswordCubit extends Cubit<ResetPasswordState> {
final ResetPasswordRepository resetPasswordRepository;
ResetPasswordCubit({required this.resetPasswordRepository}) : super(const ResetPasswordState.empty());
Future<void> trySendEmailCode(String email) async {
emit(const ResetPasswordState.loadingSendEmailCode());
try {
if (await resetPasswordRepository.sendEmailCodeForPasswordChange(email)) {
emit(const ResetPasswordState.codeSendSuccess());
} else {
emit(const ResetPasswordState.codeSendError());
}
} catch (e) {
emit(const ResetPasswordState.codeSendError());
}
}
Future<void> tryResetPassword(String email, String code, String password, String passwordConfirmation) async {
emit(const ResetPasswordState.loadingResetPassword());
try {
if (await resetPasswordRepository.confirmPasswordChange(code, password, passwordConfirmation, email)) {
emit(const ResetPasswordState.resetPasswordSuccess());
} else {
emit(const ResetPasswordState.resetPasswordError());
}
} catch (e) {
emit(const ResetPasswordState.resetPasswordError());
}
}
}

View File

@@ -0,0 +1,22 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'reset_password_state.freezed.dart';
@freezed
abstract class ResetPasswordState with _$ResetPasswordState {
const factory ResetPasswordState() = _ResetPasswordState;
const factory ResetPasswordState.empty() = ResetPasswordStateEmpty;
const factory ResetPasswordState.loadingSendEmailCode() = ResetPasswordStateLoadingSendEmailCode;
const factory ResetPasswordState.codeSendSuccess() = ResetPasswordStateCodeSendSuccess;
const factory ResetPasswordState.codeSendError() = ResetPasswordStateCodeSendError;
const factory ResetPasswordState.loadingResetPassword() = ResetPasswordStateLoadingResetPassword;
const factory ResetPasswordState.resetPasswordError() = ResetPasswordStateResetPasswordError;
const factory ResetPasswordState.resetPasswordSuccess() = ResetPasswordStateResetPassowrdSuccess;
}

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -6,8 +7,8 @@ import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
@@ -52,17 +53,28 @@ class _AuthScreenState extends State<AuthScreen> {
} }
Future<void> _reconnectWebSocket(BuildContext context) async { Future<void> _reconnectWebSocket(BuildContext context) async {
await sl<WsManager>().clear(); try {
await sl<WsManager>().initialize(); await sl<WsManager>().clear();
if (context.mounted) { await sl<WsManager>().initialize();
context.read<ProfileAvatarCubit>().getMyProfile(); } catch (e) {
context.read<ChatsCubit>().getChatRoomsWithLoading(); rethrow;
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
} }
if (context.mounted) {
try {
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
} catch (e) {
rethrow;
}
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
} }
@override @override
@@ -80,10 +92,10 @@ class _AuthScreenState extends State<AuthScreen> {
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () {
tapsCount++; tapsCount++;
if (tapsCount == 6) { if (tapsCount == 7) {
tapsCount = 0; tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
} }
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}, },
child: SvgPicture.asset( child: SvgPicture.asset(
ImageConstant.imgLogoEatvio, ImageConstant.imgLogoEatvio,
@@ -127,6 +139,20 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Center(
child: GestureDetector(
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'),
child: Text(
'Забыли пароль?',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -182,38 +208,137 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( Row(
height: 48, children: [
width: MediaQuery.of(context).size.width - 32, SizedBox(
child: FloatingActionButton.extended( height: 48,
heroTag: 'authButton', width: MediaQuery.of(context).size.width - 88,
highlightElevation: 0, child: FloatingActionButton.extended(
elevation: 0, heroTag: 'authButton',
focusElevation: 0, highlightElevation: 0,
shape: RoundedRectangleBorder( elevation: 0,
borderRadius: BorderRadius.circular(20), focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
final isAuthenticated = await authCubit.getTokenByCredentials(
emailController.text,
passwordController.text,
);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
}
}
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: Text(
'Войти',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
), ),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, const SizedBox(width: 8),
onPressed: state is AuthStateLoading SizedBox(
? null height: 48,
: () async { width: 48,
final isAuthenticated = await authCubit.getTokenByCredentials( child: FloatingActionButton.extended(
emailController.text, heroTag: 'scanQrCodeButton',
passwordController.text, highlightElevation: 0,
); elevation: 0,
if (isAuthenticated) { focusElevation: 0,
if (context.mounted) { shape: RoundedRectangleBorder(
_reconnectWebSocket(context); borderRadius: BorderRadius.circular(16),
} ),
} backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
}, onPressed: state is AuthStateLoading
label: state is AuthStateLoading ? null
? const CupertinoActivityIndicator() : () async {
: Text( await showDialog(
'Войти', context: context,
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), builder: (BuildContext context) {
), return Dialog(
), child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Где взять QR-код?\n',
style: TextStyle(
fontSize: 18,
),
),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
AppRouter.navigatorKey.currentContext?.pop();
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Отмена',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
),
),
const SizedBox(width: 8),
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
AppRouter.navigatorKey.currentContext?.pop();
AppRouter.navigatorKey.currentContext?.pushNamed('scanQRCodeScreen');
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Перейти',
style: TextStyle(
color: appTheme.blueA400,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
),
);
},
);
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: const Icon(
Icons.qr_code_scanner_rounded,
color: Colors.white,
),
),
),
],
), ),
], ],
), ),

View File

@@ -0,0 +1,165 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget {
const ForgetPasswordScreen({super.key});
@override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
}
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
final emailController = TextEditingController();
final codeController = TextEditingController();
final passwordController = TextEditingController();
final passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider<ResetPasswordCubit>(
create: (BuildContext context) => sl<ResetPasswordCubit>(),
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
builder: (BuildContext context, ResetPasswordState state) {
return Scaffold(
appBar: AppBar(
title: const Text('Смена пароля'),
centerTitle: false,
),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
? const Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
const Text(
'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 24),
const Text(
'E-mail',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: emailController,
hintText: 'Введите e-mail',
),
],
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
const Text(
'Код',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: codeController,
hintText: 'Введите код',
),
const SizedBox(height: 12),
const Text(
'Пароль',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordController,
hintText: 'Введите новый пароль',
),
const SizedBox(height: 12),
const Text(
'Подтвердите пароль',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordConfirmController,
hintText: 'Введите новый пароль',
),
],
const Spacer(),
InkWell(
onTap: () {
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
context.read<ResetPasswordCubit>().tryResetPassword(
emailController.text,
codeController.text,
passwordController.text,
passwordConfirmController.text,
);
} else {
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Center(
child: Text(
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
? 'Сменить пароль'
: 'Сбросить пароль',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
),
);
},
listener: (BuildContext context, ResetPasswordState state) {
if (state is ResetPasswordStateCodeSendSuccess) {
showSuccessToast('Код на почту успешно отправлен');
}
if (state is ResetPasswordStateCodeSendError) {
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
}
if (state is ResetPasswordStateResetPasswordError) {
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
}
if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. Войдите в аккаунт');
}
},
),
);
}
}

View File

@@ -0,0 +1,109 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.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';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
class ScanQRCodeScreen extends StatefulWidget {
const ScanQRCodeScreen({super.key});
@override
State<ScanQRCodeScreen> createState() => _ScanQRCodeScreenState();
}
class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
Barcode? result;
QRViewController? controller;
@override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
} else if (Platform.isIOS) {
controller!.resumeCamera();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Сканирование QR-кода'),
centerTitle: true,
),
body: Column(
children: <Widget>[
Expanded(
child: QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
),
),
],
),
);
}
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) async {
setState(() {
result = scanData;
});
await controller.pauseCamera();
try {
final scannedUrl = scanData.code;
final uri = Uri.parse(scannedUrl ?? '');
final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : '';
baseUrl = scannedBaseUrl;
sl<Dio>().options.baseUrl = baseUrl;
final response = await Dio().getUri(
uri,
options: Options(
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
);
final token = response.data['token'];
await TokenStorage.saveTokenInStorage(token);
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
try {
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
} catch (e) {
rethrow;
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
showSuccessToast('Добро пожаловать!');
} catch (e) {
await controller.resumeCamera();
showErrorToast('Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.');
}
});
}
}

View File

@@ -0,0 +1,53 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
class ResetPasswordRepository {
final ResetPasswordService resetPasswordService;
ResetPasswordRepository({
required this.resetPasswordService,
});
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) {
rethrow;
}
}
Future<bool> confirmPasswordChange(
String code,
String password,
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) {
rethrow;
}
}
}

View File

@@ -0,0 +1,19 @@
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'reset_password_service.g.dart';
@RestApi()
abstract class ResetPasswordService {
factory ResetPasswordService(Dio dio, {String baseUrl}) = _ResetPasswordService;
@POST('/api/send_password_reset_code')
Future<HttpResponse> sendEmailCodeForPasswordChange({
@Body() required Map<String, dynamic> data,
});
@POST('/api/confirm_password_reset_code')
Future<HttpResponse> confirmPasswordChange({
@Body() required Map<String, dynamic> data,
});
}

View File

@@ -0,0 +1,21 @@
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';
class BlogPostCubit extends Cubit<BlogPostState> {
final FeedRepository feedRepository;
BlogPostCubit(this.feedRepository) : super(BlogPostState());
Future<void> getBlogPost(int id) async {
try {
emit(state.copyWith(isLoading: true));
final postModel = await feedRepository.getBlogPost(id: id);
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false));
}
}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'blog_post_state.freezed.dart';
part 'blog_post_state.g.dart';
@freezed
abstract class BlogPostState with _$BlogPostState {
factory BlogPostState({
@Default(null) FeedBlogPostModel? blogPostModel,
@Default(false) bool isLoading,
@Default(null) String? error,
}) = _BlogPostState;
factory BlogPostState.fromJson(Map<String, dynamic> json) => _$BlogPostStateFromJson(json);
}

View File

@@ -5,7 +5,8 @@ import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
@@ -19,9 +20,9 @@ import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class BlogPostScreen extends StatefulWidget { class BlogPostScreen extends StatefulWidget {
final FeedBlogPostModel blogPost; final int id;
const BlogPostScreen({super.key, required this.blogPost}); const BlogPostScreen({super.key, required this.id});
@override @override
State<BlogPostScreen> createState() => _BlogPostScreenState(); State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -31,263 +32,294 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>(); final CommentsCubit commentsCubit = sl<CommentsCubit>();
final textEditingController = TextEditingController(); final textEditingController = TextEditingController();
int? replyId; int? replyId;
late bool isLiked; bool isLiked = false;
late int totalLikes; int totalLikes = 0;
@override @override
void initState() { void initState() {
isLiked = widget.blogPost.wasLikedByMe; context.read<FeedCubit>().viewPost(widget.id);
totalLikes = widget.blogPost.totalLikes;
context.read<FeedCubit>().viewPost(widget.blogPost.id);
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<CommentsCubit>( return BlocProvider(
create: (BuildContext context) => commentsCubit..getComments(widget.blogPost.id.toString(), CommentSourceType.blogPost, true), create: (context) => sl<BlogPostCubit>()..getBlogPost(widget.id),
child: BlocConsumer<CommentsCubit, CommentsState>( child: BlocProvider<CommentsCubit>(
listener: (BuildContext context, CommentsState state) { create: (BuildContext context) => commentsCubit..getComments(widget.id.toString(), CommentSourceType.blogPost, true),
if (state is CommentsStateLoaded) { child: BlocBuilder<BlogPostCubit, BlogPostState>(
setState(() { builder: (context, blogPostState) {
replyId = state.replyId; return BlocConsumer<CommentsCubit, CommentsState>(
}); listener: (BuildContext context, CommentsState state) {
} if (state is CommentsStateLoaded) {
}, setState(() {
builder: (BuildContext context, CommentsState state) { replyId = state.replyId;
return Scaffold( });
resizeToAvoidBottomInset: true, }
bottomSheet: Container( },
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), builder: (BuildContext context, CommentsState state) {
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))), if (blogPostState.isLoading) {
child: Row( return Scaffold(
mainAxisAlignment: MainAxisAlignment.center, appBar: AppBar(
children: [ leading: BackButton(
if (replyId != null) onPressed: () {
GestureDetector( if (context.canPop()) {
onTap: () { GoRouter.of(context).pop();
context.read<CommentsCubit>().setReplyId(null); return;
setState(() { }
replyId = null; GoRouter.of(context).go('/feed-screen');
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.blogPost.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
),
],
),
),
appBar: AppBar(
title: Text(widget.blogPost.title),
actions: [
CustomPopup(
isAuthor: widget.blogPost.author.authorIsMe,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(widget.blogPost.id)}',
id: widget.blogPost.id,
feedBlogPostModel: widget.blogPost,
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
GestureDetector(
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.blogPost.author),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
baseUrl + widget.blogPost.author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.blogPost.author.authorName,
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, height: 1),
),
const SizedBox(height: 2),
Text(
widget.blogPost.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12, fontWeight: FontWeight.w400, height: 1),
),
],
),
],
),
),
),
if (widget.blogPost.description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Html(data: widget.blogPost.description, style: {"*": Style(color: Colors.black87)}),
),
const SizedBox(height: 8),
if (widget.blogPost.image.isNotEmpty)
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.blogPost.image}',
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
icon: Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(widget.blogPost.id)
: await context.read<FeedCubit>().unlikedPost(widget.blogPost.id);
sl<FeedCubit>().updateFeedBlogPostList(widget.blogPost, isLiked);
}, },
), ),
),
body: const Loader());
}
isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8), const SizedBox(width: 8),
_iconButton( GestureDetector(
icon: Icons.reply_rounded,
onTap: () { onTap: () {
final box = context.findRenderObject() as RenderBox?; if (textEditingController.text.length > 2) {
SharePlus.instance.share( commentsCubit.sendComment(
ShareParams( id: widget.id.toString(),
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.blogPost.id}'), commentText: textEditingController.text,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, parentCommentId: replyId,
), commentSourceType: CommentSourceType.blogPost,
); );
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
}, },
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
), ),
const Spacer(),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(widget.blogPost.totalViews.toString(), style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
], ],
), ),
), ),
Container( appBar: AppBar(
width: MediaQuery.of(context).size.width - 32, title: Text(blogPostState.blogPostModel?.title ?? ''),
height: 0.4, leading: BackButton(
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)), onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
actions: [
CustomPopup(
isAuthor: blogPostState.blogPostModel?.author.authorIsMe ?? false,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(widget.id)}',
id: widget.id,
feedBlogPostModel: blogPostState.blogPostModel,
userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
),
const SizedBox(width: 16),
],
), ),
const SizedBox(height: 8), body: SingleChildScrollView(
Container( padding: const EdgeInsets.only(bottom: 80),
padding: const EdgeInsets.symmetric(horizontal: 16), physics: const BouncingScrollPhysics(),
alignment: Alignment.centerLeft, child: Column(
child: Text( children: [
'${widget.blogPost.totalComments} КОММЕНТАРИЕВ', GestureDetector(
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400), onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: blogPostState.blogPostModel?.author),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''),
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
blogPostState.blogPostModel?.author.authorName ?? '',
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, height: 1),
),
const SizedBox(height: 2),
Text(
(blogPostState.blogPostModel?.createdAt ?? DateTime.now()).format('dd MMMM', 'ru'),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12, fontWeight: FontWeight.w400, height: 1),
),
],
),
],
),
),
),
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'), style: {"*": Style(color: Colors.black87)}),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}',
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
icon: Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked ? await context.read<FeedCubit>().likePost(widget.id) : await context.read<FeedCubit>().unlikedPost(widget.id);
sl<FeedCubit>().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked);
},
),
const SizedBox(width: 8),
_iconButton(
icon: Icons.reply_rounded,
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
),
const Spacer(),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(blogPostState.blogPostModel?.totalViews.toString() ?? '',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
],
),
),
Container(
width: MediaQuery.of(context).size.width - 32,
height: 0.4,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
'${blogPostState.blogPostModel?.totalComments} КОММЕНТАРИЕВ',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
),
const SizedBox(height: 16),
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId),
],
), ),
), ),
const SizedBox(height: 16), );
if (state is CommentsStateLoading) const Loader(), },
if (state is CommentsStateLoaded) );
CommentsWidget(id: widget.blogPost.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId), },
], ),
),
),
);
},
), ),
); );
} }
_iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) { InkWell _iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) {
return InkWell( return InkWell(
onTap: onTap ?? () {}, onTap: onTap ?? () {},
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),

View File

@@ -80,7 +80,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
); );
} }
_showImagePicker() { void _showImagePicker() {
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
context: context, context: context,
builder: (context) { builder: (context) {
@@ -145,7 +145,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
); );
} }
_buildBlogPostCreate(BuildContext context) { Scaffold _buildBlogPostCreate(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: AppBar(), appBar: AppBar(),
@@ -253,7 +253,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
); );
} }
_buildAddPhoto() { GestureDetector _buildAddPhoto() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_showImagePicker(); _showImagePicker();

View File

@@ -6,7 +6,7 @@ class NewMessagesCubit extends Cubit<NewMessagesState> {
NewMessagesCubit() : super(const NewMessagesState.empty()); NewMessagesCubit() : super(const NewMessagesState.empty());
incrementCounter() { void incrementCounter() {
_messagesCount++; _messagesCount++;
emit(NewMessagesState.loaded(messagesCount: _messagesCount)); emit(NewMessagesState.loaded(messagesCount: _messagesCount));
} }

View File

@@ -1,6 +1,8 @@
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/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> { class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
final ProfileRepository profileRepository; final ProfileRepository profileRepository;
@@ -9,7 +11,7 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
required this.profileRepository, required this.profileRepository,
}) : super(const ProfileAvatarState.loading()); }) : super(const ProfileAvatarState.loading());
getMyProfile() async { Future<void> getMyProfile() async {
emit(const ProfileAvatarState.loading()); emit(const ProfileAvatarState.loading());
try { try {
emit( emit(
@@ -18,6 +20,7 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
), ),
); );
} catch (e) { } catch (e) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString())); emit(ProfileAvatarState.error(errorMessage: e.toString()));
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -30,6 +31,7 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> { class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1; int currentBottomNavBarIndex = 1;
late final List<String> pages; late final List<String> pages;
int tapsCount = 0;
@override @override
void initState() { void initState() {
@@ -58,12 +60,29 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
} }
void _onItemTapped(int index) async { void _onItemTapped(int index) async {
if (pages.length == 7) {
if (index == 6) {
tapsCount++;
if (tapsCount == 7) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
}
} else {
if (index == 5) {
tapsCount++;
if (tapsCount == 7) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
}
}
if (currentBottomNavBarIndex != index) { if (currentBottomNavBarIndex != index) {
setState(() { setState(() {
currentBottomNavBarIndex = index; currentBottomNavBarIndex = index;
}); });
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
} }
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
} }
@override @override
@@ -82,7 +101,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
BottomNavigationBarItem( BottomNavigationBarItem(
label: '', label: '',
icon: CircleAvatar( icon: CircleAvatar(
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl), foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'), backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16, radius: 16,
), ),

View File

@@ -7,7 +7,7 @@ class CalendarCubit extends Cubit<CalendarState> {
CalendarCubit({required this.calendarRepository}) : super(const CalendarState.loading()); CalendarCubit({required this.calendarRepository}) : super(const CalendarState.loading());
loadCalendarList() async { Future<void> loadCalendarList() async {
emit(const CalendarState.loading()); emit(const CalendarState.loading());
try { try {
final calendarList = await calendarRepository.getCalendar(); final calendarList = await calendarRepository.getCalendar();

View File

@@ -89,7 +89,7 @@ class CalendarScreen extends StatelessWidget {
); );
} }
_buildCalendarBody(BuildContext context, CalendarStateLoaded state) { ListView _buildCalendarBody(BuildContext context, CalendarStateLoaded state) {
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
shrinkWrap: true, shrinkWrap: true,

View File

@@ -7,11 +7,13 @@ import 'package:audioplayers/audioplayers.dart';
import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart'; import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart'; 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/my_finance/repositories/cards_repository.dart';
@@ -45,7 +47,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
_timer?.cancel(); _timer?.cancel();
} }
loadNextPage(String chatId) async { Future<void> loadNextPage(String chatId) async {
if (currentPage < lastPage && !isLoading) { if (currentPage < lastPage && !isLoading) {
isLoading = true; isLoading = true;
@@ -87,7 +89,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
} }
initChat(String chatId, {bool sendInitialMessage = false}) async { Future<void> initChat(String chatId, {bool sendInitialMessage = false}) async {
try { try {
final chatRoom = await chatRepository.getChatRoom(chatId); final chatRoom = await chatRepository.getChatRoom(chatId);
ChosenPlanModel? chosenPlan; ChosenPlanModel? chosenPlan;
@@ -167,6 +169,11 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
chatRepository.emitNewMessageToChats(message); chatRepository.emitNewMessageToChats(message);
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
}); });
typingSubscription = wsManager.clientTypingStream?.listen((event) { typingSubscription = wsManager.clientTypingStream?.listen((event) {
@@ -176,9 +183,17 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}); });
} }
Future<bool> sendMessage({required String chatId, String? data, List<File>? files}) async { Future<bool> sendMessage({
String? data,
List<File>? files,
required String userSlug,
}) async {
try { try {
await chatRepository.sendMessage(chatId: chatId, data: data, files: files != null ? await ImageCompressUtility.compressFilesList(files) : null); await chatRepository.sendMessage(
data: data,
files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
slug: userSlug,
);
AudioPlayer().play((AssetSource('audio/send_message.wav'))); AudioPlayer().play((AssetSource('audio/send_message.wav')));
return true; return true;
} catch (e, st) { } catch (e, st) {

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.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/data_error_widget.dart';
@@ -8,6 +9,7 @@ import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
@@ -21,15 +23,18 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
final String chatId; final String chatId;
final bool? sendInitialMessage; final bool? sendInitialMessage;
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage}); const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
@override @override
@@ -44,6 +49,7 @@ class _ChatScreenState extends State<ChatScreen> {
final List<({int id, int index})> unreadMessagesIdsCopy = []; final List<({int id, int index})> unreadMessagesIdsCopy = [];
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates(); final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
late AppLifecycleListener _listener; late AppLifecycleListener _listener;
List<File> pickedFiles = [];
void _onHide() { void _onHide() {
chatRoomCubit.cancelTimer(); chatRoomCubit.cancelTimer();
@@ -63,7 +69,7 @@ class _ChatScreenState extends State<ChatScreen> {
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume); _listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
} }
_onScroll() { void _onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) { if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId); chatRoomCubit.loadNextPage(widget.chatId);
} }
@@ -96,7 +102,7 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
_buildChat(BuildContext context, ChatRoomStateLoaded state) { Scaffold _buildChat(BuildContext context, ChatRoomStateLoaded state) {
final chosenPlan = state.chatRoom.chosenPlan; final chosenPlan = state.chatRoom.chosenPlan;
final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id; final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id;
final showUserDetailsButton = [ final showUserDetailsButton = [
@@ -333,100 +339,155 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
Container( Container(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(width: 8), SingleChildScrollView(
InkWell( padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () async { physics: const BouncingScrollPhysics(),
FilePickerResult? result = await FilePicker.platform.pickFiles( scrollDirection: Axis.horizontal,
allowMultiple: true, child: Row(
type: FileType.custom, mainAxisSize: MainAxisSize.min,
allowedExtensions: [ mainAxisAlignment: MainAxisAlignment.start,
'pdf', children: [
'doc', for (int i = 0; i < pickedFiles.length; i++) ...[
'docx', Stack(
'json', clipBehavior: Clip.none,
'ppt', children: [
'pptx', _isImageFile(pickedFiles[i].path)
'rtf', ? GestureDetector(
'txt', onTap: () {
'xls', final imageFiles = pickedFiles.where((f) => _isImageFile(f.path)).toList();
'xlsx', final imageIndex = _imageIndexAt(pickedFiles, i);
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) { context.pushTransparentRoute(
List<File> files = result.paths.map((path) => File(path!)).toList(); PhotoViewScreen(
await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files); imageUrls: imageFiles.map((e) => e.path).toList(),
} isLocal: true,
}, index: imageIndex,
customBorder: const CircleBorder(), ),
child: Ink( );
height: 40, },
width: 40, child: Hero(
child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), tag: pickedFiles[i].path,
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
height: 104,
width: 104,
child: Image.file(
pickedFiles[i],
fit: BoxFit.cover,
),
),
),
),
)
: _buildFilePreview(pickedFiles[i]),
Positioned(
top: -8,
right: -8,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
setState(() {
pickedFiles.remove(pickedFiles[i]);
});
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: appTheme.gray50,
shape: BoxShape.circle,
),
child: const Icon(
size: 20,
Icons.close,
color: Colors.black,
),
),
),
),
],
),
const SizedBox(width: 8),
],
],
), ),
), ),
const SizedBox(width: 8), const SizedBox(height: 12),
Expanded( Row(
child: SizedBox( crossAxisAlignment: CrossAxisAlignment.end,
height: 40, children: [
child: TextFormField( const SizedBox(width: 8),
expands: true, InkWell(
textCapitalization: TextCapitalization.sentences, onTap: () => showImagePicker(state.chatRoom.users.receiver.slug),
controller: textEditingController, customBorder: const CircleBorder(),
maxLines: null, child: Ink(
minLines: null, height: 40,
keyboardType: TextInputType.multiline, width: 40,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))),
decoration: InputDecoration( ),
isDense: true, ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), const SizedBox(width: 8),
hintText: 'Сообщение', Expanded(
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)), child: SizedBox(
filled: true, height: 40,
border: OutlineInputBorder( child: TextFormField(
borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)), expands: true,
enabledBorder: OutlineInputBorder( textCapitalization: TextCapitalization.sentences,
borderRadius: BorderRadius.circular(100), controller: textEditingController,
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), maxLines: null,
), minLines: null,
focusedBorder: OutlineInputBorder( keyboardType: TextInputType.multiline,
borderRadius: BorderRadius.circular(100), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
), ),
), ),
), ),
), const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () {
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
files: pickedFiles.isEmpty ? null : pickedFiles,
userSlug: state.chatRoom.users.receiver.slug,
);
setState(() {
textEditingController.clear();
pickedFiles.clear();
});
}
},
child: Ink(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 40,
width: 40,
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
),
),
const SizedBox(width: 8),
],
), ),
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () {
if (textEditingController.text.isNotEmpty) {
chatRoomCubit.sendMessage(chatId: widget.chatId, data: textEditingController.text.trim());
setState(() {
textEditingController.clear();
});
}
},
child: Ink(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 40,
width: 40,
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
),
),
const SizedBox(width: 8),
], ],
), ),
), ),
@@ -434,21 +495,173 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
if (showAboutOrderButton) if (showAboutOrderButton)
Positioned( Positioned(
top: 8, top: 8,
right: 15, right: 15,
left: 16, left: 16,
child: AboutOrderButton( child: AboutOrderButton(
onTap: () async { onTap: () async {
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id}); GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
}, },
)), ),
),
], ],
), ),
), ),
); );
} }
_messageDate(DateTime date) { Widget _buildFilePreview(File file) {
final fileName = file.path.split('/').last;
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
height: 104,
width: 104,
color: const Color(0xFFE0E0E0), // Светло-серый фон
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.attach_file, size: 32, color: Colors.black54),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
fileName,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
],
),
),
);
}
bool _isImageFile(String path) {
final lower = path.toLowerCase();
return lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.gif') ||
lower.endsWith('.bmp') ||
lower.endsWith('.webp');
}
int _imageIndexAt(List<File> allFiles, int targetIndex) {
int count = 0;
for (int i = 0; i < allFiles.length; i++) {
if (_isImageFile(allFiles[i].path)) {
if (i == targetIndex) return count;
count++;
}
}
return 0;
}
void showImagePicker(String slug) {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
setState(() {
if (image != null) {
pickedFiles.add(File(image.path));
}
});
// if (image != null) {
// await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug);
// }
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
List<XFile> images = await picker.pickMultiImage();
if (images != []) {
setState(() {
pickedFiles.addAll(images.map((image) => File(image.path)).toList());
});
// List<File> files = images.map((image) => File(image.path)).toList();
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
}
},
child: const Row(
children: [
Icon(Icons.photo_library_rounded),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
// List<File> files = result.paths.map((path) => File(path!)).toList();
setState(() {
pickedFiles.addAll(result.paths.map((path) => File(path!)).toList());
});
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
}
},
child: const Row(
children: [
Icon(Icons.file_present_rounded),
SizedBox(width: 8),
Text('Файлы'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
}
Center _messageDate(DateTime date) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 32), padding: const EdgeInsets.only(top: 32),

View File

@@ -67,18 +67,23 @@ class _ChatsScreenState extends State<ChatsScreen> {
), ),
]; ];
}, },
body: switch (state.state) { body: RefreshIndicator(
BaseState.error => DataErrorWidget(onReload: () => context.read<ChatsCubit>().getChatRooms()), child: switch (state.state) {
BaseState.success => _buildChatList(context, state), BaseState.error => DataErrorWidget(onReload: () => context.read<ChatsCubit>().getChatRooms()),
_ => const Loader(), BaseState.success => _buildChatList(context, state),
}, _ => const Loader(),
},
onRefresh: () async {
await context.read<ChatsCubit>().getChatRooms();
},
),
), ),
); );
}, },
); );
} }
_buildChatList(BuildContext context, ChatsState state) { NotificationListener<ScrollNotification> _buildChatList(BuildContext context, ChatsState state) {
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
loadNewContent(notification, context.read<ChatsCubit>().loadMoreChatRooms); loadNewContent(notification, context.read<ChatsCubit>().loadMoreChatRooms);

View File

@@ -0,0 +1,289 @@
import 'dart:io';
import 'dart:math';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
class EmptyChatScreen extends StatefulWidget {
final Author author;
const EmptyChatScreen({
super.key,
required this.author,
});
@override
State<EmptyChatScreen> createState() => _EmptyChatScreenState();
}
class _EmptyChatScreenState extends State<EmptyChatScreen> {
final TextEditingController textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
titleSpacing: 0,
backgroundColor: appTheme.gray100,
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: widget.author.authorSlug,
authorName: widget.author.authorName,
authorProfileIcon: widget.author.authorProfileIcon,
authorIsMe: false,
),
);
},
child: Row(
children: [
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(widget.author.authorProfileIcon),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.author.authorName,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
),
],
),
],
),
),
),
body: SafeArea(
child: Column(
children: [
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(width: 8),
InkWell(
onTap: () => showImagePicker(widget.author.authorSlug),
customBorder: const CircleBorder(),
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(
angle: pi / 4,
child: const Icon(
Icons.attach_file_rounded,
size: 30,
color: Color(0xFF858E99),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () async {
if (textEditingController.text.isNotEmpty) {
await sl<ChatRepository>().sendMessage(
data: textEditingController.text.trim(),
slug: widget.author.authorSlug,
);
setState(() {
textEditingController.clear();
});
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
},
);
}
},
child: Ink(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 40,
width: 40,
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
),
),
const SizedBox(width: 8),
],
),
),
],
),
),
);
}
void showImagePicker(String slug) {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await sl<ChatRepository>().sendMessage(
files: [File(image.path)],
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
},
);
}
},
child: const Row(
children: [
Icon(Icons.photo_library_rounded),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
List<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
List<File> files = images.map((image) => File(image.path)).toList();
await sl<ChatRepository>().sendMessage(
files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
},
);
}
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await sl<ChatRepository>().sendMessage(
files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
},
);
}
},
child: const Row(
children: [
Icon(Icons.file_present_rounded),
SizedBox(width: 8),
Text('Файлы'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
}
}

View File

@@ -20,15 +20,14 @@ class ChatEntryWidget extends StatefulWidget {
class _ChatEntryWidgetState extends State<ChatEntryWidget> { class _ChatEntryWidgetState extends State<ChatEntryWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return GestureDetector(
borderRadius: BorderRadius.circular(16),
onTap: () => GoRouter.of(context).pushNamed( onTap: () => GoRouter.of(context).pushNamed(
'chatScreen', 'chatScreen',
extra: { extra: {
'chatId': widget.chatRoom.id.toString(), 'chatId': widget.chatRoom.id.toString(),
}, },
), ),
child: Ink( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -61,7 +60,13 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
SizedBox( SizedBox(
width: MediaQuery.sizeOf(context).width * 0.5, width: MediaQuery.sizeOf(context).width * 0.5,
child: Text( child: Text(
widget.chatRoom.lastMessage!.data.toString().trim(), widget.chatRoom.lastMessage!.mediaData != null
? widget.chatRoom.lastMessage!.isCurrentUserAuthor == true
? 'Вы: 📎 Вложение'
: '📎 Вложение'
: widget.chatRoom.lastMessage!.isCurrentUserAuthor == true
? 'Вы: ${widget.chatRoom.lastMessage!.data.toString().trim()}'
: widget.chatRoom.lastMessage!.data.toString().trim(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: CustomTextStyles.bodyMediumBluegray800, style: CustomTextStyles.bodyMediumBluegray800,
@@ -77,7 +82,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
Padding( Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsets.only(left: 8),
child: Text( child: Text(
widget.chatRoom.lastMessage!.updatedAt.format('dd MMM', 'ru'), calculateDifference(widget.chatRoom.lastMessage!.updatedAt, 'dd MMM'),
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
), ),

View File

@@ -78,7 +78,7 @@ class ChatSearchBarWidget extends StatelessWidget {
return searchViewWidget(context); return searchViewWidget(context);
} }
searchViewWidget(BuildContext context) => SizedBox( SizedBox searchViewWidget(BuildContext context) => SizedBox(
width: width ?? MediaQuery.of(context).size.width, width: width ?? MediaQuery.of(context).size.width,
child: TextFormField( child: TextFormField(
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
@@ -27,7 +28,7 @@ class ChatRepository {
} }
Future<void> sendMessage({ Future<void> sendMessage({
required String chatId, required String slug,
String? data, String? data,
List<File>? files, List<File>? files,
}) async { }) async {
@@ -46,12 +47,13 @@ class ChatRepository {
} }
} }
await chatService.sendMessage( await chatService.sendMessageByUserSlug(
id: chatId, userSlug: slug,
data: data, data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles, mediaData: multipartFiles.isEmpty ? null : multipartFiles,
); );
} catch (e) { } catch (e, st) {
log(e.toString(), stackTrace: st);
rethrow; rethrow;
} }
} }
@@ -154,4 +156,17 @@ class ChatRepository {
rethrow; rethrow;
} }
} }
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) {
return 'error';
}
}
} }

View File

@@ -24,7 +24,7 @@ abstract class ChatService {
@Path() required String id, @Path() required String id,
}); });
@POST('/api/chat/{id}/message/send') @POST('/api/v2/chat/message/send/{id}')
@MultiPart() @MultiPart()
Future<void> sendMessage({ Future<void> sendMessage({
@Path() required String id, @Path() required String id,
@@ -80,4 +80,17 @@ abstract class ChatService {
@Path() required int id, @Path() required int id,
@Body() required PlanComplainModel body, @Body() required PlanComplainModel body,
}); });
@GET('/api/v2/chat/get_by_user/{slug}')
Future<HttpResponse> getChatByUserSlug({
@Path() required String slug,
});
@POST('/api/v2/chat/message/send/{userSlug}')
@MultiPart()
Future<void> sendMessageByUserSlug({
@Path() required String userSlug,
@Part() String? data,
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
});
} }

View File

@@ -37,7 +37,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
} }
} }
getMyRequestStatus(String slug) async { Future<void> getMyRequestStatus(String slug) async {
_update = true; _update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug); final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
@@ -81,11 +81,11 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
); );
} }
disableLoading() { void disableLoading() {
emit(state.copyWith(loading: false)); emit(state.copyWith(loading: false));
} }
updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) { void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit( emit(
state.copyWith( state.copyWith(
consultant: consultant, consultant: consultant,
@@ -95,7 +95,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
); );
} }
registerConsultant() async { Future<void> registerConsultant() async {
if (state.consultant.photos != null) { if (state.consultant.photos != null) {
updateConsultantProfileModel( updateConsultantProfileModel(
state.consultant.copyWith( state.consultant.copyWith(

View File

@@ -76,7 +76,7 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
); );
} }
_tile({ ListTile _tile({
required String title, required String title,
required Function() onTap, required Function() onTap,
required bool value, required bool value,
@@ -102,7 +102,7 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
); );
} }
_divider(BuildContext context) { Center _divider(BuildContext context) {
return Center( return Center(
child: Container( child: Container(
height: 0.4, height: 0.4,

View File

@@ -35,7 +35,7 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
); );
} }
_startRegistrationButton() { InkWell _startRegistrationButton() {
return InkWell( return InkWell(
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'), onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),

View File

@@ -82,7 +82,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
); );
} }
_divider(BuildContext context) { Container _divider(BuildContext context) {
return Container( return Container(
height: 0.4, height: 0.4,
width: MediaQuery.of(context).size.width - 32, width: MediaQuery.of(context).size.width - 32,
@@ -93,7 +93,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
); );
} }
_diplomaInfo({ Column _diplomaInfo({
required DiplomaEntity diploma, required DiplomaEntity diploma,
}) { }) {
return Column( return Column(
@@ -159,7 +159,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
); );
} }
onTapCalendar(BuildContext context, DiplomaEntity diploma) { Null Function() onTapCalendar(BuildContext context, DiplomaEntity diploma) {
return () { return () {
_showDialog( _showDialog(
CupertinoDatePicker( CupertinoDatePicker(
@@ -198,7 +198,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
); );
} }
checkLast() { void checkLast() {
if (diplomasList.last.textEditingController.text.isNotEmpty || diplomasList.last.date != null) { if (diplomasList.last.textEditingController.text.isNotEmpty || diplomasList.last.date != null) {
setState(() { setState(() {
diplomasList.add( diplomasList.add(
@@ -219,7 +219,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
} }
} }
enableDeleteButton(DiplomaEntity diploma) { void enableDeleteButton(DiplomaEntity diploma) {
if (diploma.textEditingController.text.isNotEmpty || diploma.date != null) { if (diploma.textEditingController.text.isNotEmpty || diploma.date != null) {
setState(() { setState(() {
diplomasList[diplomasList.indexOf(diploma)].enableDeleteButton = true; diplomasList[diplomasList.indexOf(diploma)].enableDeleteButton = true;
@@ -228,7 +228,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
_saveState(); _saveState();
} }
_saveState() { void _saveState() {
context.read<RegisterConsultantCubit>().updateConsultantProfileModel( context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
context.read<RegisterConsultantCubit>().state.consultant.copyWith( context.read<RegisterConsultantCubit>().state.consultant.copyWith(
certificates: diplomasList.map((e) => CertificatesModel(title: e.textEditingController.text, date: e.date.toString())).toList().sublist( certificates: diplomasList.map((e) => CertificatesModel(title: e.textEditingController.text, date: e.date.toString())).toList().sublist(

View File

@@ -80,7 +80,7 @@ class _LastStepScreenState extends State<LastStepScreen> {
); );
} }
_divider(BuildContext context) { Container _divider(BuildContext context) {
return Container( return Container(
height: 0.4, height: 0.4,
width: MediaQuery.of(context).size.width - 32, width: MediaQuery.of(context).size.width - 32,

View File

@@ -148,7 +148,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
); );
} }
_saveState() { void _saveState() {
context.read<RegisterConsultantCubit>().updateConsultantProfileModel( context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
context.read<RegisterConsultantCubit>().state.consultant.copyWith( context.read<RegisterConsultantCubit>().state.consultant.copyWith(
photos: imagesPaths, photos: imagesPaths,

View File

@@ -104,7 +104,7 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
); );
} }
_divider(BuildContext context) { Container _divider(BuildContext context) {
return Container( return Container(
height: 0.4, height: 0.4,
width: MediaQuery.of(context).size.width - 32, width: MediaQuery.of(context).size.width - 32,

View File

@@ -158,7 +158,7 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
); );
} }
setLinksState({ void setLinksState({
String? tgLink, String? tgLink,
String? vkLink, String? vkLink,
String? personalSiteLink, String? personalSiteLink,

View File

@@ -1,42 +1,16 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'rating_model.freezed.dart'; part 'rating_model.freezed.dart';
part 'rating_model.g.dart'; part 'rating_model.g.dart';
@freezed
abstract class RatingPaginationModel with _$RatingPaginationModel {
factory RatingPaginationModel({
required List<RatingModel> data,
}) = _RatingPaginationModel;
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
}
@freezed @freezed
abstract class RatingModel with _$RatingModel { abstract class RatingModel with _$RatingModel {
factory RatingModel({ factory RatingModel({
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date, required DateTime date,
@JsonKey(name: 'log_data') required RatingEnum type, required String title,
@JsonKey(name: 'log_scores') required int amount, required int amount,
}) = _RatingModel; }) = _RatingModel;
factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json); factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json);
} }
enum RatingEnum {
@JsonValue('ADDED_RECIPE')
addedRecipe,
@JsonValue('ADDED_PUBLIC_DIET')
addedPublicDiet,
@JsonValue('ADDED_BLOG_POST')
addedBlogPost,
@JsonValue('INVITED_A_NEW_USER')
invitedUser,
@JsonValue('HAS_MEDICAL_DEGREE')
hasMedicalDegree,
@JsonValue('CONSULTED')
consulted,
@JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED')
addedGlossary;
}

View File

@@ -1,19 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'statistics_model.freezed.dart';
part 'statistics_model.g.dart';
@freezed
abstract class StatisticsModel with _$StatisticsModel {
factory StatisticsModel({
@JsonKey(name: 'count_referal') int? countReferal,
@JsonKey(name: 'count_blog_posts') int? blogPostsCount,
@JsonKey(name: 'count_recipes') int? recipesCount,
@JsonKey(name: 'count_diets') int? dietsCount,
@JsonKey(name: 'count_consultants') int? consultationsCount,
@JsonKey(name: 'count_products_nutritional_values') int? qwe,
}) = _StatisticsModel;
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);
}

View File

@@ -1,16 +1,12 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.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/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantRatingScreen extends StatelessWidget { class ConsultantRatingScreen extends StatelessWidget {
static const name = 'consultantRatingScreen'; static const name = 'consultantRatingScreen';
@@ -19,10 +15,7 @@ class ConsultantRatingScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<ConsultantRatingCubit>( return const ConsultantRatingPageContent();
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
child: const ConsultantRatingPageContent(),
);
} }
} }
@@ -32,56 +25,52 @@ class ConsultantRatingPageContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>( return Scaffold(
builder: (context, state) { backgroundColor: appTheme.gray100,
return Scaffold( appBar: AppBar(
backgroundColor: appTheme.gray100, centerTitle: true,
appBar: AppBar( title: Text(
centerTitle: true, s.rating_and_statistics,
title: Text( style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
s.rating_and_statistics, ),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), ),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 16),
const RatingWidget(
rating: 1058,
), ),
), const SizedBox(height: 16),
body: state.error != null Padding(
? DataErrorWidget(onReload: () async { padding: const EdgeInsets.symmetric(horizontal: 16),
await context.read<ConsultantRatingCubit>().getConsultantStatistics(); child: CustomOutlinedButton(
if (!context.mounted) return; text: s.copy_referral_link,
await context.read<ConsultantRatingCubit>().getScoreLogs(); height: 52,
}) buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
: SingleChildScrollView( buttonStyle: ButtonStyle(
child: Column( side: const WidgetStatePropertyAll(BorderSide.none),
children: [ backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
const SizedBox(height: 16),
const RatingWidget(
rating: 1058,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CustomOutlinedButton(
text: s.copy_referral_link,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () {},
),
),
const SizedBox(height: 16),
StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,),
const SizedBox(height: 16),
RatingHistoryWidget(
isLoading: state.isRatingLoading,
ratingHistory: state.ratings,
),
],
),
), ),
); onPressed: () {},
}, ),
),
const SizedBox(height: 16),
const StatisticWidget(),
const SizedBox(height: 16),
RatingHistoryWidget(
ratingHistory: [
RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300),
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323),
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20),
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212),
RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323),
RatingModel(date: DateTime.now(), title: 'ee', amount: 1),
],
),
],
),
),
); );
} }
} }

View File

@@ -1,32 +0,0 @@
import 'dart:developer';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final ConsultantRatingRepository repository;
ConsultantRatingCubit(this.repository) : super(ConsultantRatingState());
Future<void> getConsultantStatistics({String? date}) async {
try {
emit(state.copyWith(isStatisticsLoading: true));
final statistics = await repository.getConsultantStatistics(date: date);
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null));
} catch (e, st) {
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}
}
Future<void> getScoreLogs({String? date}) async {
try {
emit(state.copyWith(isRatingLoading: true));
final scoreLogs = await repository.getScoreLogs(date: date);
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
} catch (e, st) {
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}
}
}

View File

@@ -1,16 +0,0 @@
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'consultant_rating_state.freezed.dart';
@freezed
abstract class ConsultantRatingState with _$ConsultantRatingState {
factory ConsultantRatingState({
@Default(false) bool isStatisticsLoading,
@Default(false) bool isRatingLoading,
@Default(null) StatisticsModel? statistics,
@Default(null) String? error,
@Default(<RatingModel>[]) List<RatingModel> ratings,
}) = _ConsultantRatingState;
}

View File

@@ -1,29 +1,42 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PeriodFilterWidget extends StatefulWidget { class PeriodFilterWidget extends StatefulWidget {
const PeriodFilterWidget({super.key, required this.isStatistic}); const PeriodFilterWidget({super.key});
final bool isStatistic;
@override @override
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState(); State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
} }
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> { class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
PeriodFilterStatus _currentState = PeriodFilterStatus.all; final List<String> periodDropDownTitles = [];
int periodDropDownIndex = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final s = S.of(context);
if (periodDropDownTitles.isEmpty) {
periodDropDownTitles.addAll([
s.for_whole_period_of_time,
s.for_a_year,
s.for_a_month,
s.for_a_week,
]);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CustomDropDownButton<PeriodFilterStatus>( CustomDropDownButton(
hint: Text( hint: Text(
s.choose_diet_duration, s.choose_diet_duration,
style: TextStyles.textSMedium.copyWith(color: appTheme.black), style: TextStyles.textSMedium.copyWith(color: appTheme.black),
@@ -31,59 +44,24 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
setState(() { setState(() {
_currentState = value; periodDropDownIndex = periodDropDownTitles.indexOf(value);
}); });
if (widget.isStatistic) {
if (value == PeriodFilterStatus.all) {
context.read<ConsultantRatingCubit>().getConsultantStatistics();
return;
}
context.read<ConsultantRatingCubit>().getConsultantStatistics(date: value.value);
return;
}
if (value == PeriodFilterStatus.all) {
context.read<ConsultantRatingCubit>().getScoreLogs();
return;
}
context.read<ConsultantRatingCubit>().getScoreLogs(date: value.value);
return;
} }
}, },
items: PeriodFilterStatus.values items: periodDropDownTitles
.mapIndexed( .mapIndexed(
(index, value) => DropdownMenuItem( (index, value) => DropdownMenuItem(
value: PeriodFilterStatus.values[index], value: periodDropDownTitles[index],
child: Text( child: Text(
PeriodFilterStatus.values[index].name, periodDropDownTitles[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black), style: TextStyles.textMMedium.copyWith(color: appTheme.black),
), ),
), ),
) )
.toList(), .toList(),
value: _currentState, value: periodDropDownTitles[periodDropDownIndex],
), ),
], ],
); );
} }
} }
enum PeriodFilterStatus {
all,
month,
week,
year;
String get name => switch (this) {
PeriodFilterStatus.all => 'Все периоды',
PeriodFilterStatus.month => 'За месяц',
PeriodFilterStatus.week => 'За неделю',
PeriodFilterStatus.year => 'За год',
};
String? get value => switch (this) {
PeriodFilterStatus.all => null,
PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'),
PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'),
PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'),
};
}

View File

@@ -11,6 +11,7 @@ class RatingHistoryItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 80,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: appTheme.gray100, color: appTheme.gray100,
@@ -28,7 +29,7 @@ class RatingHistoryItem extends StatelessWidget {
date.format('dd.MM.yyyy'), date.format('dd.MM.yyyy'),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray), style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray),
), ),
const SizedBox(height: 12), const Spacer(),
Text( Text(
title, title,
style: TextStyles.textMMedium.copyWith(color: appTheme.black), style: TextStyles.textMMedium.copyWith(color: appTheme.black),

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
@@ -10,8 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
class RatingHistoryWidget extends StatelessWidget { class RatingHistoryWidget extends StatelessWidget {
const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading}); const RatingHistoryWidget({super.key, required this.ratingHistory});
final bool isLoading;
final List<RatingModel> ratingHistory; final List<RatingModel> ratingHistory;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -22,60 +20,50 @@ class RatingHistoryWidget extends StatelessWidget {
right: 16, right: 16,
top: 16, top: 16,
), ),
child: isLoading child: Column(
? const Center(child: Loader()) children: [
: Column( Row(
children: [ children: [
Row( Text(
children: [ s.rating_history,
Text( style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
s.rating_history, ),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), const Spacer(),
), SvgPicture.asset(
const Spacer(), 'assets/icons/question_mark.svg',
SvgPicture.asset( height: 22,
'assets/icons/question_mark.svg', width: 22,
height: 22, colorFilter: ColorFilter.mode(
width: 22, appTheme.blueA400,
colorFilter: ColorFilter.mode( BlendMode.srcIn,
appTheme.blueA400,
BlendMode.srcIn,
),
),
],
), ),
const SizedBox( ),
height: 16, ],
),
const SizedBox(
height: 16,
),
const PeriodFilterWidget(),
const SizedBox(
height: 12,
),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Padding(
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
child: RatingHistoryItem(
title: ratingHistory[index].title,
amount: ratingHistory[index].amount,
date: ratingHistory[index].date,
), ),
const PeriodFilterWidget(isStatistic: false), );
const SizedBox( },
height: 12, itemCount: ratingHistory.length,
), shrinkWrap: true,
ListView.builder( ),
physics: const NeverScrollableScrollPhysics(), ],
itemBuilder: (context, index) { ),
return Padding(
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
child: RatingHistoryItem(
title: switch (ratingHistory[index].type) {
RatingEnum.addedRecipe => 'Добавление рецепта',
RatingEnum.addedPublicDiet => 'Добавление публичной диеты',
RatingEnum.addedBlogPost => 'Добавление поста',
RatingEnum.invitedUser => 'Регистрация по реф. ссылке',
RatingEnum.hasMedicalDegree => 'Получение мед. степени',
RatingEnum.consulted => 'Консультация',
RatingEnum.addedGlossary => 'Добавление продукта в глоссарий',
},
amount: ratingHistory[index].amount,
date: ratingHistory[index].date,
),
);
},
itemCount: ratingHistory.length,
shrinkWrap: true,
),
],
),
); );
} }
} }

View File

@@ -1,8 +1,6 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
@@ -10,15 +8,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
class StatisticWidget extends StatelessWidget { class StatisticWidget extends StatelessWidget {
const StatisticWidget({super.key, required this.isLoading, required this.statistics}); const StatisticWidget({super.key});
final bool isLoading;
final StatisticsModel? statistics;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return MainBackgroundBouble( return MainBackgroundBouble(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: isLoading ? const Center(child: Loader()) : Column( child: Column(
children: [ children: [
Row( Row(
children: [ children: [
@@ -41,17 +38,15 @@ class StatisticWidget extends StatelessWidget {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
const PeriodFilterWidget( const PeriodFilterWidget(),
isStatistic: true,
),
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
Row( Row(
children: [ children: [
Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)), Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)), Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
], ],
), ),
const SizedBox( const SizedBox(
@@ -59,9 +54,9 @@ class StatisticWidget extends StatelessWidget {
), ),
Row( Row(
children: [ children: [
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)), Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)), Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
], ],
), ),
const SizedBox( const SizedBox(
@@ -69,9 +64,9 @@ class StatisticWidget extends StatelessWidget {
), ),
Row( Row(
children: [ children: [
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
], ],
), ),
const SizedBox( const SizedBox(

View File

@@ -1,26 +0,0 @@
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';
class ConsultantRatingRepository {
final ConsultantRatingService service;
ConsultantRatingRepository(this.service);
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
try {
return await service.getConsultantStatistics(date: date);
} catch (e) {
rethrow;
}
}
Future<List<RatingModel>> getScoreLogs({String? date}) async {
try {
final data = (await service.getScoreLogs(date: date)).data;
return data;
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,18 +0,0 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:retrofit/retrofit.dart';
part 'consultant_rating_service.g.dart';
@RestApi()
abstract class ConsultantRatingService {
factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService;
@GET('/api/consultants/rating/statistics')
Future<StatisticsModel> getConsultantStatistics({@Query('filters[duration][\$gte]') String? date});
@GET('/api/consultants/score/logs')
Future<RatingPaginationModel> getScoreLogs({@Query('filters[duration][\$gte]') String? date});
}

View File

@@ -354,7 +354,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
); );
} }
_legendTitle(String text, Color color) { Row _legendTitle(String text, Color color) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@@ -194,6 +194,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
isAuthor: widget.diet.author.authorIsMe, isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}', path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet, complainedContentType: ComplainedContent.diet,
userSlug: widget.diet.author.authorSlug,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
], ],
@@ -347,7 +348,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
); );
} }
_dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) { Padding _dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) {
choiceChipWidget(int index) { choiceChipWidget(int index) {
return ChoiceChip( return ChoiceChip(
selectedColor: appTheme.blueA400, selectedColor: appTheme.blueA400,
@@ -434,7 +435,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
); );
} }
_daily(DailySchedule? daily, S s) { SingleChildRenderObjectWidget _daily(DailySchedule? daily, S s) {
if (daily == null) return const SizedBox.shrink(); if (daily == null) return const SizedBox.shrink();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),

View File

@@ -289,7 +289,7 @@ class _DayWidgetState extends State<DayWidget> {
); );
} }
_product(int index, String product, int quantity, String portion, int dayIndex, int mealIndex) { Material _product(int index, String product, int quantity, String portion, int dayIndex, int mealIndex) {
return Material( return Material(
child: ListTile( child: ListTile(
onTap: () { onTap: () {
@@ -313,7 +313,7 @@ class _DayWidgetState extends State<DayWidget> {
); );
} }
_modalBottomSheet({ Future _modalBottomSheet({
required CreateDietCubit createDietCubit, required CreateDietCubit createDietCubit,
required int dayIndex, required int dayIndex,
required int mealIndex, required int mealIndex,

View File

@@ -32,7 +32,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
), ),
); );
addStep() { void addStep() {
emit( emit(
state.copyWith.entity( state.copyWith.entity(
steps: [ steps: [
@@ -43,13 +43,13 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
); );
} }
removeStep(StepEntity step) { void removeStep(StepEntity step) {
List<StepEntity> steps = [...state.entity.steps]; List<StepEntity> steps = [...state.entity.steps];
steps.remove(step); steps.remove(step);
emit(state.copyWith.entity(steps: steps)); emit(state.copyWith.entity(steps: steps));
} }
addIngredient(i.Ingredient entity) { void addIngredient(i.Ingredient entity) {
emit( emit(
state.copyWith.entity( state.copyWith.entity(
ingredients: [ ingredients: [
@@ -60,7 +60,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
); );
} }
removeIngredient(int index) { void removeIngredient(int index) {
List<i.Ingredient> ingredients = [...state.entity.ingredients]; List<i.Ingredient> ingredients = [...state.entity.ingredients];
ingredients.removeAt(index); ingredients.removeAt(index);
@@ -73,26 +73,26 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(ingredients: ingredients)); emit(state.copyWith.entity(ingredients: ingredients));
} }
replaceIngredientPosition(int oldIndex, int newIndex) { void replaceIngredientPosition(int oldIndex, int newIndex) {
List<i.Ingredient> ingredients = [...state.entity.ingredients]; List<i.Ingredient> ingredients = [...state.entity.ingredients];
final item = ingredients.removeAt(oldIndex); final item = ingredients.removeAt(oldIndex);
ingredients.insert(newIndex, item); ingredients.insert(newIndex, item);
emit(state.copyWith.entity(ingredients: ingredients)); emit(state.copyWith.entity(ingredients: ingredients));
} }
replaceTitle(String title) { void replaceTitle(String title) {
emit(state.copyWith.entity(title: title == '' ? null : title)); emit(state.copyWith.entity(title: title == '' ? null : title));
} }
replaceDescription(String description) { void replaceDescription(String description) {
emit(state.copyWith.entity(description: description == '' ? null : description)); emit(state.copyWith.entity(description: description == '' ? null : description));
} }
changeType(TypeOfRecipe typeOfRecipe) { void changeType(TypeOfRecipe typeOfRecipe) {
emit(state.copyWith.entity(typeOfRecipe: typeOfRecipe)); emit(state.copyWith.entity(typeOfRecipe: typeOfRecipe));
} }
addTag(String tag) { void addTag(String tag) {
emit( emit(
state.copyWith.entity( state.copyWith.entity(
tags: [ tags: [
@@ -103,46 +103,46 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
); );
} }
removeTag(String tag) { void removeTag(String tag) {
List<String> tags = [...state.entity.tags]; List<String> tags = [...state.entity.tags];
tags.remove(tag); tags.remove(tag);
emit(state.copyWith.entity(tags: tags)); emit(state.copyWith.entity(tags: tags));
} }
replacePortionsCount(int count) { void replacePortionsCount(int count) {
emit(state.copyWith.entity(countOfPortions: count)); emit(state.copyWith.entity(countOfPortions: count));
} }
replaceEnegryInPortion(double energy) { void replaceEnegryInPortion(double energy) {
emit(state.copyWith.entity(energyInOnePortion: energy)); emit(state.copyWith.entity(energyInOnePortion: energy));
} }
setTotalCookingMinutes(int minutes) { void setTotalCookingMinutes(int minutes) {
emit(state.copyWith.entity(totalCookMinutes: minutes)); emit(state.copyWith.entity(totalCookMinutes: minutes));
} }
setTotalCookingHours(int hours) { void setTotalCookingHours(int hours) {
emit(state.copyWith.entity(totalCookHours: hours)); emit(state.copyWith.entity(totalCookHours: hours));
} }
setActiveCookingMinutes(int minutes) { void setActiveCookingMinutes(int minutes) {
emit(state.copyWith.entity(activeCookMinutes: minutes)); emit(state.copyWith.entity(activeCookMinutes: minutes));
} }
setActiveCookingHours(int hours) { void setActiveCookingHours(int hours) {
emit(state.copyWith.entity(activeCookHours: hours)); emit(state.copyWith.entity(activeCookHours: hours));
} }
replaceVideoUrl(String videoUrl) { void replaceVideoUrl(String videoUrl) {
emit(state.copyWith.entity(videoUrl: videoUrl)); emit(state.copyWith.entity(videoUrl: videoUrl));
} }
replaceImage(File? image) { void replaceImage(File? image) {
emit(state.copyWith.entity(image: image)); emit(state.copyWith.entity(image: image));
} }
replaceDescriptionInStep(String description, int index) { void replaceDescriptionInStep(String description, int index) {
List<StepEntity> steps = [...state.entity.steps]; List<StepEntity> steps = [...state.entity.steps];
steps[index] = steps[index].copyWith(description: description); steps[index] = steps[index].copyWith(description: description);
@@ -150,7 +150,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(steps: steps)); emit(state.copyWith.entity(steps: steps));
} }
replaceImageInStep(File? image, int index) { void replaceImageInStep(File? image, int index) {
List<StepEntity> steps = [...state.entity.steps]; List<StepEntity> steps = [...state.entity.steps];
steps[index] = steps[index].copyWith(image: image); steps[index] = steps[index].copyWith(image: image);
@@ -182,7 +182,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
} }
} }
disableLoading() { void disableLoading() {
emit(state.copyWith(loading: false)); emit(state.copyWith(loading: false));
} }

View File

@@ -69,7 +69,7 @@ class _ImageWidgetState extends State<ImageWidget> {
); );
} }
_showImagePicker() { void _showImagePicker() {
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
context: context, context: context,
builder: (context) { builder: (context) {

View File

@@ -112,7 +112,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
); );
} }
_modalBottomSheet({ Future _modalBottomSheet({
required CreateRecipeCubit createRecipeCubit, required CreateRecipeCubit createRecipeCubit,
int? portionIndex, int? portionIndex,
String? productNameControllerText, String? productNameControllerText,

View File

@@ -82,7 +82,7 @@ class _StepImageWidgetState extends State<StepImageWidget> {
); );
} }
_showImagePicker() { void _showImagePicker() {
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
context: context, context: context,
builder: (context) { builder: (context) {

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
@@ -51,6 +52,29 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
GetPlansComments(), GetPlansComments(),
SendPlansComment(), SendPlansComment(),
GetRecipesComments(), GetRecipesComments(),
AuthServiceTest(),
DiaryServiceTest(),
BlockedUserServiceTest(),
PlanServiceTest(),
AboutRecipeServiceTest(),
BlogPostServiceTest(),
CalendarServiceTest(),
CardsServiceTest(),
ConsultantServiceTest(),
ConsultationServiceTest(),
CreateDietServiceTest(),
CreatePlanServiceTest(),
GlossaryServiceTest(),
MyDietsServiceTest(),
MyPlansServiceTest(),
MyRecipesServiceTest(),
NotificationsServiceTest(),
PlansServiceTest(),
ProfileServiceTest(),
RegistrationServiceTest(),
ResetPasswordServiceTest(),
SettingsServiceTest(),
WsServiceTest(),
]; ];
final controller = TextEditingController(); final controller = TextEditingController();
late final List<TestState> testsStates; late final List<TestState> testsStates;
@@ -81,6 +105,8 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
onPressed: () { onPressed: () {
setState(() { setState(() {
baseUrlForTest = controller.text; baseUrlForTest = controller.text;
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear(); controller.clear();
}); });
}, },
@@ -89,43 +115,50 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (baseUrlForTest != baseUrl) CustomOutlinedButton(
CustomOutlinedButton( text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url',
text: 'Set dev2 base url', onPressed: () {
onPressed: () { setState(() {
setState(() { if (baseUrl == 'https://eatvio.ru') {
baseUrlForTest = baseUrl; baseUrlForTest = 'https://dev2.eatvio.com';
}); } else {
}, baseUrlForTest = 'https://eatvio.ru';
buttonStyle: const ButtonStyle(), }
), baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear();
});
},
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
ListView.builder( if (baseUrl.toLowerCase() != 'https://eatvio.ru')
shrinkWrap: true, ListView.builder(
itemCount: testCases.length, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), itemCount: testCases.length,
itemBuilder: physics: const NeverScrollableScrollPhysics(),
(context, index) => Padding( itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10.0), padding: const EdgeInsets.only(bottom: 10.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: Text('${index + 1}. ${testCases[index].name}')), Expanded(child: Text('${index + 1}. ${testCases[index].name}')),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
CustomOutlinedButton( CustomOutlinedButton(
text: 'run', text: 'run',
height: 20, height: 20,
width: 60, width: 60,
onPressed: () async { onPressed: () async {
if (testsStates[index] == TestState.loading) return; if (testsStates[index] == TestState.loading) return;
setState(() { setState(() {
testsStates[index] = TestState.loading; testsStates[index] = TestState.loading;
}); });
try {
final result = await testCases[index].run(baseUrl: baseUrlForTest); final result = await testCases[index].run(baseUrl: baseUrlForTest);
if (result) { if (result.success) {
setState(() { setState(() {
testsStates[index] = TestState.success; testsStates[index] = TestState.success;
}); });
@@ -135,27 +168,65 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
testsStates[index] = TestState.fail; testsStates[index] = TestState.fail;
}); });
showErrorToast('${testCases[index].name} failed'); showErrorToast('${testCases[index].name} failed');
// Show AlertDialog with error description
if (context.mounted) {
showDialog(
context: AppRouter.navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
} }
}, } catch (e, stack) {
buttonStyle: const ButtonStyle(), setState(() {
), testsStates[index] = TestState.fail;
const SizedBox(width: 5), });
switch (testsStates[index]) { showErrorToast('${testCases[index].name} failed');
TestState.success => Container(width: 20, height: 20, color: Colors.green), // Show AlertDialog with error description
TestState.fail => Container(width: 20, height: 20, color: Colors.red), if (context.mounted) {
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), showDialog(
TestState.loading => const SizedBox( context: AppRouter.navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text('$e\n$stack')),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
}
},
buttonStyle: const ButtonStyle(),
),
const SizedBox(width: 5),
switch (testsStates[index]) {
TestState.success => Container(width: 20, height: 20, color: Colors.green),
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
), ),
}, },
], ],
), ),
], ],
),
), ),
), ),
),
const SizedBox(height: 10), const SizedBox(height: 10),
CustomOutlinedButton( CustomOutlinedButton(
text: 'add token to server', text: 'add token to server',

View File

@@ -4,19 +4,48 @@ import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart'; import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
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';
class TestResult {
final bool success;
final String? error;
TestResult(this.success, [this.error]);
}
sealed class TestCases { sealed class TestCases {
String get name => throw UnimplementedError(); String get name => throw UnimplementedError();
Future<bool> run({required String baseUrl}) async => throw UnimplementedError(); Future<TestResult> run({required String baseUrl}) async => throw UnimplementedError();
} }
class CreateRecipeTask implements TestCases { class CreateRecipeTask implements TestCases {
@@ -24,7 +53,7 @@ class CreateRecipeTask implements TestCases {
String get name => 'api/recipes/create'; String get name => 'api/recipes/create';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl)); final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
@@ -41,10 +70,16 @@ class CreateRecipeTask implements TestCases {
steps: [Step(description: 'desc')], steps: [Step(description: 'desc')],
), ),
); );
} catch (e) { } on DioException catch (e) {
return false; final res = e.response;
if (res != null && (res.statusCode == null || res.statusCode! < 200 || res.statusCode! >= 300)) {
return TestResult(false, 'DioError: statusCode=${res.statusCode}\n${res.data}');
}
return TestResult(false, e.toString());
} catch (e, stack) {
return TestResult(false, '$e\n$stack');
} }
return true; return TestResult(true);
} }
} }
@@ -53,69 +88,54 @@ class Test850task implements TestCases {
String get name => 'task #850'; String get name => 'task #850';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
// get all of needed services
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final profileService = ProfileService(dio, baseUrl: baseUrl); final profileService = ProfileService(dio, baseUrl: baseUrl);
final dietsService = DietService(dio, baseUrl: baseUrl); final dietsService = DietService(dio, baseUrl: baseUrl);
// get all of needed data
final dietsPaginationModel = await feedService.getDiets(page: 1); final dietsPaginationModel = await feedService.getDiets(page: 1);
final chosenDietsPaginationModel = await profileService.getChosenDiets(); final chosenDietsPaginationModel = await profileService.getChosenDiets();
final lastChosenDiet = chosenDietsPaginationModel.diets.firstOrNull; final lastChosenDiet = chosenDietsPaginationModel.diets.firstOrNull;
final today = DateTime.now(); final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1)); final yesterday = today.subtract(const Duration(days: 1));
final tomorrow = today.add(const Duration(days: 1)); final tomorrow = today.add(const Duration(days: 1));
// run test case
for (final diet in dietsPaginationModel.data) { for (final diet in dietsPaginationModel.data) {
if (!diet.author.authorIsMe) { if (!diet.author.authorIsMe) {
if (lastChosenDiet?.state == 'started' || lastChosenDiet?.state == 'inited') { if (lastChosenDiet?.state == 'started' || lastChosenDiet?.state == 'inited') {
try { try {
await dietsService.cancelDiet(id: lastChosenDiet?.dietId ?? 0); await dietsService.cancelDiet(id: lastChosenDiet?.dietId ?? 0);
} catch (e) { } catch (e, stack) {
log('unexpected error7: $e'); log('unexpected error7: $e');
return false; return TestResult(false, '$e\n$stack');
} }
await Future.delayed(const Duration(seconds: 3)); await Future.delayed(const Duration(seconds: 3));
} }
try { try {
await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')}); await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')});
return TestResult(false, 'Expected error but got success');
return false;
} catch (e) { } catch (e) {
log('expect ot have error: $e'); log('expect ot have error: $e');
} }
// test start diet with correct date
try { try {
await dietsService.startDiet(id: diet.id, body: {'date': today.format('yyyy-MM-dd')}); await dietsService.startDiet(id: diet.id, body: {'date': today.format('yyyy-MM-dd')});
} catch (e) { } catch (e, stack) {
log('unexpected error2: $e'); log('unexpected error2: $e');
return false; return TestResult(false, '$e\n$stack');
} }
// test cancelation of diet
try { try {
await dietsService.cancelDiet(id: diet.id); await dietsService.cancelDiet(id: diet.id);
} catch (e) { } catch (e, stack) {
log('unexpected error3: $e'); log('unexpected error3: $e');
return false; return TestResult(false, '$e\n$stack');
} }
await Future.delayed(const Duration(seconds: 3)); await Future.delayed(const Duration(seconds: 3));
// test start diet with correct date
try { try {
await dietsService.startDiet(id: diet.id, body: {'date': tomorrow.format('yyyy-MM-dd')}); await dietsService.startDiet(id: diet.id, body: {'date': tomorrow.format('yyyy-MM-dd')});
} catch (e) { } catch (e, stack) {
log('unexpected error4: $e'); log('unexpected error4: $e');
return false; return TestResult(false, '$e\n$stack');
} }
// test cancelation of diet
try { try {
await dietsService.cancelDiet(id: diet.id); await dietsService.cancelDiet(id: diet.id);
} catch (e) { } catch (e) {
@@ -124,11 +144,10 @@ class Test850task implements TestCases {
break; break;
} }
} }
return TestResult(true);
return true; } catch (e, stack) {
} catch (e) {
log('unexpected error6: $e'); log('unexpected error6: $e');
return false; return TestResult(false, '$e\n$stack');
} }
} }
} }
@@ -139,16 +158,16 @@ class GetChatRoomPreviews implements TestCases {
String get name => '/api/chat/index'; String get name => '/api/chat/index';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
// get all of needed services // get all of needed services
// final dio = sl<Dio>(); // final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl); // final chatService = ChatService(dio, baseUrl: baseUrl);
// await chatService.getChatRooms(); // await chatService.getChatRooms();
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -158,16 +177,16 @@ class GetChatRoom implements TestCases {
String get name => '/api/chat/show/{id}'; String get name => '/api/chat/show/{id}';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
// final dio = sl<Dio>(); // final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl); // final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews(); // final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatRoom(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? ''); // await chatService.getChatRoom(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '');
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -177,16 +196,16 @@ class SendMessage implements TestCases {
String get name => '/api/chat/{id}/message/send'; String get name => '/api/chat/{id}/message/send';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
// final dio = sl<Dio>(); // final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl); // final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews(); // final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.sendMessage(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', data: 'test message'); // await chatService.sendMessage(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', data: 'test message');
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -196,16 +215,16 @@ class GetChatMessages implements TestCases {
String get name => '/api/chat/show/{id}/messages?page={page}'; String get name => '/api/chat/show/{id}/messages?page={page}';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
// final dio = sl<Dio>(); // final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl); // final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews(); // final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatMessages(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', page: 1); // await chatService.getChatMessages(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', page: 1);
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -215,7 +234,7 @@ class GetChatRoomIdByUserSlug implements TestCases {
String get name => '/api/chat/get_by_user/{user}'; String get name => '/api/chat/get_by_user/{user}';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl); final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -230,9 +249,9 @@ class GetChatRoomIdByUserSlug implements TestCases {
break; break;
} }
} }
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -243,14 +262,14 @@ class GetBlogPostList implements TestCases {
String get name => '/api/blogPosts/index'; String get name => '/api/blogPosts/index';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getBlogPostList(page: 1); await feedService.getBlogPostList(page: 1);
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -260,7 +279,7 @@ class GetRecipes implements TestCases {
String get name => '/api/recipes/index'; String get name => '/api/recipes/index';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -269,9 +288,9 @@ class GetRecipes implements TestCases {
final tags = tagsModel.data; final tags = tagsModel.data;
final tagsIds = tags.map((e) => e.id).toList(); final tagsIds = tags.map((e) => e.id).toList();
await feedService.getRecipes(page: 1, tags: tagsIds, query: null); await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -281,14 +300,14 @@ class GetDiets implements TestCases {
String get name => '/api/diets/index'; String get name => '/api/diets/index';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getDiets(page: 1, query: null); await feedService.getDiets(page: 1, query: null);
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -298,14 +317,14 @@ class GetPlans implements TestCases {
String get name => '/api/plans/index'; String get name => '/api/plans/index';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getPlans(page: 1, query: null); await feedService.getPlans(page: 1, query: null);
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -315,16 +334,16 @@ class GetBlogPostComments implements TestCases {
String get name => '/api/blogPosts/{id}/comments/show'; String get name => '/api/blogPosts/{id}/comments/show';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null); final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList; final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.getBlogPostComments(id: blogPosts.first.id.toString()); await feedService.getBlogPostComments(id: blogPosts.first.id.toString());
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -334,16 +353,16 @@ class SendBlogPostComment implements TestCases {
String get name => '/api/blogPosts/{id}/comment/create'; String get name => '/api/blogPosts/{id}/comment/create';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null); final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList; final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.sendBlogPostComment(id: blogPosts.first.id.toString(), body: 'auto test message'); await feedService.sendBlogPostComment(id: blogPosts.first.id.toString(), body: 'auto test message');
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -353,16 +372,16 @@ class GetDietsComments implements TestCases {
String get name => '/api/diets/{id}/comments/show'; String get name => '/api/diets/{id}/comments/show';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null); final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data; final diets = dietsModel.data;
await feedService.getDietsComments(id: diets.first.id.toString()); await feedService.getDietsComments(id: diets.first.id.toString());
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -372,16 +391,16 @@ class SendDietsComment implements TestCases {
String get name => '/api/diets/{id}/comment/create'; String get name => '/api/diets/{id}/comment/create';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null); final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data; final diets = dietsModel.data;
await feedService.sendDietsComment(id: diets.first.id.toString(), body: 'auto test message'); await feedService.sendDietsComment(id: diets.first.id.toString(), body: 'auto test message');
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -391,16 +410,16 @@ class GetPlansComments implements TestCases {
String get name => '/api/plans/{slug}/comments/show'; String get name => '/api/plans/{slug}/comments/show';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null); final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data; final plans = plansModel.data;
await feedService.getPlansComments(slug: plans.first.slug); await feedService.getPlansComments(slug: plans.first.slug);
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -410,16 +429,16 @@ class SendPlansComment implements TestCases {
String get name => '/api/plans/{slug}/comment/create'; String get name => '/api/plans/{slug}/comment/create';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null); final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data; final plans = plansModel.data;
await feedService.sendPlansComment(slug: plans.first.slug, body: 'auto test message'); await feedService.sendPlansComment(slug: plans.first.slug, body: 'auto test message');
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
} }
} }
} }
@@ -429,7 +448,7 @@ class GetRecipesComments implements TestCases {
String get name => '/api/recipes/{id}/comments/show'; String get name => '/api/recipes/{id}/comments/show';
@override @override
Future<bool> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl); final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -440,9 +459,426 @@ class GetRecipesComments implements TestCases {
final recipesModel = await feedService.getRecipes(page: 1, tags: tagsIds, query: null); final recipesModel = await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
final recipes = recipesModel.recipesList; final recipes = recipesModel.recipesList;
await feedService.getRecipesComments(id: recipes.first.id.toString()); await feedService.getRecipesComments(id: recipes.first.id.toString());
return true; return TestResult(true);
} catch (e) { } catch (e) {
return false; return TestResult(false, e.toString());
}
}
}
// AuthService
class AuthServiceTest implements TestCases {
@override
String get name => 'AuthService: getTokenByCredentials';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final authService = AuthService(dio, baseUrl: baseUrl);
// Use dummy credentials, expecting failure but no crash
await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890'));
return TestResult(false, 'Should not succeed');
} catch (e) {
return TestResult(true, 'Exception expected for dummy credentials');
}
}
}
// DiaryService
class DiaryServiceTest implements TestCases {
@override
String get name => 'DiaryService: getDiaryIndexByDate';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final diaryService = DiaryService(dio, baseUrl: baseUrl);
await diaryService.getDiaryIndexByDate(date: '2023-01-01');
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// BlockedUserService
class BlockedUserServiceTest implements TestCases {
@override
String get name => 'BlockedUserService: getBlockedUsers';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final blockedUserService = BlockedUserService(dio, baseUrl: baseUrl);
await blockedUserService.getBlockedUsers();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// PlanService
class PlanServiceTest implements TestCases {
@override
String get name => 'PlanService: getPlan';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final planService = PlanService(dio, baseUrl: baseUrl);
// Use a dummy slug, expecting failure but no crash
await planService.getPlan('dummy-slug');
return TestResult(false, 'Should not succeed');
} catch (e) {
return TestResult(true, 'Exception expected for dummy slug');
}
}
}
// AboutRecipeService
class AboutRecipeServiceTest implements TestCases {
@override
String get name => 'AboutRecipeService: getRecipe';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final aboutRecipeService = AboutRecipeService(dio, baseUrl: baseUrl);
await aboutRecipeService.getRecipe('dummy-id');
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for dummy id');
}
}
}
// BlogPostService
class BlogPostServiceTest implements TestCases {
@override
String get name => 'BlogPostService: createBlogPost';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final blogPostService = BlogPostService(dio, baseUrl: baseUrl);
// Use empty FormData, expecting failure but no crash
await blogPostService.createBlogPost(formData: FormData());
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for empty FormData');
}
}
}
// CalendarService
class CalendarServiceTest implements TestCases {
@override
String get name => 'CalendarService: getCalendar';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final calendarService = CalendarService(dio, baseUrl: baseUrl);
await calendarService.getCalendar();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// CardsService
class CardsServiceTest implements TestCases {
@override
String get name => 'CardsService: getCards';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final cardsService = CardsService(dio, baseUrl: baseUrl);
await cardsService.getCards();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// ConsultantService
class ConsultantServiceTest implements TestCases {
@override
String get name => 'ConsultantService: getMyRequestStatus';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final consultantService = ConsultantService(dio, baseUrl: baseUrl);
await consultantService.getMyRequestStatus(slug: 'dummy-slug');
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for dummy slug');
}
}
}
// ConsultationService
class ConsultationServiceTest implements TestCases {
@override
String get name => 'ConsultationService: getConsultations';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final consultationService = ConsultationService(dio);
await consultationService.getConsultations();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// CreateDietService
class CreateDietServiceTest implements TestCases {
@override
String get name => 'CreateDietService: getDietPortionsNutrition';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final createDietService = CreateDietService(dio, baseUrl: baseUrl);
await createDietService.getDietPortionsNutrition(diet: 0);
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for diet: 0');
}
}
}
// CreatePlanService
class CreatePlanServiceTest implements TestCases {
@override
String get name => 'CreatePlanService: createPlan';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final createPlanService = CreatePlanService(dio, baseUrl: baseUrl);
await createPlanService.createPlan(formData: FormData());
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for empty FormData');
}
}
}
// GlossaryService
class GlossaryServiceTest implements TestCases {
@override
String get name => 'GlossaryService: getGlossaryByQuery';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final glossaryService = GlossaryService(dio, baseUrl: baseUrl);
await glossaryService.getGlossaryByQuery(query: 'test');
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// MyDietsService
class MyDietsServiceTest implements TestCases {
@override
String get name => 'MyDietsService: getDiets';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final myDietsService = MyDietsService(dio, baseUrl: baseUrl);
await myDietsService.getDiets(page: 1);
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// MyPlansService
class MyPlansServiceTest implements TestCases {
@override
String get name => 'MyPlansService: getPlans';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final myPlansService = MyPlansService(dio, baseUrl: baseUrl);
await myPlansService.getPlans(page: 1);
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// MyRecipesService
class MyRecipesServiceTest implements TestCases {
@override
String get name => 'MyRecipesService: getMyRecipes';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final myRecipesService = MyRecipesService(dio, baseUrl: baseUrl);
await myRecipesService.getMyRecipes();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// NotificationsService
class NotificationsServiceTest implements TestCases {
@override
String get name => 'NotificationsService: getNotifications';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final notificationsService = NotificationsService(dio);
await notificationsService.getNotifications(page: 1);
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// PlansService
class PlansServiceTest implements TestCases {
@override
String get name => 'PlansService: getPlansBySlug';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final plansService = PlansService(dio, baseUrl: baseUrl);
await plansService.getPlansBySlug(author: 'dummy-author');
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for dummy author');
}
}
}
// ProfileService
class ProfileServiceTest implements TestCases {
@override
String get name => 'ProfileService: getMyProfile';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final profileService = ProfileService(dio, baseUrl: baseUrl);
await profileService.getMyProfile();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// RegistrationService
class RegistrationServiceTest implements TestCases {
@override
String get name => 'RegistrationService: getLocationsByAddress';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final registrationService = RegistrationService(dio, baseUrl: baseUrl);
await registrationService.getLocationsByAddress(query: 'test');
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// ResetPasswordService
class ResetPasswordServiceTest implements TestCases {
@override
String get name => 'ResetPasswordService: sendEmailCodeForPasswordChange';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final resetPasswordService = ResetPasswordService(dio, baseUrl: baseUrl);
await resetPasswordService.sendEmailCodeForPasswordChange(data: {'email': 'test@example.com'});
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for test@example.com');
}
}
}
// SettingsService
class SettingsServiceTest implements TestCases {
@override
String get name => 'SettingsService: getProfileSettings';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final settingsService = SettingsService(dio, baseUrl: baseUrl);
await settingsService.getProfileSettings();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// WsService
class WsServiceTest implements TestCases {
@override
String get name => 'WsService: getWebsocket';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final wsService = WsService(dio, baseUrl: baseUrl);
await wsService.getWebsocket();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
} }
} }
} }

View File

@@ -64,7 +64,7 @@ abstract class DiaryFutureDietDayModel with _$DiaryFutureDietDayModel {
factory DiaryFutureDietDayModel.fromJson(Map<String, dynamic> json) => _$DiaryFutureDietDayModelFromJson(json); factory DiaryFutureDietDayModel.fromJson(Map<String, dynamic> json) => _$DiaryFutureDietDayModelFromJson(json);
} }
DiaryDietModel? _parseDiet(json) { DiaryDietModel? _parseDiet(dynamic json) {
if (json == null) return null; if (json == null) return null;
return DiaryDietModel.fromJson(json['diet']); return DiaryDietModel.fromJson(json['diet']);
} }
@@ -80,7 +80,7 @@ abstract class DiaryDietModel with _$DiaryDietModel {
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json); factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
} }
TypeOfDiaryModel _parseDayTypeForFutureDay(json) { TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
return TypeOfDiaryModel( return TypeOfDiaryModel(
hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenDiet: json['has_chosen_diet'] as bool,
hasChosenPlan: json['has_chosen_plan'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool,

View File

@@ -55,7 +55,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return null; return null;
} }
setSelectedMeal(int meal) { void setSelectedMeal(int meal) {
selectedMeal = meal; selectedMeal = meal;
final currentState = state; final currentState = state;
if (currentState is DiaryStateLoaded) { if (currentState is DiaryStateLoaded) {
@@ -154,7 +154,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return false; return false;
} }
fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async { Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
this.diaryId ??= diaryId; this.diaryId ??= diaryId;
if (needLoading) { if (needLoading) {
@@ -215,7 +215,7 @@ class DiaryCubit extends Cubit<DiaryState> {
} }
} }
finishDiaryDay({ Future<void> finishDiaryDay({
required int diaryId, required int diaryId,
required String wakeUp, required String wakeUp,
required String retire, required String retire,
@@ -233,7 +233,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return (await diaryRepository.getNutritionData(index)); return (await diaryRepository.getNutritionData(index));
} }
addProductToMeal({ Future<void> addProductToMeal({
required int diaryId, required int diaryId,
required int mealId, required int mealId,
required String productName, required String productName,
@@ -252,7 +252,7 @@ class DiaryCubit extends Cubit<DiaryState> {
await fetchDiary(); await fetchDiary();
} }
updateProductInMeal({ Future<void> updateProductInMeal({
required int diaryId, required int diaryId,
required int mealId, required int mealId,
required int portionId, required int portionId,
@@ -286,22 +286,22 @@ class DiaryCubit extends Cubit<DiaryState> {
await fetchDiary(); await fetchDiary();
} }
markEaten({required int diaryId, required int mealId, required int portionId}) async { Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary(); await fetchDiary();
} }
unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary(); await fetchDiary();
} }
restore({required int diaryId, required int mealId, required int portionId}) async { Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId); await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary(); await fetchDiary();
} }
deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId); await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary(); await fetchDiary();
} }
@@ -310,12 +310,12 @@ class DiaryCubit extends Cubit<DiaryState> {
return await glossaryRepository.getGlossaryListByQuery(query); return await glossaryRepository.getGlossaryListByQuery(query);
} }
uploadPhotos({required List<File> files, required int diaryId}) async { Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal); await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await fetchDiary(); await fetchDiary();
} }
deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async { Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal); await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await fetchDiary(); await fetchDiary();
} }
@@ -335,32 +335,32 @@ class DiaryCubit extends Cubit<DiaryState> {
return null; return null;
} }
fillTime({required int diaryId, required String time}) async { Future<void> fillTime({required int diaryId, required String time}) async {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time); await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await fetchDiary(); await fetchDiary();
} }
fillWakeup({required int diaryId, required String time}) async { Future<void> fillWakeup({required int diaryId, required String time}) async {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time); await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await fetchDiary(); await fetchDiary();
} }
fillRetire({required int diaryId, required String time}) async { Future<void> fillRetire({required int diaryId, required String time}) async {
await diaryRepository.fillRetire(diaryId: diaryId, time: time); await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await fetchDiary(); await fetchDiary();
} }
fillSatiety({required int diaryId, required int satiety}) async { Future<void> fillSatiety({required int diaryId, required int satiety}) async {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety); await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await fetchDiary(); await fetchDiary();
} }
fillMood({required int diaryId, required int mood}) async { Future<void> fillMood({required int diaryId, required int mood}) async {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood); await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await fetchDiary(); await fetchDiary();
} }
fillComment(String comment) { void fillComment(String comment) {
this.comment = comment; this.comment = comment;
} }
} }

View File

@@ -24,8 +24,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DiaryScreen extends StatelessWidget { class DiaryScreen extends StatelessWidget {
const DiaryScreen({super.key}); const DiaryScreen({super.key, this.date});
final DateTime? date;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const DiaryScreenContent(); return const DiaryScreenContent();
@@ -52,9 +52,10 @@ class UserDiaryScreen extends StatelessWidget {
} }
class DiaryScreenContent extends StatelessWidget { class DiaryScreenContent extends StatelessWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay}); const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay, this.date});
final int? diaryId; final int? diaryId;
final int? currentPlanDay; final int? currentPlanDay;
final DateTime? date;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);

View File

@@ -21,7 +21,7 @@ class _AddProductButtonWidgetState extends State<AddProductButtonWidget> {
diaryCubit = context.read<DiaryCubit>(); diaryCubit = context.read<DiaryCubit>();
} }
addProductInMeal(DiaryStateLoaded state, [int? portionId]) { Future addProductInMeal(DiaryStateLoaded state, [int? portionId]) {
return showModalBottomSheet( return showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,

View File

@@ -8,8 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
class CalendarWidget extends StatefulWidget { class CalendarWidget extends StatefulWidget {
const CalendarWidget({super.key}); const CalendarWidget({super.key,});
@override @override
State<CalendarWidget> createState() => _CalendarWidgetState(); State<CalendarWidget> createState() => _CalendarWidgetState();
} }
@@ -134,13 +133,13 @@ class _CalendarWidgetState extends State<CalendarWidget> {
); );
} }
_nextCalendarFormat() { void _nextCalendarFormat() {
setState(() { setState(() {
_calendarFormat = CalendarFormat.values[(_calendarFormat.index - 1) % CalendarFormat.values.length]; _calendarFormat = CalendarFormat.values[(_calendarFormat.index - 1) % CalendarFormat.values.length];
}); });
} }
_arrowDown() { GestureDetector _arrowDown() {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onVerticalDragStart: (_) { onVerticalDragStart: (_) {

View File

@@ -242,7 +242,7 @@ class CaloriesChartWidget extends StatelessWidget {
); );
} }
_chartData(String title, String value) { Row _chartData(String title, String value) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@@ -46,7 +46,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
productNameController.text = widget.productName; productNameController.text = widget.productName;
} }
_alert() { Padding _alert() {
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@@ -137,7 +137,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
); );
} }
_success() { Padding _success() {
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@@ -189,7 +189,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
); );
} }
_addNewGlossaryProduct() { Padding _addNewGlossaryProduct() {
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(

View File

@@ -34,7 +34,7 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
diaryCubit = context.read<DiaryCubit>(); diaryCubit = context.read<DiaryCubit>();
} }
showImagePicker(DiaryDayModel diaryDay) { void showImagePicker(DiaryDayModel diaryDay) {
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
context: context, context: context,
builder: (context) { builder: (context) {

View File

@@ -44,7 +44,7 @@ class _ProductWidgetState extends State<ProductWidget> {
isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false; isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false;
} }
updateProductInMeal(DiaryStateLoaded state, PortionModel portion) { Future updateProductInMeal(DiaryStateLoaded state, PortionModel portion) {
return showModalBottomSheet( return showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,

View File

@@ -23,7 +23,7 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
diaryCubit = context.read<DiaryCubit>(); diaryCubit = context.read<DiaryCubit>();
} }
showTimePicker(int diaryId) { void showTimePicker(int diaryId) {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,

View File

@@ -26,7 +26,7 @@ class _WakeupAndRetireWidgetState extends State<WakeupAndRetireWidget> {
diaryCubit = context.read<DiaryCubit>(); diaryCubit = context.read<DiaryCubit>();
} }
showTimePicker(int diaryId, bool isWakeup) { void showTimePicker(int diaryId, bool isWakeup) {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart'; import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,10 +17,11 @@ class AboutDietCubit extends Cubit<AboutDietState> {
bool isCurrentDietStarted = false; bool isCurrentDietStarted = false;
bool hasStartedDiet = false; bool hasStartedDiet = false;
String? userStartedDietName; String? userStartedDietName;
List<int> _selectedIndexes = [];
Future<void> getDiet(int id, {int? index}) async { Future<void> getDiet(String slug, {int? index}) async {
try { try {
final diet = await dietsRepository.getDiet(id); final diet = await dietsRepository.getDietBySlug(slug);
if (index != null) { if (index != null) {
dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments)); dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments));
} }
@@ -30,10 +32,10 @@ class AboutDietCubit extends Cubit<AboutDietState> {
} }
} }
Future<void> initDietPage(int id) async { Future<void> initDietPage(String slug) async {
try { try {
emit(const AboutDietState.loading()); emit(const AboutDietState.loading());
final diet = await dietsRepository.getDiet(id); final diet = await dietsRepository.getDietBySlug(slug);
final chosenDiets = await profileRepository.getChosenDiets(); final chosenDiets = await profileRepository.getChosenDiets();
final lastChosenDiet = chosenDiets.firstOrNull; final lastChosenDiet = chosenDiets.firstOrNull;
if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) { if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) {
@@ -47,7 +49,13 @@ class AboutDietCubit extends Cubit<AboutDietState> {
} }
} }
} }
emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); _selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0);
emit(AboutDietState.loaded(
diet: diet,
isStarted: isCurrentDietStarted,
hasStartedDiet: hasStartedDiet,
selectedIndexes: _selectedIndexes,
));
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
emit(AboutDietState.error(message: e.toString())); emit(AboutDietState.error(message: e.toString()));
@@ -67,7 +75,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndexInFeed != null) { if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
} }
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
} else { } else {
final updatedDiet = currentDiet.copyWith( final updatedDiet = currentDiet.copyWith(
totalLikes: currentDiet.totalLikes + 1, totalLikes: currentDiet.totalLikes + 1,
@@ -76,7 +84,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndexInFeed != null) { if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
} }
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
} }
EasyDebounce.debounce( EasyDebounce.debounce(
@@ -91,7 +99,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
dietsRepository dietsRepository
.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); .emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
} }
emit(AboutDietState.loaded(diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
} }
}, },
); );
@@ -108,7 +116,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) { if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
} }
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce( EasyDebounce.debounce(
'add_or_remove_diet_to_favorites', 'add_or_remove_diet_to_favorites',
@@ -122,7 +130,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) { if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
} }
emit(AboutDietState.loaded(diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
} }
}, },
); );
@@ -139,7 +147,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) { if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
} }
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce( EasyDebounce.debounce(
'add_or_remove_diet_to_favorites', 'add_or_remove_diet_to_favorites',
@@ -153,7 +161,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) { if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
} }
emit(AboutDietState.loaded(diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
} }
}, },
); );
@@ -191,12 +199,31 @@ class AboutDietCubit extends Cubit<AboutDietState> {
void clearStartedDietsState() { void clearStartedDietsState() {
if (state is! DietStateLoaded) return; if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet; final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(diet: diet)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet));
} }
void emitCurrentStartedDietsState() { void emitCurrentStartedDietsState() {
if (state is! DietStateLoaded) return; if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet; final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(diet: diet, isStarted: true)); emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet, isStarted: true));
}
Future<Diet?> getDietBySlug(String slug) async {
try {
return await dietsRepository.getDietBySlug(slug);
} catch (e) {
log(e.toString());
}
return null;
}
void setSelectedIndex({required int dayNumber, required int index}) {
final currentState = state;
if (currentState is DietStateLoaded) {
final updatedSelectedIndexes = [...currentState.selectedIndexes];
updatedSelectedIndexes[dayNumber] = index;
_selectedIndexes = updatedSelectedIndexes;
emit(currentState.copyWith(selectedIndexes: _selectedIndexes));
}
} }
} }

View File

@@ -11,6 +11,7 @@ abstract class AboutDietState with _$AboutDietState {
const factory AboutDietState.loading() = DietStateLoading; const factory AboutDietState.loading() = DietStateLoading;
const factory AboutDietState.loaded({ const factory AboutDietState.loaded({
required Diet diet, required Diet diet,
@Default([]) List<int> selectedIndexes,
@Default(false) bool isStarted, @Default(false) bool isStarted,
@Default(false) bool hasStartedDiet, @Default(false) bool hasStartedDiet,
}) = DietStateLoaded; }) = DietStateLoaded;

View File

@@ -32,16 +32,14 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class AboutDietScreen extends StatefulWidget { class AboutDietScreen extends StatefulWidget {
final Diet diet; final String slug;
final int? index; final int? index;
final bool navigateToComments; final bool navigateToComments;
late final List<int> selectedIndexes;
AboutDietScreen({super.key, required this.diet, required this.index, required this.navigateToComments}) { const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug});
selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0);
}
@override @override
State<AboutDietScreen> createState() => _AboutDietScreenState(); State<AboutDietScreen> createState() => _AboutDietScreenState();
@@ -55,11 +53,11 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
final textEditingController = TextEditingController(); final textEditingController = TextEditingController();
bool isMoreOptionsVisible = false; bool isMoreOptionsVisible = false;
bool _jumped = false; bool _jumped = false;
bool _isFirstLaunch = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
context.read<FeedCubit>().viewDiet(widget.diet.id);
scrollController.addListener(() { scrollController.addListener(() {
if (isMoreOptionsVisible) { if (isMoreOptionsVisible) {
setState(() { setState(() {
@@ -79,15 +77,11 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return BlocProvider( return BlocProvider(
create: (context) => dietsCubit..initDietPage(widget.diet.id), create: (context) => dietsCubit..initDietPage(widget.slug),
child: BlocProvider<CommentsCubit>( child: BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit create: (BuildContext context) => commentsCubit,
..getComments(
widget.diet.id.toString(),
CommentSourceType.diet,
true,
),
child: BlocConsumer<CommentsCubit, CommentsState>( child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState commentsState) { listener: (BuildContext context, CommentsState commentsState) {
if (commentsState is CommentsStateLoaded) { if (commentsState is CommentsStateLoaded) {
@@ -100,6 +94,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
return BlocBuilder<AboutDietCubit, AboutDietState>( return BlocBuilder<AboutDietCubit, AboutDietState>(
builder: (context, dietState) { builder: (context, dietState) {
if (dietState is DietStateLoaded) { if (dietState is DietStateLoaded) {
if (_isFirstLaunch) {
context.read<FeedCubit>().viewDiet(dietState.diet.id);
context.read<CommentsCubit>().getComments(
dietState.diet.id.toString(),
CommentSourceType.diet,
true,
);
_isFirstLaunch = false;
}
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (widget.navigateToComments && !_jumped) { if (widget.navigateToComments && !_jumped) {
_scrollToBottom(); _scrollToBottom();
@@ -176,9 +179,9 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
isFavorite: dietState.diet.isFavorite, isFavorite: dietState.diet.isFavorite,
onTap: () { onTap: () {
if (dietState.diet.isFavorite) { if (dietState.diet.isFavorite) {
dietsCubit.removeFromFavorite(widget.diet.id, widget.index); dietsCubit.removeFromFavorite(dietState.diet.id, widget.index);
} else { } else {
dietsCubit.addToFavorite(widget.diet.id, widget.index); dietsCubit.addToFavorite(dietState.diet.id, widget.index);
} }
}, },
width: 52, width: 52,
@@ -269,7 +272,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
onTap: () { onTap: () {
if (textEditingController.text.length > 2) { if (textEditingController.text.length > 2) {
commentsCubit.sendComment( commentsCubit.sendComment(
id: widget.diet.id.toString(), id: dietState.diet.id.toString(),
commentText: textEditingController.text, commentText: textEditingController.text,
parentCommentId: replyId, parentCommentId: replyId,
commentSourceType: CommentSourceType.diet, commentSourceType: CommentSourceType.diet,
@@ -280,7 +283,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
replyId = null; replyId = null;
}); });
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
dietsCubit.getDiet(widget.diet.id, index: widget.index); dietsCubit.getDiet(dietState.diet.slug, index: widget.index);
}); });
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
} }
@@ -307,8 +310,17 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
), ),
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text( title: Text(
widget.diet.name, dietState.diet.name,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -317,11 +329,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
), ),
actions: [ actions: [
CustomPopup( CustomPopup(
isAuthor: widget.diet.author.authorIsMe, isAuthor: dietState.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}', path: 'diet/${dietState.diet.slug}',
complainedContentType: ComplainedContent.diet, complainedContentType: ComplainedContent.diet,
id: widget.diet.id, id: dietState.diet.id,
diet: widget.diet, diet: dietState.diet,
userSlug: dietState.diet.author.authorSlug,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
], ],
@@ -340,7 +353,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
Stack( Stack(
children: [ children: [
CachedNetworkImage( CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.diet.image}', imageUrl: '$baseUrl/storage/${dietState.diet.image}',
height: 300, height: 300,
width: MediaQuery.sizeOf(context).width, width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill, fit: BoxFit.fill,
@@ -366,7 +379,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
dietsCubit.likeDiet(widget.diet.id, widget.index); dietsCubit.likeDiet(dietState.diet.id, widget.index);
}, },
child: InfoBouble( child: InfoBouble(
backgroundColor: appTheme.whiteA700, backgroundColor: appTheme.whiteA700,
@@ -412,17 +425,17 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
InfoRow( InfoRow(
diet: widget.diet, diet: dietState.diet,
type: ProductType.diet, type: ProductType.diet,
), ),
ExpandableText( ExpandableText(
text: widget.diet.details, text: dietState.diet.details,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard( child: AuthorCard(
author: widget.diet.author, author: dietState.diet.author,
title: s.diet_author, title: s.diet_author,
), ),
), ),
@@ -436,7 +449,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center( child: Center(
child: Text( child: Text(
'${s.diet_plan}(${widget.diet.duration.toString()} ${getDayForm(widget.diet.duration, s)})', '${s.diet_plan}(${dietState.diet.duration.toString()} ${getDayForm(dietState.diet.duration, s)})',
style: TextStyles.headingXLSemibold.copyWith( style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black, color: appTheme.black,
), ),
@@ -449,9 +462,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s); return _dayOfPlan(
dietState.diet.dailySchedule[index + 1] ?? {},
index,
s,
dietState.selectedIndexes,
dietState.diet,
);
}, },
itemCount: widget.diet.dailySchedule.length, itemCount: dietState.diet.dailySchedule.length,
), ),
MainBackgroundBouble( MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
@@ -471,7 +490,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (state is CommentsStateLoading) const Loader(), if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded) if (state is CommentsStateLoaded)
CommentsWidget( CommentsWidget(
id: widget.diet.id.toString(), id: dietState.diet.id.toString(),
commentSourceType: CommentSourceType.diet, commentSourceType: CommentSourceType.diet,
comment: state.comment, comment: state.comment,
replyId: replyId, replyId: replyId,
@@ -488,51 +507,49 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text( title: Text(
widget.diet.name, '',
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: true, softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
), ),
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
),
const SizedBox(width: 16),
],
), ),
body: DataErrorWidget( body: DataErrorWidget(
onReload: () => context.read<AboutDietCubit>().getDiet(widget.diet.id, index: widget.index), onReload: () => context.read<AboutDietCubit>().getDiet(widget.slug, index: widget.index),
), ),
); );
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text( title: Text(
widget.diet.name, '',
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: true, softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
), ),
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
),
const SizedBox(width: 16),
],
), ),
body: const Loader(), body: const Loader(),
); );
@@ -544,7 +561,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
); );
} }
_dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) { Padding _dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s, List<int> selectedIndexes, Diet diet) {
choiceChipWidget(int index) { choiceChipWidget(int index) {
return ChoiceChip( return ChoiceChip(
selectedColor: appTheme.blueA400, selectedColor: appTheme.blueA400,
@@ -557,14 +574,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
label: Text((index + 1).toString()), label: Text((index + 1).toString()),
labelStyle: TextStyles.textXSSemibold.copyWith( labelStyle: TextStyles.textXSSemibold.copyWith(
color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600, color: index == selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
), ),
selected: widget.selectedIndexes[dayNumber] == index, selected: selectedIndexes[dayNumber] == index,
backgroundColor: appTheme.gray100, backgroundColor: appTheme.gray100,
onSelected: (bool selected) { onSelected: (bool selected) {
setState(() { dietsCubit.setSelectedIndex(dayNumber: dayNumber, index: index);
widget.selectedIndexes[dayNumber] = index;
});
}, },
); );
} }
@@ -612,18 +627,18 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber] + 1]?[index + 1], s), itemBuilder: (context, index) => _daily(day[selectedIndexes[dayNumber] + 1]?[index + 1], s),
itemCount: day[widget.selectedIndexes[dayNumber] + 1]?.length ?? 0, itemCount: day[selectedIndexes[dayNumber] + 1]?.length ?? 0,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
), ),
CpfcContainer( CpfcContainer(
calories: widget.diet.dailyNutrition[dayNumber].energy, calories: diet.dailyNutrition[dayNumber].energy,
proteins: widget.diet.dailyNutrition[dayNumber].proteins, proteins: diet.dailyNutrition[dayNumber].proteins,
fats: widget.diet.dailyNutrition[dayNumber].fats, fats: diet.dailyNutrition[dayNumber].fats,
carbohydrates: widget.diet.dailyNutrition[dayNumber].carbohydrates, carbohydrates: diet.dailyNutrition[dayNumber].carbohydrates,
), ),
], ],
), ),
@@ -631,7 +646,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
); );
} }
_daily(DailySchedule? daily, S s) { SingleChildRenderObjectWidget _daily(DailySchedule? daily, S s) {
if (daily == null) return const SizedBox(); if (daily == null) return const SizedBox();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),

View File

@@ -33,6 +33,15 @@ class DietRepository {
} }
} }
Future<Diet> getDietBySlug(String slug) async {
try {
final diet = await dietsService.getDietBySlug(slug: slug);
return diet;
} catch (e) {
rethrow;
}
}
Future<Diet> addToFavorite(int id) async { Future<Diet> addToFavorite(int id) async {
try { try {
return await dietsService.addToFavorite(id: id); return await dietsService.addToFavorite(id: id);

View File

@@ -29,6 +29,11 @@ abstract class DietService {
@Path() required int id, @Path() required int id,
}); });
@GET('/api/diets/show_by_slug/{slug}')
Future<Diet> getDietBySlug({
@Path() required String slug,
});
@POST('/api/diets/{id}/start') @POST('/api/diets/{id}/start')
Future<void> startDiet({ Future<void> startDiet({
@Path() required int id, @Path() required int id,

View File

@@ -46,7 +46,7 @@ abstract class PlanModel with _$PlanModel {
factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json); factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json);
} }
PacketOptions? parseOption(json) { PacketOptions? parseOption(dynamic json) {
if (json is Map<String, dynamic>) { if (json is Map<String, dynamic>) {
return PacketOptions.fromJson(json); return PacketOptions.fromJson(json);
} }
@@ -74,7 +74,7 @@ abstract class PacketOptions with _$PacketOptions {
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: ''); factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '');
} }
List<String> _parseItems(json) { List<String> _parseItems(dynamic json) {
if (json is Map<String, dynamic>) { if (json is Map<String, dynamic>) {
return json.values.map((e) => e as String).toList(); return json.values.map((e) => e as String).toList();
} }

View File

@@ -53,7 +53,7 @@ abstract class Ingredient with _$Ingredient {
factory Ingredient.fromJson(Map<String, Object?> json) => _$IngredientFromJson(json); factory Ingredient.fromJson(Map<String, Object?> json) => _$IngredientFromJson(json);
} }
int _parseProductId(json) { int _parseProductId(dynamic json) {
if (json is String) { if (json is String) {
return int.parse(json); return int.parse(json);
} }

View File

@@ -10,11 +10,11 @@ class CommentsCubit extends Cubit<CommentsState> {
required this.feedRepository, required this.feedRepository,
}) : super(const CommentsState()); }) : super(const CommentsState());
setReplyId(int? replyId) { void setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId)); emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
} }
getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async { Future<void> getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
if (isFirstFetch) emit(const CommentsState.loading()); if (isFirstFetch) emit(const CommentsState.loading());
try { try {
switch (commentSourceType) { switch (commentSourceType) {
@@ -36,7 +36,7 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
} }
sendComment({ Future<void> sendComment({
required String id, required String id,
required String commentText, required String commentText,
required CommentSourceType commentSourceType, required CommentSourceType commentSourceType,

View File

@@ -11,7 +11,7 @@ class ConsultantsCubit extends Cubit<ConsultantsState> {
required this.feedRepository, required this.feedRepository,
}) : super(const ConsultantsState()); }) : super(const ConsultantsState());
getConsultants() async { Future<void> getConsultants() async {
emit(const ConsultantsState.loading()); emit(const ConsultantsState.loading());
try { try {
emit( emit(

View File

@@ -86,7 +86,7 @@ class DietsCubit extends Cubit<DietsState> {
); );
} }
loadDiets({ Future<void> loadDiets({
String? query, String? query,
}) async { }) async {
try { try {

View File

@@ -16,7 +16,8 @@ class FeedCubit extends Cubit<FeedState> {
required this.feedRepository, required this.feedRepository,
}) : super(const FeedState.loading()); }) : super(const FeedState.loading());
updateFeedBlogPostList(FeedBlogPostModel feedBlogPostModel, bool wasLikedByMe) { void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
if (feedBlogPostModel == null) return;
List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexOf(feedBlogPostModel); final int index = feedBlogPostList.indexOf(feedBlogPostModel);
feedBlogPostList[index] = feedBlogPostModel.copyWith( feedBlogPostList[index] = feedBlogPostModel.copyWith(
@@ -31,7 +32,7 @@ class FeedCubit extends Cubit<FeedState> {
); );
} }
loadFeedBlogPostList({ Future<void> loadFeedBlogPostList({
String? query, String? query,
}) async { }) async {
try { try {
@@ -79,7 +80,7 @@ class FeedCubit extends Cubit<FeedState> {
} }
} }
likePost(int id) async { Future<void> likePost(int id) async {
try { try {
await feedRepository.likeBlogPost(id: id); await feedRepository.likeBlogPost(id: id);
} catch (e, st) { } catch (e, st) {
@@ -87,10 +88,10 @@ class FeedCubit extends Cubit<FeedState> {
} }
} }
unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id: id); Future<void> unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id: id);
viewPost(int id) async => await feedRepository.viewPost(id); Future<void> viewPost(int id) async => await feedRepository.viewPost(id);
viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future<void> viewRecipe(String slug) async => await feedRepository.viewRecipe(slug);
viewDiet(int id) async => await feedRepository.viewDiet(id); Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id);
viewPlan(String slug) async => await feedRepository.viewPlan(slug); Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug);
} }

View File

@@ -132,7 +132,7 @@ class PlansCubit extends Cubit<PlansState> {
); );
} }
loadPlans({ Future<void> loadPlans({
String? query, String? query,
}) async { }) async {
try { try {

View File

@@ -40,7 +40,7 @@ class RecipesCubit extends Cubit<RecipesState> {
}); });
} }
loadRecipesList({ Future<void> loadRecipesList({
String? query, String? query,
}) async { }) async {
try { try {
@@ -99,7 +99,7 @@ class RecipesCubit extends Cubit<RecipesState> {
} }
} }
updateTags(RecipeTag tag) { void updateTags(RecipeTag tag) {
_selectedTags.contains(tag.id) ? _selectedTags.remove(tag.id) : _selectedTags.add(tag.id); _selectedTags.contains(tag.id) ? _selectedTags.remove(tag.id) : _selectedTags.add(tag.id);
emit( emit(
state.copyWith( state.copyWith(

View File

@@ -21,6 +21,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../widgets/recipes.dart'; import '../widgets/recipes.dart';
@@ -84,7 +85,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
_onScrollConsultants() { void _onScrollConsultants() {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
} }
@@ -100,6 +101,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return Scaffold( return Scaffold(
floatingActionButton: _selectedChipIndex == 1 floatingActionButton: _selectedChipIndex == 1
? FloatingActionButton.small( ? FloatingActionButton.small(

View File

@@ -46,7 +46,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed( onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen', 'blogPostScreen',
extra: state.feedBlogPostList[widget.index], extra: state.feedBlogPostList[widget.index].id,
), ),
child: Material( child: Material(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -56,81 +56,86 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 12), const SizedBox(height: 12),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'profileScreen', 'profileScreen',
extra: Author( extra: Author(
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug, authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
authorName: state.feedBlogPostList[widget.index].author.authorName, authorName: state.feedBlogPostList[widget.index].author.authorName,
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon, authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe, ? state.feedBlogPostList[widget.index].author.authorProfileIcon
), : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
); authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
},
child: CircleAvatar(
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),
const SizedBox(width: 8),
// Имя и дата внутри Expanded
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.feedBlogPostList[widget.index].author.authorName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
fontSize: 16,
), ),
);
},
child: CircleAvatar(
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
? state.feedBlogPostList[widget.index].author.authorProfileIcon
: baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
), ),
const SizedBox(height: 4), backgroundImage: const AssetImage('assets/images/no_avatar.png'),
Text(
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
height: 1,
color: Colors.black45,
fontSize: 14,
),
),
],
),
),
// Кнопка подписки (если нужно)
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
child: SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
), ),
), ),
// Кнопка меню const SizedBox(width: 8),
CustomPopup( // Имя и дата внутри Expanded
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe, Expanded(
complainedContentType: ComplainedContent.posts, child: Column(
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}', crossAxisAlignment: CrossAxisAlignment.start,
id: state.feedBlogPostList[widget.index].id, children: [
feedBlogPostModel: state.feedBlogPostList[widget.index], Text(
), state.feedBlogPostList[widget.index].author.authorName,
], overflow: TextOverflow.ellipsis,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
height: 1,
color: Colors.black45,
fontSize: 14,
),
),
],
),
),
// Кнопка подписки (если нужно)
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
child: SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
),
),
// Кнопка меню
CustomPopup(
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
id: state.feedBlogPostList[widget.index].id,
feedBlogPostModel: state.feedBlogPostList[widget.index],
userSlug: state.feedBlogPostList[widget.index].author.authorSlug,
),
],
),
), ),
), if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[
if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
@@ -151,9 +156,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html( child: Html(
data: state.feedBlogPostList[widget.index].description.length > 200 data: (state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200) ? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description, : state.feedBlogPostList[widget.index].description).replaceAll('\n', '<br>'),
style: { style: {
"*": Style( "*": Style(
fontSize: FontSize(16), fontSize: FontSize(16),
@@ -234,7 +239,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
text: state.feedBlogPostList[widget.index].totalComments.toString(), text: state.feedBlogPostList[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed( onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen', 'blogPostScreen',
extra: state.feedBlogPostList[widget.index], extra: state.feedBlogPostList[widget.index].id,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),

View File

@@ -53,7 +53,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
); );
} }
_comment(S s, Comment comment, [double radius = 18, bool isChild = false]) { Container _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
return Container( return Container(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -72,13 +72,15 @@ class _CommentsWidgetState extends State<CommentsWidget> {
extra: Author( extra: Author(
authorSlug: comment.authorSlug, authorSlug: comment.authorSlug,
authorName: comment.authorName, authorName: comment.authorName,
authorProfileIcon: comment.authorProfileIcon, authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false, authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false,
), ),
); );
}, },
child: CircleAvatar( child: CircleAvatar(
foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon), foregroundImage: NetworkImage(
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'), backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius, radius: radius,
), ),

View File

@@ -121,7 +121,7 @@ class ConsultantWidget extends StatelessWidget {
); );
} }
_badges(List<BadgeModel> badges, BuildContext context) { SingleChildScrollView _badges(List<BadgeModel> badges, BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(

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