Compare commits

..

14 Commits

Author SHA1 Message Date
Username1548
45774c3875 fix 2025-08-14 16:58:00 +03:00
Username1548
62c0914fe2 fix 2025-08-14 16:55:15 +03:00
Username1548
48f5b64223 feat(emailVerification): added timer for email code resending 2025-08-14 16:54:13 +03:00
Username1548
91bb5cf483 feat(menu): fixed indentation 2025-08-12 09:01:31 +03:00
Username1548
c4dedac3f3 added build date 2025-08-12 12:27:58 +07:00
mso7nueh
8ed243f63c Merge pull request 'fix: upd ver' (#638) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#638
2025-08-11 14:19:46 +00:00
mso7nueh
a10fe6d2f5 fix: upd ver 2025-08-11 17:19:14 +03:00
mso7nueh
35b78bdeee Merge pull request 'fix(blackList+notifications): Issue-523, 191' (#636) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#636
2025-08-11 14:00:25 +00:00
mso7nueh
ef037eb4d5 Merge pull request 'feat(chats): added message resend and deleting, feat(menu): added build number' (#633) from misha/eatvio_app:issue-594 into main
Reviewed-on: eatvio/eatvio_app#633
2025-08-11 14:00:14 +00:00
mso7nueh
5a3385cacf Merge pull request 'removed public offer from ui on ios' (#637) from misha/eatvio_app:issue-635 into main
Reviewed-on: eatvio/eatvio_app#637
2025-08-11 14:00:05 +00:00
Username1548
ef4d91a5d8 removed public offert from ui on ios 2025-08-11 20:46:09 +07:00
mso7nueh
dca4b4c471 fix(blackList+notifications): Issue-523, 191 2025-08-11 14:53:43 +03:00
mso7nueh
cfbd7597fa Merge pull request 'fix(archive_of_consultatons): Issue-331' (#631) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#631
2025-08-08 10:43:27 +00:00
mso7nueh
412ac0aeeb fix(archive_of_consultatons): Issue-331 2025-08-08 13:43:04 +03:00
24 changed files with 814 additions and 386 deletions

View File

@@ -144,7 +144,11 @@ jobs:
run: flutter test
- name: Build apk
run: flutter build apk --release --dart-define=HIDE_PLANS=false
run: |
BUILD_DATE=$(TZ=Europe/Moscow date +'%Y-%m-%d %H:%M:%S')
flutter build apk --release \
--dart-define=HIDE_PLANS=false \
--dart-define=BUILD_DATE="$BUILD_DATE"
- name: Rename and move apk
run: |

View File

@@ -396,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 67;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -546,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 67;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -584,7 +584,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 67;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>67</string>
<string>68</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -46,13 +46,15 @@ platform :ios do
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true"
xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'"
)

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/env_config.dart';
String baseUrl = EnvConfig.baseUrl;
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');
const List<int> durationValues = [7, 14, 21];

View File

@@ -73,10 +73,7 @@ class _AuthScreenState extends State<AuthScreen> {
rethrow;
}
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
@override
@@ -102,11 +99,7 @@ class _AuthScreenState extends State<AuthScreen> {
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
fit: BoxFit.contain,
),
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
),
),
body: SafeArea(
@@ -137,10 +130,7 @@ class _AuthScreenState extends State<AuthScreen> {
hidePassword = !hidePassword;
});
},
icon: Icon(
hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.grey,
),
icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey),
),
),
const SizedBox(height: 16),
@@ -149,11 +139,7 @@ class _AuthScreenState extends State<AuthScreen> {
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
),
@@ -166,11 +152,7 @@ class _AuthScreenState extends State<AuthScreen> {
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text(
'Зарегистрироваться',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
],
@@ -181,23 +163,22 @@ class _AuthScreenState extends State<AuthScreen> {
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(
fontSize: 12,
color: Colors.black,
),
style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
if (!hidePlans) ...[
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
const TextSpan(text: ' и '),
const TextSpan(text: ' и '),
],
TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(
@@ -223,17 +204,12 @@ class _AuthScreenState extends State<AuthScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
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,
);
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
@@ -242,10 +218,7 @@ class _AuthScreenState extends State<AuthScreen> {
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: Text(
'Войти',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
const SizedBox(width: 8),
@@ -257,9 +230,7 @@ class _AuthScreenState extends State<AuthScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
@@ -274,14 +245,10 @@ class _AuthScreenState extends State<AuthScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Где взять QR-код?\n',
style: TextStyle(
fontSize: 18,
),
),
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
@@ -293,15 +260,10 @@ class _AuthScreenState extends State<AuthScreen> {
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: const Text(
'Отмена',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
),
),
),
@@ -314,15 +276,10 @@ class _AuthScreenState extends State<AuthScreen> {
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Text(
'Перейти',
style: TextStyle(
color: appTheme.blueA400,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
),
),
),
@@ -337,10 +294,7 @@ class _AuthScreenState extends State<AuthScreen> {
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: const Icon(
Icons.qr_code_scanner_rounded,
color: Colors.white,
),
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
),
],

View File

@@ -13,6 +13,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
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';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.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/presentation/widgets/notification_banner.dart';
@@ -446,7 +448,27 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () {
onTap: () async {
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),

View File

@@ -12,6 +12,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
class EmptyChatScreen extends StatefulWidget {
final Author author;
@@ -133,21 +135,45 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
InkWell(
customBorder: const CircleBorder(),
onTap: () async {
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if (textEditingController.text.isNotEmpty) {
await sl<ChatRepository>().sendMessage(
data: textEditingController.text.trim(),
slug: widget.author.authorSlug,
);
setState(() {
textEditingController.clear();
});
if (mounted) {
setState(() {
textEditingController.clear();
});
}
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
if (context.mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: Ink(
@@ -175,20 +201,44 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
List<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
List<File> files = images.map((image) => File(image.path)).toList();
await sl<ChatRepository>().sendMessage(
files: [File(image.path)],
files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
if (mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: const Row(
@@ -211,12 +261,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
if (mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: const Row(
@@ -261,12 +313,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
if (mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: const Row(

View File

@@ -25,24 +25,16 @@ class _LastStepScreenState extends State<LastStepScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Последний шаг',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
height: 1.1,
),
),
child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
),
const SizedBox(height: 16),
CheckboxListTile(
value: checkboxState,
title: const Text(
'Я ознакомлен и принимаю договор публичной офёрты',
style: TextStyle(
fontSize: 18,
height: 1,
),
title: Text(
hidePlans
? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных'
: 'Я ознакомлен и принимаю договор публичной офёрты',
style: const TextStyle(fontSize: 18, height: 1),
),
onChanged: (_) {
setState(() {
@@ -53,25 +45,14 @@ class _LastStepScreenState extends State<LastStepScreen> {
activeColor: appTheme.blueA400,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: _divider(context),
),
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: TextButton(
onPressed: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
child: const Text(
'Договор офёрты',
style: TextStyle(
height: 1,
fontSize: 16,
color: Color(0xFF323F4B),
decoration: TextDecoration.underline,
),
onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')),
child: Text(
hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты',
style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),
),
),
),
@@ -84,10 +65,7 @@ class _LastStepScreenState extends State<LastStepScreen> {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
);
}
}

View File

@@ -50,6 +50,7 @@ class ConsultationService {
else if (state != null && stateComparison != null)
'filters[last_state][${stateComparison.value}]': state,
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'created_at:desc',
},
);
return ConsultationPaginationModel.fromJson(response.data);

View File

@@ -31,9 +31,7 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
? null
: state.consultationFilterStatus == ConsultationFilterStatus.inProgress
? FiltersComparisonEnum.notInArray
: FiltersComparisonEnum.equal,
: FiltersComparisonEnum.equal,
);
emit(state.copyWith(consultations: consultations, isLoading: false));
} catch (e) {
@@ -67,20 +65,17 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
enum ConsultationFilterStatus {
all,
canceled,
finished,
inProgress;
finished;
String get name => switch (this) {
ConsultationFilterStatus.all => 'Все консультации',
ConsultationFilterStatus.canceled => 'Отменён',
ConsultationFilterStatus.finished => 'Завершён',
ConsultationFilterStatus.inProgress => 'В процессе',
};
String? get value => switch (this) {
ConsultationFilterStatus.all => null,
ConsultationFilterStatus.canceled => 'canceled_by_the_initiator',
ConsultationFilterStatus.finished => 'charge_was_collected',
ConsultationFilterStatus.inProgress => 'active',
};
}

View File

@@ -195,7 +195,7 @@ class _ConsultationsArchiveScreenState extends State<ConsultationsArchiveScreen>
),
),
Text(
state.consultations[index].startDate.format('yyyy.MM.dd'),
state.consultations[index].startDate.format('dd.MM.yy'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,

View File

@@ -156,7 +156,13 @@ class _AppInfoState extends State<AppInfo> {
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsetsGeometry.only(left: 16),
child: Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
if (buildDate != '') Text('Дата сборки: $buildDate', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
],
),
),
);
}

View File

@@ -538,6 +538,297 @@ class EntityCommented implements BaseNotificationModel {
}
}
class PlanSubscriptionExpiredNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
@override
final DateTime createdAt;
PlanSubscriptionExpiredNotification({
required this.type,
required this.planName,
required this.customerName,
required this.isRead,
required this.createdAt,
});
factory PlanSubscriptionExpiredNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PlanSubscriptionExpiredNotification(
type: type,
planName: json['plan_name'],
customerName: json['user_name'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class PlanSubscriptionRenewedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
final String renewalDate;
@override
final DateTime createdAt;
PlanSubscriptionRenewedNotification({
required this.type,
required this.planName,
required this.customerName,
required this.renewalDate,
required this.isRead,
required this.createdAt,
});
factory PlanSubscriptionRenewedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PlanSubscriptionRenewedNotification(
type: type,
planName: json['plan_name'],
customerName: json['user_name'],
renewalDate: json['renewal_date'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ConsultantAssignedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String consultantName;
final String planName;
final int planId;
@override
final DateTime createdAt;
ConsultantAssignedNotification({
required this.type,
required this.consultantName,
required this.planName,
required this.planId,
required this.isRead,
required this.createdAt,
});
factory ConsultantAssignedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ConsultantAssignedNotification(
type: type,
consultantName: json['consultant_name'],
planName: json['plan_name'],
planId: json['plan_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ConsultantUnassignedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String consultantName;
final String planName;
final int planId;
@override
final DateTime createdAt;
ConsultantUnassignedNotification({
required this.type,
required this.consultantName,
required this.planName,
required this.planId,
required this.isRead,
required this.createdAt,
});
factory ConsultantUnassignedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ConsultantUnassignedNotification(
type: type,
consultantName: json['consultant_name'],
planName: json['plan_name'],
planId: json['plan_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class PaymentFailedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String errorMessage;
final String paymentMethod;
@override
final DateTime createdAt;
PaymentFailedNotification({
required this.type,
required this.planName,
required this.errorMessage,
required this.paymentMethod,
required this.isRead,
required this.createdAt,
});
factory PaymentFailedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PaymentFailedNotification(
type: type,
planName: json['plan_name'],
errorMessage: json['error_message'],
paymentMethod: json['payment_method'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class PaymentSuccessfulNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final double amount;
final String currency;
final String transactionId;
@override
final DateTime createdAt;
PaymentSuccessfulNotification({
required this.type,
required this.planName,
required this.amount,
required this.currency,
required this.transactionId,
required this.isRead,
required this.createdAt,
});
factory PaymentSuccessfulNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PaymentSuccessfulNotification(
type: type,
planName: json['plan_name'],
amount: (json['amount'] as num).toDouble(),
currency: json['currency'],
transactionId: json['transaction_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class GoalAchievedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String goalName;
final String goalType;
final double achievedValue;
final double targetValue;
@override
final DateTime createdAt;
GoalAchievedNotification({
required this.type,
required this.goalName,
required this.goalType,
required this.achievedValue,
required this.targetValue,
required this.isRead,
required this.createdAt,
});
factory GoalAchievedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return GoalAchievedNotification(
type: type,
goalName: json['goal_name'],
goalType: json['goal_type'],
achievedValue: (json['achieved_value'] as num).toDouble(),
targetValue: (json['target_value'] as num).toDouble(),
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class WorkoutReminderNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String workoutName;
final String workoutType;
final int duration;
@override
final DateTime createdAt;
WorkoutReminderNotification({
required this.type,
required this.workoutName,
required this.workoutType,
required this.duration,
required this.isRead,
required this.createdAt,
});
factory WorkoutReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return WorkoutReminderNotification(
type: type,
workoutName: json['workout_name'],
workoutType: json['workout_type'],
duration: json['duration'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class MealReminderNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String mealName;
final String mealType;
final int calories;
@override
final DateTime createdAt;
MealReminderNotification({
required this.type,
required this.mealName,
required this.mealType,
required this.calories,
required this.isRead,
required this.createdAt,
});
factory MealReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return MealReminderNotification(
type: type,
mealName: json['meal_name'],
mealType: json['meal_type'],
calories: json['calories'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class UnknownNotification implements BaseNotificationModel {
@override
final NotificationType type;
@@ -570,6 +861,16 @@ enum NotificationType {
commentReplied,
scoreAdded,
entityCommented,
// Дополнительные типы уведомлений
planSubscriptionExpired,
planSubscriptionRenewed,
consultantAssigned,
consultantUnassigned,
paymentFailed,
paymentSuccessful,
goalAchieved,
workoutReminder,
mealReminder,
unknown;
static NotificationType getTypeFromString(String type) => switch (type) {
@@ -593,6 +894,16 @@ enum NotificationType {
'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied,
'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded,
'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented,
// Дополнительные маппинги
'App\\Notifications\\Plan\\PlanSubscriptionExpired' => NotificationType.planSubscriptionExpired,
'App\\Notifications\\Plan\\PlanSubscriptionRenewed' => NotificationType.planSubscriptionRenewed,
'App\\Notifications\\Plan\\ConsultantAssigned' => NotificationType.consultantAssigned,
'App\\Notifications\\Plan\\ConsultantUnassigned' => NotificationType.consultantUnassigned,
'App\\Notifications\\Payment\\PaymentFailed' => NotificationType.paymentFailed,
'App\\Notifications\\Payment\\PaymentSuccessful' => NotificationType.paymentSuccessful,
'App\\Notifications\\Goal\\GoalAchieved' => NotificationType.goalAchieved,
'App\\Notifications\\Workout\\WorkoutReminder' => NotificationType.workoutReminder,
'App\\Notifications\\Meal\\MealReminder' => NotificationType.mealReminder,
_ => NotificationType.unknown,
};
}

View File

@@ -113,6 +113,16 @@ class NotificationsCubit extends Cubit<NotificationsState> {
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
// Новые типы уведомлений
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {

View File

@@ -47,6 +47,16 @@ class NotificationWidget extends StatelessWidget {
CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied),
ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded),
EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented),
// Новые типы уведомлений
PlanSubscriptionExpiredNotification() => _PlanSubscriptionExpiredWidget(notification: notification as PlanSubscriptionExpiredNotification),
PlanSubscriptionRenewedNotification() => _PlanSubscriptionRenewedWidget(notification: notification as PlanSubscriptionRenewedNotification),
ConsultantAssignedNotification() => _ConsultantAssignedWidget(notification: notification as ConsultantAssignedNotification),
ConsultantUnassignedNotification() => _ConsultantUnassignedWidget(notification: notification as ConsultantUnassignedNotification),
PaymentFailedNotification() => _PaymentFailedWidget(notification: notification as PaymentFailedNotification),
PaymentSuccessfulNotification() => _PaymentSuccessfulWidget(notification: notification as PaymentSuccessfulNotification),
GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification),
WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification),
MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification),
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
};
}
@@ -443,3 +453,121 @@ class _UnknownNotificationWidget extends StatelessWidget {
);
}
}
// Новые виджеты для дополнительных типов уведомлений
class _PlanSubscriptionExpiredWidget extends StatelessWidget {
const _PlanSubscriptionExpiredWidget({required this.notification});
final PlanSubscriptionExpiredNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка на план "${notification.planName}" истекла',
createdAt: notification.createdAt,
);
}
}
class _PlanSubscriptionRenewedWidget extends StatelessWidget {
const _PlanSubscriptionRenewedWidget({required this.notification});
final PlanSubscriptionRenewedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantAssignedWidget extends StatelessWidget {
const _ConsultantAssignedWidget({required this.notification});
final ConsultantAssignedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantUnassignedWidget extends StatelessWidget {
const _ConsultantUnassignedWidget({required this.notification});
final ConsultantUnassignedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _PaymentFailedWidget extends StatelessWidget {
const _PaymentFailedWidget({required this.notification});
final PaymentFailedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}',
createdAt: notification.createdAt,
);
}
}
class _PaymentSuccessfulWidget extends StatelessWidget {
const _PaymentSuccessfulWidget({required this.notification});
final PaymentSuccessfulNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}',
createdAt: notification.createdAt,
);
}
}
class _GoalAchievedWidget extends StatelessWidget {
const _GoalAchievedWidget({required this.notification});
final GoalAchievedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}',
createdAt: notification.createdAt,
);
}
}
class _WorkoutReminderWidget extends StatelessWidget {
const _WorkoutReminderWidget({required this.notification});
final WorkoutReminderNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)',
createdAt: notification.createdAt,
);
}
}
class _MealReminderWidget extends StatelessWidget {
const _MealReminderWidget({required this.notification});
final MealReminderNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)',
createdAt: notification.createdAt,
);
}
}

View File

@@ -119,6 +119,61 @@ class NotificationsService {
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
// Новые типы уведомлений
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.mealReminder => MealReminderNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
};
} catch (e) {

View File

@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug,
@JsonKey(name: "profile_photo_url") required String profilePhotoUrl,

View File

@@ -93,6 +93,39 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
isFirstFetch = false;
}
// Проверяем, заблокировал ли пользователь меня
if (state.profile.isBlockedByUser) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.block,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Пользователь ограничил доступ к своей странице',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
],
),
),
),
);
}
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) {
return Scaffold(
@@ -174,10 +207,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans) _activePlans(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state),
_blogPosts(state),
],
),

