forked from eatvio/eatvio_app
Compare commits
91 Commits
diaryFixes
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26ff548752 | ||
|
|
43e9098d8d | ||
|
|
ad538472f0 | ||
|
|
49827e979e | ||
|
|
24cff8b4e9 | ||
|
|
be5a858c73 | ||
|
|
e80b660210 | ||
|
|
d42604dc87 | ||
|
|
4bed103cec | ||
|
|
8d429862dd | ||
|
|
dd90f62286 | ||
|
|
7809624186 | ||
|
|
7c131f8970 | ||
|
|
0926b50537 | ||
|
|
3e96fdcfc2 | ||
|
|
3ce6caa0af | ||
|
|
58855b7f11 | ||
|
|
f0d5dacc00 | ||
|
|
86199d08f0 | ||
|
|
1801324fb5 | ||
|
|
d4a05f47d1 | ||
|
|
300a708dfc | ||
|
|
693260afb6 | ||
|
|
9b4328a2ee | ||
|
|
591a8a597d | ||
|
|
bc660e2aff | ||
|
|
d129687fc6 | ||
|
|
2e5e1b3af4 | ||
|
|
c103d214c4 | ||
|
|
9946abc0be | ||
|
|
87bf277248 | ||
|
|
23a606bfd4 | ||
|
|
5f57bded51 | ||
|
|
535dd595b7 | ||
|
|
80afcd8949 | ||
|
|
8c7c7f51cf | ||
|
|
9c168d37ce | ||
|
|
55bc361b05 | ||
|
|
d14fbbb8d3 | ||
|
|
7530270a2e | ||
|
|
afc36e6b63 | ||
|
|
f7e40e654d | ||
|
|
aa78d385a2 | ||
|
|
75fa9ebc5c | ||
|
|
105c7cb462 | ||
|
|
c3647288a6 | ||
|
|
85d67ba9c4 | ||
|
|
15ffb4a3ec | ||
|
|
2a705fdb66 | ||
|
|
41ca97d030 | ||
|
|
84b88282ad | ||
|
|
84d52de935 | ||
|
|
fee92801b8 | ||
|
|
3b25e30b41 | ||
|
|
6aef2a9a08 | ||
|
|
6a2525dbfd | ||
|
|
21ce0fc9f8 | ||
|
|
b0c29345a2 | ||
|
|
7682859d32 | ||
|
|
d0426f6e1e | ||
|
|
ca296040f2 | ||
|
|
0581da038a | ||
|
|
fa1192c5e5 | ||
|
|
41f948c8f7 | ||
|
|
896b2783ae | ||
|
|
9c004f8593 | ||
|
|
a577a8d58a | ||
|
|
3b2c4fdff5 | ||
|
|
44d6487709 | ||
|
|
319fd2dfe3 | ||
|
|
a98043758b | ||
|
|
a84d2d7daa | ||
|
|
4acb861b69 | ||
|
|
f163608a90 | ||
| 6bbec1f022 | |||
|
|
c0311bf213 | ||
|
|
9d1e621769 | ||
|
|
11b695cefd | ||
|
|
926f0de450 | ||
|
|
dc81085ae4 | ||
|
|
8c0efc1a34 | ||
|
|
483cf6d8ee | ||
|
|
87d3f058f6 | ||
|
|
7a3c6e6b07 | ||
|
|
e6bad9d35d | ||
|
|
72cf203658 | ||
|
|
086fd1cac4 | ||
| 88e33765d3 | |||
|
|
6fa4bffd7e | ||
|
|
70620d8d9e | ||
|
|
0b2464ea19 |
@@ -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
4
.gitignore
vendored
@@ -11,4 +11,6 @@
|
||||
/android/app/upload-keystore.jks
|
||||
/android/app/.cxx
|
||||
*.g.dart
|
||||
*.freezed.dart
|
||||
*.freezed.dart
|
||||
android/build/
|
||||
ios/XCBuildData/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
29
assets/icons/blocked_users.svg
Normal file
29
assets/icons/blocked_users.svg
Normal 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 |
11
ios/Podfile
11
ios/Podfile
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
23
lib/core/permission_handler/permission_handler.dart
Normal file
23
lib/core/permission_handler/permission_handler.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
165
lib/features/auth/presentation/pages/forget_password_screen.dart
Normal file
165
lib/features/auth/presentation/pages/forget_password_screen.dart
Normal 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('Пароль успешно сменён. Войдите в аккаунт');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/features/auth/presentation/pages/scan_qr_code_screen.dart
Normal file
109
lib/features/auth/presentation/pages/scan_qr_code_screen.dart
Normal 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-кодом или войдите с помощью логина и пароля.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
lib/features/auth/services/reset_password_service.dart
Normal file
19
lib/features/auth/services/reset_password_service.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ class NewMessagesCubit extends Cubit<NewMessagesState> {
|
||||
|
||||
NewMessagesCubit() : super(const NewMessagesState.empty());
|
||||
|
||||
incrementCounter() {
|
||||
void incrementCounter() {
|
||||
_messagesCount++;
|
||||
emit(NewMessagesState.loaded(messagesCount: _messagesCount));
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
289
lib/features/chat/presentation/pages/empty_chat_screen.dart
Normal file
289
lib/features/chat/presentation/pages/empty_chat_screen.dart
Normal 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('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -35,7 +35,7 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
_startRegistrationButton() {
|
||||
InkWell _startRegistrationButton() {
|
||||
return InkWell(
|
||||
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -148,7 +148,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
_saveState() {
|
||||
void _saveState() {
|
||||
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
|
||||
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
|
||||
photos: imagesPaths,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -158,7 +158,7 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
setLinksState({
|
||||
void setLinksState({
|
||||
String? tgLink,
|
||||
String? vkLink,
|
||||
String? personalSiteLink,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
_modalBottomSheet({
|
||||
Future _modalBottomSheet({
|
||||
required CreateRecipeCubit createRecipeCubit,
|
||||
int? portionIndex,
|
||||
String? productNameControllerText,
|
||||
|
||||
@@ -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('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: (_) {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -86,7 +86,7 @@ class DietsCubit extends Cubit<DietsState> {
|
||||
);
|
||||
}
|
||||
|
||||
loadDiets({
|
||||
Future<void> loadDiets({
|
||||
String? query,
|
||||
}) async {
|
||||
try {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class PlansCubit extends Cubit<PlansState> {
|
||||
);
|
||||
}
|
||||
|
||||
loadPlans({
|
||||
Future<void> loadPlans({
|
||||
String? query,
|
||||
}) async {
|
||||
try {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user