Merge pull request 'Build 1.2.4+54' (#571) from main into test-flight

Reviewed-on: eatvio/eatvio_app#571
This commit is contained in:
mso7nueh
2025-08-01 12:00:33 +00:00
54 changed files with 861 additions and 863 deletions

View File

@@ -20,6 +20,7 @@ jobs:
- name: Flutter build
run: |
echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8
flutter clean
flutter pub get
flutter pub upgrade
@@ -31,6 +32,8 @@ jobs:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}

View File

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

View File

@@ -19,17 +19,17 @@ android {
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = 21
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
applicationId = "com.eatvioapp.app"
minSdk = flutter.minSdkVersion
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@@ -22,7 +22,7 @@ plugins {
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.2" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"

View File

@@ -213,10 +213,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -283,10 +287,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -388,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 64;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -406,7 +414,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.50;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -538,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 64;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -556,7 +564,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.50;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -576,7 +584,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 64;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -594,7 +602,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.50;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";

View File

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

View File

@@ -18,9 +18,10 @@ default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
match(type: "appstore")
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution")
app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8")
upload_to_testflight
end
end

View File

@@ -5,22 +5,24 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000206">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.195115">
<testcase classname="fastlane.lanes" name="1: match" time="8.292217">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="116.686863">
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.201631">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="338.642203">
<testcase classname="fastlane.lanes" name="3: build_app" time="9.870647">
<failure message="/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in &apos;Fastlane::Actions.execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:255:in &apos;block in Fastlane::Runner#execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in &apos;Dir.chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in &apos;Fastlane::Runner#execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:157:in &apos;Fastlane::Runner#trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/fast_file.rb:159:in &apos;Fastlane::FastFile#method_missing&apos;&#10;Fastfile:23:in &apos;block (2 levels) in Fastlane::FastFile#parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane.rb:41:in &apos;Fastlane::Lane#call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:49:in &apos;block in Fastlane::Runner#execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in &apos;Dir.chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in &apos;Fastlane::Runner#execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane_manager.rb:46:in &apos;Fastlane::LaneManager.cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/command_line_handler.rb:34:in &apos;Fastlane::CommandLineHandler.handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:110:in &apos;block (2 levels) in Fastlane::CommandsGenerator#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in &apos;Commander::Command#call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in &apos;Commander::Command#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in &apos;Commander::Runner#run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in &apos;Commander::Runner#run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in &apos;Commander::Delegates#run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:363:in &apos;Fastlane::CommandsGenerator#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:43:in &apos;Fastlane::CommandsGenerator.start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in &apos;Fastlane::CLIToolsDistributor.take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/bin/fastlane:23:in &apos;&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/bin/fastlane:25:in &apos;Kernel#load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/bin/fastlane:25:in &apos;&lt;main&gt;&apos;&#10;&#10;Error building the application - see the log above" />
</testcase>

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -121,7 +119,9 @@ class AppRouter {
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,),
builder: (context, state) => ForgetPasswordScreen(
isAuth: state.extra as bool,
),
),
GoRoute(
path: 'scan-qr-code-screen',
@@ -205,21 +205,9 @@ class AppRouter {
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
final fbToken = await firebaseApi.getToken();
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken,
},
);
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
@@ -228,7 +216,7 @@ class AppRouter {
return '/auth-screen';
}
} catch (e) {
rethrow;
return '/feed-screen';
}
},
),

View File

@@ -23,7 +23,7 @@ void showCommentsActionsBottomSheet(
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom),
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
@@ -189,11 +189,12 @@ class _ActionWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
Expanded(
child: Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
),
const Spacer(),
SvgPicture.asset(iconPath)
],
),

View File

@@ -1,5 +1,5 @@
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
bool hidePlans = true;
const List<int> durationValues = [7, 14, 21];

View File

