Compare commits

..

1 Commits

Author SHA1 Message Date
Username1548
96e80ced38 feat(consultantRating): added api requests and state to consultant rating page 2025-06-17 13:33:41 +07:00
165 changed files with 1959 additions and 4515 deletions

View File

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

4
.gitignore vendored
View File

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

View File

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

View File

@@ -2,13 +2,9 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application
android:label="Eatvio"
android:usesCleartextTraffic="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -29,30 +25,8 @@
android:resource="@style/NormalTheme"
/>
<intent-filter>
<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/.*" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.

View File

@@ -1,29 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 986 B

View File

@@ -1,3 +0,0 @@
<svg width="19" height="14" viewBox="0 0 19 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.56039 6.73215L7.06827 10.2925C7.22519 10.452 7.3026 10.6385 7.30049 10.8521C7.29855 11.0656 7.21569 11.2555 7.0519 11.4218C6.88812 11.577 6.70269 11.6573 6.49563 11.6629C6.28874 11.6682 6.1034 11.5879 5.93961 11.4218L1.19568 6.60653C1.0048 6.41278 0.909363 6.1868 0.909363 5.92858C0.909363 5.67037 1.0048 5.44439 1.19568 5.25064L5.93961 0.435368C6.08581 0.287154 6.26675 0.211261 6.48243 0.207689C6.69829 0.204297 6.88812 0.280189 7.0519 0.435368C7.21569 0.601618 7.29759 0.792601 7.29759 1.00831C7.29759 1.22385 7.21569 1.41474 7.0519 1.58099L3.56039 5.12501H13.4583C14.8441 5.12501 16.0261 5.62162 17.0045 6.61483C17.983 7.60787 18.4722 8.80769 18.4722 10.2143V13.1607C18.4722 13.3888 18.3964 13.5797 18.2448 13.7334C18.0933 13.8873 17.9052 13.9643 17.6806 13.9643C17.4559 13.9643 17.2678 13.8873 17.1164 13.7334C16.9647 13.5797 16.8889 13.3888 16.8889 13.1607V10.2143C16.8889 9.25269 16.5539 8.43197 15.884 7.75215C15.2143 7.07215 14.4057 6.73215 13.4583 6.73215H3.56039Z" fill="#2F80ED"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>39</string>
<string>27</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -31,8 +31,6 @@
<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>
<key>NSLocationWhenInUseUsageDescription</key>

View File

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

View File