View File

@@ -1,5 +1,10 @@
import 'dart:async';
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/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:flutter/material.dart';
@@ -18,11 +23,14 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
final List<TextEditingController> _controllers = List.generate(4, (_) => TextEditingController());
String email = '';
late final GoRouter goRouter;
late Timer _timer;
int _secondsRemaining = 60;
bool _canSendAgain = false;
@override
void initState() {
super.initState();
goRouter = GoRouter.of(context);
goRouter = GoRouter.of(context);
_startTimer();
}
@override
@@ -36,6 +44,23 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
super.dispose();
}
void _startTimer() {
setState(() {
_canSendAgain = false;
_secondsRemaining = 60;
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_secondsRemaining > 0) {
_secondsRemaining--;
} else {
_canSendAgain = true;
_timer.cancel();
}
});
});
}
void _checkAndSubmitCode(BuildContext context) {
final code = _controllers.map((c) => c.text).join();
if (code.length == 4) {
@@ -74,18 +99,25 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
children: [
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) => _buildCodeField(context, index)),
),
const SizedBox(height: 20),
const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)),
TextButton(
onPressed: () {
context.read<VerifyEmailCubit>().sendEmailCode();
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))),
const SizedBox(height: 40),
CustomOutlinedButton(
text: 'Запросить код повторно',
height: 52,
isDisabled: !_canSendAgain,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray),
),
onPressed: () async {
await context.read<VerifyEmailCubit>().sendEmailCode();
_startTimer();
},
child: const Text("Не приходит код?"),
),
const SizedBox(height: 8),
if (!_canSendAgain)
Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)),
],
),
),