@@ -1,3 +1,10 @@
import 'dart:developer';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
class FirebaseApi {
@@ -7,4 +14,22 @@ class FirebaseApi {
await _firebaseMessaging.requestPermission();
return await _firebaseMessaging.getToken();
}
Future<void> registerPushToken() async {
if (await TokenStorage.haveTokenInStorage()) {
try {
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
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';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
@@ -66,6 +67,7 @@ class _AuthScreenState extends State<AuthScreen> {
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (e) {
rethrow;

View File

@@ -9,7 +9,7 @@ part 'chat_room_model.g.dart';
@freezed
abstract class ChatRoomModel with _$ChatRoomModel {
const factory ChatRoomModel({
required String id,
required String id,
@JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings,
required Users users,
@JsonKey(name: 'chosen_plan_id') required int? chosenPlanId,
@@ -22,13 +22,9 @@ abstract class ChatRoomModel with _$ChatRoomModel {
@freezed
abstract class Users with _$Users {
const factory Users({
@JsonKey(name: 'from_chat_user') required ChatUserModel sender,
@JsonKey(name: 'to_chat_user') required ChatUserModel receiver,
}) = _Users;
factory Users.fromJson(Map<String, Object?> json) => _$UsersFromJson(json);
}

View File

@@ -6,7 +6,7 @@ part 'chat_room_settings_model.g.dart';
@freezed
abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel {
const factory ChatRoomSettingsModel({
required String name,
@JsonKey(name: 'room_name') required String name,
required String state,
}) = _ChatRoomSettingsModel;

View File

@@ -223,6 +223,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
files: compressedFiles,
slug: userSlug,
);
sl<ChatsCubit>().getChatRooms();
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.dart';
@@ -11,7 +9,7 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:url_launcher/url_launcher.dart';
class PhotoViewScreen extends StatefulWidget {
final List<String> imageUrls;
final List<dynamic> imageUrls;
final int index;
final bool isLocal;
@@ -50,14 +48,12 @@ class _PhotoViewScreenState extends State<PhotoViewScreen> {
),
scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) {
final imageProvider = widget.isLocal
? FileImage(widget.imageUrls[index]) as ImageProvider<Object>
: CachedNetworkImageProvider(widget.imageUrls[index]) as ImageProvider<Object>;
return PhotoViewGalleryPageOptions(
imageProvider: widget.isLocal
? FileImage(
File(widget.imageUrls[index]),
) as ImageProvider
: CachedNetworkImageProvider(
widget.imageUrls[index],
),
imageProvider: imageProvider,
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(

View File

@@ -110,7 +110,7 @@ class ChatBubbleWidget extends StatelessWidget {
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: mediaData.map((e) => '$e').toList(),
imageUrls: mediaData,
index: index,
isLocal: mediaData[index] is File ? true : false,
),

View File

@@ -63,7 +63,8 @@ class ChatRepository {
Future<ChatRoomModel> getChatRoom(String uuid) async {
try {
return (await chatService.getChatRoom(uuid: uuid));
} catch (e) {
} catch (e, st) {
log(e.toString(), stackTrace: st);
rethrow;
}
}

View File

@@ -13,18 +13,18 @@ part 'chat_service.g.dart';
abstract class ChatService {
factory ChatService(Dio dio, {String baseUrl}) = _ChatService;
@GET('/api/chat/index')
@GET('/api/v2/chat/index')
Future<ChatRoomPaginationModel> getChatRooms({
@Query('page') required int page,
@Query('find') String? query,
@Query('filters[data][\$conversation_name]') String? query,
});
@GET('/api/chat/show/{uuid}')
@GET('/api/v2/chat/show/{uuid}')
Future<ChatRoomModel> getChatRoom({
@Path() required String uuid,
});
@POST('/api/chat/message/send/{slug}')
@POST('/api/v2/chat/message/send/{slug}')
@MultiPart()
Future<void> sendMessage({
@Header('X-Socket-ID') required String socketId,
@@ -33,13 +33,13 @@ abstract class ChatService {
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
});
@GET('/api/chat/show/{id}/messages?page={page}')
@GET('/api/v2/chat/show/{id}/messages?page={page}')
Future<ChatMessagesModel> getChatMessages({
@Path() required String id,
@Path() required int page,
});
@GET('/api/chat/get_by_user/{user}')
@GET('/api/v2/chat/get_by_user/{user}')
Future<ChatRoomModel> getChatRoomIdByUserSlug({
@Path() required String user,
});
@@ -82,7 +82,7 @@ abstract class ChatService {
@Body() required PlanComplainModel body,
});
@GET('/api/v2/chat/get_by_user/{slug}')
@GET('/api/chat/get_by_user/{slug}')
Future<HttpResponse> getChatByUserSlug({
@Path() required String slug,
});

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -194,63 +195,74 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(
s.lawViolation,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 8),
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.7,
child: Text(
s.what_do_you_want_to_complaint_about,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
const SizedBox(height: 24),
...tags.mapIndexed((index, element) {
return Row(
children: [
Checkbox(
activeColor: appTheme.blueA400,
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
value: element.$1,
onChanged: (isChecked) {
setState(() {
if (isChecked == null) return;
if (index == tags.length - 1 && !isChecked) {
context.read<ComplainingCubit>().toggleDescription();
}
tags[index] = (isChecked, element.$2);
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
context.read<ComplainingCubit>().setTags(activeTags);
});
return BlocBuilder<ComplainingCubit, ComplainingState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(
s.lawViolation,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 8),
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.7,
child: Text(
s.what_do_you_want_to_complaint_about,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
const SizedBox(height: 24),
...tags.mapIndexed((index, element) {
return Row(
children: [
Checkbox(
activeColor: appTheme.blueA400,
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
value: element.$1,
onChanged: (isChecked) {
setState(() {
if (isChecked == null) return;
if (index == tags.length - 1 && !isChecked) {
context.read<ComplainingCubit>().toggleDescription();
}
tags[index] = (isChecked, element.$2);
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
context.read<ComplainingCubit>().setTags(activeTags);
});
},
),
Text(
element.$2,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
],
);
}),
if (tags.last.$1) ...[
const SizedBox(height: 12),
CustomTextFormField(
hintText: s.type_here_your_complaint,
hasError: (state.complainInfo.description?.length ?? 10) < 10,
onChanged: (value) {
context.read<ComplainingCubit>().setDescription(value);
},
),
Text(
element.$2,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
if ((state.complainInfo.description?.length ?? 10) < 10) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Минимум 10 символов'),
]
],
);
}),
if (tags.last.$1) ...[
const SizedBox(height: 12),
CustomTextFormField(
hintText: s.type_here_your_complaint,
onChanged: (value) {
context.read<ComplainingCubit>().setDescription(value);
},
),
],
const SizedBox(height: 20),
const _ComplainButton(),
],
const SizedBox(height: 20),
_ComplainButton(
isDisabled: (state.complainInfo.description?.length ?? 0) < 10,
),
],
);
},
);
}
}
@@ -515,8 +527,8 @@ class _ComplaintFailedWidget extends StatelessWidget {
}
class _ComplainButton extends StatelessWidget {
const _ComplainButton();
const _ComplainButton({this.isDisabled = false});
final bool isDisabled;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -534,6 +546,10 @@ class _ComplainButton extends StatelessWidget {
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
if (isDisabled) {
context.read<ComplainingCubit>().setDescription('');
return;
}
await context.read<ComplainingCubit>().complain();
},
);

View File

@@ -302,7 +302,6 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
'diaryId': lastActiveDay.$2.id,
'currentDiaryDay': lastActiveDay.$1,
});
GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id);
return;
}
showErrorDialog(context,

View File

@@ -57,7 +57,7 @@ class RecipeRepository {
'type': recipe.type,
'state': 'active',
'title': recipe.title,
'tags': recipe.tags,
'tags[]': recipe.tags ?? [],
'description_md': recipe.description,
'video_url': recipe.videoUrl,
'ingredients': ingredientsMap,

View File

@@ -179,11 +179,14 @@ class DiaryCubit extends Cubit<DiaryState> {
int? currentPlanDay;
String? planName;
if (diaryDay.chosenPlanId != null) {
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
if (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented) {
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
}
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
planDuration = chosenPlanModel.plan.duration;
planName = chosenPlanModel.plan.name;
currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
currentPlanDay = consultantReview.currentPlanDay;
}
final nutrition = await getNutrition(diaryDay.id);
@@ -194,20 +197,19 @@ class DiaryCubit extends Cubit<DiaryState> {
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
emit(
DiaryState.loaded(
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: this.diaryId != null,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
),
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: this.diaryId != null,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled),
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
@@ -241,6 +243,7 @@ class DiaryCubit extends Cubit<DiaryState> {
required int quantity,
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,

View File

@@ -29,6 +29,7 @@ abstract class DiaryState with _$DiaryState {
@Default(false) bool isDiaryCommented,
@Default(false) bool isWakeUpAndRetireEnabled,
@Default(false) bool isMoodEnabled,
@Default(false) bool isLoadingAddingProductInMeal,
}) = DiaryStateLoaded;
const factory DiaryState.loadedTomorrowWithDiet({

View File

@@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ProductLoaderWidget extends StatefulWidget {
const ProductLoaderWidget({
super.key,
});
@override
State<ProductLoaderWidget> createState() => _ProductLoaderWidgetState();
}
class _ProductLoaderWidgetState extends State<ProductLoaderWidget> {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 0,
contentPadding: const EdgeInsets.only(right: 24, left: 12),
title: const CupertinoActivityIndicator(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -25,6 +26,11 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
if (state.isLoadingAddingProductInMeal)
const Padding(
padding: EdgeInsets.only(top: 8),
child: ProductLoaderWidget(),
),
],
);
}

View File

@@ -297,4 +297,12 @@ class DiaryRepository {
rethrow;
}
}
Future<void> viewCommentOfDiaryDayByConsultant({required int diaryId}) async {
try {
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
} catch (e) {
rethrow;
}
}
}

View File

@@ -150,4 +150,9 @@ abstract class DiaryService {
@Path() required int planId,
@Path() required int diaryId,
});
@POST('/api/diary/{diaryId}/consultant/comment/view')
Future<void> viewCommentOfDiaryDayByConsultant({
@Path() required int diaryId,
});
}

View File

@@ -1,4 +1,5 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
@@ -32,24 +33,12 @@ class FeedScreen extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<RecipesCubit>(
create: (context) => sl<RecipesCubit>()..loadRecipesList(),
),
BlocProvider<FeedProfileCubit>(
create: (context) => sl<FeedProfileCubit>()..getProfile(),
),
BlocProvider<FeedCubit>(
create: (context) => sl<FeedCubit>()..loadFeedBlogPostList(),
),
BlocProvider<DietsCubit>(
create: (context) => sl<DietsCubit>()..loadDiets(),
),
BlocProvider<PlansCubit>(
create: (context) => sl<PlansCubit>()..loadPlans(),
),
BlocProvider<ConsultantsCubit>(
create: (context) => sl<ConsultantsCubit>()..getConsultants(),
),
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
BlocProvider<FeedProfileCubit>(create: (context) => sl<FeedProfileCubit>()..getProfile()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<DietsCubit>(create: (context) => sl<DietsCubit>()..loadDiets()),
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
],
child: const FeedScreenContent(),
);
@@ -69,7 +58,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final dietsController = TextEditingController();
final plansController = TextEditingController();
final consultantsController = TextEditingController();
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
late final List<String> chipsLabelList;
int _selectedChipIndex = 0;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
@@ -78,12 +68,13 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsScrollController = ScrollController();
@override
void initState() {
void initState() {
// Initialize chipsLabelList based on hidePlans setting
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants);
super.initState();
}
}
void _onScrollConsultants() {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
@@ -108,45 +99,36 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
heroTag: 'recipesTagsFilter',
backgroundColor: appTheme.blueA400,
shape: const CircleBorder(),
child: const Icon(
Icons.filter_list_rounded,
color: Colors.white,
),
child: const Icon(Icons.filter_list_rounded, color: Colors.white),
onPressed: () async {
final recipesCubit = context.read<RecipesCubit>();
await showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => TagsModalWidget(
recipesCubit: recipesCubit,
),
builder: (context) => TagsModalWidget(recipesCubit: recipesCubit),
);
recipesCubit.loadRecipesList();
},
)
: null,
appBar: AppBar(
backgroundColor: Colors.white,
toolbarHeight: 0,
),
appBar: AppBar(backgroundColor: Colors.white, toolbarHeight: 0),
backgroundColor: Colors.grey.shade200,
body: NestedScrollView(
controller: switch (_selectedChipIndex) {
0 => blogPostsScrollController,
1 => recipesScrollController,
2 => dietsScrollController,
3 => plansScrollController,
4 => consultantsScrollController,
_ => blogPostsScrollController
3 when !hidePlans => plansScrollController,
3 when hidePlans => consultantsScrollController,
4 when !hidePlans => consultantsScrollController,
_ => blogPostsScrollController,
},
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
BlocBuilder<FeedProfileCubit, ProfileState>(
builder: (context, state) {
return SliverAppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
backgroundColor: Colors.white,
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
@@ -172,78 +154,71 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
searchController: switch (_selectedChipIndex) {
1 => recipesController,
2 => dietsController,
3 => plansController,
4 => consultantsController,
3 when !hidePlans => plansController,
3 when hidePlans => consultantsController,
4 when !hidePlans => consultantsController,
_ => postsController,
},
onChanged: (query) {
if (query.length > 2) {
EasyDebounce.debounce(
'searchFeed',
const Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList(query: query);
break;
case 1:
context.read<RecipesCubit>().loadRecipesList(query: query);
break;
case 2:
context.read<DietsCubit>().loadDiets(query: query);
break;
case 3:
context.read<PlansCubit>().loadPlans(query: query);
break;
case 4:
context.read<ConsultantsCubit>().getConsultants();
break;
}
},
);
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList(query: query);
break;
case 1:
context.read<RecipesCubit>().loadRecipesList(query: query);
break;
case 2:
context.read<DietsCubit>().loadDiets(query: query);
break;
case 3 when !hidePlans:
context.read<PlansCubit>().loadPlans(query: query);
break;
case 3 when hidePlans:
case 4 when !hidePlans:
context.read<ConsultantsCubit>().getConsultants();
break;
}
});
} else {
EasyDebounce.debounce(
'searchFeed',
const Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList();
break;
case 1:
context.read<RecipesCubit>().loadRecipesList();
break;
case 2:
context.read<DietsCubit>().loadDiets();
break;
case 3:
context.read<PlansCubit>().loadPlans();
break;
case 4:
context.read<ConsultantsCubit>().getConsultants();
break;
}
},
);
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList();
break;
case 1:
context.read<RecipesCubit>().loadRecipesList();
break;
case 2:
context.read<DietsCubit>().loadDiets();
break;
case 3 when !hidePlans:
context.read<PlansCubit>().loadPlans();
break;
case 3 when hidePlans:
case 4 when !hidePlans:
context.read<ConsultantsCubit>().getConsultants();
break;
}
});
}
},
),
const SizedBox(width: 12),
switch (_selectedChipIndex) {
1 => AddItemButton(
onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
),
1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)),
2 => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false
? () async {
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
}
: null,
),
3 => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
),
_ => const AddItemButton()
onTap: state.profile?.roles.contains('consultant') ?? false
? () async {
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
}
: null,
),
3 when !hidePlans => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
),
_ => const AddItemButton(),
},
],
),
@@ -256,8 +231,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
0 => const BlogPosts(),
1 => const Recipes(),
2 => const Diets(),
3 => const Plans(),
4 => const Consultants(),
3 when !hidePlans => const Plans(),
3 when hidePlans => const Consultants(),
4 when !hidePlans => const Consultants(),
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
},
),