@@ -7,8 +7,6 @@ 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';
@@ -17,7 +15,6 @@ 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';
@@ -25,7 +22,6 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:eatvio_app/features/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';
@@ -46,6 +42,7 @@ 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';
@@ -70,12 +67,10 @@ 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>();
@@ -83,30 +78,6 @@ 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',
@@ -118,16 +89,6 @@ class AppRouter {
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,),
),
GoRoute(
path: 'scan-qr-code-screen',
name: 'scanQRCodeScreen',
builder: (context, state) => const ScanQRCodeScreen(),
),
],
),
ShellRoute(
@@ -156,10 +117,10 @@ class AppRouter {
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (e) {
rethrow;
}
return null;
return '/feed-screen';
},
),
GoRoute(
@@ -194,11 +155,6 @@ class AppRouter {
),
],
),
GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute(
path: '/welcome-screen',
name: 'welcomeScreen',
@@ -212,24 +168,22 @@ class AppRouter {
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'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;
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
return '/feed-screen';
} else {
return null;
}
},
),
GoRoute(
@@ -237,18 +191,14 @@ class AppRouter {
name: 'updateScreen',
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
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) {
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';
}
},
@@ -285,17 +235,8 @@ 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(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutPlanScreen(plan: data['plan'] as PlanModel, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
@@ -303,18 +244,9 @@ 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(
recipeSlug: data['recipe'] as String,
recipe: data['recipe'] as Recipe,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
@@ -337,16 +269,8 @@ 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(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutDietScreen(diet: data['diet'] as Diet, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
@@ -354,20 +278,7 @@ 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) {
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: '/blog-post-screen', name: 'blogPostScreen', builder: (context, state) => BlogPostScreen(blogPost: state.extra as FeedBlogPostModel)),
GoRoute(
path: '/chat-screen',
name: 'chatScreen',
@@ -381,11 +292,6 @@ class AppRouter {
);
},
),
GoRoute(
path: '/empty-chat-screen',
name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
),
GoRoute(
path: '/settings-screen',
name: 'settingsScreen',

View File

@@ -1,12 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'comment_update_model.freezed.dart';
part 'comment_update_model.g.dart';
@freezed
abstract class CommentUpdateModel with _$CommentUpdateModel {
factory CommentUpdateModel({required String body}) = _CommentUpdateModel;
factory CommentUpdateModel.fromJson(Map<String, dynamic> json) => _$CommentUpdateModelFromJson(json);
}

View File

@@ -1,203 +0,0 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
void showCommentsActionsBottomSheet(
BuildContext context, {
required void Function()? reply,
required void Function() copy,
required void Function(String text) edit,
required void Function() delete,
required void Function() complain,
required String commentText,
required bool isMyComment,
}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
),
child: CommentsActionsBottomSheet(
isMyComment: isMyComment,
reply: reply,
copy: copy,
edit: edit,
delete: delete,
complain: complain,
commentText: commentText,
),
));
}
class CommentsActionsBottomSheet extends StatefulWidget {
const CommentsActionsBottomSheet({
super.key,
required this.isMyComment,
required this.reply,
required this.copy,
required this.edit,
required this.delete,
required this.complain,
required this.commentText,
});
final bool isMyComment;
final void Function()? reply;
final void Function() copy;
final void Function(String text) edit;
final void Function() delete;
final void Function() complain;
final String commentText;
@override
State<CommentsActionsBottomSheet> createState() => _CommentsActionsBottomSheetState();
}
class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet> {
bool isUserEditingComment = false;
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.text = widget.commentText;
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: isUserEditingComment
? Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
Expanded(
child: CustomTextFormField(
autofocus: true,
controller: controller,
onChanged: (value) => setState(() {}),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
widget.edit(controller.text);
GoRouter.of(context).pop();
},
child: Container(
decoration: BoxDecoration(
color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
shape: BoxShape.circle,
),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
),
),
)
])
],
),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 7,
),
const BottomSheetHandlerBar(),
if (widget.isMyComment)
_ActionWidget(
text: s.edit,
iconPath: 'assets/icons/edit_paint.svg',
needToPop: false,
onTap: () => setState(() {
isUserEditingComment = true;
}),
)
else if (widget.reply != null)
_ActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
if (widget.reply != null) const CustomDivider(),
_ActionWidget(
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
),
if (widget.isMyComment) ...[
if (widget.reply != null) ...[
const CustomDivider(),
_ActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
],
const CustomDivider(),
_ActionWidget(
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
] else ...[
const CustomDivider(),
_ActionWidget(
text: s.complain,
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
),
]
],
),
);
}
}
class _ActionWidget extends StatelessWidget {
const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true});
final String text;
final String iconPath;
final void Function() onTap;
final bool needToPop;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (needToPop) Navigator.pop(context);
onTap();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
const Spacer(),
SvgPicture.asset(iconPath)
],
),
),
);
}
}

View File

@@ -1,24 +0,0 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
class CommentsRepository {
final CommentsService service;
CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e) {
rethrow;
}
}
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,16 +0,0 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:retrofit/retrofit.dart';
part 'comments_service.g.dart';
@RestApi()
abstract class CommentsService {
factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService;
@DELETE('/api/comments/{id}')
Future<void> deleteComment(@Path('id') int id);
@PATCH('/api/comments/{id}')
Future<void> updateComment(@Path('id') int id, @Body() CommentUpdateModel body);
}

View File

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

View File

@@ -1 +1,7 @@
enum ComplainedContent { diet, plan, recipe, profile, posts, comment }
enum ComplainedContent {
diet,
plan,
recipe,
profile,
posts
}

