forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main' into issue-738
This commit is contained in:
@@ -140,7 +140,21 @@ jobs:
|
||||
run: flutter analyze
|
||||
|
||||
- name: Test
|
||||
run: flutter test
|
||||
run: flutter test --coverage
|
||||
|
||||
- name: Test coverage
|
||||
run: |
|
||||
awk -F: '
|
||||
/^LF:/ { total += $2 }
|
||||
/^LH:/ { covered += $2 }
|
||||
END {
|
||||
if (total == 0) {
|
||||
print "Coverage: 0%"
|
||||
} else {
|
||||
printf "Coverage: %.2f%%\n", (covered / total) * 100
|
||||
}
|
||||
}
|
||||
' coverage/lcov.info
|
||||
|
||||
- name: Build apk
|
||||
run: |
|
||||
|
||||
@@ -19,15 +19,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
|
||||
android {
|
||||
namespace = "com.eatvioapp.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -557,7 +557,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -596,7 +596,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
MARKETING_VERSION = 1.3.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>82</string>
|
||||
<string>83</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -411,7 +411,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
label: state is AuthStateLoading || _isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
@@ -426,8 +426,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||
onPressed: state is AuthStateLoading
|
||||
backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||
onPressed: state is AuthStateLoading || _isLoading
|
||||
? null
|
||||
: () async {
|
||||
await showDialog(
|
||||
@@ -487,7 +487,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
);
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
label: state is AuthStateLoading || _isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/core/theme/app_decoration.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
|
||||
@@ -620,16 +621,16 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: const Center(
|
||||
child: isLoading
|
||||
? const Center(child: Loader())
|
||||
: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text('Создать', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,25 +63,28 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.consultationsModel.userName,
|
||||
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.consultationsModel.planTitle,
|
||||
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
GestureDetector(
|
||||
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.consultationsModel.userSlug),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.consultationsModel.userName,
|
||||
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.consultationsModel.planTitle,
|
||||
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
import 'package:eatvio_app/core/enums/product_type.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/utils/string_extentions.dart';
|
||||
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||
import 'package:eatvio_app/core/widgets/info_row.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
|
||||
import 'package:eatvio_app/core/widgets/cpfc_container.dart';
|
||||
@@ -36,20 +31,11 @@ class DietPreviewScreen extends StatefulWidget {
|
||||
|
||||
class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final textEditingController = TextEditingController();
|
||||
bool isMoreOptionsVisible = false;
|
||||
late final Uint8List imageData;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageData = base64.decode(widget.diet.image);
|
||||
scrollController.addListener(() {
|
||||
if (isMoreOptionsVisible) {
|
||||
setState(() {
|
||||
isMoreOptionsVisible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -58,86 +44,6 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BookmarkButton(
|
||||
isFavorite: widget.diet.isFavorite,
|
||||
onTap: () {},
|
||||
width: 52,
|
||||
height: 52,
|
||||
size: 24,
|
||||
shape: BoxShape.circle,
|
||||
color: appTheme.gray50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: TextFormField(
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
controller: textEditingController,
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
hintText: s.comment.capitalize(),
|
||||
hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600),
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
@@ -148,15 +54,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
softWrap: true,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
userSlug: widget.diet.author.authorSlug,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
actions: const [],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
|
||||
@@ -33,7 +33,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
|
||||
bool _mdToolbarVisible = false;
|
||||
|
||||
bool _isRecipeBeingPublished = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -58,9 +58,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null
|
||||
? _mdKey.currentState!.buildToolbar(context)
|
||||
: null,
|
||||
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
|
||||
appBar: AppBar(
|
||||
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
|
||||
centerTitle: true,
|
||||
@@ -83,44 +81,55 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
builder: (context, state) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return InkWell(
|
||||
key: AppKeys.recipeCreateScreenPublishButton,
|
||||
onTap: () async {
|
||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
||||
widget.recipe == null ? false : true,
|
||||
widget.recipe?.slug,
|
||||
return _isRecipeBeingPublished
|
||||
? const Center(child: Loader())
|
||||
: InkWell(
|
||||
key: AppKeys.recipeCreateScreenPublishButton,
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
_isRecipeBeingPublished = true;
|
||||
});
|
||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
||||
widget.recipe == null ? false : true,
|
||||
widget.recipe?.slug,
|
||||
);
|
||||
setState(() {
|
||||
_isRecipeBeingPublished = false;
|
||||
});
|
||||
if (isSuccess) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isRecipeBeingPublished = false;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
widget.recipe != null ? 'Обновить' : 'Опубликовать',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (isSuccess) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
|
||||
}
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
widget.recipe != null ? 'Обновить' : 'Опубликовать',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -44,16 +44,24 @@ class UserDiaryScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DiaryScreenContent extends StatelessWidget {
|
||||
class DiaryScreenContent extends StatefulWidget {
|
||||
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
|
||||
final int? diaryId;
|
||||
final int? currentPlanDay;
|
||||
|
||||
@override
|
||||
State<DiaryScreenContent> createState() => _DiaryScreenContentState();
|
||||
}
|
||||
|
||||
class _DiaryScreenContentState extends State<DiaryScreenContent> {
|
||||
bool _isDiaryFinishing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: diaryId)),
|
||||
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)),
|
||||
BlocProvider(create: (context) => sl<ConsultantCommentsCubit>()),
|
||||
],
|
||||
child: BlocBuilder<DiaryCubit, DiaryState>(
|
||||
@@ -63,13 +71,13 @@ class DiaryScreenContent extends StatelessWidget {
|
||||
}
|
||||
if (state is DiaryStateError) {
|
||||
return Scaffold(
|
||||
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiaryV2(needLoading: false, diaryId: diaryId)),
|
||||
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiaryV2(needLoading: false, diaryId: widget.diaryId)),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: diaryId == null
|
||||
appBar: widget.diaryId == null
|
||||
? AppBar(toolbarHeight: 0)
|
||||
: AppBar(
|
||||
title: Text(
|
||||
@@ -83,7 +91,7 @@ class DiaryScreenContent extends StatelessWidget {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (diaryId == null) const CalendarWidget(),
|
||||
if (widget.diaryId == null) const CalendarWidget(),
|
||||
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
|
||||
if (state is DiaryStateLoaded)
|
||||
WakeupAndRetireWidget(
|
||||
@@ -116,7 +124,23 @@ class DiaryScreenContent extends StatelessWidget {
|
||||
if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: FinishDayButtonWidget(diaryDay: state.diary),
|
||||
child: _isDiaryFinishing
|
||||
? const Center(child: Loader())
|
||||
: FinishDayButtonWidget(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_isDiaryFinishing = true;
|
||||
});
|
||||
await context.read<DiaryCubit>().finishDiaryDay(
|
||||
diaryId: state.diary.id,
|
||||
wakeUp: state.diary.wakeup.toString(),
|
||||
retire: state.diary.retire.toString(),
|
||||
);
|
||||
setState(() {
|
||||
_isDiaryFinishing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class FinishDayButtonWidget extends StatefulWidget {
|
||||
final DiaryDayEntity diaryDay;
|
||||
final Future<void> Function() onPressed;
|
||||
|
||||
const FinishDayButtonWidget({super.key, required this.diaryDay});
|
||||
const FinishDayButtonWidget({super.key, required this.onPressed});
|
||||
|
||||
@override
|
||||
State<FinishDayButtonWidget> createState() => _FinishDayButtonWidgetState();
|
||||
@@ -17,11 +14,7 @@ class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => context.read<DiaryCubit>().finishDiaryDay(
|
||||
diaryId: widget.diaryDay.id,
|
||||
wakeUp: widget.diaryDay.wakeup.toString(),
|
||||
retire: widget.diaryDay.retire.toString(),
|
||||
),
|
||||
onTap: widget.onPressed,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
|
||||
44
lib/features/settings/models/oauth_account_model.dart
Normal file
44
lib/features/settings/models/oauth_account_model.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'oauth_account_model.freezed.dart';
|
||||
part 'oauth_account_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class OAuthAccountModel with _$OAuthAccountModel {
|
||||
const factory OAuthAccountModel({
|
||||
@JsonKey(name: 'oauth_id') required String oauthId,
|
||||
required String name,
|
||||
required String email,
|
||||
required String phone,
|
||||
@JsonKey(name: 'linked_at') required DateTime linkedAt,
|
||||
}) = _OAuthAccountModel;
|
||||
|
||||
factory OAuthAccountModel.fromJson(Map<String, dynamic> json) => _$OAuthAccountModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class OAuthAccountsResponseModel with _$OAuthAccountsResponseModel {
|
||||
const factory OAuthAccountsResponseModel({required bool success, required OAuthAccountsDataModel data}) = _OAuthAccountsResponseModel;
|
||||
|
||||
factory OAuthAccountsResponseModel.fromJson(Map<String, dynamic> json) => _$OAuthAccountsResponseModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class OAuthAccountsDataModel with _$OAuthAccountsDataModel {
|
||||
const factory OAuthAccountsDataModel({
|
||||
@JsonKey(fromJson: _oauthAccountFromJson) OAuthAccountModel? vk,
|
||||
@JsonKey(fromJson: _oauthAccountFromJson) OAuthAccountModel? yandex,
|
||||
}) = _OAuthAccountsDataModel;
|
||||
|
||||
factory OAuthAccountsDataModel.fromJson(Map<String, dynamic> json) => _$OAuthAccountsDataModelFromJson(json);
|
||||
}
|
||||
|
||||
// Custom converter to handle both empty array [] and object
|
||||
OAuthAccountModel? _oauthAccountFromJson(dynamic json) {
|
||||
if (json == null) return null;
|
||||
if (json is List && json.isEmpty) return null;
|
||||
if (json is Map<String, dynamic>) {
|
||||
return OAuthAccountModel.fromJson(json);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
17
lib/features/settings/models/oauth_attach_request.dart
Normal file
17
lib/features/settings/models/oauth_attach_request.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'oauth_attach_request.freezed.dart';
|
||||
part 'oauth_attach_request.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class OAuthAttachRequest with _$OAuthAttachRequest {
|
||||
const factory OAuthAttachRequest({
|
||||
required String provider,
|
||||
@JsonKey(name: 'client_id') String? clientId,
|
||||
@JsonKey(name: 'access_token') required String accessToken,
|
||||
@JsonKey(name: 'refresh_token') required String refreshToken,
|
||||
}) = _OAuthAttachRequest;
|
||||
|
||||
factory OAuthAttachRequest.fromJson(Map<String, dynamic> json) => _$OAuthAttachRequestFromJson(json);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
|
||||
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
@@ -26,6 +28,7 @@ class SettingsCubit extends Cubit<SettingsState> {
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
late final ConsultationRequestModel? consultantInfo;
|
||||
late final bool hasActiveConsultation;
|
||||
late final OAuthAccountsDataModel? oauthAccounts;
|
||||
|
||||
try {
|
||||
if (profile.phoneConfirmationState.contains('confirmed')) {
|
||||
@@ -43,6 +46,13 @@ class SettingsCubit extends Cubit<SettingsState> {
|
||||
hasActiveConsultation = false;
|
||||
}
|
||||
|
||||
try {
|
||||
final oauthResponse = await settingsRepository.getOAuthAccounts();
|
||||
oauthAccounts = oauthResponse.data;
|
||||
} catch (e) {
|
||||
oauthAccounts = null;
|
||||
}
|
||||
|
||||
try {
|
||||
emit(
|
||||
SettingsState.loaded(
|
||||
@@ -51,6 +61,7 @@ class SettingsCubit extends Cubit<SettingsState> {
|
||||
profile: profile,
|
||||
consultationRequestModel: consultantInfo,
|
||||
hasActiveConsultation: hasActiveConsultation,
|
||||
oauthAccounts: oauthAccounts,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -87,6 +98,35 @@ class SettingsCubit extends Cubit<SettingsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> linkOAuthAccount({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
required String refreshToken,
|
||||
String? clientId,
|
||||
}) async {
|
||||
try {
|
||||
final request = OAuthAttachRequest(
|
||||
provider: provider,
|
||||
clientId: clientId,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
);
|
||||
await settingsRepository.linkOAuthAccount(request: request);
|
||||
await init();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unlinkOAuthAccount({required String provider}) async {
|
||||
try {
|
||||
await settingsRepository.unlinkOAuthAccount(provider: provider);
|
||||
await init();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkActiveConsultation() async {
|
||||
try {
|
||||
final userPurchases = await consultationsRepository.getUserPurchases(null, null);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -17,6 +18,7 @@ abstract class SettingsState with _$SettingsState {
|
||||
required ProfileModel profile,
|
||||
ConsultationRequestModel? consultationRequestModel,
|
||||
required bool hasActiveConsultation,
|
||||
OAuthAccountsDataModel? oauthAccounts,
|
||||
}) = SettingsStateLoaded;
|
||||
|
||||
const factory SettingsState.error({
|
||||
|
||||
@@ -6,6 +6,7 @@ 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/auth/sdk/social_auth_facade.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';
|
||||
@@ -16,6 +17,7 @@ import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
@@ -39,12 +41,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
return AlertDialog(
|
||||
title: const Text('Предупреждение'),
|
||||
content: const Text('Во время консультации этот параметр нельзя отключить'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Понятно'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Понятно'))],
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -74,6 +71,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
_oauthAccounts(state, settingsCubit),
|
||||
const SizedBox(height: 12),
|
||||
_additionalInfo(state),
|
||||
const SizedBox(height: 12),
|
||||
_notifications(state),
|
||||
@@ -328,6 +327,326 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _oauthAccounts(SettingsStateLoaded state, SettingsCubit settingsCubit) {
|
||||
final isVkLinked = state.oauthAccounts?.vk != null;
|
||||
final isYandexLinked = state.oauthAccounts?.yandex != null;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Привязанные аккаунты',
|
||||
style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_OAuthAccountButton(
|
||||
onTap: () => _onVkPressed(state, settingsCubit),
|
||||
backgroundColor: const Color(0xFF2787F5),
|
||||
tooltip: 'VK ID',
|
||||
icon: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24),
|
||||
isLinked: isVkLinked,
|
||||
accountName: state.oauthAccounts?.vk?.name,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_OAuthAccountButton(
|
||||
onTap: () => _onOkPressed(state, settingsCubit),
|
||||
backgroundColor: const Color(0xFFF58220),
|
||||
tooltip: 'Одноклассники',
|
||||
icon: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24),
|
||||
isLinked: isVkLinked,
|
||||
accountName: state.oauthAccounts?.vk?.name,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_OAuthAccountButton(
|
||||
onTap: () => _onMailRuPressed(state, settingsCubit),
|
||||
backgroundColor: const Color(0xFF005FF9),
|
||||
tooltip: 'Mail.ru',
|
||||
icon: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24),
|
||||
isLinked: isVkLinked,
|
||||
accountName: state.oauthAccounts?.vk?.name,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_OAuthAccountButton(
|
||||
onTap: () => _onYandexPressed(state, settingsCubit),
|
||||
backgroundColor: const Color(0xFFFFCC00),
|
||||
tooltip: 'Яндекс ID',
|
||||
icon: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24),
|
||||
isLinked: isYandexLinked,
|
||||
accountName: state.oauthAccounts?.yandex?.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onVkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
|
||||
if (state.oauthAccounts?.vk != null) {
|
||||
// Отвязываем аккаунт
|
||||
if (!mounted) return;
|
||||
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'VK ID');
|
||||
if (shouldUnlink == true) {
|
||||
try {
|
||||
await settingsCubit.unlinkOAuthAccount(provider: 'vk');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!mounted) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
|
||||
if (authResult == null) return;
|
||||
|
||||
try {
|
||||
await settingsCubit.linkOAuthAccount(
|
||||
provider: 'vk',
|
||||
accessToken: authResult.accessToken,
|
||||
refreshToken: authResult.refreshToken ?? '',
|
||||
clientId: authResult.clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onOkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
|
||||
if (state.oauthAccounts?.vk != null) {
|
||||
// Отвязываем аккаунт (OK использует тот же VK ID провайдер)
|
||||
if (!mounted) return;
|
||||
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Одноклассники');
|
||||
if (shouldUnlink == true) {
|
||||
try {
|
||||
await settingsCubit.unlinkOAuthAccount(provider: 'vk');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!mounted) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
|
||||
if (authResult == null) return;
|
||||
|
||||
try {
|
||||
await settingsCubit.linkOAuthAccount(
|
||||
provider: 'vk',
|
||||
accessToken: authResult.accessToken,
|
||||
refreshToken: authResult.refreshToken ?? '',
|
||||
clientId: authResult.clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onMailRuPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
|
||||
if (state.oauthAccounts?.vk != null) {
|
||||
// Отвязываем аккаунт (Mail.ru использует тот же VK ID провайдер)
|
||||
if (!mounted) return;
|
||||
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Mail.ru');
|
||||
if (shouldUnlink == true) {
|
||||
try {
|
||||
await settingsCubit.unlinkOAuthAccount(provider: 'vk');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!mounted) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
|
||||
if (authResult == null) return;
|
||||
|
||||
try {
|
||||
await settingsCubit.linkOAuthAccount(
|
||||
provider: 'vk',
|
||||
accessToken: authResult.accessToken,
|
||||
refreshToken: authResult.refreshToken ?? '',
|
||||
clientId: authResult.clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onYandexPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
|
||||
if (state.oauthAccounts?.yandex != null) {
|
||||
// Отвязываем аккаунт
|
||||
if (!mounted) return;
|
||||
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Яндекс ID');
|
||||
if (shouldUnlink == true) {
|
||||
try {
|
||||
await settingsCubit.unlinkOAuthAccount(provider: 'yandex');
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!mounted) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithYandex();
|
||||
if (authResult == null) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось авторизоваться через Яндекс');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что refreshToken не пустой для Яндекс
|
||||
if (authResult.refreshToken == null || authResult.refreshToken!.isEmpty) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось получить токен обновления от Яндекс');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что clientId не пустой для Яндекс
|
||||
if (authResult.clientId.isEmpty) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось получить client ID от Яндекс');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await settingsCubit.linkOAuthAccount(
|
||||
provider: 'yandex',
|
||||
accessToken: authResult.accessToken,
|
||||
refreshToken: authResult.refreshToken!,
|
||||
clientId: authResult.clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> _showUnlinkConfirmationDialog(BuildContext context, String providerName) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: const Text('Отвязать аккаунт'),
|
||||
content: Text('Вы точно хотите отвязать аккаунт $providerName?'),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отвязать'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OAuthAccountButton extends StatelessWidget {
|
||||
const _OAuthAccountButton({
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
required this.backgroundColor,
|
||||
required this.tooltip,
|
||||
required this.isLinked,
|
||||
this.accountName,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final Widget icon;
|
||||
final Color backgroundColor;
|
||||
final String tooltip;
|
||||
final bool isLinked;
|
||||
final String? accountName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Opacity(
|
||||
opacity: isLinked ? 0.5 : 1.0,
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: onTap,
|
||||
child: Ink(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFDBDBDB), width: 1),
|
||||
),
|
||||
child: Center(child: icon),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteAccountBottomSheet extends StatefulWidget {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
|
||||
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
|
||||
@@ -15,25 +17,41 @@ class SettingsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProfileSettingsModel> updateProfileSettings({
|
||||
required ProfileSettingsModel profileSettingsModel,
|
||||
}) async {
|
||||
Future<ProfileSettingsModel> updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async {
|
||||
try {
|
||||
return await settingsService.updateProfileSettings(
|
||||
data: profileSettingsModel.toJson(),
|
||||
);
|
||||
return await settingsService.updateProfileSettings(data: profileSettingsModel.toJson());
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProfileDetailsModel> updateProfileDetails({
|
||||
required ProfileDetailsModel profileDetailsModel,
|
||||
}) async {
|
||||
Future<ProfileDetailsModel> updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async {
|
||||
try {
|
||||
return await settingsService.updateProfileDetails(
|
||||
data: profileDetailsModel.toJson(),
|
||||
);
|
||||
return await settingsService.updateProfileDetails(data: profileDetailsModel.toJson());
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<OAuthAccountsResponseModel> getOAuthAccounts() async {
|
||||
try {
|
||||
return await settingsService.getOAuthAccounts();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> linkOAuthAccount({required OAuthAttachRequest request}) async {
|
||||
try {
|
||||
return await settingsService.linkOAuthAccount(request);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unlinkOAuthAccount({required String provider}) async {
|
||||
try {
|
||||
return await settingsService.unlinkOAuthAccount(provider);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
|
||||
import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
|
||||
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
@@ -13,12 +15,17 @@ abstract class SettingsService {
|
||||
Future<ProfileSettingsModel> getProfileSettings();
|
||||
|
||||
@PUT('/api/profile/settings/update')
|
||||
Future<ProfileSettingsModel> updateProfileSettings({
|
||||
@Body() required Map<String, dynamic> data,
|
||||
});
|
||||
Future<ProfileSettingsModel> updateProfileSettings({@Body() required Map<String, dynamic> data});
|
||||
|
||||
@PUT('/api/profile/update')
|
||||
Future<ProfileDetailsModel> updateProfileDetails({
|
||||
@Body() required Map<String, dynamic> data,
|
||||
});
|
||||
Future<ProfileDetailsModel> updateProfileDetails({@Body() required Map<String, dynamic> data});
|
||||
|
||||
@GET('/api/oauth/accounts')
|
||||
Future<OAuthAccountsResponseModel> getOAuthAccounts();
|
||||
|
||||
@POST('/api/oauth/attach')
|
||||
Future<void> linkOAuthAccount(@Body() OAuthAttachRequest request);
|
||||
|
||||
@DELETE('/api/oauth/accounts/{provider}')
|
||||
Future<void> unlinkOAuthAccount(@Path('provider') String provider);
|
||||
}
|
||||
|
||||
27
pubspec.yaml
27
pubspec.yaml
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.3+82
|
||||
version: 1.3.4+83
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
@@ -25,10 +25,10 @@ dependencies:
|
||||
flutter_form_builder: ^10.2.0
|
||||
image_picker: ^1.2.0
|
||||
flutter_typeahead: ^5.2.0
|
||||
go_router: ^16.1.0
|
||||
go_router: ^17.0.0
|
||||
keyboard_dismisser: ^3.0.0
|
||||
retrofit: ^4.7.1
|
||||
get_it: ^8.2.0
|
||||
get_it: ^9.0.5
|
||||
dio: ^5.9.0
|
||||
dart_pusher_channels: ^1.2.3
|
||||
photo_view: ^0.15.0
|
||||
@@ -37,11 +37,11 @@ dependencies:
|
||||
easy_debounce: ^2.0.3
|
||||
badges: ^3.1.2
|
||||
google_fonts: ^6.3.0
|
||||
syncfusion_flutter_charts: ^30.2.5
|
||||
syncfusion_flutter_gauges: ^30.2.5
|
||||
syncfusion_flutter_charts: ^31.2.10
|
||||
syncfusion_flutter_gauges: ^31.2.10
|
||||
table_calendar: ^3.2.0
|
||||
flutter_keyboard_visibility: ^6.0.0
|
||||
share_plus: ^11.1.0
|
||||
share_plus: ^12.0.1
|
||||
flutter_html: ^3.0.0
|
||||
file_picker: ^10.3.1
|
||||
audioplayers: ^6.5.0
|
||||
@@ -49,7 +49,7 @@ dependencies:
|
||||
mask_text_input_formatter: ^2.9.0
|
||||
lottie: ^3.3.1
|
||||
flutter_portal: ^1.1.4
|
||||
fluttertoast: ^8.2.12
|
||||
fluttertoast: ^9.0.0
|
||||
flutter_rating_bar: ^4.0.1
|
||||
flutter_image_compress: ^2.4.0
|
||||
image: ^4.5.4
|
||||
@@ -57,8 +57,8 @@ dependencies:
|
||||
http: ^1.5.0
|
||||
flutter_native_splash: ^2.4.6
|
||||
webview_flutter: ^4.13.0
|
||||
sentry_flutter: ^9.6.0
|
||||
connectivity_plus: ^6.1.5
|
||||
sentry_flutter: ^9.8.0
|
||||
connectivity_plus: ^7.0.0
|
||||
visibility_detector: ^0.4.0+2
|
||||
firebase_core: ^4.0.0
|
||||
firebase_messaging: ^16.0.0
|
||||
@@ -67,17 +67,18 @@ dependencies:
|
||||
collection: ^1.19.1
|
||||
shared_preferences: ^2.5.3
|
||||
version: ^3.0.2
|
||||
package_info_plus: ^8.3.1
|
||||
device_info_plus: ^11.5.0
|
||||
package_info_plus: ^9.0.0
|
||||
device_info_plus: ^12.2.0
|
||||
permission_handler: ^12.0.1
|
||||
qr_code_scanner_plus: ^2.0.10+1
|
||||
flutter_dotenv: ^5.2.1
|
||||
uuid: ^4.5.1
|
||||
flutter_dotenv: ^6.0.0
|
||||
uuid: ^4.5.2
|
||||
appmetrica_plugin: ^3.3.0
|
||||
vk_id: ^1.0.3
|
||||
flutter_login_yandex: ^0.0.5
|
||||
markdown: ^7.3.0
|
||||
gpassword: ^1.0.0
|
||||
mobile_scanner: ^7.1.3
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.7.0
|
||||
|
||||
@@ -35,7 +35,7 @@ void main() {
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
// Ensure dotenv has required keys before AppConfig constructor reads them
|
||||
dotenv.testLoad(fileInput: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||
// Register minimal AppConfig for tests to satisfy dependencies (e.g. image URL building)
|
||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||
mockCreateDietRepository = MockCreateDietRepository();
|
||||
|
||||
@@ -35,7 +35,7 @@ void main() {
|
||||
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
dotenv.testLoad(fileInput: 'DEV_BASE_URL=\nBASE_URL=\n');
|
||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=\nBASE_URL=\n');
|
||||
mockRecipeRepository = MockRecipeRepository();
|
||||
mockGlossaryRepository = MockGlossaryRepository();
|
||||
createRecipeCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository);
|
||||
|
||||
@@ -220,7 +220,7 @@ void main() {
|
||||
|
||||
testWidgets('should display FinishDayButtonWidget', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final widget = FinishDayButtonWidget(diaryDay: testDiaryEntity);
|
||||
final widget = FinishDayButtonWidget(onPressed: () async {});
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(widget));
|
||||
@@ -234,7 +234,7 @@ void main() {
|
||||
// Arrange
|
||||
diaryCubit.emit(DiaryState.loaded(diary: testDiaryEntity, nutrition: testNutrition, selectedDay: DateTime(2024, 1, 15), selectedMeal: 1));
|
||||
|
||||
final widget = FinishDayButtonWidget(diaryDay: testDiaryEntity);
|
||||
final widget = FinishDayButtonWidget(onPressed: () async {});
|
||||
|
||||
await tester.pumpWidget(createTestWidget(widget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in New Issue
Block a user