View File

@@ -2,12 +2,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget
import 'package:flutter/widgets.dart';
class ChipGroup extends StatelessWidget {
const ChipGroup({
super.key,
required this.selectedChipIndex,
required this.chipsLabelList,
required this.setSelectedChipIndex,
});
const ChipGroup({super.key, required this.selectedChipIndex, required this.chipsLabelList, required this.setSelectedChipIndex});
final int selectedChipIndex;
final List<String> chipsLabelList;
final void Function(int) setSelectedChipIndex;
@@ -22,7 +17,7 @@ class ChipGroup extends StatelessWidget {
child: Row(
children: [
...List.generate(
5,
chipsLabelList.length,
(index) => Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ChoiceChipWidget(

View File

@@ -106,8 +106,7 @@ class DiaryReminderNotification implements BaseNotificationModel {
final NotificationType type;
@override
final bool isRead;
DiaryReminderNotification({required this.type, required this.isRead});
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
@@ -297,6 +296,25 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
}
}
class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
ChosenPlanLastDayFinishedForUser({
required this.type,
required this.isRead,
});
factory ChosenPlanLastDayFinishedForUser.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanLastDayFinishedForUser(
type: type,
isRead: isRead ?? false,
);
}
}
class UnknownNotification implements BaseNotificationModel {
@override
final NotificationType type;
@@ -323,6 +341,7 @@ enum NotificationType {
chosenPlanTomorrowReminderToConsultantNotification,
planSubscriptionPaidToAuthor,
chosenPlanFinishedToConsultant,
chosenPlanLastDayFinishedForUser,
unknown;
static NotificationType getTypeFromString(String type) => switch (type) {
@@ -339,6 +358,7 @@ enum NotificationType {
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification,
'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor,
'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant,
'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser,
_ => NotificationType.unknown,
};
}

View File

@@ -42,6 +42,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type),
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(notificationJson, type),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {

View File

@@ -53,6 +53,7 @@ class NotificationWidget extends StatelessWidget {
ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget(
notification: notification as ChosenPlanFinishedToConsultantNotification,
),
ChosenPlanLastDayFinishedForUser() => Text((notification as ChosenPlanLastDayFinishedForUser).type.toString()),
UnknownNotification() => const SizedBox.shrink(),
};
}

View File

@@ -40,6 +40,11 @@ class NotificationsService {
PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanFinishedToConsultant =>
ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {

View File

@@ -14,6 +14,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "email") required String email,
@JsonKey(name: "rating_place") int? ratingPlace,
@JsonKey(name: "email_verified_at") DateTime? emailVerifiedAt,
@JsonKey(name: "phone_confirmation_state") required String phoneConfirmationState,
@JsonKey(name: "created_at") DateTime? createdAt,
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,

View File

@@ -45,10 +45,7 @@ import 'package:url_launcher/url_launcher.dart';
class ProfileScreen extends StatefulWidget {
final Author? author;
const ProfileScreen({
super.key,
this.author,
});
const ProfileScreen({super.key, this.author});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
@@ -91,7 +88,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
if (isFirstFetch) {
canSub = !state.profile.isEntitySubscribedByMe;
if (state.profile.roles.contains('consultant')) {
if (state.profile.roles.contains('consultant') && !hidePlans) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
@@ -109,10 +106,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
);
} else {
return Scaffold(
appBar: AppBar(),
body: const Loader(),
);
return Scaffold(appBar: AppBar(), body: const Loader());
}
},
),
@@ -130,15 +124,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(16),
),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_header(state),
if (state.profile.roles.contains('consultant'))
if (state.profile.roles.contains('consultant') && state.consultantInfo != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -147,28 +139,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
const SizedBox(width: 16),
const Text(
'Консультант',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(
0xFF323F4B,
),
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)),
child: Text(
'${state.consultantInfo!.ratingPlace}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400),
),
),
],
@@ -177,17 +156,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
const Row(
children: [
SizedBox(width: 16),
Icon(
Icons.done_rounded,
color: Colors.green,
),
Icon(Icons.done_rounded, color: Colors.green),
Text(
'Медицинское образование',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600),
),
],
),
@@ -202,10 +174,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')) _socialLinks(state),
if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant')) _activePlans(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),
_blogPosts(state),
],
),
@@ -216,7 +188,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
Scaffold _myProfileScreen(ProfileStateLoaded state) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: AppBar(),
appBar: AppBar(toolbarHeight: widget.author == null ? 0 : null),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -224,9 +196,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(16),
),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -241,28 +211,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
const SizedBox(width: 16),
const Text(
'Консультант',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(
0xFF323F4B,
),
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(12),
),
decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)),
child: Text(
'${state.consultantInfo!.ratingPlace}-й в рейтинге',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.blueA400,
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400),
),
),
],
@@ -271,17 +228,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
const Row(
children: [
SizedBox(width: 16),
Icon(
Icons.done_rounded,
color: Colors.green,
),
Icon(Icons.done_rounded, color: Colors.green),
Text(
'Медицинское образование',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600),
),
],
),
@@ -297,7 +247,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.roles.contains('consultant')) _activePlans(state),
if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state),
_newBlogPostButton(),
_blogPosts(state),
],
@@ -307,7 +257,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
Widget _activePlans(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant')) {
if (!state.profile.roles.contains('consultant') || hidePlans) {
return const SizedBox.shrink();
}
return BlocBuilder<PlansCubit, PlansState>(
@@ -326,12 +276,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
Text(
'Активные планы',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 24,
height: 1,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600),
),
],
),
@@ -341,10 +286,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
height: 781,
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
return PlanWidget(
plan: state.plansList[index],
index: index,
);
return PlanWidget(plan: state.plansList[index], index: index);
},
onPageChanged: (bullet) {
setState(() {
@@ -368,17 +310,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
Container _bullets(int currentBullet, int bulletCount) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < bulletCount; i++) ...[
_bullet(i == currentBullet),
i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink(),
],
for (int i = 0; i < bulletCount; i++) ...[_bullet(i == currentBullet), i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink()],
],
),
);
@@ -389,10 +325,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
width: isActive ? 10 : 8,
height: isActive ? 10 : 8,
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC),
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC), shape: BoxShape.circle),
);
}
@@ -409,25 +342,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
borderRadius: BorderRadius.circular(8),
child: Ink(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF6FAFF),
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(8)),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_circle_outline_rounded,
color: Color(0xFF2F80ED),
),
Icon(Icons.add_circle_outline_rounded, color: Color(0xFF2F80ED)),
SizedBox(width: 12),
Text(
'Новая запись',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF2F80ED),
),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF2F80ED)),
),
],
),
@@ -452,12 +375,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
const Text(
'Дипломы\nи сертификаты',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 24,
height: 1,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600),
),
GestureDetector(
onTap: () {
@@ -465,11 +383,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
},
child: Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500),
),
),
],
@@ -489,17 +403,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: Row(
children: [
if (state.consultantInfo!.photos.isNotEmpty)
...List.generate(
state.consultantInfo!.photos.length * 2 - 1,
(index) {
if (index.isEven) {
final photo = state.consultantInfo!.photos[index ~/ 2];
return _diplomaImage(photo.url);
} else {
return const SizedBox(width: 8);
}
},
),
...List.generate(state.consultantInfo!.photos.length * 2 - 1, (index) {
if (index.isEven) {
final photo = state.consultantInfo!.photos[index ~/ 2];
return _diplomaImage(photo.url);
} else {
return const SizedBox(width: 8);
}
}),
],
),
),
@@ -513,24 +424,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
GestureDetector _diplomaImage(String url) {
return GestureDetector(
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [url],
index: 0,
),
);
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0));
},
child: Hero(
tag: url,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.fitHeight,
width: MediaQuery.of(context).size.width / 3 - (60 / 3),
height: 160,
),
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160),
),
),
);
@@ -553,11 +453,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
const Text(
'Образование',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 24,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600),
),
GestureDetector(
onTap: () {
@@ -565,11 +461,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
},
child: Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500),
),
),
],
@@ -588,8 +480,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _educationBlock(state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
state.consultantInfo!.educationModel.completedCourses![index].values.first.date),
itemBuilder: (context, index) => _educationBlock(
state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
state.consultantInfo!.educationModel.completedCourses![index].values.first.date,
),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.consultantInfo!.educationModel.completedCourses!.length,
),
@@ -619,22 +513,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
Text(
title,
style: const TextStyle(
height: 1,
color: Color(0xFF323F4B),
fontWeight: FontWeight.w500,
fontSize: 16,
),
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontWeight: FontWeight.w500, fontSize: 16),
),
const SizedBox(height: 4),
Text(
formattedDate,
style: const TextStyle(
height: 1,
color: Color(0xFF6D6F71),
fontWeight: FontWeight.w500,
fontSize: 14,
),
style: const TextStyle(height: 1, color: Color(0xFF6D6F71), fontWeight: FontWeight.w500, fontSize: 14),
),
],
),
@@ -644,7 +528,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
SingleChildRenderObjectWidget _socialLinks(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant')) {
if (!state.profile.roles.contains('consultant') && state.consultantInfo != null) {
return const SizedBox.shrink();
}
return Padding(
@@ -658,12 +542,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.consultantInfo!.educationModel.vkLink ?? '',
),
mode: LaunchMode.externalApplication,
);
launchUrl(Uri.parse(state.consultantInfo!.educationModel.vkLink ?? ''), mode: LaunchMode.externalApplication);
},
'assets/icons/vk.svg',
state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false,
@@ -671,12 +550,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
const SizedBox(width: 12),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.consultantInfo!.educationModel.tgLink ?? '',
),
mode: LaunchMode.externalApplication,
);
launchUrl(Uri.parse(state.consultantInfo!.educationModel.tgLink ?? ''), mode: LaunchMode.externalApplication);
},
'assets/icons/telegram.svg',
state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false,
@@ -684,12 +558,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
const SizedBox(width: 12),
_socialLinkButton(
() {
launchUrl(
Uri.parse(
state.consultantInfo!.educationModel.personalSiteLink ?? '',
),
mode: LaunchMode.externalApplication,
);
launchUrl(Uri.parse(state.consultantInfo!.educationModel.personalSiteLink ?? ''), mode: LaunchMode.externalApplication);
},
'assets/icons/web.svg',
state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false,
@@ -707,10 +576,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width / 3 - (56 / 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: SvgPicture.asset(
@@ -732,20 +598,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: Ink(
padding: const EdgeInsets.all(8),
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Достижения',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
SingleChildScrollView(
@@ -757,51 +616,25 @@ class _ProfileScreenState extends State<ProfileScreen> {
? Tooltip(
message: state.profile.badges[i].description,
triggerMode: TooltipTriggerMode.tap,
textStyle: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.2).toInt()),
blurRadius: 32,
)
],
),
child: SvgPicture.network(
'$baseUrl/storage/${state.profile.badges[i].image}',
width: 40,
height: 40,
boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)],
),
child: SvgPicture.network('$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40),
)
: Tooltip(
message: state.profile.badges[i].description,
margin: const EdgeInsets.symmetric(horizontal: 32),
triggerMode: TooltipTriggerMode.tap,
textStyle: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.2).toInt()),
blurRadius: 32,
)
],
),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}',
width: 40,
height: 40,
boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)],
),
child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40),
),
i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(),
],
@@ -821,21 +654,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: Row(
children: [
InkWell(
onTap: () => GoRouter.of(context).pushNamed(
'subsScreen',
extra: SubType.subscriptions,
pathParameters: {
'slug': state.profile.slug,
},
),
onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscriptions, pathParameters: {'slug': state.profile.slug}),
borderRadius: BorderRadius.circular(12),
child: Ink(
padding: const EdgeInsets.all(8),
width: MediaQuery.of(context).size.width / 2 - 6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
@@ -845,47 +669,27 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
const Text(
'Подписки',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600),
),
Text(
'${state.profile.subscriptions.length}',
style: const TextStyle(
color: Color(0xFF323F4B),
fontSize: 22,
fontWeight: FontWeight.w700,
),
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700),
),
],
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: _miniAvatars(state.profile.subscriptions),
),
Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscriptions)),
],
),
),
),
const SizedBox(width: 4),
InkWell(
onTap: () => GoRouter.of(context).pushNamed(
'subsScreen',
extra: SubType.subscribers,
pathParameters: {
'slug': state.profile.slug,
},
),
onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscribers, pathParameters: {'slug': state.profile.slug}),
borderRadius: BorderRadius.circular(12),
child: Ink(
padding: const EdgeInsets.all(8),
width: MediaQuery.of(context).size.width / 2 - 6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
@@ -895,26 +699,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
const Text(
'Подписчики',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600),
),
Text(
'${state.profile.subscribers.length}',
style: const TextStyle(
color: Color(0xFF323F4B),
fontSize: 22,
fontWeight: FontWeight.w700,
),
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700),
),
],
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: _miniAvatars(state.profile.subscribers),
),
Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscribers)),
],
),
),
@@ -932,13 +725,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
for (int i = 0; i < maxLength; i++)
Align(
widthFactor: 0.8,
child: CircleAvatar(
radius: 14,
foregroundImage: NetworkImage(
urls[i],
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
child: CircleAvatar(radius: 14, foregroundImage: NetworkImage(urls[i]), backgroundImage: const AssetImage('assets/images/no_avatar.png')),
),
],
);
@@ -949,10 +736,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.symmetric(vertical: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => ProfileBlogPostWidget(
index: index,
profileCubit: profileCubit,
),
itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.blogPosts.length,
);
@@ -966,13 +750,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16),
child: Text(
text,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.4),
height: 1,
),
),
child: Text(text, style: TextStyle(color: Colors.black.withValues(alpha: 0.4), height: 1)),
);
}
@@ -984,13 +762,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
PopupMenuButton(
onSelected: (value) async {
if (value == 'view') {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [baseUrl + state.profile.profilePhotoUrl],
index: 0,
),
);
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0));
}
if (value == 'replace') {
showCupertinoModalPopup<void>(
@@ -1017,13 +789,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
@@ -1043,13 +809,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
@@ -1067,20 +827,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'view',
child: Text('Посмотреть'),
),
if (widget.author == null || widget.author!.authorIsMe)
const PopupMenuItem(
value: 'replace',
child: Text('Заменить'),
),
if (widget.author == null || widget.author!.authorIsMe)
const PopupMenuItem(
value: 'delete',
child: Text('Удалить'),
),
const PopupMenuItem(value: 'view', child: Text('Посмотреть')),
if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'replace', child: Text('Заменить')),
if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'delete', child: Text('Удалить')),
],
child: Hero(
tag: state.profile.profilePhotoUrl,
@@ -1099,12 +848,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
Text(
'${state.profileDetails.firstName} ${state.profileDetails.lastName}',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 16,
height: 1,
),
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600, fontSize: 16, height: 1),
),
const SizedBox(height: 8),
GestureDetector(
@@ -1122,11 +866,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
const Text(
'Подробнее',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFF323F4B),
),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
_additional('Дата рожденя: ', state.profileDetails.birthdate),
@@ -1146,19 +886,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
},
child: const Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: Color(0xFF6D6F71),
),
Icon(Icons.info_outline, size: 20, color: Color(0xFF6D6F71)),
SizedBox(width: 4),
Text(
'Дополнительно',
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
@@ -1171,10 +903,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
if (value == 'share') {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
);
} else if (value == 'block') {
isFirstFetch = true;
@@ -1196,33 +925,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(
complainedContentType: ComplainedContent.profile,
slug: widget.author?.authorSlug,
),
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug),
);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'share',
child: Text('Поделиться'),
),
if (widget.author != null)
const PopupMenuItem(
value: 'complain',
child: Text('Пожаловаться'),
),
if (widget.author != null && !state.profile.isBlockedByMe)
const PopupMenuItem(
value: 'block',
child: Text('Заблокировать'),
),
if (widget.author != null && state.profile.isBlockedByMe)
const PopupMenuItem(
value: 'unblock',
child: Text('Разблокировать'),
),
const PopupMenuItem(value: 'share', child: Text('Поделиться')),
if (widget.author != null) const PopupMenuItem(value: 'complain', child: Text('Пожаловаться')),
if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem(value: 'block', child: Text('Заблокировать')),
if (widget.author != null && state.profile.isBlockedByMe) const PopupMenuItem(value: 'unblock', child: Text('Разблокировать')),
],
child: const Icon(Icons.more_horiz_rounded),
),
@@ -1236,19 +947,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF6D6F71),
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6D6F71)),
),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF323F4B),
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)),
),
],
);
@@ -1264,16 +967,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
width: (MediaQuery.of(context).size.width - 40) / 2,
decoration: BoxDecoration(
color: appTheme.blueA400.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Сообщение',
style: TextStyle(
color: appTheme.blueA400,
),
),
decoration: BoxDecoration(color: appTheme.blueA400.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16)),
child: Text('Сообщение', style: TextStyle(color: appTheme.blueA400)),
),
),
const SizedBox(width: 8),
@@ -1288,16 +983,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
width: (MediaQuery.of(context).size.width - 40) / 2,
decoration: BoxDecoration(
color: !canSub ? Colors.grey.shade200 : appTheme.blueA400,
borderRadius: BorderRadius.circular(16),
),
child: Text(
!canSub ? 'Отписаться' : 'Подписаться',
style: TextStyle(
color: !canSub ? Colors.black : Colors.white,
),
),
decoration: BoxDecoration(color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, borderRadius: BorderRadius.circular(16)),
child: Text(!canSub ? 'Отписаться' : 'Подписаться', style: TextStyle(color: !canSub ? Colors.black : Colors.white)),
),
),
const SizedBox(width: 16),
@@ -1316,33 +1003,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
children: [
_choiceChipWidget(0),
const SizedBox(width: 8),
_choiceChipWidget(1),
const SizedBox(width: 8),
_choiceChipWidget(2),
],
),
child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]),
),
Builder(
builder: (context) {
switch (_selectedChipIndex) {
case 0:
return WeightChartWidget(
weightPoints: weightPoints,
profileCubit: profileCubit,
);
return WeightChartWidget(weightPoints: weightPoints, profileCubit: profileCubit);
case 1:
return CircumferenceChartWidget(
circumferencePoints: circumferencePoints,
profileCubit: profileCubit,
);
return CircumferenceChartWidget(circumferencePoints: circumferencePoints, profileCubit: profileCubit);
case 2:
return ImtChartWidget(
weightPoints: weightPoints,
);
return ImtChartWidget(weightPoints: weightPoints);
default:
return const SizedBox.shrink();
}
@@ -1375,16 +1046,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsIntervalLabelList[index]),
labelStyle: TextStyle(
color: appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12),
selected: _selectedIntervalChipIndex == index,
onSelected: (bool selected) {
profileCubit.getMyProfile(period: chipsIntervalList[index]);
@@ -1401,16 +1066,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsLabelList[index]),
labelStyle: TextStyle(
color: index == _selectedChipIndex ? Colors.white : appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12),
selected: _selectedChipIndex == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {

View File

@@ -203,4 +203,12 @@ class ProfileRepository {
return false;
}
}
Future<void> deleteAccount() async {
try {
await profileService.deleteAccount();
} catch (e) {
rethrow;
}
}
}