View File

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

View File

@@ -1,25 +1,22 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/core/comments/services/comments_service.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/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_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';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
@@ -39,9 +36,6 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
import 'package:eatvio_app/features/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';
@@ -58,7 +52,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
@@ -130,7 +124,7 @@ Future<void> init() async {
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
@@ -167,9 +161,7 @@ Future<void> init() async {
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()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -198,9 +190,7 @@ 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()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -229,11 +219,9 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
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()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
// External
sl.registerLazySingleton(
@@ -244,7 +232,6 @@ Future<void> init() async {
'accept': 'application/json',
'content-type': 'application/json',
},
validateStatus: (status) => status != null && status < 500,
),
)..interceptors.addAll(
[

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
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;
@@ -42,7 +39,6 @@ class WsManager {
metadata: const PusherChannelsOptionsMetadata.byDefault(),
port: 443,
);
client = PusherChannelsClient.websocket(
options: options,
connectionErrorHandler: (exception, trace, refresh) {
@@ -74,11 +70,6 @@ class WsManager {
}
});
client?.eventStream.listen((event) {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
log(event.rootObject.toString());
});
_subscribe();
@@ -145,7 +136,7 @@ class WsManager {
_privateDirectChatChannel?.unsubscribe();
}
Future<void> clear() async {
Future<void> clear() async{
await client?.disconnect();
client = null;
userId = null;

View File

@@ -56,7 +56,7 @@ class CustomDropDown extends StatelessWidget {
: _buildDropDownWidget(context);
}
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
_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(dynamic value) {
void onDropDownChanged(value) {
onChanged!(value!);
}

View File

@@ -11,7 +11,6 @@ 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';
@@ -29,7 +28,6 @@ class CustomPopup extends StatelessWidget {
final FeedBlogPostModel? feedBlogPostModel;
final Recipe? recipe;
final Diet? diet;
final String userSlug;
const CustomPopup({
super.key,
@@ -41,7 +39,6 @@ class CustomPopup extends StatelessWidget {
this.feedBlogPostModel,
this.recipe,
this.diet,
required this.userSlug
});
@override
@@ -76,7 +73,7 @@ class CustomPopup extends StatelessWidget {
);
}
List<PopupMenuEntry> _buildMenuItems(S s,BuildContext context) {
List<PopupMenuEntry> _buildMenuItems(s, context) {
final entries = <PopupMenuEntry>[];
void addItem(String icon, String text, Function() onTap) {
@@ -110,28 +107,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 && context.mounted) {
if (!result) {
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 && context.mounted) {
if (!result) {
showErrorDialog(context, title: 'Не удалось удалить рецепт');
return;
}
showSuccessToast('Рецепт удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.diet) {
final result = await sl<DietsCubit>().deleteDiet(id!);
if (!result && context.mounted) {
if (!result) {
showErrorDialog(context, title: 'Не удалось удалить диету');
return;
}
showSuccessToast('Диета удалена, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().deletePlan(slug!);
if (!result && context.mounted) {
if (!result) {
showErrorDialog(context, title: 'Не удалось удалить план');
return;
}
@@ -167,13 +164,6 @@ class CustomPopup extends StatelessWidget {
);
}
});
addItem(
'assets/icons/complain.svg',
'Заблокировать автора',
() {
sl<BlockedUsersCubit>().blockUser(userSlug);
},
);
}
if (entries.isNotEmpty && entries.last is PopupMenuItem) {

View File

@@ -26,13 +26,9 @@ class SuccessPublishedRecipeBottomSheet extends StatelessWidget {
s.recipe_publishing,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
'"$title" ${s.successfully_published}',
textAlign: TextAlign.center,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
'"$title" ${s.successfully_published}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(
height: 20,

View File

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

View File

@@ -1,35 +0,0 @@
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ResetPasswordCubit extends Cubit<ResetPasswordState> {
final ResetPasswordRepository resetPasswordRepository;
ResetPasswordCubit({required this.resetPasswordRepository}) : super(const ResetPasswordState.empty());
Future<void> trySendEmailCode(String email) async {
emit(const ResetPasswordState.loadingSendEmailCode());
try {
if (await resetPasswordRepository.sendEmailCodeForPasswordChange(email)) {
emit(const ResetPasswordState.codeSendSuccess());
} else {
emit(const ResetPasswordState.codeSendError());
}
} catch (e) {
emit(const ResetPasswordState.codeSendError());
}
}
Future<void> tryResetPassword(String email, String code, String password, String passwordConfirmation) async {
emit(const ResetPasswordState.loadingResetPassword());
try {
if (await resetPasswordRepository.confirmPasswordChange(code, password, passwordConfirmation, email)) {
emit(const ResetPasswordState.resetPasswordSuccess());
} else {
emit(const ResetPasswordState.resetPasswordError());
}
} catch (e) {
emit(const ResetPasswordState.resetPasswordError());
}
}
}

View File

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

View File

@@ -1,5 +1,4 @@
import 'dart:async';
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';
@@ -7,8 +6,8 @@ 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/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/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';
@@ -53,28 +52,17 @@ class _AuthScreenState extends State<AuthScreen> {
}
Future<void> _reconnectWebSocket(BuildContext context) async {
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
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;
}
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});
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
}
@override
@@ -92,10 +80,10 @@ class _AuthScreenState extends State<AuthScreen> {
title: GestureDetector(
onTap: () {
tapsCount++;
if (tapsCount == 7) {
if (tapsCount == 6) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
GoRouter.of(context).pushNamed(DevToolsScreen.name);
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
@@ -139,20 +127,6 @@ class _AuthScreenState extends State<AuthScreen> {
),
),
const SizedBox(height: 16),
Center(
child: GestureDetector(
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -208,137 +182,38 @@ class _AuthScreenState extends State<AuthScreen> {
),
),
const SizedBox(height: 16),
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),
),
),
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),
),
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,
),
),
),
],
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),
),
),
),
],
),