View File

@@ -51,9 +51,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
builder: (BuildContext context, SettingsState state) {
if (state is SettingsStateLoaded) {
return Scaffold(
appBar: AppBar(
title: const Text('Настройки'),
),
appBar: AppBar(title: const Text('Настройки')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -64,8 +62,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 12),
_notifications(state),
const SizedBox(height: 12),
_aboutEatvio(state),
const SizedBox(height: 12),
if (!hidePlans) ...[_aboutEatvio(state), const SizedBox(height: 12)],
_becomeConsultantButton(state),
_deleteAccountButton(state, settingsCubit),
],
@@ -76,16 +73,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
if (state is SettingsStateError) {
return Scaffold(
appBar: AppBar(
title: const Text('Настройки'),
),
appBar: AppBar(title: const Text('Настройки')),
body: DataErrorWidget(onReload: () => settingsCubit.init()),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Настройки'),
),
appBar: AppBar(title: const Text('Настройки')),
body: const Loader(),
);
},
@@ -101,22 +94,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Дополнительаная информация',
style: TextStyle(
fontSize: 16,
height: 1,
color: Colors.grey,
fontFamily: 'Mulish',
),
style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'),
),
),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'generalInfoScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Общая информация',
),
@@ -124,11 +108,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'physicalFormScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Физическая форма',
),
@@ -136,11 +116,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'pressureScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Давление',
),
@@ -148,11 +124,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'otherInfoScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Прочая информация',
),
@@ -160,31 +132,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'physicalActivityScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Физическая нагрузка',
),
_divider(context),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'forgetPasswordScreen',
extra: true,
),
title: 'Сменить пароль',
),
_tile(onTap: () => GoRouter.of(context).pushNamed('forgetPasswordScreen', extra: true), title: 'Сменить пароль'),
_divider(context),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'changeEmailScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Сменить Email',
),
@@ -198,14 +156,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Уведомления',
style: TextStyle(
fontSize: 16,
height: 1,
color: Colors.grey,
),
),
child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -213,11 +164,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
updatesNotificationsSwitchState = !updatesNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
newUpdatesNotifications: updatesNotificationsSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState));
},
title: 'Уведомление о новых обновлениях',
trailing: CupertinoSwitch(
@@ -228,9 +175,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
updatesNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
newUpdatesNotifications: updatesNotificationsSwitchState,
),
profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState),
);
},
),
@@ -243,9 +188,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
diaryReminderNotifications: fillDiaryNotificationsSwitchState,
),
profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
title: 'Напоминание о заполнении дневника',
@@ -257,9 +200,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
fillDiaryNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
diaryReminderNotifications: fillDiaryNotificationsSwitchState,
),
profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
),
@@ -271,11 +212,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
moodSwitchState = !moodSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isMoodFillable: moodSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
title: 'Указывать “настроение” в заполнение дневника',
trailing: CupertinoSwitch(
@@ -285,11 +222,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
moodSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isMoodFillable: moodSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
),
),
@@ -300,11 +233,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
bedTimeSwitchState = !bedTimeSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isBedtimeFillable: bedTimeSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
title: 'Указывать “отход ко сну” в заполнение дневника',
trailing: CupertinoSwitch(
@@ -314,11 +243,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
bedTimeSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isBedtimeFillable: bedTimeSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
),
),
@@ -332,24 +257,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'О Eatvio',
style: TextStyle(
fontSize: 16,
height: 1,
color: Colors.grey,
),
),
),
_tile(
title: 'О компании',
onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details')),
child: Text('О Eatvio', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
_tile(title: 'О компании', onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details'))),
_divider(context),
_tile(
title: 'Пользовательское соглашение',
onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
_tile(title: 'Пользовательское соглашение', onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf'))),
],
);
}
@@ -364,19 +276,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Стать консультантом',
style: TextStyle(
color: Colors.white,
height: 1,
fontSize: 18,
),
),
child: Text('Стать консультантом', style: TextStyle(color: Colors.white, height: 1, fontSize: 18)),
),
),
),
@@ -388,19 +290,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFEAEAEA),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
child: Text(
'Данные на проверке',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
);
@@ -413,19 +305,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
child: const Center(
child: Text(
'Заявка обработана',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
child: Text('Заявка обработана', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
),
@@ -444,57 +326,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
userRootNavigator: true,
context: context,
height: null,
child: DeleteAccountBottomSheet(
cubit: settingsCubit,
),
child: DeleteAccountBottomSheet(cubit: settingsCubit),
);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFEAEAEA),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
child: Text(
'Удалить аккаунт',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
child: Text('Удалить аккаунт', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
),
);
}
ListTile _tile({
required String title,
required Function() onTap,
Widget? trailing,
EdgeInsets? padding,
}) {
ListTile _tile({required String title, required Function() onTap, Widget? trailing, EdgeInsets? padding}) {
return ListTile(
contentPadding: padding,
onTap: onTap,
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1,
),
),
trailing: trailing ??
const Icon(
Icons.arrow_forward_ios_rounded,
size: 12,
color: Colors.black38,
),
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)),
trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
);
}
@@ -503,10 +356,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
),
);
}
@@ -530,33 +380,23 @@ class _DeleteAccountBottomSheet extends State<DeleteAccountBottomSheet> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const SizedBox(height: 8),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
const SizedBox(height: 16),
Text(
s.youShureYouWantToDeleteAccount,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
const SizedBox(height: 8),
Text(
s.itCantBeRecovered,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
const SizedBox(height: 8),
CustomOutlinedButton(
text: s.delete,
height: 52,
@@ -573,9 +413,7 @@ class _DeleteAccountBottomSheet extends State<DeleteAccountBottomSheet> {
showErrorDialog(context, description: s.cannotDeleteAccount);
},
),
const SizedBox(
height: 40,
),
const SizedBox(height: 40),
],
),
);

View File

@@ -69,15 +69,17 @@ class SupportScreen extends StatelessWidget {
child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)),
),
const SizedBox(height: 12),
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)),
if (!hidePlans) ...[
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)),
),
),
),
const SizedBox(height: 8),
const SizedBox(height: 8),
],
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
borderRadius: BorderRadius.circular(16),

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.8+68
version: 1.2.9+69
publish_to: 'none'
environment:

View File

@@ -17,6 +17,7 @@ void main() {
(_) async => await Future.value(
const ProfileModel(
id: 0,
isBlockedByUser: false,
name: 'Alexander Avanesyan',
email: 'mso7nueh@mail.ru',
phoneConfirmationState: '',