View File

@@ -119,4 +119,7 @@ abstract class ProfileService {
Future<void> tryVerifyEmailByCode({
@Body() required String code,
});
@DELETE('/api/profile/delete_my_account')
Future<void> deleteAccount();
}

View File

@@ -1,6 +1,8 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
@@ -58,9 +60,11 @@ class RegistrationCubit extends Cubit<RegistrationState> {
),
);
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen');
await sl<FirebaseApi>().registerPushToken();
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(RegistrationState(sheet: state.sheet, error: e.toString()));
}
}

View File

@@ -7,5 +7,6 @@ part 'registration_state.freezed.dart';
abstract class RegistrationState with _$RegistrationState {
const factory RegistrationState({
required UserRegistrationSheet sheet,
String? error,
}) = _RegistrationState;
}

View File

@@ -43,142 +43,161 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(
create: (BuildContext context) => registrationCubit,
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
child: BlocListener<RegistrationCubit, RegistrationState>(
listener: (context, state) {
if (state.error != null && state.error!.isNotEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
],
),
);
}
},
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.nextPage(
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
@@ -19,6 +20,7 @@ class VerifyScreen extends StatefulWidget {
class _VerifyScreenState extends State<VerifyScreen> {
late final WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
@@ -27,6 +29,11 @@ class _VerifyScreenState extends State<VerifyScreen> {
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
setState(() {
_isLoading = false;
});
},
onNavigationRequest: (NavigationRequest request) async {
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
GoRouter.of(context).pop();
@@ -105,11 +112,17 @@ class _VerifyScreenState extends State<VerifyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,
),
appBar: AppBar(toolbarHeight: 0),
body: SafeArea(
child: WebViewWidget(controller: _controller),
child: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: Loader(),
),
],
),
),
);
}