View File

@@ -1,166 +0,0 @@
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 {
final bool isAuth;
const ForgetPasswordScreen({super.key, required this.isAuth});
@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: widget.isAuth ? const Text('Смена пароля') : 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) ...[
Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
style: const 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('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
}
},
),
);
}
}

View File

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

View File

@@ -1,53 +0,0 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
class ResetPasswordRepository {
final ResetPasswordService resetPasswordService;
ResetPasswordRepository({
required this.resetPasswordService,
});
Future<bool> sendEmailCodeForPasswordChange(String email) async {
try {
final response = await resetPasswordService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e) {
rethrow;
}
}
Future<bool> confirmPasswordChange(
String code,
String password,
String passwordConfirmation,
String email,
) async {
try {
final response = await resetPasswordService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e) {
rethrow;
}
}
}

View File

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

View File

@@ -1,21 +0,0 @@
import 'dart:developer';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BlogPostCubit extends Cubit<BlogPostState> {
final FeedRepository feedRepository;
BlogPostCubit(this.feedRepository) : super(BlogPostState());
Future<void> getBlogPost(int id) async {
try {
emit(state.copyWith(isLoading: true));
final postModel = await feedRepository.getBlogPost(id: id);
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false));
}
}
}

View File

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

View File

@@ -5,10 +5,9 @@ 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/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/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.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';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -20,9 +19,9 @@ import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class BlogPostScreen extends StatefulWidget {
final int id;
final FeedBlogPostModel blogPost;
const BlogPostScreen({super.key, required this.id});
const BlogPostScreen({super.key, required this.blogPost});
@override
State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -32,294 +31,263 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>();
final textEditingController = TextEditingController();
int? replyId;
bool isLiked = false;
int totalLikes = 0;
late bool isLiked;
late int totalLikes;
@override
void initState() {
context.read<FeedCubit>().viewPost(widget.id);
isLiked = widget.blogPost.wasLikedByMe;
totalLikes = widget.blogPost.totalLikes;
context.read<FeedCubit>().viewPost(widget.blogPost.id);
super.initState();
}
@override
Widget build(BuildContext context) {
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');
},
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),
),
),
),
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),
GestureDetector(
onTap: () {
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),
),
),
],
),
),
appBar: AppBar(
title: Text(blogPostState.blogPostModel?.title ?? ''),
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
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),
),
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 ?? '',
),
],
),
),
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: 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),
),
],
),
],
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'),
),
),
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(
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
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);
},
Text(
widget.blogPost.author.authorName,
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, height: 1),
),
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 SizedBox(height: 2),
Text(
widget.blogPost.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12, fontWeight: FontWeight.w400, height: 1),
),
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)),
],
),
],
),
),
),
if (widget.blogPost.description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Html(data: widget.blogPost.description, style: {"*": Style(color: Colors.black87)}),
),
const SizedBox(height: 8),
if (widget.blogPost.image.isNotEmpty)
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.blogPost.image}',
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
icon: Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(widget.blogPost.id)
: await context.read<FeedCubit>().unlikedPost(widget.blogPost.id);
sl<FeedCubit>().updateFeedBlogPostList(widget.blogPost, isLiked);
},
),
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(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.blogPost.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
),
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 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)),
),
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),
),
),
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),
],
),
),
);
},
),
);
}
InkWell _iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) {
_iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) {
return InkWell(
onTap: onTap ?? () {},
borderRadius: BorderRadius.circular(16),

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
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;
@@ -11,7 +9,7 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
required this.profileRepository,
}) : super(const ProfileAvatarState.loading());
Future<void> getMyProfile() async {
getMyProfile() async {
emit(const ProfileAvatarState.loading());
try {
emit(
@@ -20,7 +18,6 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
),
);
} catch (e) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));
}
}

