forked from eatvio/eatvio_app
Compare commits
2 Commits
1-3-42_bug
...
issue-1037
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b03a0d5e1 | ||
|
|
0208ed753a |
@@ -1,11 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io' as io;
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart' as di;
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_fcm_handler.dart';
|
||||
import 'package:eatvio_app/firebase_options.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
@@ -14,6 +18,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// Класс для инициализации приложения
|
||||
class AppInitializer {
|
||||
|
||||
/// Инициализация всех сервисов приложения
|
||||
static Future<void> initialize({required AppBuildType buildType}) async {
|
||||
await _initializeEnvironment();
|
||||
@@ -37,21 +42,25 @@ class AppInitializer {
|
||||
|
||||
/// Инициализация Firebase
|
||||
static Future<void> _initializeFirebase() async {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
).timeout(
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('Firebase initialization timeout');
|
||||
},
|
||||
);
|
||||
VideoCallFcmHandler.registerBackgroundHandler();
|
||||
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
||||
try {
|
||||
await FirebaseMessaging.instance.requestPermission().timeout(const Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
// Разрешение не критично, продолжаем работу
|
||||
log('Failed to request Firebase messaging permission: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Инициализация системы внедрения зависимостей
|
||||
static Future<void> _initializeDependencyInjection({
|
||||
required AppBuildType buildType,
|
||||
}) async {
|
||||
static Future<void> _initializeDependencyInjection({required AppBuildType buildType}) async {
|
||||
await di.init(buildType: buildType);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/notifications/notification_permission_requester.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
@@ -17,14 +16,9 @@ import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
||||
|
||||
class FirebaseApi {
|
||||
final ErrorReporter _errorReporter;
|
||||
final NotificationPermissionRequester _notificationPermissionRequester;
|
||||
final _firebaseMessaging = FirebaseMessaging.instance;
|
||||
|
||||
FirebaseApi({
|
||||
required ErrorReporter errorReporter,
|
||||
NotificationPermissionRequester? notificationPermissionRequester,
|
||||
}) : _errorReporter = errorReporter,
|
||||
_notificationPermissionRequester = notificationPermissionRequester ?? NotificationPermissionRequester();
|
||||
FirebaseApi({required ErrorReporter errorReporter}) : _errorReporter = errorReporter;
|
||||
|
||||
Future<String?> getToken() async {
|
||||
String? token;
|
||||
@@ -35,6 +29,16 @@ class FirebaseApi {
|
||||
onTimeout: () => null,
|
||||
);
|
||||
} else {
|
||||
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
||||
try {
|
||||
await _firebaseMessaging.requestPermission().timeout(
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
} catch (e) {
|
||||
// Разрешение не критично, продолжаем
|
||||
log('Failed to request Firebase messaging permission: $e');
|
||||
}
|
||||
}
|
||||
token = await _firebaseMessaging.getToken().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
@@ -43,20 +47,8 @@ class FirebaseApi {
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<void> _requestNotificationPermission() async {
|
||||
if (sl<AppConfig>().buildType != AppBuildType.prod) return;
|
||||
try {
|
||||
await _notificationPermissionRequester.requestOnce(
|
||||
() => _firebaseMessaging.requestPermission(),
|
||||
);
|
||||
} catch (e) {
|
||||
log('Failed to request Firebase messaging permission: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerAnonymousPushToken() async {
|
||||
try {
|
||||
await _requestNotificationPermission();
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
await sl<Dio>().post(
|
||||
'/api/anonymous_token',
|
||||
@@ -81,7 +73,6 @@ class FirebaseApi {
|
||||
Future<void> registerPushToken() async {
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
try {
|
||||
await _requestNotificationPermission();
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
|
||||
await sl<Dio>().post(
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
class NotificationPermissionRequester {
|
||||
Future<void>? _inFlight;
|
||||
|
||||
/// Coalesces overlapping calls so the OS prompt is shown at most once at a time.
|
||||
Future<void> requestOnce(Future<void> Function() requestPermission) async {
|
||||
final inFlight = _inFlight;
|
||||
if (inFlight != null) {
|
||||
await inFlight;
|
||||
return;
|
||||
}
|
||||
|
||||
final future = requestPermission();
|
||||
_inFlight = future;
|
||||
try {
|
||||
await future;
|
||||
} finally {
|
||||
if (identical(_inFlight, future)) {
|
||||
_inFlight = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,17 @@ class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
|
||||
}
|
||||
}
|
||||
|
||||
void setImagesError(String? error) {
|
||||
if (state.imagesError == error &&
|
||||
state.formError == null &&
|
||||
state.errorMessage == null) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(imagesError: error, formError: null, errorMessage: null),
|
||||
);
|
||||
}
|
||||
|
||||
void resetFormErrors() {
|
||||
if (!state.hasValidationErrors &&
|
||||
!state.isSuccess &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:eatvio_app/app_keys.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
@@ -38,11 +39,15 @@ class BlogPostCreateScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
static const _minImageWidth = 500;
|
||||
static const _minImageHeight = 500;
|
||||
|
||||
final blogPostCubit = sl<BlogPostCreateCubit>();
|
||||
bool shrinkImage = false;
|
||||
late StreamSubscription<bool> keyboardSubscription;
|
||||
final List<FeedGalleryImage> savedImages = [];
|
||||
final List<XFile> pendingImages = [];
|
||||
final Set<String> _undersizedPendingPaths = {};
|
||||
final List<int> mediaIdsToDelete = [];
|
||||
final titleController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
@@ -531,10 +536,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
if (image != null && _remainingSlots > 0) {
|
||||
setState(() {
|
||||
pendingImages.add(image);
|
||||
});
|
||||
blogPostCubit.clearImagesError();
|
||||
await _addPendingImages([image]);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
@@ -561,10 +563,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
limit: _remainingSlots,
|
||||
);
|
||||
if (images.isNotEmpty) {
|
||||
setState(() {
|
||||
pendingImages.addAll(images.take(_remainingSlots));
|
||||
});
|
||||
blogPostCubit.clearImagesError();
|
||||
await _addPendingImages(
|
||||
images.take(_remainingSlots).toList(),
|
||||
);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
@@ -603,9 +604,12 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
}
|
||||
|
||||
void _removePendingImage(int index) {
|
||||
final path = pendingImages[index].path;
|
||||
setState(() {
|
||||
pendingImages.removeAt(index);
|
||||
_undersizedPendingPaths.remove(path);
|
||||
});
|
||||
_syncImagesSizeError();
|
||||
}
|
||||
|
||||
void _onReorderGallery(int oldIndex, int newIndex) {
|
||||
@@ -839,6 +843,11 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_undersizedPendingPaths.isNotEmpty) {
|
||||
blogPostCubit.setImagesError(S.current.blog_post_image_min_size_error);
|
||||
return;
|
||||
}
|
||||
|
||||
final title = titleController.text.trim();
|
||||
final description = descriptionController.text.trim();
|
||||
final videoUrl = videoUrlController.text.trim();
|
||||
@@ -907,6 +916,50 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addPendingImages(List<XFile> images) async {
|
||||
if (images.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final undersizedPaths = <String>{};
|
||||
for (final image in images) {
|
||||
if (!await _isImageLargeEnough(image)) {
|
||||
undersizedPaths.add(image.path);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
pendingImages.addAll(images);
|
||||
_undersizedPendingPaths.addAll(undersizedPaths);
|
||||
});
|
||||
_syncImagesSizeError();
|
||||
}
|
||||
|
||||
void _syncImagesSizeError() {
|
||||
final minSizeError = S.current.blog_post_image_min_size_error;
|
||||
if (_undersizedPendingPaths.isNotEmpty) {
|
||||
blogPostCubit.setImagesError(minSizeError);
|
||||
return;
|
||||
}
|
||||
if (blogPostCubit.state.imagesError == minSizeError) {
|
||||
blogPostCubit.clearImagesError();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isImageLargeEnough(XFile image) async {
|
||||
try {
|
||||
final bytes = await File(image.path).readAsBytes();
|
||||
final descriptor = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
final codec = await ui.instantiateImageCodecFromBuffer(descriptor);
|
||||
final frame = await codec.getNextFrame();
|
||||
final width = frame.image.width;
|
||||
final height = frame.image.height;
|
||||
return width >= _minImageWidth && height >= _minImageHeight;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildGallery() {
|
||||
final itemHeight = shrinkImage ? 72.0 : 112.0;
|
||||
|
||||
@@ -946,6 +999,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
child: _buildGalleryTile(
|
||||
index: index,
|
||||
height: itemHeight,
|
||||
hasError: _undersizedPendingPaths.contains(pending.path),
|
||||
imageProvider: FileImage(File(pending.path)),
|
||||
onRemove: () => _removePendingImage(pendingIndex),
|
||||
),
|
||||
@@ -985,6 +1039,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
required double height,
|
||||
required ImageProvider imageProvider,
|
||||
required VoidCallback onRemove,
|
||||
bool hasError = false,
|
||||
}) {
|
||||
return ReorderableDelayedDragStartListener(
|
||||
index: index,
|
||||
@@ -992,12 +1047,16 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
width: height,
|
||||
height: height,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: hasError ? Border.all(color: appTheme.red, width: 2) : null,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Image(image: imageProvider, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,7 +33,9 @@ class _ComplainingBottomSheetState extends State<ComplainingBottomSheet> {
|
||||
create: (context) => sl<ComplainingCubit>()..setContentTypeAndIdentifier(contentType: widget.complainedContentType, slug: widget.slug, id: widget.id),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 24, left: 16, right: 16),
|
||||
decoration: const BoxDecoration(borderRadius: BorderRadius.vertical(top: Radius.circular(12))),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
child: BlocBuilder<ComplainingCubit, ComplainingState>(
|
||||
builder: (context, state) {
|
||||
final category = state.category;
|
||||
@@ -70,7 +72,10 @@ class _CategoryWidget extends StatelessWidget {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.complaint, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.complaint,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
s.complaint_question,
|
||||
@@ -109,7 +114,10 @@ class _SpamWidget extends StatelessWidget {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.spam, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.spam,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -137,7 +145,10 @@ class _ExplicitImageWidget extends StatelessWidget {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.explicitImages, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.explicitImages,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -191,7 +202,10 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.lawViolation, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.lawViolation,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -221,7 +235,10 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(element.$2, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
element.$2,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
@@ -237,10 +254,12 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
|
||||
if ((state.complainInfo.description?.length ?? 10) < 10) ...[
|
||||
const SizedBox(height: 4),
|
||||
const ValidationErrorWidget(error: 'Минимум 10 символов'),
|
||||
],
|
||||
]
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
_ComplainButton(isDisabled: tags.firstWhere((tag) => tag.$2 == s.other).$1 && (state.complainInfo.description?.length ?? 0) < 10),
|
||||
_ComplainButton(
|
||||
isDisabled: (state.complainInfo.description?.length ?? 0) < 10,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -282,7 +301,10 @@ class _ViolenceAndHostilityState extends State<_ViolenceAndHostility> {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.lawViolation, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.lawViolation,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -310,7 +332,10 @@ class _ViolenceAndHostilityState extends State<_ViolenceAndHostility> {
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(element.$2, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
child: Text(
|
||||
element.$2,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -338,9 +363,15 @@ class _DeceptionWidgetState extends State<_DeceptionWidget> {
|
||||
super.didChangeDependencies();
|
||||
final s = S.of(context);
|
||||
if (tags.isEmpty && descriptions.isEmpty) {
|
||||
tags.addAll([(false, s.misrepresentation), (false, s.fraud)]);
|
||||
tags.addAll([
|
||||
(false, s.misrepresentation),
|
||||
(false, s.fraud),
|
||||
]);
|
||||
|
||||
descriptions.addAll([s.misrepresentation_description, s.fraud_description]);
|
||||
descriptions.addAll([
|
||||
s.misrepresentation_description,
|
||||
s.fraud_description,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +383,10 @@ class _DeceptionWidgetState extends State<_DeceptionWidget> {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.lawViolation, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.lawViolation,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -385,10 +419,16 @@ class _DeceptionWidgetState extends State<_DeceptionWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(element.$2, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
element.$2,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
child: Text(descriptions[index], style: TextStyles.textSSemibold.copyWith(color: appTheme.lightGray600)),
|
||||
child: Text(
|
||||
descriptions[index],
|
||||
style: TextStyles.textSSemibold.copyWith(color: appTheme.lightGray600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -413,7 +453,10 @@ class _ComplaintSentWidget extends StatelessWidget {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.complaint_sent, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.complaint_sent,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -428,7 +471,10 @@ class _ComplaintSentWidget extends StatelessWidget {
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
@@ -449,7 +495,10 @@ class _ComplaintFailedWidget extends StatelessWidget {
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(s.could_not_send_complain, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
s.could_not_send_complain,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
@@ -464,7 +513,10 @@ class _ComplaintFailedWidget extends StatelessWidget {
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
@@ -483,13 +535,16 @@ class _ComplainButton extends StatelessWidget {
|
||||
return BlocBuilder<ComplainingCubit, ComplainingState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const SizedBox(height: 52, child: Loader());
|
||||
return const Loader();
|
||||
}
|
||||
return CustomOutlinedButton(
|
||||
text: s.send_complain,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isDisabled) {
|
||||
context.read<ComplainingCubit>().setDescription('');
|
||||
@@ -515,7 +570,10 @@ class _CategoryButton extends StatelessWidget {
|
||||
child: Ink(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(category.getTranslatedText(), style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
category.getTranslatedText(),
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset('assets/icons/right_arrow.svg'),
|
||||
],
|
||||
@@ -533,18 +591,18 @@ enum ComplainType {
|
||||
explicitImage;
|
||||
|
||||
String getTranslatedText() => switch (this) {
|
||||
ComplainType.spam => S.current.spam,
|
||||
ComplainType.lawViolation => S.current.lawViolation,
|
||||
ComplainType.deception => S.current.deception,
|
||||
ComplainType.violenceDndHostility => S.current.violenceAndHostility,
|
||||
ComplainType.explicitImage => S.current.explicitImage,
|
||||
};
|
||||
ComplainType.spam => S.current.spam,
|
||||
ComplainType.lawViolation => S.current.lawViolation,
|
||||
ComplainType.deception => S.current.deception,
|
||||
ComplainType.violenceDndHostility => S.current.violenceAndHostility,
|
||||
ComplainType.explicitImage => S.current.explicitImage,
|
||||
};
|
||||
|
||||
String getValue() => switch (this) {
|
||||
ComplainType.spam => 'spam',
|
||||
ComplainType.lawViolation => 'legalViolation',
|
||||
ComplainType.deception => 'fraud',
|
||||
ComplainType.violenceDndHostility => 'violenceAndHatred',
|
||||
ComplainType.explicitImage => 'explicitImage',
|
||||
};
|
||||
ComplainType.spam => 'spam',
|
||||
ComplainType.lawViolation => 'legalViolation',
|
||||
ComplainType.deception => 'fraud',
|
||||
ComplainType.violenceDndHostility => 'violenceAndHatred',
|
||||
ComplainType.explicitImage => 'explicitImage',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ class ComplainingCubit extends Cubit<ComplainingState> {
|
||||
}
|
||||
|
||||
Future<void> complain() async {
|
||||
if (state.isLoading) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
final category = state.category;
|
||||
final id = state.contentId;
|
||||
@@ -50,37 +46,37 @@ class ComplainingCubit extends Cubit<ComplainingState> {
|
||||
case ComplainedContent.diet:
|
||||
if (id != null) {
|
||||
await repository.sendDietComplain(id: id, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
case ComplainedContent.plan:
|
||||
if (slug != null) {
|
||||
await repository.sendPlanComplain(slug: slug, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
case ComplainedContent.recipe:
|
||||
if (slug != null) {
|
||||
await repository.sendRecipeComplain(slug: slug, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
case ComplainedContent.profile:
|
||||
if (slug != null) {
|
||||
await repository.sendProfileComplain(slug: slug, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
case ComplainedContent.posts:
|
||||
if (id != null) {
|
||||
await repository.sendBlogPostsComplain(id: id, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
case ComplainedContent.comment:
|
||||
if (id != null) {
|
||||
await repository.sendCommentComplain(id: id, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -89,6 +85,6 @@ class ComplainingCubit extends Cubit<ComplainingState> {
|
||||
await reportFrom(e, st, this);
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
emit(state.copyWith(wasSentSuccessfully: false, isLoading: false));
|
||||
emit(state.copyWith(wasSentSuccessfully: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
|
||||
if (state.planCreated) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
context.pop(true);
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
|
||||
@@ -16,9 +16,6 @@ class CalendarWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
static const _kDaysOfWeekHeight = 16.0;
|
||||
static const _kRowHeight = 52.0;
|
||||
|
||||
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime _selectedDay = DateTime.now();
|
||||
@@ -43,8 +40,6 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
final textScaler = MediaQuery.textScalerOf(context);
|
||||
final daysOfWeekHeight = _scaledDaysOfWeekHeight(context);
|
||||
return BlocConsumer<DiaryCubit, DiaryState>(
|
||||
listener: (BuildContext context, DiaryState state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
@@ -66,8 +61,6 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
locale: 'ru_RU',
|
||||
startingDayOfWeek: StartingDayOfWeek.monday,
|
||||
calendarFormat: _calendarFormat,
|
||||
daysOfWeekHeight: daysOfWeekHeight,
|
||||
rowHeight: textScaler.scale(_kRowHeight),
|
||||
headerStyle: const HeaderStyle(titleCentered: true, formatButtonVisible: false, leftChevronVisible: false, rightChevronVisible: false),
|
||||
onDaySelected: (selectedDay, focusedDay) async {
|
||||
if (!isSameDay(_selectedDay, selectedDay)) {
|
||||
@@ -123,18 +116,6 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
double _scaledDaysOfWeekHeight(BuildContext context) {
|
||||
final painter = TextPainter(
|
||||
text: TextSpan(text: 'Щy', style: DefaultTextStyle.of(context).style),
|
||||
textDirection: Directionality.of(context),
|
||||
textScaler: MediaQuery.textScalerOf(context),
|
||||
maxLines: 1,
|
||||
)..layout();
|
||||
final height = painter.height;
|
||||
painter.dispose();
|
||||
return height.clamp(_kDaysOfWeekHeight, double.infinity);
|
||||
}
|
||||
|
||||
void _nextCalendarFormat() {
|
||||
setState(() {
|
||||
_calendarFormat = CalendarFormat.values[(_calendarFormat.index - 1) % CalendarFormat.values.length];
|
||||
|
||||
@@ -92,7 +92,6 @@ class DietsCubit extends Cubit<DietsState> {
|
||||
}) async {
|
||||
try {
|
||||
_query = query;
|
||||
currentPage = 1;
|
||||
final type = _types[state.filterState.typeIndex];
|
||||
final duration = _durationValues[state.filterState.durationIndex];
|
||||
final sorting = _sortingValues[state.filterState.sortingIndex];
|
||||
|
||||
@@ -149,7 +149,6 @@ class PlansCubit extends Cubit<PlansState> {
|
||||
}) async {
|
||||
try {
|
||||
_query = query;
|
||||
currentPage = 1;
|
||||
final type = _types[state.filterState.typeIndex];
|
||||
final duration = _durationValues[state.filterState.durationIndex];
|
||||
final minPrice = state.filterState.priceFilter.$1;
|
||||
|
||||
@@ -46,7 +46,6 @@ class RecipesCubit extends Cubit<RecipesState> {
|
||||
}) async {
|
||||
try {
|
||||
_query = query;
|
||||
currentPage = 1;
|
||||
final recipesList = await feedRepository.getRecipes(
|
||||
tags: _selectedTags.toList(),
|
||||
query: query,
|
||||
|
||||
@@ -123,19 +123,6 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openCreateAndReloadFeed({
|
||||
required Future<Object?> Function() openCreate,
|
||||
required Future<void> Function() reload,
|
||||
}) async {
|
||||
final result = await openCreate();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (shouldReloadFeedAfterCreate(result)) {
|
||||
await reload();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
FlutterNativeSplash.remove();
|
||||
@@ -267,43 +254,26 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
const SizedBox(width: 12),
|
||||
switch (_selectedChipIndex) {
|
||||
1 when !hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () => _openCreateAndReloadFeed(
|
||||
openCreate: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name),
|
||||
reload: () => context.read<PlansCubit>().loadPlans(),
|
||||
)
|
||||
: null,
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
|
||||
),
|
||||
2 when hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () => _openCreateAndReloadFeed(
|
||||
openCreate: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
|
||||
reload: () => context.read<RecipesCubit>().loadRecipesList(),
|
||||
)
|
||||
: null,
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name) : null,
|
||||
),
|
||||
3 when !hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () => _openCreateAndReloadFeed(
|
||||
openCreate: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
|
||||
reload: () => context.read<RecipesCubit>().loadRecipesList(),
|
||||
)
|
||||
: null,
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name) : null,
|
||||
),
|
||||
3 when hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () => _openCreateAndReloadFeed(
|
||||
openCreate: () => GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'isEditing': false}),
|
||||
reload: () => context.read<DietsCubit>().loadDiets(),
|
||||
)
|
||||
? () async {
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'isEditing': false});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
4 when !hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () => _openCreateAndReloadFeed(
|
||||
openCreate: () => GoRouter.of(context).pushNamed(CreateDietScreen.name),
|
||||
reload: () => context.read<DietsCubit>().loadDiets(),
|
||||
)
|
||||
? () async {
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
_ => const AddItemButton(key: AppKeys.feedScreenBlogPostCreationButton),
|
||||
@@ -331,9 +301,6 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create screens pop `true` (diet/plan) or `'active'` (recipe) after a successful publish.
|
||||
bool shouldReloadFeedAfterCreate(Object? result) => result == true || result == 'active';
|
||||
|
||||
enum ViewableContentType {
|
||||
plans,
|
||||
posts,
|
||||
|
||||
@@ -221,51 +221,49 @@ class _GeneralInfoScreenState extends State<GeneralInfoScreen> {
|
||||
}
|
||||
|
||||
Widget _saveButton() {
|
||||
if (isLoading) {
|
||||
return const SizedBox(height: 52, child: Loader());
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final hasError = _checkFields();
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
await widget.settingsEntity.settingsCubit.updateProfileDetails(
|
||||
profileDetailsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileDetails.copyWith(
|
||||
firstName: firstNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
birthdate: selectedDate.toString(),
|
||||
gender: gender.value,
|
||||
country: countryController.text,
|
||||
city: cityController.text,
|
||||
lang: languageController.text,
|
||||
),
|
||||
);
|
||||
await widget.settingsEntity.settingsCubit.updateProfileSettings(
|
||||
profileSettingsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileSettings.copyWith(
|
||||
isPublicProfile: isPublicProfile.value,
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Сохранить',
|
||||
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return isLoading
|
||||
? const Center(child: Loader())
|
||||
: InkWell(
|
||||
onTap: () async {
|
||||
final hasError = _checkFields();
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
await widget.settingsEntity.settingsCubit.updateProfileDetails(
|
||||
profileDetailsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileDetails.copyWith(
|
||||
firstName: firstNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
birthdate: selectedDate.toString(),
|
||||
gender: gender.value,
|
||||
country: countryController.text,
|
||||
city: cityController.text,
|
||||
lang: languageController.text,
|
||||
),
|
||||
);
|
||||
await widget.settingsEntity.settingsCubit.updateProfileSettings(
|
||||
profileSettingsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileSettings.copyWith(
|
||||
isPublicProfile: isPublicProfile.value,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Сохранить',
|
||||
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,17 @@ class VideoCallCallKitCoordinator {
|
||||
onResumed: _onAndroidAppResumed,
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_androidResumeObserver!);
|
||||
_androidCallPushChannel.setMethodCallHandler(
|
||||
_onAndroidCallPushMethodCall,
|
||||
);
|
||||
_androidCallPushChannel.setMethodCallHandler(_onAndroidCallPushMethodCall);
|
||||
await FlutterCallkitIncoming.requestNotificationPermission(<
|
||||
String,
|
||||
dynamic
|
||||
>{
|
||||
'title': 'Уведомления',
|
||||
'rationaleMessagePermission':
|
||||
'Разрешите уведомления, чтобы получать входящие видеозвонки.',
|
||||
'postNotificationMessageRequired':
|
||||
'Разрешите уведомления в настройках, чтобы получать входящие видеозвонки.',
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_pollNativePendingAcceptAfterResume());
|
||||
});
|
||||
|
||||
@@ -137,6 +137,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Средние показатели давления:",
|
||||
),
|
||||
"back_to_chat": MessageLookupByLibrary.simpleMessage("Вернуться в чат"),
|
||||
"blog_post_image_min_size_error": MessageLookupByLibrary.simpleMessage(
|
||||
"Минимальный размер изображения — 500×500 px",
|
||||
),
|
||||
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
|
||||
"by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
||||
"calls_for_bullying": MessageLookupByLibrary.simpleMessage(
|
||||
|
||||
@@ -55,6 +55,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"average_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Средние показатели давления:",
|
||||
),
|
||||
"blog_post_image_min_size_error": MessageLookupByLibrary.simpleMessage(
|
||||
"Минимальный размер изображения — 500×500 px",
|
||||
),
|
||||
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
|
||||
"by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
||||
"calories": MessageLookupByLibrary.simpleMessage("Калории"),
|
||||
|
||||
@@ -2569,6 +2569,16 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Минимальный размер изображения — 500×500 px`
|
||||
String get blog_post_image_min_size_error {
|
||||
return Intl.message(
|
||||
'Минимальный размер изображения — 500×500 px',
|
||||
name: 'blog_post_image_min_size_error',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Информация о заказе`
|
||||
String get infoAboutOrder {
|
||||
return Intl.message(
|
||||
|
||||
@@ -317,6 +317,7 @@
|
||||
"could_not_publish_blog_post": "Не удалось опубликовать пост",
|
||||
"could_not_update_blog_post": "Не удалось обновить пост",
|
||||
"could_not_delete_blog_post": "Не удалось удалить пост",
|
||||
"blog_post_image_min_size_error": "Минимальный размер изображения — 500×500 px",
|
||||
"infoAboutOrder": "Информация о заказе",
|
||||
"to_order": "Заказать",
|
||||
"can_not_choose_start_date": "Дату старта плана невозможно выбрать, пока Вы не привяжите карту",
|
||||
|
||||
@@ -270,5 +270,6 @@
|
||||
"video_call_close": "Закрыть",
|
||||
"video_call_notification_in_progress": "Звонок идёт",
|
||||
"video_call_notification_return": "Вернуться к звонку",
|
||||
"video_call_notification_end": "Завершить звонок"
|
||||
"video_call_notification_end": "Завершить звонок",
|
||||
"blog_post_image_min_size_error": "Минимальный размер изображения — 500×500 px"
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/notifications/notification_permission_requester.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('NotificationPermissionRequester', () {
|
||||
test('вызывает requestPermission один раз', () async {
|
||||
final requester = NotificationPermissionRequester();
|
||||
var calls = 0;
|
||||
|
||||
await requester.requestOnce(() async {
|
||||
calls++;
|
||||
});
|
||||
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('параллельные вызовы ждут один и тот же запрос', () async {
|
||||
final requester = NotificationPermissionRequester();
|
||||
var calls = 0;
|
||||
|
||||
Future<void> request() {
|
||||
return requester.requestOnce(() async {
|
||||
calls++;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
});
|
||||
}
|
||||
|
||||
await Future.wait<void>([request(), request(), request()]);
|
||||
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('после завершения можно запросить снова', () async {
|
||||
final requester = NotificationPermissionRequester();
|
||||
var calls = 0;
|
||||
|
||||
Future<void> request() {
|
||||
return requester.requestOnce(() async {
|
||||
calls++;
|
||||
});
|
||||
}
|
||||
|
||||
await request();
|
||||
await request();
|
||||
|
||||
expect(calls, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('единая точка запроса разрешения на уведомления', () {
|
||||
test('старт приложения и CallKit не показывают отдельные системные окна', () {
|
||||
final initializer = File('lib/core/app_initializer.dart').readAsStringSync();
|
||||
final callkit = File('lib/features/video_calls/services/video_call_callkit_coordinator.dart').readAsStringSync();
|
||||
final firebaseApi = File('lib/core/firebase_api.dart').readAsStringSync();
|
||||
|
||||
expect(initializer.contains('requestPermission'), isFalse);
|
||||
expect(callkit.contains('requestNotificationPermission'), isFalse);
|
||||
expect(firebaseApi.contains('_requestNotificationPermission'), isTrue);
|
||||
expect(firebaseApi.contains('_firebaseMessaging.requestPermission()'), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
|
||||
@@ -15,7 +12,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'complaining_bottom_sheet_smoke_test.mocks.dart';
|
||||
|
||||
@@ -63,40 +59,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ComplainingBottomSheet), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('replaces send button with Loader while request is in progress', (tester) async {
|
||||
final completer = Completer<void>();
|
||||
when(
|
||||
repository.sendPlanComplain(
|
||||
slug: anyNamed('slug'),
|
||||
category: anyNamed('category'),
|
||||
body: anyNamed('body'),
|
||||
),
|
||||
).thenAnswer((_) => completer.future);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
const ComplainingBottomSheet(
|
||||
complainedContentType: ComplainedContent.plan,
|
||||
slug: 'test-slug',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Спам'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Отправить жалобу'), findsOneWidget);
|
||||
await tester.tap(find.text('Отправить жалобу'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Loader), findsOneWidget);
|
||||
expect(find.text('Отправить жалобу'), findsNothing);
|
||||
|
||||
completer.complete();
|
||||
await tester.pump();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
import 'package:eatvio_app/features/complaining/models/complain_model.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
|
||||
@@ -99,7 +97,6 @@ void main() {
|
||||
await cubit.complain();
|
||||
|
||||
expect(cubit.state.wasSentSuccessfully, true);
|
||||
expect(cubit.state.isLoading, false);
|
||||
verify(() => mockRepository.sendDietComplain(
|
||||
id: id,
|
||||
category: any(named: 'category'),
|
||||
@@ -107,30 +104,6 @@ void main() {
|
||||
)).called(1);
|
||||
});
|
||||
|
||||
test('complain - ставит isLoading на время запроса', () async {
|
||||
const id = 1;
|
||||
final completer = Completer<void>();
|
||||
|
||||
when(() => mockRepository.sendDietComplain(
|
||||
id: id,
|
||||
category: any(named: 'category'),
|
||||
body: any(named: 'body'),
|
||||
)).thenAnswer((_) => completer.future);
|
||||
|
||||
cubit.setContentTypeAndIdentifier(contentType: ComplainedContent.diet, id: id);
|
||||
cubit.setCategory(ComplainType.spam);
|
||||
|
||||
final future = cubit.complain();
|
||||
expect(cubit.state.isLoading, true);
|
||||
expect(cubit.state.wasSentSuccessfully, isNull);
|
||||
|
||||
completer.complete();
|
||||
await future;
|
||||
|
||||
expect(cubit.state.isLoading, false);
|
||||
expect(cubit.state.wasSentSuccessfully, true);
|
||||
});
|
||||
|
||||
test('complain - возвращает false при ошибке', () async {
|
||||
const id = 1;
|
||||
|
||||
@@ -146,7 +119,6 @@ void main() {
|
||||
await cubit.complain();
|
||||
|
||||
expect(cubit.state.wasSentSuccessfully, false);
|
||||
expect(cubit.state.isLoading, false);
|
||||
});
|
||||
|
||||
test('complain - отправляет жалобу на plan', () async {
|
||||
|
||||
@@ -4,12 +4,9 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/calendar_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
|
||||
FakeDiaryCubit(super.initialState);
|
||||
@@ -19,20 +16,9 @@ class FakeDiaryCubit extends Cubit<DiaryState> implements DiaryCubit {
|
||||
|
||||
void main() {
|
||||
group('CalendarWidget smoke', () {
|
||||
setUpAll(() async {
|
||||
await initializeDateFormatting('ru', null);
|
||||
await initializeDateFormatting('ru_RU', null);
|
||||
});
|
||||
|
||||
Widget wrap(Widget child, DiaryCubit cubit, {double textScale = 1}) {
|
||||
Widget wrap(Widget child, DiaryCubit cubit) {
|
||||
return MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
builder: (context, appChild) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(textScale)),
|
||||
child: appChild!,
|
||||
);
|
||||
},
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
@@ -51,26 +37,9 @@ void main() {
|
||||
final cubit = FakeDiaryCubit(const DiaryState.loading());
|
||||
await tester.pumpWidget(wrap(const CalendarWidget(), cubit));
|
||||
await tester.pump();
|
||||
// Just make sure it builds.
|
||||
expect(find.byType(CalendarWidget), findsOneWidget);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
testWidgets('scales weekday row when system font is large', (tester) async {
|
||||
const textScale = 2.0;
|
||||
final cubit = FakeDiaryCubit(const DiaryState.loading());
|
||||
await tester.pumpWidget(wrap(const CalendarWidget(), cubit, textScale: textScale));
|
||||
await tester.pump();
|
||||
|
||||
final calendar = tester.widget<TableCalendar<dynamic>>(find.byType(TableCalendar<dynamic>));
|
||||
expect(calendar.daysOfWeekHeight, greaterThan(16 * textScale * 0.9));
|
||||
expect(calendar.rowHeight, 52 * textScale);
|
||||
|
||||
expect(find.text('пн'), findsWidgets);
|
||||
final weekday = tester.renderObject<RenderParagraph>(find.text('пн').first);
|
||||
expect(weekday.size.height, lessThanOrEqualTo(calendar.daysOfWeekHeight + 0.5));
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await cubit.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,44 +156,6 @@ void main() {
|
||||
expect(dietsCubit!.state.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('loadDiets - сбрасывает currentPage при повторной загрузке', () async {
|
||||
dietsCubit = DietsCubit(
|
||||
feedRepository: mockFeedRepository,
|
||||
dietsRepository: mockDietRepository,
|
||||
);
|
||||
|
||||
const dietsPagination = DietsPagination(
|
||||
data: [],
|
||||
links: Links(
|
||||
first: 'https://example.com?page=1',
|
||||
last: 'https://example.com?page=1',
|
||||
prev: null,
|
||||
next: null,
|
||||
),
|
||||
meta: Meta(
|
||||
currentPage: 1,
|
||||
from: null,
|
||||
lastPage: 1,
|
||||
links: [],
|
||||
perPage: 10,
|
||||
to: null,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
when(mockFeedRepository.getDiets(
|
||||
query: anyNamed('query'),
|
||||
types: anyNamed('types'),
|
||||
duration: anyNamed('duration'),
|
||||
sorting: anyNamed('sorting'),
|
||||
)).thenAnswer((_) async => dietsPagination);
|
||||
|
||||
dietsCubit!.currentPage = 4;
|
||||
await dietsCubit!.loadDiets();
|
||||
|
||||
expect(dietsCubit!.currentPage, 1);
|
||||
});
|
||||
|
||||
test('loadDiets - обработка ошибки', () async {
|
||||
dietsCubit = DietsCubit(
|
||||
feedRepository: mockFeedRepository,
|
||||
|
||||
@@ -131,31 +131,6 @@ void main() {
|
||||
expect(plansCubit!.state.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('loadPlans - сбрасывает currentPage при повторной загрузке', () async {
|
||||
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||
|
||||
const plansPagination = PlanPaginationModel(
|
||||
data: [],
|
||||
links: Links(first: 'https://example.com?page=1', last: 'https://example.com?page=1', prev: null, next: null),
|
||||
meta: Meta(currentPage: 1, from: 1, lastPage: 1, links: [], perPage: 10, to: 1, total: 0),
|
||||
);
|
||||
|
||||
when(
|
||||
mockFeedRepository.getPlans(
|
||||
query: anyNamed('query'),
|
||||
types: anyNamed('types'),
|
||||
duration: anyNamed('duration'),
|
||||
minPrice: anyNamed('minPrice'),
|
||||
maxPrice: anyNamed('maxPrice'),
|
||||
),
|
||||
).thenAnswer((_) async => plansPagination);
|
||||
|
||||
plansCubit!.currentPage = 4;
|
||||
await plansCubit!.loadPlans();
|
||||
|
||||
expect(plansCubit!.currentPage, 1);
|
||||
});
|
||||
|
||||
test('loadPlans - обработка ошибки', () async {
|
||||
plansCubit = PlansCubit(feedRepository: mockFeedRepository, planRepository: mockPlanRepository);
|
||||
|
||||
|
||||
@@ -110,18 +110,6 @@ void main() {
|
||||
expect(recipesCubit!.state.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('loadRecipesList - сбрасывает currentPage при повторной загрузке', () async {
|
||||
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||
|
||||
when(mockFeedRepository.getRecipes(tags: anyNamed('tags'), query: anyNamed('query'))).thenAnswer((_) async => []);
|
||||
when(mockRecipeRepository.getRecipesTags()).thenAnswer((_) async => []);
|
||||
|
||||
recipesCubit!.currentPage = 4;
|
||||
await recipesCubit!.loadRecipesList();
|
||||
|
||||
expect(recipesCubit!.currentPage, 1);
|
||||
});
|
||||
|
||||
test('updateTags - добавляет и удаляет теги', () {
|
||||
recipesCubit = RecipesCubit(feedRepository: mockFeedRepository, recipeRepository: mockRecipeRepository, aboutRecipeRepository: mockAboutRecipeRepository);
|
||||
|
||||
|
||||
@@ -136,19 +136,4 @@ void main() {
|
||||
expect(find.byType(FeedScreen), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('shouldReloadFeedAfterCreate', () {
|
||||
test('перезагружает ленту после публикации диеты или плана', () {
|
||||
expect(shouldReloadFeedAfterCreate(true), isTrue);
|
||||
});
|
||||
|
||||
test('перезагружает ленту после публикации рецепта', () {
|
||||
expect(shouldReloadFeedAfterCreate('active'), isTrue);
|
||||
});
|
||||
|
||||
test('не перезагружает ленту при отмене или черновике рецепта', () {
|
||||
expect(shouldReloadFeedAfterCreate(null), isFalse);
|
||||
expect(shouldReloadFeedAfterCreate('drafted'), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
|
||||
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
|
||||
@@ -133,52 +130,5 @@ void main() {
|
||||
|
||||
expect(find.byType(GeneralInfoScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('лоадер при сохранении появляется вместо кнопки, а не поверх экрана', (tester) async {
|
||||
final cubit = MockSettingsCubit();
|
||||
const state = SettingsState.loaded(
|
||||
profileSettings: mockProfileSettings,
|
||||
profileDetails: mockProfileDetails,
|
||||
profile: mockProfile,
|
||||
hasActiveConsultation: false,
|
||||
);
|
||||
final saveCompleter = Completer<void>();
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.updateProfileDetails(profileDetailsModel: anyNamed('profileDetailsModel'))).thenAnswer((_) => saveCompleter.future);
|
||||
when(cubit.updateProfileSettings(profileSettingsModel: anyNamed('profileSettingsModel'))).thenAnswer((_) async {});
|
||||
|
||||
final settingsEntity = SettingsEntity(profileSettingsModel: mockProfileSettings, profileDetailsModel: mockProfileDetails, settingsCubit: cubit);
|
||||
|
||||
await tester.pumpWidget(
|
||||
BlocProvider<SettingsCubit>.value(
|
||||
value: cubit,
|
||||
child: wrap(GeneralInfoScreen(settingsEntity: settingsEntity)),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Сохранить'), findsOneWidget);
|
||||
expect(find.byType(Loader), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Сохранить'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Сохранить'), findsNothing);
|
||||
expect(find.byType(Loader), findsOneWidget);
|
||||
expect(find.text('Введите имя'), findsOneWidget);
|
||||
|
||||
final loaderSize = tester.getSize(find.byType(Loader));
|
||||
expect(loaderSize.height, lessThan(60));
|
||||
|
||||
saveCompleter.complete();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(find.text('Сохранить'), findsOneWidget);
|
||||
expect(find.byType(Loader), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user