View File

@@ -1,7 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
import 'package:eatvio_app/features/registration/models/location_model.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
@@ -14,13 +12,55 @@ class RegistrationRepository {
Future<String> tryRegister(UserRegistrationSheet sheet) async {
try {
final response = await registrationService.register(sheet: sheet.toJson());
try {
return (response['token']!);
} catch (e) {
showErrorToast(response.toString());
rethrow;
if (response.containsKey('token')) {
return response['token']!;
} else {
// Если нет токена, но есть message или errors
String errorMsg = '';
if (response.containsKey('message')) {
errorMsg += '${response['message']!}\n';
}
if (response.containsKey('errors')) {
final errors = response['errors'];
if (errors != null && errors is Map) {
(errors as Map).forEach((key, value) {
if (value is List && value.isNotEmpty) {
errorMsg += '${value.join('\n')}\n';
}
});
}
}
if (errorMsg.isNotEmpty) {
throw errorMsg.trim();
} else {
throw 'Ошибка регистрации: $response';
}
}
} on DioException catch (e) {
if (e.response != null && e.response?.statusCode == 422) {
final data = e.response?.data;
String errorMsg = '';
if (data is Map<String, dynamic>) {
if (data.containsKey('message')) {
errorMsg += data['message'] + '\n';
}
if (data.containsKey('errors')) {
final errors = data['errors'];
if (errors != null && errors is Map) {
(errors).forEach((key, value) {
if (value is List && value.isNotEmpty) {
errorMsg += '${value.join('\n')}\n';
}
});
}
}
}
if (errorMsg.isNotEmpty) {
throw errorMsg.trim();
} else {
throw 'Ошибка регистрации: $data';
}
}
} catch (e) {
rethrow;
}
}

View File

@@ -22,7 +22,11 @@ class SettingsCubit extends Cubit<SettingsState> {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
try {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
if (profile.phoneConfirmationState.contains('confirmed')) {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
} else {
consultantInfo = null;
}
} catch (e) {
consultantInfo = null;
}
@@ -67,4 +71,13 @@ class SettingsCubit extends Cubit<SettingsState> {
await init();
}
}
Future<bool> deleteAccount() async {
try {
await profileRepository.deleteAccount();
return true;
} catch (e) {
return false;
}
}
}

View File

@@ -1,12 +1,19 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -60,7 +67,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_aboutEatvio(state),
const SizedBox(height: 12),
_becomeConsultantButton(state),
_deleteAccountButton(state),
_deleteAccountButton(state, settingsCubit),
],
),
),
@@ -428,11 +435,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
return const SizedBox.shrink();
}
Padding _deleteAccountButton(SettingsStateLoaded state) {
Padding _deleteAccountButton(SettingsStateLoaded state, SettingsCubit settingsCubit) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: InkWell(
onTap: () {},
onTap: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: DeleteAccountBottomSheet(
cubit: settingsCubit,
),
);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
@@ -495,3 +511,73 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
}
class DeleteAccountBottomSheet extends StatefulWidget {
const DeleteAccountBottomSheet({super.key, required this.cubit});
final SettingsCubit cubit;
@override
State<DeleteAccountBottomSheet> createState() => _DeleteAccountBottomSheet();
}
class _DeleteAccountBottomSheet extends State<DeleteAccountBottomSheet> {
bool? isFailure;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
s.youShureYouWantToDeleteAccount,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
Text(
s.itCantBeRecovered,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.delete,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
final result = await widget.cubit.deleteAccount();
if (result == true) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
return;
}
AppRouter.navigatorKey.currentContext?.pop();
if (!context.mounted) return;
showErrorDialog(context, description: s.cannotDeleteAccount);
},
),
const SizedBox(
height: 40,
),
],
),
);
}
}

