Compare commits

...

91 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
mso7nueh
fa1192c5e5 Merge pull request 'upd ver -> 28' (#510) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#510
2025-06-16 11:33:55 +00:00
mso7nueh
41f948c8f7 upd ver -> 28 2025-06-16 14:33:32 +03:00
mso7nueh
896b2783ae Merge pull request 'feat(consultantFinances), feat(userPurchases)' (#494) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#494
2025-06-16 11:31:05 +00:00
mso7nueh
9c004f8593 Merge pull request 'fix(diary): fixed app review issues' (#502) from misha/eatvio_app:diaryFixes into main
Reviewed-on: eatvio/eatvio_app#502
2025-06-16 11:30:56 +00:00
mso7nueh
a577a8d58a Merge pull request 'fix(chat): time Issue-440' (#509) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#509
2025-06-16 11:25:29 +00:00
mso7nueh
3b2c4fdff5 fix(chat): time 2025-06-16 13:41:39 +03:00
mso7nueh
44d6487709 Merge pull request 'fix: testflight' (#507) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#507
2025-06-11 13:35:40 +00:00
mso7nueh
319fd2dfe3 fix: testflight 2025-06-11 16:35:21 +03:00
mso7nueh
a98043758b Merge pull request 'fix: testflight' (#506) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#506
2025-06-11 13:28:49 +00:00
mso7nueh
a84d2d7daa fix: testflight 2025-06-11 16:28:27 +03:00
Username1548
4acb861b69 merged upstram/main 2025-06-11 20:24:18 +07:00
Username1548
f163608a90 feat(permissions): added galery and camera permisshion checker 2025-06-11 20:13:26 +07:00
6bbec1f022 Merge pull request 'Issue-440 & Issue-194 & build 27' (#503) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#503
2025-06-11 12:49:09 +00:00
mso7nueh
c0311bf213 feat(chat&notifications): Issue-191 and Issue-440 and auth screen policy 2025-06-11 15:34:31 +03:00
mso7nueh
9d1e621769 feat(chat&notifications): Issue-191 and Issue-440 and auth screen policy 2025-06-11 15:34:16 +03:00
mso7nueh
11b695cefd feat(dev-tools): Issue-441 2025-06-10 17:04:29 +03:00
Username1548
926f0de450 feat(userPurchases, consultantFinances): added status to models and layout for status 2025-06-10 19:57:20 +07:00
mso7nueh
dc81085ae4 fix(chat&reg): fix chat screen, reg email valid 2025-06-09 17:57:10 +03:00
mso7nueh
8c0efc1a34 fix(chat&reg): fix chat screen, reg email valid 2025-06-09 17:57:01 +03:00
Username1548
483cf6d8ee feat(userPurchases): added user purchases, fix(consultations): fixed consultations bug 2025-06-09 14:01:05 +07:00
mso7nueh
87d3f058f6 Merge pull request 'Issue-329' (#500) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#500
2025-06-06 12:41:37 +00:00
mso7nueh
7a3c6e6b07 Merge remote-tracking branch 'origin/main' 2025-06-06 15:40:37 +03:00
mso7nueh
e6bad9d35d fix(blogPost): Issue-329 2025-06-06 15:40:27 +03:00
mso7nueh
72cf203658 Merge pull request 'build 26 + 1.1.3+33' (#495) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#495
2025-06-06 08:33:16 +00:00
Username1548
086fd1cac4 Merge remote-tracking branch 'upstream/main' 2025-06-05 22:43:50 +07:00
88e33765d3 Merge pull request 'feat(chats): added refresh timer for chat room, feat(diary): fixed plan duration in plan widget and added date to app bar)' (#492) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#492
2025-06-05 15:43:21 +00:00
Username1548
6fa4bffd7e feat(consultantFinaces): added consultant finaces screen 2025-06-05 22:42:50 +07:00
mso7nueh
70620d8d9e fix(blogPost): update without image 2025-06-05 17:36:49 +03:00
mso7nueh
0b2464ea19 fix(blogPost): update without image 2025-06-05 17:36:15 +03:00
168 changed files with 5180 additions and 2145 deletions

View File

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

4
.gitignore vendored
View File

@@ -11,4 +11,6 @@
/android/app/upload-keystore.jks
/android/app/.cxx
*.g.dart
*.freezed.dart
*.freezed.dart
android/build/
ios/XCBuildData/

View File

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

View File

@@ -5,6 +5,7 @@
<application
android:label="Eatvio"
android:usesCleartextTraffic="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -25,8 +26,30 @@
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<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>
</activity>
<!-- 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

@@ -40,6 +40,17 @@ post_install do |installer|
target.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1',
'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_MEDIA_LIBRARY=1',
]
end
end
end

View File

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

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>24</string>
<string>39</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -31,12 +31,14 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</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>
<string>Нужно для загрузки фото</string>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Нужно для загрузки фото</string>
<string>Предоставление доступа позволит Вам добавлять фото к записям</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>

View File

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

View File

@@ -1,8 +1,14 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.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/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_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';
@@ -11,11 +17,15 @@ 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/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/empty_chat_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/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_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/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/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -36,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_plans/presentation/my_plans_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/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
@@ -61,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:flutter/material.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:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:version/version.dart';
import 'dart:developer';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@@ -72,6 +83,30 @@ class AppRouter {
static GoRouter appRouter = GoRouter(
initialLocation: '/update-screen',
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: [
GoRoute(
path: '/auth-screen',
@@ -83,6 +118,16 @@ class AppRouter {
name: '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(
@@ -111,10 +156,10 @@ class AppRouter {
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
return '/feed-screen';
return null;
},
),
GoRoute(
@@ -149,17 +194,41 @@ class AppRouter {
),
],
),
GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute(
path: '/welcome-screen',
name: 'welcomeScreen',
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
return '/feed-screen';
} else {
return null;
try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
final fbToken = await firebaseApi.getToken();
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'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) {
rethrow;
}
},
),
@@ -168,23 +237,37 @@ class AppRouter {
name: 'updateScreen',
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
try {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
return '/welcome-screen';
}
} catch (e) {
return '/welcome-screen';
}
},
),
GoRoute(
path: '/consultant-finance-screen',
name: 'consultantFinanceScreen',
path: ConsultationsArchiveScreen.path,
name: ConsultationsArchiveScreen.name,
builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
@@ -202,8 +285,17 @@ class AppRouter {
path: '/about-plan-screen',
name: 'aboutPlanScreen',
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>;
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()),
@@ -211,9 +303,18 @@ class AppRouter {
path: '/about-recipes-screen',
name: 'aboutRecipesScreen',
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>;
return AboutRecipeScreen(
recipe: data['recipe'] as Recipe,
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
@@ -236,8 +337,16 @@ class AppRouter {
path: '/about-diet-screen',
name: 'aboutDietScreen',
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>;
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(
@@ -245,7 +354,20 @@ class AppRouter {
name: 'blogPostCreateScreen',
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(
path: '/chat-screen',
name: 'chatScreen',
@@ -259,6 +381,11 @@ class AppRouter {
);
},
),
GoRoute(
path: '/empty-chat-screen',
name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
),
GoRoute(
path: '/settings-screen',
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: 'password', value: password);
}
static removeCredentials() async {
static Future<void> removeCredentials() async {
await const FlutterSecureStorage().delete(key: 'email');
await const FlutterSecureStorage().delete(key: 'password');
}

View File

@@ -1,11 +1,10 @@
import 'package:firebase_messaging/firebase_messaging.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future<String?> getToken() async{
Future<String?> getToken() async {
await _firebaseMessaging.requestPermission();
return await _firebaseMessaging.getToken();
}
}
}

View File

@@ -7,11 +7,25 @@ abstract class ImageCompressUtility {
static Future<List<File>> compressFilesList(List<File> files) async {
final List<File> compressedFilesList = [];
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;
}
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 {
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;

View File

@@ -1,12 +1,17 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/firebase_api.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_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/reset_password_repository.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/repository/cancel_plan_repository.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
@@ -30,6 +35,11 @@ import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.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/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/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -153,6 +163,11 @@ Future<void> init() async {
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -181,6 +196,8 @@ Future<void> init() async {
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -210,6 +227,9 @@ Future<void> init() async {
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
// External
sl.registerLazySingleton(
@@ -220,6 +240,7 @@ Future<void> init() async {
'accept': 'application/json',
'content-type': 'application/json',
},
validateStatus: (status) => status != null && status < 500,
),
)..interceptors.addAll(
[

View File

@@ -0,0 +1,23 @@
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:permission_handler/permission_handler.dart';
class PermissionHandler {
static Future<void> checkPermission({
required Permission permission,
required String title,
required String description,
required Future<void> Function() onGranted,
required BuildContext context,
}) async {
final permissionState = await permission.request();
if (permissionState.isGranted) {
await onGranted();
return;
}
if (context.mounted) {
showErrorDialog(context, title: title, description: description, buttonTitle: S.of(context).allowAccess, needToOpenSettings: true);
}
}
}

View File

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

View File

@@ -15,7 +15,7 @@ abstract class TokenStorage {
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);
}

View File

@@ -8,31 +8,39 @@ extension DateTimeExtension on DateTime {
String pattern = dateTimeFormatPattern,
String? locale,
]) {
final localDate = isUtc ? toLocal() : this;
if (locale != null && locale.isNotEmpty) {
initializeDateFormatting(locale);
}
return DateFormat(pattern, locale).format(this);
return DateFormat(pattern, locale).format(localDate);
}
bool isYesterday(DateTime date) {
final DateTime localDate = date.isUtc ? date.toLocal() : date;
final yesterday = DateTime.now().subtract(const Duration(days: 1));
return yesterday.day == localDate.day && yesterday.month == localDate.month && yesterday.year == localDate.year;
}
bool isToday(DateTime date) {
final DateTime localDate = date.isUtc ? date.toLocal() : date;
bool isYesterday() {
final localDate = isUtc ? toLocal() : this;
final now = DateTime.now();
return now.day == localDate.day && now.month == localDate.month && now.year == localDate.year;
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
return localDate.year == yesterday.year &&
localDate.month == yesterday.month &&
localDate.day == yesterday.day;
}
bool isToday() {
final localDate = isUtc ? toLocal() : this;
final now = DateTime.now();
return now.year == localDate.year &&
now.month == localDate.month &&
now.day == localDate.day;
}
}
String calculateDifference(DateTime dateTime) {
if (dateTime.isToday(dateTime)) {
String calculateDifference(DateTime dateTime, [String? format]) {
final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime;
if (localDateTime.isToday()) {
return 'Сегодня';
} else if (dateTime.isYesterday(dateTime)) {
} else if (localDateTime.isYesterday()) {
return 'Вчера';
} else {
return dateTime.format('d MMMM', 'ru');
return localDateTime.format(format ?? 'd MMMM', 'ru');
}
}

View File

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

View File

@@ -56,7 +56,7 @@ class CustomDropDown extends StatelessWidget {
: _buildDropDownWidget(context);
}
_buildDropDownWidget(BuildContext context) {
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
return DropdownButtonFormField<SelectionPopupModel>(
focusNode: focusNode ?? FocusNode(),
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!);
}

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/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/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/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -28,6 +29,7 @@ class CustomPopup extends StatelessWidget {
final FeedBlogPostModel? feedBlogPostModel;
final Recipe? recipe;
final Diet? diet;
final String userSlug;
const CustomPopup({
super.key,
@@ -39,6 +41,7 @@ class CustomPopup extends StatelessWidget {
this.feedBlogPostModel,
this.recipe,
this.diet,
required this.userSlug
});
@override
@@ -73,7 +76,7 @@ class CustomPopup extends StatelessWidget {
);
}
List<PopupMenuEntry> _buildMenuItems(s, context) {
List<PopupMenuEntry> _buildMenuItems(S s,BuildContext context) {
final entries = <PopupMenuEntry>[];
void addItem(String icon, String text, Function() onTap) {
@@ -107,28 +110,28 @@ class CustomPopup extends StatelessWidget {
addItem('assets/icons/red_trash.svg', 'Удалить', () async {
if (complainedContentType == ComplainedContent.posts) {
final result = await sl<BlogPostCreateCubit>().deleteBlogPost(id!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
}
showSuccessToast('Пост удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.recipe) {
final result = await sl<RecipesCubit>().deleteRecipe(slug!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить рецепт');
return;
}
showSuccessToast('Рецепт удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.diet) {
final result = await sl<DietsCubit>().deleteDiet(id!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить диету');
return;
}
showSuccessToast('Диета удалена, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().deletePlan(slug!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить план');
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) {

View File

@@ -4,8 +4,9 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
Future<void> showErrorDialog(BuildContext context, {String? title, String? description}) async {
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
return await showDialog<void>(
context: context,
barrierColor: Colors.transparent,
@@ -43,8 +44,13 @@ Future<void> showErrorDialog(BuildContext context, {String? title, String? descr
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => AppRouter.navigatorKey.currentContext?.pop(),
child: Text(s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
onPressed: () {
AppRouter.navigatorKey.currentContext?.pop();
if (needToOpenSettings ?? false) {
openAppSettings();
}
},
child: Text(buttonTitle ?? s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
),
const SizedBox(height: 12),

View File

@@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:eatvio_app/core/token_storage.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: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,6 +1,5 @@
import 'dart:async';
import 'package:easy_debounce/easy_debounce.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/theme/theme_helper.dart';
@@ -8,9 +7,11 @@ import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/toasts.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/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_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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
@@ -23,8 +24,6 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import '../bloc/auth_state.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@@ -41,6 +40,7 @@ class _AuthScreenState extends State<AuthScreen> {
late StreamSubscription<bool> keyboardSubscription;
bool isFocused = false;
int tapsCount = 0;
@override
void initState() {
super.initState();
@@ -53,17 +53,28 @@ class _AuthScreenState extends State<AuthScreen> {
}
Future<void> _reconnectWebSocket(BuildContext context) async {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
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
@@ -81,13 +92,10 @@ class _AuthScreenState extends State<AuthScreen> {
title: GestureDetector(
onTap: () {
tapsCount++;
if (tapsCount == 6) {
if (tapsCount == 7) {
tapsCount = 0;
// showModalBottomSheet(context: context, builder: (context) => );
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
EasyDebounce.debounce('change env', Durations.medium4, () {
tapsCount = 0;
});
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
@@ -131,6 +139,20 @@ class _AuthScreenState extends State<AuthScreen> {
),
),
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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -153,7 +175,7 @@ class _AuthScreenState extends State<AuthScreen> {
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти", Вы соглашаетесь \n',
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(
fontSize: 12,
color: Colors.black,
@@ -170,43 +192,153 @@ class _AuthScreenState extends State<AuthScreen> {
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
const TextSpan(text: ' и '),
TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
),
],
),
),
),
const SizedBox(height: 16),
SizedBox(
height: 48,
width: MediaQuery.of(context).size.width - 32,
child: FloatingActionButton.extended(
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
Row(
children: [
SizedBox(
height: 48,
width: MediaQuery.of(context).size.width - 88,
child: FloatingActionButton.extended(
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
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,
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),
),
),
const SizedBox(width: 8),
SizedBox(
height: 48,
width: 48,
child: FloatingActionButton.extended(
heroTag: 'scanQrCodeButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
await showDialog(
context: context,
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/widgets/custom_popup.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_state.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';
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
State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -31,260 +32,294 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>();
final textEditingController = TextEditingController();
int? replyId;
late bool isLiked;
late int totalLikes;
bool isLiked = false;
int totalLikes = 0;
@override
void initState() {
isLiked = widget.blogPost.wasLikedByMe;
totalLikes = widget.blogPost.totalLikes;
context.read<FeedCubit>().viewPost(widget.blogPost.id);
context.read<FeedCubit>().viewPost(widget.id);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit..getComments(widget.blogPost.id.toString(), CommentSourceType.blogPost, true),
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
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),
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),
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);
return BlocProvider(
create: (context) => sl<BlogPostCubit>()..getBlogPost(widget.id),
child: BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit..getComments(widget.id.toString(), CommentSourceType.blogPost, true),
child: BlocBuilder<BlogPostCubit, BlogPostState>(
builder: (context, blogPostState) {
return BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
if (blogPostState.isLoading) {
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
),
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),
_iconButton(
icon: Icons.reply_rounded,
GestureDetector(
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.blogPost.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.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),
),
),
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(
width: MediaQuery.of(context).size.width - 32,
height: 0.4,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)),
appBar: AppBar(
title: Text(blogPostState.blogPostModel?.title ?? ''),
leading: BackButton(
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),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
'${widget.blogPost.totalComments} КОММЕНТАРИЕВ',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
GestureDetector(
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(
onTap: onTap ?? () {},
borderRadius: BorderRadius.circular(16),

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
@@ -17,6 +19,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class BlogPostCreateScreen extends StatefulWidget {
final FeedBlogPostModel? feedBlogPostModel;
@@ -77,54 +80,72 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (!context.mounted) return;
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
},
);
}
_buildBlogPostCreate(BuildContext context) {
Scaffold _buildBlogPostCreate(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(),
@@ -232,7 +253,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_buildAddPhoto() {
GestureDetector _buildAddPhoto() {
return GestureDetector(
onTap: () {
_showImagePicker();

View File

@@ -73,7 +73,7 @@ class BlogPostRepository {
'video_url': videoUrl,
'image': image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
: oldImageUrl != null
: oldImageUrl != '' && oldImageUrl != null
? await MultipartFile.fromFile((await downloadImage(oldImageUrl))?.path ?? '', filename: oldImageUrl.split('/').last)
: null,
});

View File

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

View File

@@ -1,6 +1,7 @@
import 'package:badges/badges.dart' as badges;
import 'package:eatvio_app/core/const.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_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -30,6 +31,7 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1;
late final List<String> pages;
int tapsCount = 0;
@override
void initState() {
@@ -58,12 +60,29 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
}
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) {
setState(() {
currentBottomNavBarIndex = index;
});
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
}
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
}
@override
@@ -82,7 +101,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
BottomNavigationBarItem(
label: '',
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'),
radius: 16,
),

View File

@@ -7,7 +7,7 @@ class CalendarCubit extends Cubit<CalendarState> {
CalendarCubit({required this.calendarRepository}) : super(const CalendarState.loading());
loadCalendarList() async {
Future<void> loadCalendarList() async {
emit(const CalendarState.loading());
try {
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(
padding: const EdgeInsets.all(8),
shrinkWrap: true,

View File

@@ -9,7 +9,6 @@ abstract class ChatUserModel with _$ChatUserModel {
required int id,
required String name,
required String slug,
@JsonKey(name: 'profile_photo_path') String? profilePhotoPath,
@JsonKey(name: 'profile_photo_url') String? profilePhotoUrl,
}) = _ChatUserModel;

View File

@@ -7,11 +7,13 @@ import 'package:audioplayers/audioplayers.dart';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.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/websockets/ws_manager.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/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/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
@@ -45,7 +47,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
_timer?.cancel();
}
loadNextPage(String chatId) async {
Future<void> loadNextPage(String chatId) async {
if (currentPage < lastPage && !isLoading) {
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 {
final chatRoom = await chatRepository.getChatRoom(chatId);
ChosenPlanModel? chosenPlan;
@@ -167,6 +169,11 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
chatRepository.emitNewMessageToChats(message);
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
});
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 {
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')));
return true;
} catch (e, st) {

View File

@@ -1,14 +1,15 @@
import 'dart:io';
import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
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/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/chat_bubble_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -22,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/generated/l10n.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget {
final String chatId;
final bool? sendInitialMessage;
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
@override
@@ -45,6 +49,7 @@ class _ChatScreenState extends State<ChatScreen> {
final List<({int id, int index})> unreadMessagesIdsCopy = [];
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
late AppLifecycleListener _listener;
List<File> pickedFiles = [];
void _onHide() {
chatRoomCubit.cancelTimer();
@@ -64,7 +69,7 @@ class _ChatScreenState extends State<ChatScreen> {
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
}
_onScroll() {
void _onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId);
}
@@ -97,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 amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id;
final showUserDetailsButton = [
@@ -155,7 +160,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!),
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
@@ -334,100 +339,155 @@ class _ChatScreenState extends State<ChatScreen> {
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 8),
InkWell(
onTap: () async {
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',
],
);
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (int i = 0; i < pickedFiles.length; i++) ...[
Stack(
clipBehavior: Clip.none,
children: [
_isImageFile(pickedFiles[i].path)
? GestureDetector(
onTap: () {
final imageFiles = pickedFiles.where((f) => _isImageFile(f.path)).toList();
final imageIndex = _imageIndexAt(pickedFiles, i);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files);
}
},
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))),
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: imageFiles.map((e) => e.path).toList(),
isLocal: true,
index: imageIndex,
),
);
},
child: Hero(
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),
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(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(width: 8),
InkWell(
onTap: () => showImagePicker(state.chatRoom.users.receiver.slug),
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: () {
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),
],
),
),
@@ -435,21 +495,173 @@ class _ChatScreenState extends State<ChatScreen> {
),
if (showAboutOrderButton)
Positioned(
top: 8,
right: 15,
left: 16,
child: AboutOrderButton(
onTap: () async {
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
},
)),
top: 8,
right: 15,
left: 16,
child: AboutOrderButton(
onTap: () async {
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(
child: Padding(
padding: const EdgeInsets.only(top: 32),

View File

@@ -67,18 +67,23 @@ class _ChatsScreenState extends State<ChatsScreen> {
),
];
},
body: switch (state.state) {
BaseState.error => DataErrorWidget(onReload: () => context.read<ChatsCubit>().getChatRooms()),
BaseState.success => _buildChatList(context, state),
_ => const Loader(),
},
body: RefreshIndicator(
child: switch (state.state) {
BaseState.error => DataErrorWidget(onReload: () => context.read<ChatsCubit>().getChatRooms()),
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>(
onNotification: (notification) {
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

@@ -1,6 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.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/utils/date_time_utils.dart';
@@ -40,7 +39,7 @@ class ChatBubbleWidget extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(right: 4, bottom: 4),
child: Text(
sendTime.format('hh:mm'),
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,
@@ -81,7 +80,7 @@ class ChatBubbleWidget extends StatelessWidget {
contentPadding: const EdgeInsets.all(4),
horizontalTitleGap: 8,
onTap: () async {
if (!await launchUrl(Uri.parse('$baseUrl/storage/${mediaData[index]}'))) {
if (!await launchUrl(Uri.parse('${mediaData[index]}'))) {
throw Exception('Could not launch ${mediaData[index].toString()}');
}
},
@@ -108,18 +107,18 @@ class ChatBubbleWidget extends StatelessWidget {
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: mediaData.map((e) => '$baseUrl/storage/$e').toList(),
imageUrls: mediaData.map((e) => '$e').toList(),
index: index,
),
);
},
child: Hero(
tag: '$baseUrl/storage/${mediaData[index]}',
tag: '${mediaData[index]}',
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${mediaData[index]}',
imageUrl: '${mediaData[index]}',
),
),
),
@@ -145,7 +144,7 @@ class ChatBubbleWidget extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 4),
child: Text(
sendTime.format('hh:mm'),
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,

View File

@@ -1,4 +1,3 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -21,15 +20,14 @@ class ChatEntryWidget extends StatefulWidget {
class _ChatEntryWidgetState extends State<ChatEntryWidget> {
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(16),
return GestureDetector(
onTap: () => GoRouter.of(context).pushNamed(
'chatScreen',
extra: {
'chatId': widget.chatRoom.id.toString(),
},
),
child: Ink(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
@@ -42,7 +40,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
CircleAvatar(
radius: 24,
foregroundImage: NetworkImage(
baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl,
widget.chatRoom.users.toChatUser.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
backgroundColor: Colors.white,
@@ -62,7 +60,13 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.5,
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,
overflow: TextOverflow.ellipsis,
style: CustomTextStyles.bodyMediumBluegray800,
@@ -78,7 +82,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
widget.chatRoom.lastMessage!.updatedAt.format('dd MMM', 'ru'),
calculateDifference(widget.chatRoom.lastMessage!.updatedAt, 'dd MMM'),
style: theme.textTheme.bodyMedium,
),
),

View File

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

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
@@ -27,7 +28,7 @@ class ChatRepository {
}
Future<void> sendMessage({
required String chatId,
required String slug,
String? data,
List<File>? files,
}) async {
@@ -46,12 +47,13 @@ class ChatRepository {
}
}
await chatService.sendMessage(
id: chatId,
await chatService.sendMessageByUserSlug(
userSlug: slug,
data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
);
} catch (e) {
} catch (e, st) {
log(e.toString(), stackTrace: st);
rethrow;
}
}
@@ -154,4 +156,17 @@ class ChatRepository {
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,
});
@POST('/api/chat/{id}/message/send')
@POST('/api/v2/chat/message/send/{id}')
@MultiPart()
Future<void> sendMessage({
@Path() required String id,
@@ -80,4 +80,17 @@ abstract class ChatService {
@Path() required int id,
@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;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
@@ -81,11 +81,11 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
);
}
disableLoading() {
void disableLoading() {
emit(state.copyWith(loading: false));
}
updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit(
state.copyWith(
consultant: consultant,
@@ -95,7 +95,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
);
}
registerConsultant() async {
Future<void> registerConsultant() async {
if (state.consultant.photos != null) {
updateConsultantProfileModel(
state.consultant.copyWith(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -67,7 +68,7 @@ class ConsultationsContent extends StatelessWidget {
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
GoRouter.of(context).pushNamed('consultantFinanceScreen');
GoRouter.of(context).pushNamed(ConsultationsArchiveScreen.name);
},
child: Ink(
width: MediaQuery.of(context).size.width - 32,

View File

@@ -185,6 +185,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: List.generate(widget.consultationsModel.duration, (index) {
final day = givenDaysWithDayNumber.firstWhereOrNull((e) => e.$1 == index);
final hasActivity = day != null && day.$2.state != DiaryStateEnum.init;
final canConsultantOpenThisDay = day != null &&
(day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted);
return Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
@@ -192,7 +194,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
InkWell(
onTap: () {
if (!hasActivity) {
if (!hasActivity || !canConsultantOpenThisDay) {
showErrorDialog(context,
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
return;
@@ -352,7 +354,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
);
}
_legendTitle(String text, Color color) {
Row _legendTitle(String text, Color color) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -1,7 +1,9 @@
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
class ConsultationsRepository {
final ConsultationService consultationService;
@@ -42,4 +44,20 @@ class ConsultationsRepository {
rethrow;
}
}
Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getConsultantFinances(date, dateComparison);
} catch (e) {
rethrow;
}
}
Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getUserPurchases(date, dateComparison);
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,7 +1,9 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
class ConsultationService {
final Dio dio;
@@ -66,4 +68,42 @@ class ConsultationService {
rethrow;
}
}
Future<List<ConsultantFinanceModel>> getConsultantFinances(
String? date,
FiltersComparisonEnum? dateComparison,
) async {
try {
final response = await dio.get(
'/api/finances/my_finances',
queryParameters: {
if (dateComparison != null && date != null) 'filters[end_date]': date,
},
);
final data = response.data as Map<String, dynamic>;
final consultations = data['data'] as List<dynamic>;
return consultations.map((consultation) => ConsultantFinanceModel.fromJson(consultation as Map<String, dynamic>)).toList();
} catch (e) {
rethrow;
}
}
Future<List<UserPurchasesModel>> getUserPurchases(
String? date,
FiltersComparisonEnum? dateComparison,
) async {
try {
final response = await dio.get(
'/api/finances/my_purchases',
queryParameters: {
if (dateComparison != null && date != null) 'filters[start_date]': date,
},
);
final data = response.data as Map<String, dynamic>;
final purchases = data['data'] as List<dynamic>;
return purchases.map((purchase) => UserPurchasesModel.fromJson(purchase as Map<String, dynamic>)).toList();
} catch (e) {
rethrow;
}
}
}

View File

@@ -194,6 +194,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
userSlug: widget.diet.author.authorSlug,
),
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) {
return ChoiceChip(
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();
return Padding(
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(
child: ListTile(
onTap: () {
@@ -313,7 +313,7 @@ class _DayWidgetState extends State<DayWidget> {
);
}
_modalBottomSheet({
Future _modalBottomSheet({
required CreateDietCubit createDietCubit,
required int dayIndex,
required int mealIndex,

View File

@@ -1,6 +1,8 @@
import 'dart:io';
import 'package:dotted_border/dotted_border.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.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/create_diet/presentation/cubit/create_diet_cubit.dart';
@@ -11,7 +13,9 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class DietImagePickerWidget extends StatefulWidget {
const DietImagePickerWidget({super.key});
@@ -28,11 +32,18 @@ class _DietImagePickerWidgetState extends State<DietImagePickerWidget> {
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
cubit.setImage(image);
if (!context.mounted) return;
Navigator.of(context).pop();
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
cubit.setImage(image);
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: Row(
children: [
@@ -44,11 +55,18 @@ class _DietImagePickerWidgetState extends State<DietImagePickerWidget> {
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
cubit.setImage(image);
if (!context.mounted) return;
Navigator.of(context).pop();
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
cubit.setImage(image);
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: Row(
children: [

View File

@@ -1,6 +1,8 @@
import 'dart:io';
import 'package:dotted_border/dotted_border.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.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/create_diet/presentation/widgets/error_widget.dart';
@@ -11,7 +13,9 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class PlanImagePickerWidget extends StatefulWidget {
const PlanImagePickerWidget({super.key});
@@ -28,11 +32,18 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
cubit.setImage(image);
if (!context.mounted) return;
Navigator.of(context).pop();
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
cubit.setImage(image);
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: Row(
children: [
@@ -44,11 +55,18 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
cubit.setImage(image);
if (!context.mounted) return;
Navigator.of(context).pop();
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
cubit.setImage(image);
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: Row(
children: [

View File

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

View File

@@ -1,11 +1,16 @@
import 'dart:io';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class ImageWidget extends StatefulWidget {
const ImageWidget({super.key, required this.image});
@@ -64,58 +69,75 @@ class _ImageWidgetState extends State<ImageWidget> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (image != null) {
createRecipeCubit.replaceImage(File(image.path));
createRecipeCubit.validateImage();
}
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (image != null) {
createRecipeCubit.replaceImage(File(image.path));
createRecipeCubit.validateImage();
}
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (image != null) {
createRecipeCubit.replaceImage(File(image.path));
createRecipeCubit.validateImage();
}
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (image != null) {
createRecipeCubit.replaceImage(File(image.path));
createRecipeCubit.validateImage();
}
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
},
);
}
}

View File

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

View File

@@ -1,10 +1,15 @@
import 'dart:io';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class StepImageWidget extends StatefulWidget {
final CreateRecipeCubit createRecipeCubit;
@@ -12,7 +17,8 @@ class StepImageWidget extends StatefulWidget {
const StepImageWidget({
super.key,
required this.index, required this.createRecipeCubit,
required this.index,
required this.createRecipeCubit,
});
@override
@@ -76,56 +82,73 @@ class _StepImageWidgetState extends State<StepImageWidget> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (image != null) {
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
}
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (image != null) {
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
}
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (image != null) {
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
}
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (image != null) {
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
}
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
},
);
}
}

View File

@@ -1,6 +1,7 @@
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/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -51,6 +52,29 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
GetPlansComments(),
SendPlansComment(),
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();
late final List<TestState> testsStates;
@@ -81,6 +105,8 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
onPressed: () {
setState(() {
baseUrlForTest = controller.text;
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear();
});
},
@@ -89,43 +115,50 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
],
),
const SizedBox(height: 10),
if (baseUrlForTest != baseUrl)
CustomOutlinedButton(
text: 'Set dev2 base url',
onPressed: () {
setState(() {
baseUrlForTest = baseUrl;
});
},
buttonStyle: const ButtonStyle(),
),
CustomOutlinedButton(
text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url',
onPressed: () {
setState(() {
if (baseUrl == 'https://eatvio.ru') {
baseUrlForTest = 'https://dev2.eatvio.com';
} else {
baseUrlForTest = 'https://eatvio.ru';
}
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear();
});
},
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder:
(context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text('${index + 1}. ${testCases[index].name}')),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CustomOutlinedButton(
text: 'run',
height: 20,
width: 60,
onPressed: () async {
if (testsStates[index] == TestState.loading) return;
if (baseUrl.toLowerCase() != 'https://eatvio.ru')
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text('${index + 1}. ${testCases[index].name}')),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CustomOutlinedButton(
text: 'run',
height: 20,
width: 60,
onPressed: () async {
if (testsStates[index] == TestState.loading) return;
setState(() {
testsStates[index] = TestState.loading;
});
setState(() {
testsStates[index] = TestState.loading;
});
try {
final result = await testCases[index].run(baseUrl: baseUrlForTest);
if (result) {
if (result.success) {
setState(() {
testsStates[index] = TestState.success;
});
@@ -135,27 +168,65 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
testsStates[index] = TestState.fail;
});
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'),
),
],
),
);
}
}
},
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(
} catch (e, stack) {
setState(() {
testsStates[index] = TestState.fail;
});
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('$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,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
},
],
),
],
),
},
],
),
],
),
),
),
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'add token to server',

View File

@@ -4,19 +4,48 @@ import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.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/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/step.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/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/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 {
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 {
@@ -24,7 +53,7 @@ class CreateRecipeTask implements TestCases {
String get name => 'api/recipes/create';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
@@ -41,10 +70,16 @@ class CreateRecipeTask implements TestCases {
steps: [Step(description: 'desc')],
),
);
} catch (e) {
return false;
} on DioException catch (e) {
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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// get all of needed services
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final profileService = ProfileService(dio, baseUrl: baseUrl);
final dietsService = DietService(dio, baseUrl: baseUrl);
// get all of needed data
final dietsPaginationModel = await feedService.getDiets(page: 1);
final chosenDietsPaginationModel = await profileService.getChosenDiets();
final lastChosenDiet = chosenDietsPaginationModel.diets.firstOrNull;
final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1));
final tomorrow = today.add(const Duration(days: 1));
// run test case
for (final diet in dietsPaginationModel.data) {
if (!diet.author.authorIsMe) {
if (lastChosenDiet?.state == 'started' || lastChosenDiet?.state == 'inited') {
try {
await dietsService.cancelDiet(id: lastChosenDiet?.dietId ?? 0);
} catch (e) {
} catch (e, stack) {
log('unexpected error7: $e');
return false;
return TestResult(false, '$e\n$stack');
}
await Future.delayed(const Duration(seconds: 3));
}
try {
await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')});
return false;
return TestResult(false, 'Expected error but got success');
} catch (e) {
log('expect ot have error: $e');
}
// test start diet with correct date
try {
await dietsService.startDiet(id: diet.id, body: {'date': today.format('yyyy-MM-dd')});
} catch (e) {
} catch (e, stack) {
log('unexpected error2: $e');
return false;
return TestResult(false, '$e\n$stack');
}
// test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
} catch (e) {
} catch (e, stack) {
log('unexpected error3: $e');
return false;
return TestResult(false, '$e\n$stack');
}
await Future.delayed(const Duration(seconds: 3));
// test start diet with correct date
try {
await dietsService.startDiet(id: diet.id, body: {'date': tomorrow.format('yyyy-MM-dd')});
} catch (e) {
} catch (e, stack) {
log('unexpected error4: $e');
return false;
return TestResult(false, '$e\n$stack');
}
// test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
} catch (e) {
@@ -124,11 +144,10 @@ class Test850task implements TestCases {
break;
}
}
return true;
} catch (e) {
return TestResult(true);
} catch (e, stack) {
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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// get all of needed services
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// await chatService.getChatRooms();
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -158,16 +177,16 @@ class GetChatRoom implements TestCases {
String get name => '/api/chat/show/{id}';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatRoom(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '');
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.sendMessage(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', data: 'test message');
return true;
return TestResult(true);
} 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}';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatMessages(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', page: 1);
return true;
return TestResult(true);
} 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}';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -230,9 +249,9 @@ class GetChatRoomIdByUserSlug implements TestCases {
break;
}
}
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -243,14 +262,14 @@ class GetBlogPostList implements TestCases {
String get name => '/api/blogPosts/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getBlogPostList(page: 1);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -260,7 +279,7 @@ class GetRecipes implements TestCases {
String get name => '/api/recipes/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -269,9 +288,9 @@ class GetRecipes implements TestCases {
final tags = tagsModel.data;
final tagsIds = tags.map((e) => e.id).toList();
await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -281,14 +300,14 @@ class GetDiets implements TestCases {
String get name => '/api/diets/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getDiets(page: 1, query: null);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -298,14 +317,14 @@ class GetPlans implements TestCases {
String get name => '/api/plans/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getPlans(page: 1, query: null);
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.getBlogPostComments(id: blogPosts.first.id.toString());
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.sendBlogPostComment(id: blogPosts.first.id.toString(), body: 'auto test message');
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data;
await feedService.getDietsComments(id: diets.first.id.toString());
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data;
await feedService.sendDietsComment(id: diets.first.id.toString(), body: 'auto test message');
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data;
await feedService.getPlansComments(slug: plans.first.slug);
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data;
await feedService.sendPlansComment(slug: plans.first.slug, body: 'auto test message');
return true;
return TestResult(true);
} 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';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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 recipes = recipesModel.recipesList;
await feedService.getRecipesComments(id: recipes.first.id.toString());
return true;
return TestResult(true);
} 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);
}
DiaryDietModel? _parseDiet(json) {
DiaryDietModel? _parseDiet(dynamic json) {
if (json == null) return null;
return DiaryDietModel.fromJson(json['diet']);
}
@@ -80,7 +80,7 @@ abstract class DiaryDietModel with _$DiaryDietModel {
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
}
TypeOfDiaryModel _parseDayTypeForFutureDay(json) {
TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
return TypeOfDiaryModel(
hasChosenDiet: json['has_chosen_diet'] as bool,
hasChosenPlan: json['has_chosen_plan'] as bool,

View File

@@ -55,7 +55,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return null;
}
setSelectedMeal(int meal) {
void setSelectedMeal(int meal) {
selectedMeal = meal;
final currentState = state;
if (currentState is DiaryStateLoaded) {
@@ -154,7 +154,7 @@ class DiaryCubit extends Cubit<DiaryState> {
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;
if (needLoading) {
@@ -215,7 +215,7 @@ class DiaryCubit extends Cubit<DiaryState> {
}
}
finishDiaryDay({
Future<void> finishDiaryDay({
required int diaryId,
required String wakeUp,
required String retire,
@@ -233,7 +233,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return (await diaryRepository.getNutritionData(index));
}
addProductToMeal({
Future<void> addProductToMeal({
required int diaryId,
required int mealId,
required String productName,
@@ -252,7 +252,7 @@ class DiaryCubit extends Cubit<DiaryState> {
await fetchDiary();
}
updateProductInMeal({
Future<void> updateProductInMeal({
required int diaryId,
required int mealId,
required int portionId,
@@ -286,22 +286,22 @@ class DiaryCubit extends Cubit<DiaryState> {
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 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 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 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 fetchDiary();
}
@@ -310,12 +310,12 @@ class DiaryCubit extends Cubit<DiaryState> {
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 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 fetchDiary();
}
@@ -335,32 +335,32 @@ class DiaryCubit extends Cubit<DiaryState> {
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 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 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 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 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 fetchDiary();
}
fillComment(String comment) {
void fillComment(String comment) {
this.comment = comment;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,21 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class PhotoGalleryWidget extends StatefulWidget {
const PhotoGalleryWidget({super.key});
@@ -29,58 +34,78 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
diaryCubit = context.read<DiaryCubit>();
}
showImagePicker(DiaryDayModel diaryDay) {
void showImagePicker(DiaryDayModel diaryDay) {
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) {
diaryCubit.uploadPhotos(
files: [File(image.path)],
diaryId: diaryDay.id,
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
diaryCubit.uploadPhotos(
files: [File(image.path)],
diaryId: diaryDay.id,
);
}
},
context: context,
);
}
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
},
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.isNotEmpty) {
diaryCubit.uploadPhotos(
files: images.map((f) => File(f.path)).toList(),
diaryId: diaryDay.id,
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
List<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
diaryCubit.uploadPhotos(
files: images.map((f) => File(f.path)).toList(),
diaryId: diaryDay.id,
);
}
},
context: context,
);
}
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
},
);
}
@@ -92,7 +117,9 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
int mealIndex = state.selectedMeal - 1;
if (state.diary.meals[mealIndex].photos.isEmpty && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) {
return GestureDetector(
onTap: () => showImagePicker(state.diary),
onTap: () async {
showImagePicker(state.diary);
},
child: SvgPicture.asset(
width: MediaQuery.of(context).size.width - 32,
'assets/no_one_image_was_added.svg',

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ class _WakeupAndRetireWidgetState extends State<WakeupAndRetireWidget> {
diaryCubit = context.read<DiaryCubit>();
}
showTimePicker(int diaryId, bool isWakeup) {
void showTimePicker(int diaryId, bool isWakeup) {
showModalBottomSheet<void>(
context: context,
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/features/diets/repositories/diet_repository.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:flutter_bloc/flutter_bloc.dart';
@@ -16,10 +17,11 @@ class AboutDietCubit extends Cubit<AboutDietState> {
bool isCurrentDietStarted = false;
bool hasStartedDiet = false;
String? userStartedDietName;
List<int> _selectedIndexes = [];
Future<void> getDiet(int id, {int? index}) async {
Future<void> getDiet(String slug, {int? index}) async {
try {
final diet = await dietsRepository.getDiet(id);
final diet = await dietsRepository.getDietBySlug(slug);
if (index != null) {
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 {
emit(const AboutDietState.loading());
final diet = await dietsRepository.getDiet(id);
final diet = await dietsRepository.getDietBySlug(slug);
final chosenDiets = await profileRepository.getChosenDiets();
final lastChosenDiet = chosenDiets.firstOrNull;
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) {
log(e.toString());
emit(AboutDietState.error(message: e.toString()));
@@ -67,7 +75,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndexInFeed != null) {
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 {
final updatedDiet = currentDiet.copyWith(
totalLikes: currentDiet.totalLikes + 1,
@@ -76,7 +84,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndexInFeed != null) {
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(
@@ -91,7 +99,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
dietsRepository
.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) {
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(
'add_or_remove_diet_to_favorites',
@@ -122,7 +130,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) {
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) {
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(
'add_or_remove_diet_to_favorites',
@@ -153,7 +161,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) {
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() {
if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(diet: diet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet));
}
void emitCurrentStartedDietsState() {
if (state is! DietStateLoaded) return;
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.loaded({
required Diet diet,
@Default([]) List<int> selectedIndexes,
@Default(false) bool isStarted,
@Default(false) bool hasStartedDiet,
}) = DietStateLoaded;

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ abstract class DietsPagination with _$DietsPagination {
abstract class Diet with _$Diet {
const factory Diet({
@JsonKey(name: "id") required int id,
@JsonKey(name: "slug") String? slug,
@JsonKey(name: "slug") required String slug,
@JsonKey(name: "name") required String name,
@JsonKey(name: "image") required String image,
@JsonKey(name: "duration") required int duration,

View File

@@ -46,7 +46,7 @@ abstract class PlanModel with _$PlanModel {
factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json);
}
PacketOptions? parseOption(json) {
PacketOptions? parseOption(dynamic json) {
if (json is Map<String, dynamic>) {
return PacketOptions.fromJson(json);
}
@@ -74,7 +74,7 @@ abstract class PacketOptions with _$PacketOptions {
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>) {
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);
}
int _parseProductId(json) {
int _parseProductId(dynamic json) {
if (json is String) {
return int.parse(json);
}

View File

@@ -10,11 +10,11 @@ class CommentsCubit extends Cubit<CommentsState> {
required this.feedRepository,
}) : super(const CommentsState());
setReplyId(int? replyId) {
void setReplyId(int? 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());
try {
switch (commentSourceType) {
@@ -36,7 +36,7 @@ class CommentsCubit extends Cubit<CommentsState> {
}
}
sendComment({
Future<void> sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,

View File

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

View File

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

View File

@@ -16,7 +16,8 @@ class FeedCubit extends Cubit<FeedState> {
required this.feedRepository,
}) : 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];
final int index = feedBlogPostList.indexOf(feedBlogPostModel);
feedBlogPostList[index] = feedBlogPostModel.copyWith(
@@ -31,7 +32,7 @@ class FeedCubit extends Cubit<FeedState> {
);
}
loadFeedBlogPostList({
Future<void> loadFeedBlogPostList({
String? query,
}) async {
try {
@@ -79,7 +80,7 @@ class FeedCubit extends Cubit<FeedState> {
}
}
likePost(int id) async {
Future<void> likePost(int id) async {
try {
await feedRepository.likeBlogPost(id: id);
} 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);
viewRecipe(String slug) async => await feedRepository.viewRecipe(slug);
viewDiet(int id) async => await feedRepository.viewDiet(id);
viewPlan(String slug) async => await feedRepository.viewPlan(slug);
Future<void> viewPost(int id) async => await feedRepository.viewPost(id);
Future<void> viewRecipe(String slug) async => await feedRepository.viewRecipe(slug);
Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id);
Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug);
}

View File

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

View File

@@ -40,7 +40,7 @@ class RecipesCubit extends Cubit<RecipesState> {
});
}
loadRecipesList({
Future<void> loadRecipesList({
String? query,
}) async {
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);
emit(
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:flutter/material.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 '../widgets/recipes.dart';
@@ -84,7 +85,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
_onScrollConsultants() {
void _onScrollConsultants() {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
}
@@ -100,6 +101,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
@override
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return Scaffold(
floatingActionButton: _selectedChipIndex == 1
? FloatingActionButton.small(

View File

@@ -46,7 +46,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index],
extra: state.feedBlogPostList[widget.index].id,
),
child: Material(
borderRadius: BorderRadius.circular(12),
@@ -56,81 +56,86 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
authorName: state.feedBlogPostList[widget.index].author.authorName,
authorProfileIcon: 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,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
authorName: state.feedBlogPostList[widget.index].author.authorName,
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
? 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(
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),
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,
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),
// Кнопка меню
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],
),
],
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,
),
),
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),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
@@ -151,9 +156,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
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,
: state.feedBlogPostList[widget.index].description).replaceAll('\n', '<br>'),
style: {
"*": Style(
fontSize: FontSize(16),
@@ -234,7 +239,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
text: state.feedBlogPostList[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index],
extra: state.feedBlogPostList[widget.index].id,
),
),
const SizedBox(width: 8),

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