View File

@@ -1,7 +1,6 @@
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';
@@ -31,7 +30,6 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1;
late final List<String> pages;
int tapsCount = 0;
@override
void initState() {
@@ -60,29 +58,12 @@ 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
@@ -101,9 +82,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl,
),
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),

View File

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

View File

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

View File

@@ -7,13 +7,11 @@ 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';
@@ -47,7 +45,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
_timer?.cancel();
}
Future<void> loadNextPage(String chatId) async {
loadNextPage(String chatId) async {
if (currentPage < lastPage && !isLoading) {
isLoading = true;
@@ -89,7 +87,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
Future<void> initChat(String chatId, {bool sendInitialMessage = false}) async {
initChat(String chatId, {bool sendInitialMessage = false}) async {
try {
final chatRoom = await chatRepository.getChatRoom(chatId);
ChosenPlanModel? chosenPlan;
@@ -169,11 +167,6 @@ 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) {
@@ -183,17 +176,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
});
}
Future<bool> sendMessage({
String? data,
List<File>? files,
required String userSlug,
}) async {
Future<bool> sendMessage({required String chatId, String? data, List<File>? files}) async {
try {
await chatRepository.sendMessage(
data: data,
files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
slug: userSlug,
);
await chatRepository.sendMessage(chatId: chatId, data: data, files: files != null ? await ImageCompressUtility.compressFilesList(files) : null);
AudioPlayer().play((AssetSource('audio/send_message.wav')));
return true;
} catch (e, st) {

View File

@@ -1,7 +1,6 @@
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/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
@@ -9,7 +8,6 @@ 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';
@@ -23,18 +21,15 @@ 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
@@ -49,7 +44,6 @@ 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();
@@ -69,7 +63,7 @@ class _ChatScreenState extends State<ChatScreen> {
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
}
void _onScroll() {
_onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId);
}
@@ -102,7 +96,7 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
Scaffold _buildChat(BuildContext context, ChatRoomStateLoaded state) {
_buildChat(BuildContext context, ChatRoomStateLoaded state) {
final chosenPlan = state.chatRoom.chosenPlan;
final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id;
final showUserDetailsButton = [
@@ -339,155 +333,100 @@ class _ChatScreenState extends State<ChatScreen> {
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
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);
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),
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',
],
],
);
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))),
),
),
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),
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),
],
),
),
@@ -495,173 +434,21 @@ 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});
},
)),
],
),
),
);
}
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) {
_messageDate(DateTime date) {
return Center(
child: Padding(
padding: const EdgeInsets.only(top: 32),

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
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';
@@ -28,7 +27,7 @@ class ChatRepository {
}
Future<void> sendMessage({
required String slug,
required String chatId,
String? data,
List<File>? files,
}) async {
@@ -47,13 +46,12 @@ class ChatRepository {
}
}
await chatService.sendMessageByUserSlug(
userSlug: slug,
await chatService.sendMessage(
id: chatId,
data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (e) {
rethrow;
}
}
@@ -156,17 +154,4 @@ class ChatRepository {
rethrow;
}
}
Future<String> getChatByUserSlug(String slug) async {
try {
final response = await chatService.getChatByUserSlug(slug: slug);
if (response.response.statusCode == 404) {
return '404';
} else {
return response.data['id'];
}
} catch (e) {
return 'error';
}
}
}

View File

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

View File

@@ -72,12 +72,6 @@ class ComplainingCubit extends Cubit<ComplainingState> {
emit(state.copyWith(wasSentSuccessfully: true));
return;
}
case ComplainedContent.comment:
if (id != null) {
await repository.sendCommentComplain(id: id, category: category.getValue(), body: state.complainInfo);
emit(state.copyWith(wasSentSuccessfully: true));
return;
}
}
}
} catch (e, st) {

View File

@@ -64,16 +64,4 @@ class ComplainingRepository {
rethrow;
}
}
Future<void> sendCommentComplain({
required int id,
required String category,
required ComplainModel body,
}) async {
try {
await service.sendCommentComplain(id: id, category: category, body: body);
} catch (e) {
rethrow;
}
}
}