View File

@@ -51,6 +51,9 @@ class MessageLookup extends MessageLookupByLibrary {
"about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"),
"about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"),
"about_recipe": MessageLookupByLibrary.simpleMessage("Об этом рецепте"),
"accountDeleting": MessageLookupByLibrary.simpleMessage(
"Удаление аккаунта",
),
"active": MessageLookupByLibrary.simpleMessage("Активен"),
"active_consultations_count": MessageLookupByLibrary.simpleMessage(
"Кол-во активных консультаций",
@@ -124,6 +127,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"),
"cancel_order": MessageLookupByLibrary.simpleMessage("Отменить заказ"),
"cannotDeleteAccount": MessageLookupByLibrary.simpleMessage(
"Не получилось удалить ваш аккаунт",
),
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
@@ -256,6 +262,7 @@ class MessageLookup extends MessageLookupByLibrary {
),
"days": MessageLookupByLibrary.simpleMessage("дней"),
"deception": MessageLookupByLibrary.simpleMessage("Обман"),
"delete": MessageLookupByLibrary.simpleMessage("Удалить"),
"deleteComment": MessageLookupByLibrary.simpleMessage(
"Удалить комментарий",
),
@@ -411,6 +418,9 @@ class MessageLookup extends MessageLookupByLibrary {
"invited_people_count": MessageLookupByLibrary.simpleMessage(
"Количество приглашенных",
),
"itCantBeRecovered": MessageLookupByLibrary.simpleMessage(
"Его нельзя будет восстановить",
),
"label": MessageLookupByLibrary.simpleMessage("Название"),
"language": MessageLookupByLibrary.simpleMessage("Язык"),
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"),
@@ -780,6 +790,9 @@ class MessageLookup extends MessageLookupByLibrary {
"youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите отменить заказ?",
),
"youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите удалить аккаунт?",
),
"you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage(
"Вы не можете перейти в этот день",
),

View File

@@ -3414,6 +3414,51 @@ class S {
args: [],
);
}
/// `Удаление аккаунта`
String get accountDeleting {
return Intl.message(
'Удаление аккаунта',
name: 'accountDeleting',
desc: '',
args: [],
);
}
/// `Вы уверены, что хотите удалить аккаунт?`
String get youShureYouWantToDeleteAccount {
return Intl.message(
'Вы уверены, что хотите удалить аккаунт?',
name: 'youShureYouWantToDeleteAccount',
desc: '',
args: [],
);
}
/// `Его нельзя будет восстановить`
String get itCantBeRecovered {
return Intl.message(
'Его нельзя будет восстановить',
name: 'itCantBeRecovered',
desc: '',
args: [],
);
}
/// `Удалить`
String get delete {
return Intl.message('Удалить', name: 'delete', desc: '', args: []);
}
/// `Не получилось удалить ваш аккаунт`
String get cannotDeleteAccount {
return Intl.message(
'Не получилось удалить ваш аккаунт',
name: 'cannotDeleteAccount',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -405,7 +405,12 @@
"allowAccess": "Разрешить доступ" ,
"copy": "Скопировать",
"reply": "Ответить",
"deleteComment": "Удалить комментарий"
"deleteComment": "Удалить комментарий",
"accountDeleting": "Удаление аккаунта",
"youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?",
"itCantBeRecovered": "Его нельзя будет восстановить",
"delete": "Удалить",
"cannotDeleteAccount": "Не получилось удалить ваш аккаунт"
}

View File

@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/internet_connection_checker/internet_connect
import 'package:eatvio_app/firebase_options.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -22,8 +23,8 @@ import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission();
await di.init();
await SentryFlutter.init(

View File

@@ -1,10 +1,10 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.2+52
version: 1.2.6+64
publish_to: 'none'
environment:
sdk: '>=3.3.4 <4.0.0'
sdk: '>=3.8.0 <4.0.0'
dependencies:
flutter:
@@ -18,28 +18,28 @@ dependencies:
cached_network_image: ^3.4.1
flutter_svg: ^2.2.0
equatable: ^2.0.7
freezed_annotation: ^3.0.0
freezed_annotation: ^3.1.0
json_annotation: ^4.9.0
validators: ^3.0.0
path_provider: ^2.1.5
carousel_slider_plus: ^7.1.1
flutter_form_builder: ^10.0.1
flutter_form_builder: ^10.1.0
image_picker: ^1.1.2
flutter_typeahead: ^5.2.0
go_router: ^16.0.0
keyboard_dismisser: ^3.0.0
retrofit: ^4.5.0
get_it: ^8.0.3
retrofit: ^4.7.0
get_it: ^8.1.0
dio: ^5.8.0+1
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
dismissible_page: ^1.0.2
url_launcher: ^6.3.1
url_launcher: ^6.3.2
easy_debounce: ^2.0.3
badges: ^3.1.2
google_fonts: ^6.2.1
syncfusion_flutter_charts: ^30.1.38
syncfusion_flutter_gauges: ^30.1.38
syncfusion_flutter_charts: ^30.1.42
syncfusion_flutter_gauges: ^30.1.42
table_calendar: ^3.2.0
flutter_keyboard_visibility: ^6.0.0
share_plus: ^11.0.0
@@ -58,11 +58,11 @@ dependencies:
http: ^1.4.0
flutter_native_splash: ^2.4.6
webview_flutter: ^4.13.0
sentry_flutter: ^8.14.2
sentry_flutter: ^9.5.0
connectivity_plus: ^6.1.4
visibility_detector: ^0.4.0+2
firebase_core: ^3.15.0
firebase_messaging: ^15.2.8
firebase_core: ^3.15.2
firebase_messaging: ^15.2.10
http_parser: ^4.1.2
path: ^1.9.1
collection: ^1.19.1
@@ -70,15 +70,15 @@ dependencies:
version: ^3.0.2
package_info_plus: ^8.3.0
device_info_plus: ^11.5.0
permission_handler: ^12.0.0+1
permission_handler: ^12.0.1
qr_code_scanner_plus: ^2.0.10+1
dev_dependencies:
build_runner: ^2.5.4
freezed: ^3.0.6
json_serializable: ^6.9.5
retrofit_generator: ^9.2.0
mockito: ^5.4.6
build_runner: ^2.6.0
freezed: ^3.2.0
json_serializable: ^6.10.0
retrofit_generator: ^10.0.1
mockito: ^5.5.0
flutter_test:
sdk: flutter

View File

@@ -19,6 +19,7 @@ void main() {
id: 0,
name: 'Alexander Avanesyan',
email: 'mso7nueh@mail.ru',
phoneConfirmationState: '',
isPublicProfile: true,
isEntitySubscribedByMe: false,
isBlockedByMe: false,