View File

@@ -42,12 +42,5 @@ abstract class ComplainingService {
@Path() required String category,
@Body() required ComplainModel body,
});
@POST('/api/comments/{id}/complain/{category}')
Future<void> sendCommentComplain({
@Path() required int id,
@Path() required String category,
@Body() required ComplainModel body,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
class ConsultantRatingRepository {
final ConsultantRatingService service;
ConsultantRatingRepository(this.service);
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
try {
return await service.getConsultantStatistics(date: date);
} catch (e) {
rethrow;
}
}
Future<List<RatingModel>> getScoreLogs({String? date}) async {
try {
final data = (await service.getScoreLogs(date: date)).data;
return data;
} catch (e) {
rethrow;
}
}
}

View File

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

View File

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

View File

@@ -194,7 +194,6 @@ 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),
],
@@ -348,7 +347,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
);
}
Padding _dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) {
_dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) {
choiceChipWidget(int index) {
return ChoiceChip(
selectedColor: appTheme.blueA400,
@@ -435,7 +434,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
);
}
SingleChildRenderObjectWidget _daily(DailySchedule? daily, S s) {
_daily(DailySchedule? daily, S s) {
if (daily == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(bottom: 12),

View File

@@ -289,7 +289,7 @@ class _DayWidgetState extends State<DayWidget> {
);
}
Material _product(int index, String product, int quantity, String portion, int dayIndex, int mealIndex) {
_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> {
);
}
Future _modalBottomSheet({
_modalBottomSheet({
required CreateDietCubit createDietCubit,
required int dayIndex,
required int mealIndex,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
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';
@@ -52,29 +51,6 @@ 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;
@@ -105,8 +81,6 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
onPressed: () {
setState(() {
baseUrlForTest = controller.text;
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear();
});
},
@@ -115,50 +89,43 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
],
),
const SizedBox(height: 10),
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(),
),
if (baseUrlForTest != baseUrl)
CustomOutlinedButton(
text: 'Set dev2 base url',
onPressed: () {
setState(() {
baseUrlForTest = baseUrl;
});
},
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
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;
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;
});
try {
setState(() {
testsStates[index] = TestState.loading;
});
final result = await testCases[index].run(baseUrl: baseUrlForTest);
if (result.success) {
if (result) {
setState(() {
testsStates[index] = TestState.success;
});
@@ -168,65 +135,27 @@ 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'),
),
],
),
);
}
}
} 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(
},
buttonStyle: const ButtonStyle(),
),
const SizedBox(width: 5),
switch (testsStates[index]) {
TestState.success => Container(width: 20, height: 20, color: Colors.green),
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox(
width: 20,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
},
],
),
],
},
],
),
],
),
),
),
),
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'add token to server',

View File

@@ -4,48 +4,19 @@ 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<TestResult> run({required String baseUrl}) async => throw UnimplementedError();
Future<bool> run({required String baseUrl}) async => throw UnimplementedError();
}
class CreateRecipeTask implements TestCases {
@@ -53,7 +24,7 @@ class CreateRecipeTask implements TestCases {
String get name => 'api/recipes/create';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
@@ -70,16 +41,10 @@ class CreateRecipeTask implements TestCases {
steps: [Step(description: 'desc')],
),
);
} 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');
} catch (e) {
return false;
}
return TestResult(true);
return true;
}
}
@@ -88,54 +53,69 @@ class Test850task implements TestCases {
String get name => 'task #850';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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, stack) {
} catch (e) {
log('unexpected error7: $e');
return TestResult(false, '$e\n$stack');
return false;
}
await Future.delayed(const Duration(seconds: 3));
}
try {
await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')});
return TestResult(false, 'Expected error but got success');
return false;
} catch (e) {
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, stack) {
} catch (e) {
log('unexpected error2: $e');
return TestResult(false, '$e\n$stack');
return false;
}
// test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
} catch (e, stack) {
} catch (e) {
log('unexpected error3: $e');
return TestResult(false, '$e\n$stack');
return false;
}
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, stack) {
} catch (e) {
log('unexpected error4: $e');
return TestResult(false, '$e\n$stack');
return false;
}
// test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
} catch (e) {
@@ -144,10 +124,11 @@ class Test850task implements TestCases {
break;
}
}
return TestResult(true);
} catch (e, stack) {
return true;
} catch (e) {
log('unexpected error6: $e');
return TestResult(false, '$e\n$stack');
return false;
}
}
}
@@ -158,16 +139,16 @@ class GetChatRoomPreviews implements TestCases {
String get name => '/api/chat/index';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -177,16 +158,16 @@ class GetChatRoom implements TestCases {
String get name => '/api/chat/show/{id}';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -196,16 +177,16 @@ class SendMessage implements TestCases {
String get name => '/api/chat/{id}/message/send';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -215,16 +196,16 @@ class GetChatMessages implements TestCases {
String get name => '/api/chat/show/{id}/messages?page={page}';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -234,7 +215,7 @@ class GetChatRoomIdByUserSlug implements TestCases {
String get name => '/api/chat/get_by_user/{user}';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -249,9 +230,9 @@ class GetChatRoomIdByUserSlug implements TestCases {
break;
}
}
return TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -262,14 +243,14 @@ class GetBlogPostList implements TestCases {
String get name => '/api/blogPosts/index';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getBlogPostList(page: 1);
return TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -279,7 +260,7 @@ class GetRecipes implements TestCases {
String get name => '/api/recipes/index';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -288,9 +269,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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -300,14 +281,14 @@ class GetDiets implements TestCases {
String get name => '/api/diets/index';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getDiets(page: 1, query: null);
return TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -317,14 +298,14 @@ class GetPlans implements TestCases {
String get name => '/api/plans/index';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getPlans(page: 1, query: null);
return TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -334,16 +315,16 @@ class GetBlogPostComments implements TestCases {
String get name => '/api/blogPosts/{id}/comments/show';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -353,16 +334,16 @@ class SendBlogPostComment implements TestCases {
String get name => '/api/blogPosts/{id}/comment/create';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -372,16 +353,16 @@ class GetDietsComments implements TestCases {
String get name => '/api/diets/{id}/comments/show';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -391,16 +372,16 @@ class SendDietsComment implements TestCases {
String get name => '/api/diets/{id}/comment/create';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -410,16 +391,16 @@ class GetPlansComments implements TestCases {
String get name => '/api/plans/{slug}/comments/show';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -429,16 +410,16 @@ class SendPlansComment implements TestCases {
String get name => '/api/plans/{slug}/comment/create';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> 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 TestResult(true);
return true;
} catch (e) {
return TestResult(false, e.toString());
return false;
}
}
}
@@ -448,7 +429,7 @@ class GetRecipesComments implements TestCases {
String get name => '/api/recipes/{id}/comments/show';
@override
Future<TestResult> run({required String baseUrl}) async {
Future<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -459,426 +440,9 @@ 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 TestResult(true);
return true;
} catch (e) {
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());
return false;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -134,13 +134,13 @@ class _CalendarWidgetState extends State<CalendarWidget> {
);
}
void _nextCalendarFormat() {
_nextCalendarFormat() {
setState(() {
_calendarFormat = CalendarFormat.values[(_calendarFormat.index - 1) % CalendarFormat.values.length];
});
}
GestureDetector _arrowDown() {
_arrowDown() {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onVerticalDragStart: (_) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,11 +10,7 @@ class ExpandableText extends StatefulWidget {
final String text;
final int maxLines;
const ExpandableText({
super.key,
required this.text,
this.maxLines = 5, // по умолчанию 5 строк
});
const ExpandableText({super.key, required this.text, this.maxLines = 3});
@override
State<ExpandableText> createState() => _ExpandableTextState();
@@ -22,29 +18,6 @@ class ExpandableText extends StatefulWidget {
class _ExpandableTextState extends State<ExpandableText> {
bool isExpanded = false;
bool showExpandButton = false;
final GlobalKey _textKey = GlobalKey();
@override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) => _checkTextOverflow());
}
void _checkTextOverflow() {
final textRenderObject = _textKey.currentContext?.findRenderObject();
if (textRenderObject is RenderBox) {
final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2;
final actualHeight = textRenderObject.size.height;
if (actualHeight > maxLinesHeight && !showExpandButton) {
setState(() {
showExpandButton = true;
});
}
}
}
void toggleExpansion() {
setState(() {
@@ -60,63 +33,52 @@ class _ExpandableTextState extends State<ExpandableText> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Stack(
children: [
Text(
widget.text,
key: _textKey,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
maxLines: isExpanded ? null : widget.maxLines,
overflow: TextOverflow.clip,
),
if (!isExpanded && showExpandButton)
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 35,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withAlpha(0),
Colors.white.withAlpha((255 * 0.6).toInt()),
Colors.white,
],
),
child: Stack(
children: [
Text(
widget.text,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
// style: {
// "*": Style(
// margin: Margins.all(0),
// color: Colors.black87,
// maxLines: isExpanded ? null : widget.maxLines,
// textOverflow: TextOverflow.clip,
// ),
// },
),
if (!isExpanded)
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 35,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white.withValues(alpha: 0.0), Colors.white.withValues(alpha: 0.6), Colors.white],
),
),
),
),
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: toggleExpansion,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(isExpanded ? s.hide : s.expand, style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400)),
Transform.rotate(angle: isExpanded ? math.pi : 0, child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12)),
],
),
),
),
if (showExpandButton)
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: toggleExpansion,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isExpanded ? s.hide : s.expand,
style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400),
),
const SizedBox(width: 4),
Transform.rotate(
angle: isExpanded ? math.pi : 0,
child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12),
),
],
),
),
),
],
);
}

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ abstract class PlanModel with _$PlanModel {
factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json);
}
PacketOptions? parseOption(dynamic json) {
PacketOptions? parseOption(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(dynamic json) {
List<String> _parseItems(json) {
if (json is Map<String, dynamic>) {
return json.values.map((e) => e as String).toList();
}

View File

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

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