forked from eatvio/eatvio_app
Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app
This commit is contained in:
@@ -9,7 +9,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ dev-office ]
|
||||
runs-on: [ ubuntu-latest ]
|
||||
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v3
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@ env/.env
|
||||
android/key.properties
|
||||
app/upload-keystore.jks
|
||||
coverage/
|
||||
backend/
|
||||
.fvm/
|
||||
.fvmrc
|
||||
/eatvio
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.huawei.hms.push.HmsMessageService
|
||||
import com.huawei.hms.push.RemoteMessage
|
||||
|
||||
@@ -13,6 +17,45 @@ class HmsPushService : HmsMessageService() {
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
super.onMessageReceived(message)
|
||||
Log.i("HMS", "onMessageReceived: ${message?.data}")
|
||||
|
||||
if (message == null) return
|
||||
|
||||
// Берём action_url из data-поля, если оно есть.
|
||||
val actionUrl = message.dataOfMap["action_url"]
|
||||
|
||||
// Строим системное уведомление, по тапу по которому откроется MainActivity с action_url.
|
||||
showNotification(message, actionUrl)
|
||||
}
|
||||
|
||||
private fun showNotification(message: RemoteMessage, actionUrl: String?) {
|
||||
val context = applicationContext
|
||||
|
||||
// Интент для запуска Flutter-активити.
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
if (!actionUrl.isNullOrBlank()) {
|
||||
putExtra("action_url", actionUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
|
||||
val channelId = "hms_push_channel"
|
||||
|
||||
val notification = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.mipmap.ic_launcher) // используем иконку приложения
|
||||
.setContentTitle(message.notification?.title ?: context.getString(R.string.app_name))
|
||||
.setContentText(message.notification?.body ?: "")
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(message.messageId.hashCode(), notification)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -14,12 +14,13 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val channelName = "com.eatvioapp/hms_push"
|
||||
private var methodChannel: MethodChannel? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
methodChannel?.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getHuaweiToken" -> {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@@ -71,5 +72,33 @@ class MainActivity: FlutterActivity() {
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем возможный action_url, с которым была открыта Activity (например, из HMS уведомления).
|
||||
handleIntentForActionUrl(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
// Обработка нового интента, когда Activity уже запущена (тап по уведомлению).
|
||||
handleIntentForActionUrl(intent)
|
||||
}
|
||||
|
||||
private fun handleIntentForActionUrl(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
// Вариант 1: action_url лежит в extras.
|
||||
val fromExtras = intent.extras?.getString("action_url")
|
||||
|
||||
// Вариант 2: action_url передан как data Uri.
|
||||
val dataUri = intent.data
|
||||
val fromData = dataUri?.toString()
|
||||
|
||||
val actionUrl = fromExtras ?: fromData
|
||||
if (actionUrl.isNullOrBlank()) return
|
||||
|
||||
android.util.Log.i("HMS", "MainActivity.handleIntentForActionUrl: $actionUrl")
|
||||
|
||||
// Проксируем URL во Flutter, где уже есть общая логика навигации по диплинкам.
|
||||
methodChannel?.invokeMethod("openActionUrl", actionUrl)
|
||||
}
|
||||
}
|
||||
|
||||
3
assets/icons/crossed_eye.svg
Normal file
3
assets/icons/crossed_eye.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -14,8 +14,8 @@ import 'package:go_router/go_router.dart';
|
||||
class AppRouter {
|
||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// Домены для deep links
|
||||
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru'];
|
||||
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
|
||||
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru', 'dev2.eatvio.com', 'www.dev2.eatvio.com'];
|
||||
|
||||
static GoRouter appRouter = GoRouter(
|
||||
initialLocation: RouteConstants.updateScreen,
|
||||
@@ -30,64 +30,59 @@ class AppRouter {
|
||||
],
|
||||
);
|
||||
|
||||
/// Обработка редиректов для deep links
|
||||
/// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
final uri = state.uri;
|
||||
static String? _resolveDeepLinkUri(Uri uri) {
|
||||
final host = uri.host.toLowerCase();
|
||||
final segments = uri.pathSegments;
|
||||
|
||||
// Проверяем, является ли это deep link по домену
|
||||
final isDeepLink = _deepLinkHosts.contains(host);
|
||||
|
||||
// Если это не deep link, не мешаем обычной навигации
|
||||
|
||||
if (!isDeepLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Логирование для отладки
|
||||
developer.log(
|
||||
'Deep link detected: host=$host, path=${uri.path}, segments=$segments',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
developer.log('Deep link detected: host=$host, path=${uri.path}, segments=$segments', name: 'AppRouter');
|
||||
|
||||
// Проверка на пустой путь или некорректный формат
|
||||
if (segments.isEmpty) {
|
||||
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
// Проверка языка (первый сегмент должен быть ru или en)
|
||||
if (segments.length >= 4 &&
|
||||
segments[0] == 'api' &&
|
||||
segments[1] == 'v2' &&
|
||||
segments[2] == 'chat' &&
|
||||
segments[3] == 'show') {
|
||||
developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
}
|
||||
|
||||
if (segments[0] != 'ru' && segments[0] != 'en') {
|
||||
developer.log(
|
||||
'Invalid language segment: ${segments[0]}, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
developer.log('Invalid language segment: ${segments[0]}, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
// Обработка deep links с учетом языка
|
||||
// Формат: /{lang}/{type}/{identifier?}
|
||||
if (segments.length == 4) {
|
||||
// Формат: /{lang}/diary/{...}
|
||||
final type = segments[1];
|
||||
|
||||
|
||||
if (type == 'diary') {
|
||||
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'Unknown deep link type for 4 segments: $type, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
if (type == 'consultations' && segments[2] == 'status') {
|
||||
final consultationId = segments[3];
|
||||
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.consultationsScreen}?id=$consultationId';
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link type for 4 segments: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
} else if (segments.length == 3) {
|
||||
// Формат: /{lang}/{type}/{identifier}
|
||||
final type = segments[1];
|
||||
final contentIdentifier = segments[2];
|
||||
|
||||
@@ -104,55 +99,111 @@ class AppRouter {
|
||||
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-plan-screen?slug=$contentIdentifier';
|
||||
case 'diet':
|
||||
developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter');
|
||||
case 'diet': {
|
||||
final dietId = int.tryParse(contentIdentifier);
|
||||
if (dietId != null) {
|
||||
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?id=$dietId';
|
||||
}
|
||||
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?slug=$contentIdentifier';
|
||||
}
|
||||
case 'chat':
|
||||
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
case 'profile':
|
||||
// В диплинке передан slug — открываем профиль пользователя по slug.
|
||||
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/profile-screen?slug=$contentIdentifier';
|
||||
case 'badge':
|
||||
// В badge нет slug, открываем вкладку «Мой профиль».
|
||||
developer.log('Deep link: badge → my-profile tab', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.myProfileScreen;
|
||||
case 'diary':
|
||||
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
case 'consultant_diary_comment':
|
||||
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
default:
|
||||
developer.log(
|
||||
'Unknown deep link type: $type, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
developer.log('Unknown deep link type: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
} else if (segments.length == 2) {
|
||||
// Формат: /{lang}/{type} - только для chat
|
||||
final type = segments[1];
|
||||
|
||||
|
||||
if (type == 'chat') {
|
||||
developer.log('Deep link: chat', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
} else {
|
||||
developer.log(
|
||||
'Unknown deep link type for 2 segments: $type, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
} else {
|
||||
// Неизвестный формат deep link
|
||||
developer.log(
|
||||
'Unknown deep link format: ${segments.length} segments, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
if (type == 'diary') {
|
||||
developer.log('Deep link: diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link type for 2 segments: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link format: ${segments.length} segments, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
return _resolveDeepLinkUri(state.uri);
|
||||
}
|
||||
|
||||
static void navigateByDeepLinkUrl(String url) {
|
||||
if (url.isEmpty) {
|
||||
developer.log('navigateByDeepLinkUrl: empty url, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri? uri;
|
||||
try {
|
||||
uri = Uri.tryParse(url);
|
||||
} catch (e) {
|
||||
developer.log('navigateByDeepLinkUrl: failed to parse url=$url, error=$e, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri == null) {
|
||||
developer.log('navigateByDeepLinkUrl: uri is null for url=$url, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) {
|
||||
uri = Uri(
|
||||
scheme: 'https',
|
||||
host: _deepLinkHosts.first,
|
||||
pathSegments: uri.pathSegments,
|
||||
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
|
||||
);
|
||||
}
|
||||
|
||||
final target = _resolveDeepLinkUri(uri);
|
||||
|
||||
if (target != null) {
|
||||
appRouter.go(target);
|
||||
} else {
|
||||
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
}
|
||||
}
|
||||
|
||||
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
|
||||
91
lib/core/notifications/push_navigation_handler.dart
Normal file
91
lib/core/notifications/push_navigation_handler.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях.
|
||||
class PushNavigationHandler {
|
||||
static bool _initialized = false;
|
||||
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
|
||||
|
||||
/// Инициализация обработчиков Firebase Messaging.
|
||||
/// Должна вызываться один раз после старта приложения.
|
||||
static Future<void> initialize() async {
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
// Сообщение, по которому запустили приложение из "убитого" состояния.
|
||||
try {
|
||||
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleMessageInternal(initialMessage, source: 'initialMessage');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'PushNavigationHandler: error while getting initialMessage: $e',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
}
|
||||
|
||||
// Тап по уведомлению, когда приложение было в фоне.
|
||||
FirebaseMessaging.onMessageOpenedApp.listen(
|
||||
(message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'),
|
||||
);
|
||||
|
||||
// Подписка на события от native-кода (HMS через MethodChannel).
|
||||
_hmsChannel.setMethodCallHandler((call) async {
|
||||
if (call.method == 'openActionUrl') {
|
||||
final url = call.arguments as String? ?? '';
|
||||
handleActionUrlFromNative(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Входная точка для навигации из native-кода (например, HMS через MethodChannel).
|
||||
static void handleActionUrlFromNative(String url) {
|
||||
if (url.isEmpty) {
|
||||
developer.log(
|
||||
'PushNavigationHandler.handleActionUrlFromNative: empty url',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
|
||||
AppRouter.navigateByDeepLinkUrl(url);
|
||||
}
|
||||
|
||||
/// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё).
|
||||
static void handleMessage(RemoteMessage message) {
|
||||
_handleMessageInternal(message, source: 'manualHandle');
|
||||
}
|
||||
|
||||
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
|
||||
final data = message.data;
|
||||
final actionUrl = data['action_url'] as String?;
|
||||
|
||||
if (actionUrl == null || actionUrl.isEmpty) {
|
||||
developer.log(
|
||||
'PushNavigationHandler: no action_url in message data, source=$source, data=$data',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'PushNavigationHandler: navigating by action_url=$actionUrl, source=$source',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
|
||||
AppRouter.navigateByDeepLinkUrl(actionUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,20 @@ class ContentRoutes {
|
||||
name: 'aboutPlanScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
if (slug != null) {
|
||||
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
|
||||
return AboutPlanScreen(slug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
return AboutPlanScreen(
|
||||
slug: data['slug'] as String,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
|
||||
highlightedCommentId: data['commentId'] as int?,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -30,12 +38,20 @@ class ContentRoutes {
|
||||
name: 'aboutRecipesScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
if (slug != null) {
|
||||
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
|
||||
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
return AboutRecipeScreen(
|
||||
recipeSlug: data['recipe'] as String,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
|
||||
highlightedCommentId: data['commentId'] as int?,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -43,16 +59,28 @@ class ContentRoutes {
|
||||
name: 'aboutDietScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final idStr = state.uri.queryParameters['id'];
|
||||
final id = int.tryParse(idStr ?? '');
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
if (slug != null) {
|
||||
return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false);
|
||||
return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
|
||||
}
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutDietScreen(
|
||||
id: data['id'] as int?,
|
||||
slug: data['slug'] as String?,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool,
|
||||
);
|
||||
if (id != null && id > 0) {
|
||||
return AboutDietScreen(id: id, slug: null, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
|
||||
}
|
||||
final data = state.extra as Map<String, dynamic>?;
|
||||
if (data != null) {
|
||||
return AboutDietScreen(
|
||||
id: data['id'] as int?,
|
||||
slug: data['slug'] as String?,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
|
||||
highlightedCommentId: data['commentId'] as int?,
|
||||
);
|
||||
}
|
||||
return const AboutDietScreen(id: null, slug: null, index: null, navigateToComments: false, highlightedCommentId: null);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -65,12 +93,20 @@ class ContentRoutes {
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
|
||||
if (parsedId != null) {
|
||||
return BlogPostScreen(id: parsedId);
|
||||
return BlogPostScreen(id: parsedId, highlightedCommentId: commentId);
|
||||
}
|
||||
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
if (state.extra is int) {
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return BlogPostScreen(id: data['id'] as int, highlightedCommentId: data['commentId'] as int?);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
|
||||
@@ -31,8 +32,11 @@ class MainNavigationRoutes {
|
||||
profileAvatarCubit.getMyProfile();
|
||||
});
|
||||
}
|
||||
|
||||
return BottomNavigationBarWidget(child: child);
|
||||
|
||||
return _MainShellWithNavScope(
|
||||
currentLocation: state.matchedLocation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -58,7 +62,7 @@ class MainNavigationRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.diaryScreen,
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
|
||||
pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
@@ -68,7 +72,10 @@ class MainNavigationRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.chatSelectScreen,
|
||||
name: 'chatSelectScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
|
||||
pageBuilder: (context, state) {
|
||||
final initialSearchQuery = state.extra is String ? state.extra as String : null;
|
||||
return NoTransitionPage(child: ChatsScreen(initialSearchQuery: initialSearchQuery));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.notificationsScreen,
|
||||
@@ -94,3 +101,33 @@ class MainNavigationRoutes {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
|
||||
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
|
||||
class _MainShellWithNavScope extends StatefulWidget {
|
||||
const _MainShellWithNavScope({
|
||||
required this.child,
|
||||
required this.currentLocation,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final String currentLocation;
|
||||
|
||||
@override
|
||||
State<_MainShellWithNavScope> createState() => _MainShellWithNavScopeState();
|
||||
}
|
||||
|
||||
class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
|
||||
final BottomNavController _navController = BottomNavController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomNavScope(
|
||||
controller: _navController,
|
||||
child: BottomNavigationBarWidget(
|
||||
currentLocation: widget.currentLocation,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: '${RouteConstants.subsScreen}/:slug',
|
||||
name: 'subsScreen',
|
||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
|
||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug'], subType: state.extra as SubType),
|
||||
),
|
||||
|
||||
// Chat routes
|
||||
@@ -229,7 +229,12 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.stepperBecomeConsultantScreen,
|
||||
name: 'stepperBecomeConsultantScreen',
|
||||
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final slug = data['slug'] as String?;
|
||||
final isCanceled = data['isCanceled'] as bool?;
|
||||
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,);
|
||||
},
|
||||
),
|
||||
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
||||
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||
@@ -285,8 +290,7 @@ class OtherRoutes {
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, int>;
|
||||
final diaryId = data['diaryId'] as int;
|
||||
final currentPlanDay = data['currentDiaryDay'] as int;
|
||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
return UserDiaryScreen(diaryId: diaryId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:eatvio_app/core/websockets/ws_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
|
||||
class WsManager {
|
||||
final WsService wsService;
|
||||
@@ -111,6 +112,7 @@ class WsManager {
|
||||
}
|
||||
});
|
||||
_subscribe();
|
||||
await _connectCubitsToStreams();
|
||||
} catch (e, st) {
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
log(e.toString(), stackTrace: st);
|
||||
@@ -118,6 +120,20 @@ class WsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Подключает ChatsCubit и NotificationsCubit к стримам сокета.
|
||||
/// Вызывается после успешного initialize(), чтобы уведомления и чаты работали
|
||||
/// и при входе уже залогиненного пользователя (редирект на feed), не только после экрана логина.
|
||||
Future<void> _connectCubitsToStreams() async {
|
||||
try {
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
} catch (e, st) {
|
||||
log('WsManager._connectCubitsToStreams: $e', stackTrace: st);
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initPrivateNotificationsChannel() async {
|
||||
final token = await TokenStorage.getTokenFromStorage();
|
||||
_privateNotificationChannel = client?.privateChannel(
|
||||
|
||||
@@ -21,7 +21,6 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -80,9 +79,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
@@ -213,9 +209,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
await sl<WsManager>().initialize();
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
||||
@@ -74,9 +73,6 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешная регистрация');
|
||||
} catch (e) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
@@ -76,9 +75,6 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
|
||||
@@ -21,8 +21,9 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class BlogPostScreen extends StatefulWidget {
|
||||
final int id;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const BlogPostScreen({super.key, required this.id});
|
||||
const BlogPostScreen({super.key, required this.id, this.highlightedCommentId});
|
||||
|
||||
@override
|
||||
State<BlogPostScreen> createState() => _BlogPostScreenState();
|
||||
@@ -303,7 +304,13 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
const SizedBox(height: 16),
|
||||
if (state is CommentsStateLoading) const Loader(),
|
||||
if (state is CommentsStateLoaded)
|
||||
CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId),
|
||||
CommentsWidget(
|
||||
id: widget.id.toString(),
|
||||
commentSourceType: CommentSourceType.blogPost,
|
||||
comment: state.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomNavController {
|
||||
BottomNavController();
|
||||
|
||||
void Function(String path, Object? extra)? _onSwitchToRoute;
|
||||
|
||||
void register(void Function(String path, Object? extra) callback) {
|
||||
_onSwitchToRoute = callback;
|
||||
}
|
||||
|
||||
void switchToRoute(String path, [Object? extra]) {
|
||||
_onSwitchToRoute?.call(path, extra);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomNavScope extends InheritedWidget {
|
||||
const BottomNavScope({
|
||||
required this.controller,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BottomNavController controller;
|
||||
|
||||
static BottomNavController? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<BottomNavScope>()?.controller;
|
||||
}
|
||||
|
||||
static BottomNavController of(BuildContext context) {
|
||||
final controller = maybeOf(context);
|
||||
assert(controller != null, 'No BottomNavScope found in context');
|
||||
return controller!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BottomNavScope oldWidget) => controller != oldWidget.controller;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
@@ -18,9 +19,15 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class BottomNavigationBarWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
const BottomNavigationBarWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.currentLocation,
|
||||
});
|
||||
|
||||
const BottomNavigationBarWidget({super.key, required this.child});
|
||||
final Widget child;
|
||||
/// Текущий путь из GoRouter (matchedLocation), чтобы подсветка таба совпадала с экраном при навигации через go() (диплинки, пуши).
|
||||
final String currentLocation;
|
||||
|
||||
@override
|
||||
State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState();
|
||||
@@ -66,6 +73,24 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to the tab that corresponds to [path], optionally with [extra]
|
||||
/// (e.g. date for diary). If [path] is not in the current tab list, falls back to push.
|
||||
void _switchToRoute(String path, [Object? extra]) {
|
||||
if (pages == null) return;
|
||||
final index = pages!.indexOf(path);
|
||||
if (index >= 0 && index != currentBottomNavBarIndex) {
|
||||
setState(() {
|
||||
currentBottomNavBarIndex = index;
|
||||
});
|
||||
GoRouter.of(context).replace(path, extra: extra);
|
||||
} else if (index >= 0) {
|
||||
// Same tab but with new extra (e.g. diary with date)
|
||||
GoRouter.of(context).replace(path, extra: extra);
|
||||
} else {
|
||||
GoRouter.of(context).push(path, extra: extra);
|
||||
}
|
||||
}
|
||||
|
||||
static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500
|
||||
|
||||
Widget _buildLogoIcon() {
|
||||
@@ -121,6 +146,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
// Инициализируем pages на основе profileModel
|
||||
pages ??= _getPages(isConsultant);
|
||||
|
||||
// Expose tab switching to descendants (e.g. notification widget)
|
||||
final controller = BottomNavScope.maybeOf(context);
|
||||
if (controller != null) {
|
||||
controller.register(_switchToRoute);
|
||||
}
|
||||
|
||||
// Подсветка таба по текущему маршруту (совпадает при навигации через go(), в т.ч. диплинки/пуши).
|
||||
final pathWithoutQuery = widget.currentLocation.split('?').first;
|
||||
final indexFromRoute = pages!.indexOf(pathWithoutQuery);
|
||||
final effectiveIndex = indexFromRoute >= 0 ? indexFromRoute : currentBottomNavBarIndex;
|
||||
if (indexFromRoute >= 0 && indexFromRoute != currentBottomNavBarIndex) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => currentBottomNavBarIndex = indexFromRoute);
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
@@ -129,7 +170,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
selectedFontSize: 10,
|
||||
unselectedFontSize: 10,
|
||||
selectedItemColor: appTheme.blueA400,
|
||||
currentIndex: currentBottomNavBarIndex,
|
||||
currentIndex: effectiveIndex,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
@@ -204,7 +245,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
ImageConstant.icnChat,
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
|
||||
colorFilter: pages![effectiveIndex] == '/chat-select-screen'
|
||||
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
|
||||
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||
),
|
||||
@@ -226,7 +267,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SvgPicture.asset(
|
||||
ImageConstant.icnBell,
|
||||
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
|
||||
colorFilter: pages![effectiveIndex] == '/notifications-screen'
|
||||
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
|
||||
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||
width: 20,
|
||||
|
||||
@@ -11,7 +11,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ChatsScreen extends StatefulWidget {
|
||||
const ChatsScreen({super.key});
|
||||
const ChatsScreen({super.key, this.initialSearchQuery});
|
||||
|
||||
/// When set (e.g. from notification tap), the search field is pre-filled
|
||||
/// and chat list is filtered by this query.
|
||||
final String? initialSearchQuery;
|
||||
|
||||
@override
|
||||
State<ChatsScreen> createState() => _ChatsScreenState();
|
||||
@@ -23,12 +27,17 @@ class _ChatsScreenState extends State<ChatsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Инициализация ChatsCubit при первом открытии экрана
|
||||
final query = widget.initialSearchQuery?.trim();
|
||||
if (query != null && query.isNotEmpty) {
|
||||
searchController.text = query;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final cubit = context.read<ChatsCubit>();
|
||||
final state = cubit.state;
|
||||
if (state.state == BaseState.initial) {
|
||||
cubit.getChatRoomsWithLoading();
|
||||
cubit.getChatRoomsWithLoading(query: query?.isNotEmpty == true ? query : null);
|
||||
} else if (query != null && query.isNotEmpty) {
|
||||
cubit.getChatRooms(query: query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
|
||||
|
||||
static const String visibleGroup = 'user_certs';
|
||||
static const String hiddenGroup = 'hidden_user_certs';
|
||||
static const String pendingGroup = 'pending_user_certs';
|
||||
|
||||
bool get isHidden => group == hiddenGroup;
|
||||
|
||||
bool get isActive => group == visibleGroup || group == hiddenGroup;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||
@@ -16,13 +15,14 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true),
|
||||
loading: true,
|
||||
acceptOffer: false,
|
||||
isUpdating: false,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> getMyRequestStatus(String slug) async {
|
||||
_update = true;
|
||||
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
|
||||
final certPhotos = await _fetchVisibleCertPhotos();
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
|
||||
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
|
||||
final course = courseMap.values.first;
|
||||
@@ -51,12 +51,13 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
loading: false,
|
||||
requestModel: myRequestStatus,
|
||||
certPhotos: certPhotos,
|
||||
isUpdating: _update
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> disableLoading() async {
|
||||
final certPhotos = await _fetchVisibleCertPhotos();
|
||||
final certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
emit(state.copyWith(loading: false, certPhotos: certPhotos));
|
||||
}
|
||||
|
||||
@@ -65,33 +66,33 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
}
|
||||
|
||||
Future<void> registerConsultant() async {
|
||||
if (state.newAddedPhotos.isNotEmpty) {
|
||||
await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos);
|
||||
}
|
||||
if (state.deletedPhotoIds.isNotEmpty) {
|
||||
for (final int id in state.deletedPhotoIds) {
|
||||
await consultantRepository.deletePhoto(id);
|
||||
}
|
||||
}
|
||||
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
|
||||
}
|
||||
|
||||
Future<void> uploadCertPhotos(List<File> files) async {
|
||||
Future<void> addCertPhotos(List<File> files) async {
|
||||
if (files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
|
||||
final uploaded = await consultantRepository.storeConsultantCertPhotos(files: compressedFiles);
|
||||
emit(state.copyWith(certPhotos: _onlyVisible(uploaded)));
|
||||
|
||||
emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles]));
|
||||
}
|
||||
|
||||
Future<void> removeCertPhoto(int id) async {
|
||||
await consultantRepository.hideConsultantCertPhoto(id);
|
||||
emit(state.copyWith(certPhotos: state.certPhotos.where((photo) => photo.id != id).toList()));
|
||||
}
|
||||
|
||||
Future<List<ConsultantCertPhotoModel>> _fetchVisibleCertPhotos() async {
|
||||
try {
|
||||
final photos = await consultantRepository.getConsultantCertPhotos();
|
||||
return _onlyVisible(photos);
|
||||
} catch (e) {
|
||||
return [];
|
||||
Future<void> removeCertPhoto(int? id, String? path) async {
|
||||
if (id != null) {
|
||||
emit(state.copyWith(deletedPhotoIds: [...state.deletedPhotoIds, id]));
|
||||
return;
|
||||
}
|
||||
if (path != null) {
|
||||
emit(state.copyWith(newAddedPhotos: state.newAddedPhotos.where((file) => file.uri.path != path).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
List<ConsultantCertPhotoModel> _onlyVisible(List<ConsultantCertPhotoModel> photos) {
|
||||
return photos.where((photo) => !photo.isHidden).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
@@ -11,7 +13,10 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
|
||||
required ConsultantProfileModel consultant,
|
||||
required bool acceptOffer,
|
||||
required bool loading,
|
||||
required bool isUpdating,
|
||||
ConsultationRequestModel? requestModel,
|
||||
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
|
||||
@Default(<File>[]) List<File> newAddedPhotos,
|
||||
@Default(<int>[]) List<int> deletedPhotoIds,
|
||||
}) = _RegisterConsultantState;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
||||
builder: (context, state) {
|
||||
final photos = state.certPhotos;
|
||||
final photos = state.certPhotos.where((photo) => !state.deletedPhotoIds.contains(photo.id)).toList();
|
||||
final addedPhotos = state.newAddedPhotos;
|
||||
final totalPhotosCount = photos.length + addedPhotos.length;
|
||||
return Column(
|
||||
children: [
|
||||
if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)],
|
||||
@@ -47,7 +49,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == photos.length) {
|
||||
if (index == totalPhotosCount) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => _pickAndUploadPhotos(),
|
||||
@@ -57,10 +59,25 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _CertPhotoCard(photo: photos[index], allPhotos: photos, onRemove: () => _removePhoto(photos[index].id));
|
||||
if (index < photos.length) {
|
||||
final photo = photos[index];
|
||||
return _CertPhotoCard(
|
||||
photo: photo,
|
||||
allPhotos: photos,
|
||||
onRemove: () => _removePhoto(id: photo.id),
|
||||
);
|
||||
} else {
|
||||
final localIndex = index - photos.length;
|
||||
final localPhoto = addedPhotos[localIndex];
|
||||
return _LocalCertPhotoCard(
|
||||
photo: localPhoto,
|
||||
allPhotos: addedPhotos,
|
||||
onRemove: () => _removePhoto(path: localPhoto.uri.path),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemCount: photos.length + 1,
|
||||
itemCount: totalPhotosCount + 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -82,7 +99,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
});
|
||||
try {
|
||||
// ignore: use_build_context_synchronously
|
||||
await context.read<RegisterConsultantCubit>().uploadCertPhotos(newImages.map((file) => File(file.path)).toList());
|
||||
await context.read<RegisterConsultantCubit>().addCertPhotos(newImages.map((file) => File(file.path)).toList());
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -92,8 +109,8 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removePhoto(int id) async {
|
||||
await context.read<RegisterConsultantCubit>().removeCertPhoto(id);
|
||||
Future<void> _removePhoto({int? id, String? path}) async {
|
||||
await context.read<RegisterConsultantCubit>().removeCertPhoto(id, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,3 +164,61 @@ class _CertPhotoCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalCertPhotoCard extends StatelessWidget {
|
||||
final File photo;
|
||||
final List<File> allPhotos;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const _LocalCertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Hero(
|
||||
tag: photo.path,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
rootNavigator: true,
|
||||
PhotoViewScreen(
|
||||
imageUrls: allPhotos,
|
||||
isLocal: true,
|
||||
index: allPhotos.indexOf(photo),
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: FileImage(photo),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -12,
|
||||
top: -12,
|
||||
child: InkWell(
|
||||
onTap: onRemove,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
|
||||
child: const Icon(size: 20, Icons.close, color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/additional_settings_screen.dart';
|
||||
@@ -19,8 +18,8 @@ import 'package:lottie/lottie.dart';
|
||||
|
||||
class StepperBecomeConsultantScreen extends StatefulWidget {
|
||||
final String? slug;
|
||||
|
||||
const StepperBecomeConsultantScreen({super.key, this.slug});
|
||||
final bool? isCanceled;
|
||||
const StepperBecomeConsultantScreen({super.key, this.slug, this.isCanceled});
|
||||
|
||||
@override
|
||||
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
|
||||
@@ -63,25 +62,17 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: const Color(0xFFEAF2FD),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
||||
});
|
||||
},
|
||||
label: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
if (state.requestModel?.educationModel.requestState == ModerationStatus.canceled)
|
||||
if (widget.isCanceled == true)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
@@ -94,15 +85,10 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
children: [
|
||||
for (int i = 0; i < state.requestModel!.canceledModeration!.additional!.length; i++) ...[
|
||||
Text('• ${state.requestModel!.canceledModeration!.additional![i]}'),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -110,26 +96,18 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
child: Ink(
|
||||
height: 48,
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0x33FFCC00),
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Комментарий',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF323F4B), height: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 48,
|
||||
width: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
width: widget.isCanceled == true
|
||||
? MediaQuery.of(context).size.width * 0.2
|
||||
: MediaQuery.of(context).size.width * 0.8,
|
||||
padding: const EdgeInsets.only(left: 4, right: 16),
|
||||
@@ -138,18 +116,19 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: isLoading
|
||||
? Colors.grey
|
||||
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? const Color(0xFFEAF2FD)
|
||||
: appTheme.blueA400,
|
||||
: widget.isCanceled == true
|
||||
? const Color(0xFFEAF2FD)
|
||||
: appTheme.blueA400,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (step == 6) {
|
||||
if (widget.isCanceled == true) {
|
||||
return;
|
||||
}
|
||||
if (registrationConsultantCubit.state.acceptOffer) {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
@@ -170,27 +149,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
const Text(
|
||||
'Спасибо!\nВаши данные отправлены на модерацию',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Lottie.asset(
|
||||
'assets/lottie_animations/success.json',
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
@@ -201,18 +168,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Закрыть',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -263,24 +223,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
} else {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
setState(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
||||
});
|
||||
}
|
||||
},
|
||||
label: isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: appTheme.blueA400,
|
||||
)
|
||||
: Text(
|
||||
'Продолжить',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
: widget.isCanceled == true
|
||||
? Icon(Icons.arrow_forward_ios_outlined, color: appTheme.blueA400)
|
||||
: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -288,23 +239,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
),
|
||||
builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Text('шаг $step из 6'),
|
||||
),
|
||||
],
|
||||
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
|
||||
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 6'))],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
||||
child: Padding(
|
||||
@@ -312,10 +251,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 1 / 6,
|
||||
end: progressValue,
|
||||
),
|
||||
tween: Tween<double>(begin: 1 / 6, end: progressValue),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -347,9 +283,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Scaffold(
|
||||
body: Loader(),
|
||||
);
|
||||
return const Scaffold(body: Loader());
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -90,4 +90,13 @@ class ConsultantRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deletePhoto(int id) async {
|
||||
try {
|
||||
await consultantService.deletePhoto(id: id);
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,7 @@ abstract class ConsultantService {
|
||||
|
||||
@POST('/api/v2/consultant_request/cert_photos/un_hide')
|
||||
Future<void> unHideConsultantCertPhoto({@Body() required Map<String, dynamic> payload});
|
||||
|
||||
@POST('/api/v2/consultant_request/cert_photos/{id}/queue-for-deletion')
|
||||
Future<void> deletePhoto({@Path() required int id});
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1});
|
||||
},
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id,});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
@@ -255,7 +255,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
.where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished)
|
||||
.lastOrNull;
|
||||
if (lastActiveDay != null) {
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1});
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id,});
|
||||
return;
|
||||
}
|
||||
showErrorDialog(
|
||||
|
||||
@@ -48,6 +48,11 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
|
||||
String comment = '';
|
||||
|
||||
void setInitialDate(DateTime? date) {
|
||||
if (date == null) return;
|
||||
selectedDay = date;
|
||||
}
|
||||
|
||||
Future<bool> setSelectedDay(DateTime day) async {
|
||||
final prevDay = selectedDay;
|
||||
final prevMeal = selectedMeal;
|
||||
@@ -63,7 +68,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
return canSelect;
|
||||
}
|
||||
|
||||
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
|
||||
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId, DateTime? date}) async {
|
||||
final currentState = state;
|
||||
try {
|
||||
if (needLoading) {
|
||||
@@ -77,6 +82,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final diaryDay = await getDiary();
|
||||
if (diaryDay == null) {
|
||||
emit(const DiaryState.error(errorMessage: 'Diary not found'));
|
||||
|
||||
@@ -24,31 +24,29 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DiaryScreen extends StatelessWidget {
|
||||
const DiaryScreen({super.key});
|
||||
|
||||
const DiaryScreen({super.key, required this.date});
|
||||
final DateTime? date;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DiaryScreenContent();
|
||||
return DiaryScreenContent(date: date);
|
||||
}
|
||||
}
|
||||
|
||||
class UserDiaryScreen extends StatelessWidget {
|
||||
static const path = '/user-diary-screen';
|
||||
static const name = 'userDiaryScreen';
|
||||
const UserDiaryScreen({super.key, required this.diaryId, required this.currentPlanDay});
|
||||
const UserDiaryScreen({super.key, required this.diaryId});
|
||||
final int diaryId;
|
||||
final int currentPlanDay;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DiaryScreenContent(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
return DiaryScreenContent(diaryId: diaryId,);
|
||||
}
|
||||
}
|
||||
|
||||
class DiaryScreenContent extends StatefulWidget {
|
||||
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
|
||||
const DiaryScreenContent({super.key, this.diaryId,this.date});
|
||||
final int? diaryId;
|
||||
final int? currentPlanDay;
|
||||
|
||||
final DateTime? date;
|
||||
@override
|
||||
State<DiaryScreenContent> createState() => _DiaryScreenContentState();
|
||||
}
|
||||
@@ -61,7 +59,11 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
|
||||
final s = S.of(context);
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)),
|
||||
BlocProvider<DiaryCubit>(
|
||||
create: (BuildContext context) => sl<DiaryCubit>()
|
||||
..setInitialDate(widget.date)
|
||||
..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId),
|
||||
),
|
||||
BlocProvider(create: (context) => sl<ConsultantCommentsCubit>()),
|
||||
],
|
||||
child: BlocBuilder<DiaryCubit, DiaryState>(
|
||||
@@ -91,7 +93,7 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.diaryId == null) const CalendarWidget(),
|
||||
if (widget.diaryId == null) CalendarWidget(date: widget.date),
|
||||
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
|
||||
if (state is DiaryStateLoaded)
|
||||
WakeupAndRetireWidget(
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
class CalendarWidget extends StatefulWidget {
|
||||
const CalendarWidget({super.key});
|
||||
const CalendarWidget({super.key, this.date});
|
||||
final DateTime? date;
|
||||
|
||||
@override
|
||||
State<CalendarWidget> createState() => _CalendarWidgetState();
|
||||
@@ -18,6 +19,23 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime _selectedDay = DateTime.now();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.date != null) {
|
||||
_selectedDay = widget.date!;
|
||||
_focusedDay = widget.date!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CalendarWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.date != null && widget.date != oldWidget.date) {
|
||||
_selectedDay = widget.date!;
|
||||
_focusedDay = widget.date!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -37,8 +37,9 @@ class AboutDietScreen extends StatefulWidget {
|
||||
final int? id;
|
||||
final int? index;
|
||||
final bool navigateToComments;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id});
|
||||
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id, this.highlightedCommentId});
|
||||
|
||||
@override
|
||||
State<AboutDietScreen> createState() => _AboutDietScreenState();
|
||||
@@ -433,6 +434,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
||||
commentSourceType: CommentSourceType.diet,
|
||||
comment: state.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -23,14 +23,60 @@ class CommentsWidget extends StatefulWidget {
|
||||
final CommentSourceType commentSourceType;
|
||||
final CommentModel comment;
|
||||
final int? replyId;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId});
|
||||
const CommentsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.commentSourceType,
|
||||
required this.comment,
|
||||
this.replyId,
|
||||
this.highlightedCommentId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommentsWidget> createState() => _CommentsWidgetState();
|
||||
}
|
||||
|
||||
class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
final Map<int, GlobalKey> commentKeys = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.highlightedCommentId != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToHighlightedComment();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CommentsWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.highlightedCommentId != null && widget.highlightedCommentId != oldWidget.highlightedCommentId) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToHighlightedComment();
|
||||
});
|
||||
} else if (widget.comment != oldWidget.comment && widget.highlightedCommentId != null) {
|
||||
// Comments might have just loaded
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToHighlightedComment();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToHighlightedComment() {
|
||||
final key = commentKeys[widget.highlightedCommentId];
|
||||
if (key != null && key.currentContext != null) {
|
||||
Scrollable.ensureVisible(
|
||||
key.currentContext!,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -49,8 +95,10 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
}
|
||||
|
||||
Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
|
||||
final key = commentKeys.putIfAbsent(comment.id, () => GlobalKey());
|
||||
final isMe = (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug;
|
||||
return GestureDetector(
|
||||
key: key,
|
||||
onTap: () {
|
||||
showCommentsActionsBottomSheet(
|
||||
context,
|
||||
@@ -83,7 +131,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
|
||||
color: (widget.replyId == comment.id || widget.highlightedCommentId == comment.id) ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -267,33 +267,113 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.motivateUser => MotivateUserNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt ?? jsonEncode(notificationJson))),
|
||||
};
|
||||
} catch (e) {
|
||||
return UnknownNotification(type: NotificationType.unknown);
|
||||
return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNotification(String notificationData) {
|
||||
final json = jsonDecode(notificationData) as Map<String, dynamic>;
|
||||
|
||||
// Улучшенная логика определения типа уведомления
|
||||
// Улучшенная логика определения типа уведомления (порядок: database_type → type → notification_class_name)
|
||||
String? stringifiedType;
|
||||
|
||||
// Сначала проверяем поле 'type' в корне JSON
|
||||
if (json.containsKey('type') && json['type'] != null) {
|
||||
// Сначала проверяем поле 'database_type' в корне JSON
|
||||
if (json.containsKey('database_type') && json['database_type'] != null) {
|
||||
stringifiedType = json['database_type'].toString();
|
||||
log('Found database_type in root: $stringifiedType');
|
||||
}
|
||||
// Затем поле 'type' в корне
|
||||
else if (json.containsKey('type') && json['type'] != null) {
|
||||
stringifiedType = json['type'].toString();
|
||||
log('Found type in root: $stringifiedType');
|
||||
}
|
||||
// Затем проверяем поле 'notification_class_name' в корне JSON
|
||||
// Затем поле 'notification_class_name' в корне
|
||||
else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) {
|
||||
stringifiedType = json['notification_class_name'].toString();
|
||||
log('Found notification_class_name in root: $stringifiedType');
|
||||
}
|
||||
// Если есть поле 'data', проверяем внутри него
|
||||
// Если есть поле 'data', проверяем внутри него в том же порядке
|
||||
else if (json.containsKey('data') && json['data'] is Map<String, dynamic>) {
|
||||
final data = json['data'] as Map<String, dynamic>;
|
||||
if (data.containsKey('type') && data['type'] != null) {
|
||||
if (data.containsKey('database_type') && data['database_type'] != null) {
|
||||
stringifiedType = data['database_type'].toString();
|
||||
log('Found database_type in data: $stringifiedType');
|
||||
} else if (data.containsKey('type') && data['type'] != null) {
|
||||
stringifiedType = data['type'].toString();
|
||||
log('Found type in data: $stringifiedType');
|
||||
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -413,6 +413,78 @@ class NotificationsService {
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson( // Reusing ContentUnpublishedNotification if structure is same, or create specific if different. Assuming similar for now or check backend.
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.motivateUser => MotivateUserNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -32,10 +32,18 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/widgets/option_widget.dart';
|
||||
|
||||
class AboutPlanScreen extends StatefulWidget {
|
||||
const AboutPlanScreen({super.key, required this.slug, required this.navigateToComments, required this.index});
|
||||
final String slug;
|
||||
final bool navigateToComments;
|
||||
final int? index;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const AboutPlanScreen({
|
||||
super.key,
|
||||
required this.slug,
|
||||
required this.navigateToComments,
|
||||
required this.index,
|
||||
this.highlightedCommentId,
|
||||
});
|
||||
@override
|
||||
State<AboutPlanScreen> createState() => _AboutPlanScreenState();
|
||||
}
|
||||
@@ -468,13 +476,14 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (commentsState is CommentsStateLoading) const Loader(),
|
||||
if (commentsState is CommentsStateLoaded)
|
||||
CommentsWidget(
|
||||
id: state.planModel?.id.toString() ?? '',
|
||||
commentSourceType: CommentSourceType.plan,
|
||||
comment: commentsState.comment,
|
||||
replyId: replyId,
|
||||
),
|
||||
if (commentsState is CommentsStateLoaded)
|
||||
CommentsWidget(
|
||||
id: state.planModel?.id.toString() ?? '',
|
||||
commentSourceType: CommentSourceType.plan,
|
||||
comment: commentsState.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
|
||||
@@ -24,13 +25,20 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
try {
|
||||
final profile = await profileRepository.getProfileBySlug(slug);
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
final isMyProfile = myProfile.slug == profile.slug;
|
||||
List<ConsultantCertPhotoModel> certPhotos = [];
|
||||
if (isMyProfile) {
|
||||
certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
certPhotos = _filterActivePhotos(certPhotos);
|
||||
}
|
||||
emit(
|
||||
ProfileState.loaded(
|
||||
profile: profile,
|
||||
isMyProfile: myProfile.slug == profile.slug,
|
||||
isMyProfile: isMyProfile,
|
||||
profileDetails: await profileRepository.getProfileDetailsBySlug(slug),
|
||||
blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug),
|
||||
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null,
|
||||
certPhotos: certPhotos,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -44,6 +52,8 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
_currentPeriod = period;
|
||||
try {
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
certPhotos = _filterActivePhotos(certPhotos);
|
||||
emit(
|
||||
ProfileState.loaded(
|
||||
profile: profile,
|
||||
@@ -53,6 +63,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
weightPoints: await profileRepository.getWeight(period: period),
|
||||
circumferencePoints: await profileRepository.getCircumferences(period: period),
|
||||
consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null,
|
||||
certPhotos: certPhotos,
|
||||
),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
@@ -77,6 +88,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
circumferencePoints: (state as ProfileStateLoaded).circumferencePoints,
|
||||
consultantInfo: (state as ProfileStateLoaded).consultantInfo,
|
||||
isMyProfile: (state as ProfileStateLoaded).isMyProfile,
|
||||
certPhotos: (state as ProfileStateLoaded).certPhotos,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -131,4 +143,36 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
await profileRepository.deletePhoto();
|
||||
return await getMyProfile();
|
||||
}
|
||||
|
||||
Future<void> hideCertPhoto(int id) async {
|
||||
try {
|
||||
await consultantRepository.hideConsultantCertPhoto(id);
|
||||
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
certPhotos = _filterActivePhotos(certPhotos);
|
||||
final currentState = state;
|
||||
if (currentState is ProfileStateLoaded) {
|
||||
emit(currentState.copyWith(certPhotos: certPhotos));
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unhideCertPhoto(int id) async {
|
||||
try {
|
||||
await consultantRepository.unHideConsultantCertPhoto(id);
|
||||
List<ConsultantCertPhotoModel> certPhotos = await consultantRepository.getConsultantCertPhotos();
|
||||
certPhotos = _filterActivePhotos(certPhotos);
|
||||
final currentState = state;
|
||||
if (currentState is ProfileStateLoaded) {
|
||||
emit(currentState.copyWith(certPhotos: certPhotos));
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
List<ConsultantCertPhotoModel> _filterActivePhotos(List<ConsultantCertPhotoModel> photos) {
|
||||
return photos.where((photo) => photo.isActive).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
|
||||
@@ -18,6 +19,7 @@ abstract class ProfileState with _$ProfileState {
|
||||
required ProfileDetailsModel profileDetails,
|
||||
required List<FeedBlogPostModel> blogPosts,
|
||||
required bool isMyProfile,
|
||||
required List<ConsultantCertPhotoModel> certPhotos,
|
||||
List<WeightModel>? weightPoints,
|
||||
List<CircumferenceModel>? circumferencePoints,
|
||||
ConsultationRequestModel? consultantInfo,
|
||||
|
||||
@@ -5,22 +5,30 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
class SubCubit extends Cubit<SubState> {
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
SubCubit({
|
||||
required this.profileRepository,
|
||||
}) : super(const SubState());
|
||||
SubCubit({required this.profileRepository}) : super(const SubState());
|
||||
|
||||
Future<void> getSubscriptions(String slug, [String? query]) async {
|
||||
Future<void> getSubscriptions(String? slug, [String? query]) async {
|
||||
emit(const SubState.loading());
|
||||
try {
|
||||
if (slug == null) {
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(myProfile.slug, query)));
|
||||
return;
|
||||
}
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(slug, query)));
|
||||
} catch (e) {
|
||||
emit(SubState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getSubscribers(String slug, [String? query]) async {
|
||||
Future<void> getSubscribers(String? slug, [String? query]) async {
|
||||
emit(const SubState.loading());
|
||||
try {
|
||||
if (slug == null) {
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(myProfile.slug, query)));
|
||||
return;
|
||||
}
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(slug, query)));
|
||||
} catch (e) {
|
||||
emit(SubState.error(errorMessage: e.toString()));
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
@@ -26,6 +27,7 @@ import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/profile_blog_post_widget.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/toggle_cert_photo_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -117,7 +119,6 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state);
|
||||
} else if (state is ProfileStateError) {
|
||||
return Scaffold(
|
||||
@@ -436,9 +437,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
SingleChildRenderObjectWidget _diplomasAndCertificates(ProfileStateLoaded state) {
|
||||
if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) {
|
||||
if (!state.profile.roles.contains('consultant')) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (state.isMyProfile && state.certPhotos.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!state.isMyProfile && state.consultantInfo!.photos.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
@@ -454,7 +462,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug});
|
||||
},
|
||||
child: Text(
|
||||
state.isMyProfile ? 'Редактировать' : '',
|
||||
@@ -477,14 +485,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (state.consultantInfo!.photos.isNotEmpty)
|
||||
...List.generate(state.consultantInfo!.photos.length * 2 - 1, (index) {
|
||||
if (index.isEven) {
|
||||
final photo = state.consultantInfo!.photos[index ~/ 2];
|
||||
return _diplomaImage(photo.url);
|
||||
} else {
|
||||
return const SizedBox(width: 8);
|
||||
}
|
||||
if (state.isMyProfile && state.certPhotos.isNotEmpty)
|
||||
...List.generate(state.certPhotos.length, (index) {
|
||||
final photo = state.certPhotos[index];
|
||||
return Row(children: [_diplomaImage(photo.url, state.isMyProfile, photo.isHidden, photo.id), const SizedBox(width: 8)]);
|
||||
})
|
||||
else if (state.consultantInfo!.photos.isNotEmpty)
|
||||
...List.generate(state.consultantInfo!.photos.length, (index) {
|
||||
final photo = state.consultantInfo!.photos[index];
|
||||
return Row(children: [_diplomaImage(photo.url, state.isMyProfile, false, null), const SizedBox(width: 8)]);
|
||||
}),
|
||||
],
|
||||
),
|
||||
@@ -496,16 +505,49 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _diplomaImage(String url) {
|
||||
GestureDetector _diplomaImage(String url, bool isMyProfile, bool isHidden, int? certPhotoId) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0));
|
||||
},
|
||||
child: Hero(
|
||||
tag: url,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fill, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160),
|
||||
),
|
||||
if (isMyProfile)
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (certPhotoId != null) {
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
height: 250,
|
||||
child: ToggleCertPhotoBottomSheet(profileCubit: profileCubit, isPhotoHidden: isHidden, id: certPhotoId),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(4)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 5),
|
||||
child: SvgPicture.asset(
|
||||
isHidden ? 'assets/icons/crossed_eye.svg' : 'assets/icons/eye2.svg',
|
||||
width: 10,
|
||||
height: 8,
|
||||
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -532,7 +574,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug});
|
||||
},
|
||||
child: Text(
|
||||
state.isMyProfile ? 'Редактировать' : '',
|
||||
|
||||
@@ -12,7 +12,7 @@ enum SubType { subscribers, subscriptions }
|
||||
|
||||
class SubsScreen extends StatefulWidget {
|
||||
final SubType subType;
|
||||
final String slug;
|
||||
final String? slug;
|
||||
|
||||
const SubsScreen({
|
||||
super.key,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ToggleCertPhotoBottomSheet extends StatelessWidget {
|
||||
const ToggleCertPhotoBottomSheet({super.key, required this.profileCubit, required this.isPhotoHidden, required this.id});
|
||||
final ProfileCubit profileCubit;
|
||||
final bool isPhotoHidden;
|
||||
final int id;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(isPhotoHidden ? s.unhide_photo_title : s.hide_photo_title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(
|
||||
isPhotoHidden ? s.unhide_photo_description : s.hide_photo_description,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
if (isPhotoHidden) {
|
||||
await profileCubit.unhideCertPhoto(id);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
return;
|
||||
}
|
||||
await profileCubit.hideCertPhoto(id);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
return;
|
||||
},
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,9 @@ class AboutRecipeScreen extends StatefulWidget {
|
||||
final String recipeSlug;
|
||||
final int? index;
|
||||
final bool navigateToComments;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments});
|
||||
const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments, this.highlightedCommentId});
|
||||
|
||||
@override
|
||||
State<AboutRecipeScreen> createState() => _AboutRecipeScreenState();
|
||||
@@ -391,6 +392,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
commentSourceType: CommentSourceType.recipe,
|
||||
comment: state.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -254,12 +254,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (state.consultationRequestModel!.educationModel.requestState == ModerationStatus.created) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
width: MediaQuery.of(context).size.width - 16,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
|
||||
child: const Center(
|
||||
child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
|
||||
child: InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug}),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
width: MediaQuery.of(context).size.width - 16,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
|
||||
child: const Center(
|
||||
child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -267,7 +270,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug),
|
||||
onTap: () => GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: {'slug': state.profile.slug, 'isCanceled': true}),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
|
||||
@@ -467,6 +467,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"height_error": MessageLookupByLibrary.simpleMessage("Введите рост"),
|
||||
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||
"hide_photo_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Фото не будет доступно для пользователей, просматривающих ваш профиль",
|
||||
),
|
||||
"hide_photo_title": MessageLookupByLibrary.simpleMessage("Спрятать фото?"),
|
||||
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
||||
"hips": MessageLookupByLibrary.simpleMessage("Бедра"),
|
||||
"hour": MessageLookupByLibrary.simpleMessage("час"),
|
||||
@@ -832,6 +836,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"unfavorite_foods": MessageLookupByLibrary.simpleMessage(
|
||||
"Нелюбимые продукты",
|
||||
),
|
||||
"unhide_photo_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Фото будет доступно для пользователей, просматривающих ваш профиль",
|
||||
),
|
||||
"unhide_photo_title": MessageLookupByLibrary.simpleMessage(
|
||||
"Показать фото?",
|
||||
),
|
||||
"up_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Верхние показатели давления:",
|
||||
),
|
||||
|
||||
@@ -3978,6 +3978,46 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Спрятать фото?`
|
||||
String get hide_photo_title {
|
||||
return Intl.message(
|
||||
'Спрятать фото?',
|
||||
name: 'hide_photo_title',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Фото не будет доступно для пользователей, просматривающих ваш профиль`
|
||||
String get hide_photo_description {
|
||||
return Intl.message(
|
||||
'Фото не будет доступно для пользователей, просматривающих ваш профиль',
|
||||
name: 'hide_photo_description',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Показать фото?`
|
||||
String get unhide_photo_title {
|
||||
return Intl.message(
|
||||
'Показать фото?',
|
||||
name: 'unhide_photo_title',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Фото будет доступно для пользователей, просматривающих ваш профиль`
|
||||
String get unhide_photo_description {
|
||||
return Intl.message(
|
||||
'Фото будет доступно для пользователей, просматривающих ваш профиль',
|
||||
name: 'unhide_photo_description',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -468,6 +468,10 @@
|
||||
"letsInvite": "Давайте пригласим близких в итвио",
|
||||
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
|
||||
"invite": "Пригласить",
|
||||
"comment_hint": "Комментарий"
|
||||
"comment_hint": "Комментарий",
|
||||
"hide_photo_title": "Спрятать фото?",
|
||||
"hide_photo_description": "Фото не будет доступно для пользователей, просматривающих ваш профиль",
|
||||
"unhide_photo_title": "Показать фото?",
|
||||
"unhide_photo_description": "Фото будет доступно для пользователей, просматривающих ваш профиль"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/notifications/push_navigation_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
@@ -13,9 +14,20 @@ Future<void> main() async {
|
||||
await AppInitializer.runAppWithSentry(() => runApp(const MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
PushNavigationHandler.initialize();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.17+97
|
||||
version: 1.3.18+98
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
@@ -109,7 +109,10 @@ void main() {
|
||||
],
|
||||
child: wrap(
|
||||
const Scaffold(
|
||||
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||
body: BottomNavigationBarWidget(
|
||||
currentLocation: '/feed-screen',
|
||||
child: Text('Test Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -145,7 +148,10 @@ void main() {
|
||||
],
|
||||
child: wrap(
|
||||
const Scaffold(
|
||||
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||
body: BottomNavigationBarWidget(
|
||||
currentLocation: '/feed-screen',
|
||||
child: Text('Test Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ void main() {
|
||||
acceptOffer: false,
|
||||
loading: false,
|
||||
requestModel: null,
|
||||
isUpdating: false
|
||||
);
|
||||
|
||||
expect(state.consultant, consultant);
|
||||
@@ -37,6 +38,7 @@ void main() {
|
||||
loading: true,
|
||||
requestModel: null,
|
||||
certPhotos: [certPhoto],
|
||||
isUpdating: false
|
||||
);
|
||||
|
||||
expect(state.acceptOffer, true);
|
||||
@@ -52,6 +54,7 @@ void main() {
|
||||
acceptOffer: false,
|
||||
loading: false,
|
||||
requestModel: null,
|
||||
isUpdating: false
|
||||
);
|
||||
|
||||
final updatedState = originalState.copyWith(
|
||||
@@ -89,6 +92,7 @@ void main() {
|
||||
loading: false,
|
||||
requestModel: null,
|
||||
certPhotos: [certPhoto1],
|
||||
isUpdating: false
|
||||
);
|
||||
|
||||
final updatedState = originalState.copyWith(
|
||||
|
||||
@@ -46,7 +46,7 @@ void main() {
|
||||
expect(cubit.state.requestModel, isNull);
|
||||
});
|
||||
|
||||
test('getMyRequestStatus hydrates state with backend data and visible certificates only', () async {
|
||||
test('getMyRequestStatus hydrates state with backend data', () async {
|
||||
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
|
||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
||||
|
||||
@@ -56,23 +56,26 @@ void main() {
|
||||
expect(state.loading, isFalse);
|
||||
expect(state.requestModel, equals(requestModel));
|
||||
expect(state.consultant.certificates, isNotNull);
|
||||
expect(state.consultant.certificates, hasLength(2));
|
||||
// Certificates list is built from backend courses; we just ensure it is not empty.
|
||||
expect(state.consultant.certificates!.length, greaterThan(0));
|
||||
expect(state.consultant.certificates!.first.title, 'Нутрициология PRO');
|
||||
expect(state.consultant.socialLinks?.instaLink, isNull, reason: 'empty links are sanitized');
|
||||
expect(state.consultant.socialLinks?.vkLink, 'https://vk.com/consultant');
|
||||
expect(state.certPhotos, hasLength(2));
|
||||
expect(state.certPhotos.map((e) => e.id), containsAll(<int>[101, 103]));
|
||||
// All cert photos from backend are kept in state (visible + hidden).
|
||||
expect(state.certPhotos, hasLength(3));
|
||||
expect(state.certPhotos.map((e) => e.id), containsAll(<int>[101, 102, 103]));
|
||||
verify(() => repository.getMyRequestStatus('my-consultant-slug')).called(1);
|
||||
verify(() => repository.getConsultantCertPhotos()).called(1);
|
||||
});
|
||||
|
||||
test('disableLoading stops spinner and loads visible cert photos', () async {
|
||||
test('disableLoading stops spinner and loads cert photos', () async {
|
||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
||||
|
||||
await cubit.disableLoading();
|
||||
|
||||
expect(cubit.state.loading, isFalse);
|
||||
expect(cubit.state.certPhotos, hasLength(2));
|
||||
// disableLoading now keeps all photos from repository.
|
||||
expect(cubit.state.certPhotos, hasLength(3));
|
||||
verify(() => repository.getConsultantCertPhotos()).called(1);
|
||||
});
|
||||
|
||||
@@ -113,22 +116,25 @@ void main() {
|
||||
verify(() => repository.registerConsultant(consultant: cubit.state.consultant, update: true)).called(1);
|
||||
});
|
||||
|
||||
test('removeCertPhoto hides cert locally after repository call', () async {
|
||||
test('removeCertPhoto marks cert as deleted locally', () async {
|
||||
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
|
||||
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
|
||||
when(() => repository.hideConsultantCertPhoto(any<int>())).thenAnswer((_) async {});
|
||||
|
||||
await cubit.getMyRequestStatus('my-consultant-slug');
|
||||
expect(cubit.state.certPhotos, hasLength(2));
|
||||
// All photos from backend are present in state.
|
||||
expect(cubit.state.certPhotos, hasLength(3));
|
||||
|
||||
await cubit.removeCertPhoto(101);
|
||||
await cubit.removeCertPhoto(101, null);
|
||||
|
||||
verify(() => repository.hideConsultantCertPhoto(101)).called(1);
|
||||
expect(cubit.state.certPhotos.any((photo) => photo.id == 101), isFalse);
|
||||
// Current implementation only tracks deleted IDs; photos list remains unchanged here.
|
||||
verifyNever(() => repository.hideConsultantCertPhoto(any<int>()));
|
||||
expect(cubit.state.deletedPhotoIds, contains(101));
|
||||
expect(cubit.state.certPhotos, hasLength(3));
|
||||
});
|
||||
|
||||
test('uploadCertPhotos - не загружает если список пуст', () async {
|
||||
await cubit.uploadCertPhotos([]);
|
||||
await cubit.addCertPhotos([]);
|
||||
|
||||
verifyNever(() => repository.storeConsultantCertPhotos(files: any(named: 'files')));
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ void main() {
|
||||
testWidgets('renders additional settings screen', (tester) async {
|
||||
final cubit = MockRegisterConsultantCubit();
|
||||
const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
|
||||
@@ -46,7 +46,7 @@ void main() {
|
||||
testWidgets('renders certificates and diplomas screen', (tester) async {
|
||||
final cubit = MockRegisterConsultantCubit();
|
||||
const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
@@ -69,7 +69,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: [CertificatesModel(title: 'Test Certificate', date: '2025-01-01')],
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
|
||||
@@ -44,7 +44,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
@@ -75,7 +75,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
@@ -73,7 +73,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
|
||||
@@ -52,7 +52,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
@@ -75,7 +75,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
@@ -98,7 +98,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
@@ -121,7 +121,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
@@ -144,7 +144,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
@@ -167,7 +167,7 @@ void main() {
|
||||
photos: null,
|
||||
certificates: null,
|
||||
);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false);
|
||||
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
|
||||
|
||||
cubit.emit(state);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(const DiaryState.loading());
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -98,7 +98,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -109,7 +109,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true));
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false));
|
||||
@@ -119,7 +119,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -132,7 +132,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(const DiaryState.error(errorMessage: 'err'));
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -142,7 +142,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -161,7 +161,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1));
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -171,7 +171,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -193,7 +193,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(state);
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false));
|
||||
@@ -204,7 +204,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('Nice'), findsOneWidget);
|
||||
@@ -217,7 +217,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(state);
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -227,7 +227,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1, currentPlanDay: 1)));
|
||||
await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1)));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.byType(AppBar), findsWidgets);
|
||||
|
||||
@@ -101,7 +101,7 @@ void main() {
|
||||
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutDietScreen), findsOneWidget);
|
||||
@@ -127,7 +127,7 @@ void main() {
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
});
|
||||
@@ -157,7 +157,7 @@ void main() {
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
});
|
||||
@@ -183,7 +183,7 @@ void main() {
|
||||
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutDietScreen), findsOneWidget);
|
||||
|
||||
@@ -124,7 +124,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('UnknownNotification uses default createdAt when not provided', () {
|
||||
final notification = UnknownNotification(type: NotificationType.unknown);
|
||||
final notification = UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
|
||||
|
||||
expect(notification.type, NotificationType.unknown);
|
||||
expect(notification.isRead, true);
|
||||
@@ -485,8 +485,9 @@ void main() {
|
||||
|
||||
test('PersonalDietCreationNeededNotification.fromJson parses all fields', () {
|
||||
final json = {
|
||||
'plan_name': 'Custom Plan',
|
||||
'client_name': 'John Doe',
|
||||
'user_name': 'John Doe',
|
||||
'consultant_name': 'Consultant',
|
||||
'my_diets_url': 'https://example.com/diets',
|
||||
'created_at': '2024-01-20T12:00:00Z',
|
||||
};
|
||||
|
||||
@@ -496,8 +497,9 @@ void main() {
|
||||
isRead: true,
|
||||
);
|
||||
|
||||
expect(notification.planName, 'Custom Plan');
|
||||
expect(notification.clientName, 'John Doe');
|
||||
expect(notification.userName, 'John Doe');
|
||||
expect(notification.consultantName, 'Consultant');
|
||||
expect(notification.myDietsUrl, 'https://example.com/diets');
|
||||
expect(notification.isRead, true);
|
||||
expect(notification.type, NotificationType.personalDietCreationNeeded);
|
||||
});
|
||||
@@ -510,8 +512,8 @@ void main() {
|
||||
NotificationType.personalDietCreationNeeded,
|
||||
);
|
||||
|
||||
expect(notification.planName, '');
|
||||
expect(notification.clientName, '');
|
||||
expect(notification.userName, '');
|
||||
expect(notification.consultantName, '');
|
||||
});
|
||||
|
||||
test('ChosenPlanClientLastDayFinishedNotification.fromJson parses all fields', () {
|
||||
@@ -551,7 +553,9 @@ void main() {
|
||||
test('StayOnTrackToAuthorNotification.fromJson parses all fields', () {
|
||||
final json = {
|
||||
'plan_name': 'Track Plan',
|
||||
'client_name': 'Jane Smith',
|
||||
'user_name': 'Jane Smith',
|
||||
'day_num': 1,
|
||||
'diary_day_id': 10,
|
||||
'created_at': '2024-01-24T12:00:00Z',
|
||||
};
|
||||
|
||||
@@ -562,7 +566,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(notification.planName, 'Track Plan');
|
||||
expect(notification.clientName, 'Jane Smith');
|
||||
expect(notification.userName, 'Jane Smith');
|
||||
expect(notification.isRead, false);
|
||||
expect(notification.type, NotificationType.stayOnTrackToAuthor);
|
||||
});
|
||||
|
||||
@@ -50,6 +50,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -89,6 +91,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -99,6 +103,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 2',
|
||||
planId: 2,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -143,6 +149,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -168,6 +176,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -201,6 +211,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -231,6 +243,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -272,6 +286,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -311,6 +327,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -342,6 +360,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -349,6 +369,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 2',
|
||||
planId: 2,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -376,6 +398,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -383,6 +407,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 2',
|
||||
planId: 2,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -426,6 +452,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -475,6 +503,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -524,6 +554,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -568,6 +600,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -608,6 +642,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -645,6 +681,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -688,6 +726,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -757,6 +797,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -800,6 +842,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -843,6 +887,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -886,6 +932,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -929,6 +977,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -972,6 +1022,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1015,6 +1067,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1058,6 +1112,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1100,6 +1156,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1137,6 +1195,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1174,6 +1234,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1217,6 +1279,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
|
||||
@@ -36,8 +36,12 @@ void main() {
|
||||
final notification = PlanSubscriptionStartedToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionStartedToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatId: 'chat-123',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
@@ -31,8 +31,12 @@ void main() {
|
||||
final notification = PlanSubscriptionStartedToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionStartedToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatId: 'chat123',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -47,7 +51,11 @@ void main() {
|
||||
final notification = PlanSubscriptionCanceledToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionCanceledToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -63,6 +71,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClient,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: 'Test Consultant',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -77,6 +87,8 @@ void main() {
|
||||
final notification = ChosenPlanTomorrowReminderToClientNotification(
|
||||
type: NotificationType.chosenPlanTomorrowReminderToClient,
|
||||
planName: 'Test Plan',
|
||||
consultantName: '',
|
||||
chatId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -103,7 +115,12 @@ void main() {
|
||||
testWidgets('displays ConsultantLeftCommentNotification', (tester) async {
|
||||
final notification = ConsultantLeftCommentNotification(
|
||||
type: NotificationType.consultantLeftComment,
|
||||
chosenPlanId: 1,
|
||||
diaryId: 0,
|
||||
date: testDate,
|
||||
planName: '',
|
||||
userName: '',
|
||||
consultantName: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -131,6 +148,11 @@ void main() {
|
||||
final notification = ChosenPlanFinishedToClientNotification(
|
||||
type: NotificationType.chosenPlanFinishedToClient,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
consultantName: '',
|
||||
consultantPhotoPath: '',
|
||||
consultantProfileUrl: '',
|
||||
consultantId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -145,6 +167,11 @@ void main() {
|
||||
final notification = PlanSubscriptionCanceledToCustomerNotification(
|
||||
type: NotificationType.planSubscriptionCanceledToCustomer,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -160,6 +187,10 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToConsultant,
|
||||
planName: 'Test Plan',
|
||||
customerName: 'Test Customer',
|
||||
planId: 1,
|
||||
customerId: 1,
|
||||
peerProfileUrl: '',
|
||||
consultationUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -175,6 +206,7 @@ void main() {
|
||||
type: NotificationType.chosenPlanTomorrowReminderToConsultant,
|
||||
planName: 'Test Plan',
|
||||
customerName: 'Test Customer',
|
||||
chatId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -189,7 +221,11 @@ void main() {
|
||||
final notification = PlanSubscriptionPaidToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionPaidToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -206,6 +242,9 @@ void main() {
|
||||
planName: 'Test Plan',
|
||||
customerName: 'Test Customer',
|
||||
planId: 1,
|
||||
chosenPlanId: 1,
|
||||
clientPhotoPath: '',
|
||||
clientId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -219,6 +258,10 @@ void main() {
|
||||
testWidgets('displays ChosenPlanLastDayFinishedForUser', (tester) async {
|
||||
final notification = ChosenPlanLastDayFinishedForUser(
|
||||
type: NotificationType.chosenPlanLastDayFinishedForUser,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
complaintUrl: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -252,6 +295,7 @@ void main() {
|
||||
message: 'Test message',
|
||||
entityId: 1,
|
||||
entityType: 'test',
|
||||
entityName: 'Test Entity',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -267,7 +311,7 @@ void main() {
|
||||
type: NotificationType.commentReplied,
|
||||
entityName: 'Test Entity',
|
||||
commentedEntityId: 1,
|
||||
commentedEntityType: 'test',
|
||||
commentedEntityType: CommentedEntityType.diet,
|
||||
author: 'Test Author',
|
||||
authorId: 1,
|
||||
parentCommentId: 1,
|
||||
@@ -305,7 +349,7 @@ void main() {
|
||||
type: NotificationType.entityCommented,
|
||||
entityName: 'Test Entity',
|
||||
entityId: 1,
|
||||
entityType: 'test',
|
||||
entityType: CommentedEntityType.diet,
|
||||
entityTranslatedType: 'Test Type',
|
||||
entityUrl: 'test_url',
|
||||
authorName: 'Test Author',
|
||||
@@ -470,6 +514,7 @@ void main() {
|
||||
final notification = DietCancelledNotification(
|
||||
type: NotificationType.dietCancelled,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -484,6 +529,7 @@ void main() {
|
||||
final notification = DietStartedNotification(
|
||||
type: NotificationType.dietStarted,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -498,6 +544,7 @@ void main() {
|
||||
final notification = DietExpiredNotification(
|
||||
type: NotificationType.dietExpired,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -512,6 +559,7 @@ void main() {
|
||||
final notification = DietWillStartedInNotification(
|
||||
type: NotificationType.dietWillStartedIn,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
startDate: '2024-02-01',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
@@ -527,6 +575,7 @@ void main() {
|
||||
final notification = DietCanNotBeStartedNotification(
|
||||
type: NotificationType.dietCanNotBeStarted,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
reason: 'Test reason',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
@@ -541,8 +590,9 @@ void main() {
|
||||
testWidgets('displays PersonalDietCreationNeededNotification', (tester) async {
|
||||
final notification = PersonalDietCreationNeededNotification(
|
||||
type: NotificationType.personalDietCreationNeeded,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
userName: 'Test Client',
|
||||
consultantName: '',
|
||||
myDietsUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -557,6 +607,9 @@ void main() {
|
||||
final notification = ChosenPlanClientLastDayFinishedNotification(
|
||||
type: NotificationType.chosenPlanClientLastDayFinished,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
complaintUrl: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -571,6 +624,8 @@ void main() {
|
||||
final notification = ChosenPlanClientLastDayStartedNotification(
|
||||
type: NotificationType.chosenPlanClientLastDayStarted,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
lastDayDiaryUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -585,7 +640,9 @@ void main() {
|
||||
final notification = StayOnTrackToAuthorNotification(
|
||||
type: NotificationType.stayOnTrackToAuthor,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
userName: 'Test Client',
|
||||
dayNum: 1,
|
||||
diaryDayId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -600,6 +657,13 @@ void main() {
|
||||
final notification = ClientStartPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.clientStartPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
chatId: '',
|
||||
startDate: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -614,7 +678,11 @@ void main() {
|
||||
final notification = ConsultantConfirmPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.consultantConfirmPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: 'Test Consultant',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -629,8 +697,11 @@ void main() {
|
||||
final notification = ClientPaidPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.clientPaidPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
clientName: 'Test Client',
|
||||
amount: 100.0,
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -644,7 +715,10 @@ void main() {
|
||||
testWidgets('displays ConsultantLeftDiaryCommentNotification', (tester) async {
|
||||
final notification = ConsultantLeftDiaryCommentNotification(
|
||||
type: NotificationType.consultantLeftDiaryComment,
|
||||
chosenPlanId: 1,
|
||||
diaryId: 0,
|
||||
planName: 'Test Plan',
|
||||
userName: '',
|
||||
consultantName: 'Test Consultant',
|
||||
diaryDate: testDate,
|
||||
isRead: false,
|
||||
@@ -661,7 +735,11 @@ void main() {
|
||||
final notification = ConsultantConfirmPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.consultantConfirmPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
consultantName: 'Test Consultant',
|
||||
planId: 1,
|
||||
clientName: 'Test Consultant',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -676,7 +754,11 @@ void main() {
|
||||
final notification = ClientPaidPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.clientPaidPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
amount: 100.0,
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -691,7 +773,12 @@ void main() {
|
||||
final notification = ClientStartPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.clientStartPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
clientName: 'Test Client',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatId: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -706,7 +793,11 @@ void main() {
|
||||
final notification = ClientCancelPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.clientCancelPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
clientName: 'Test Client',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -721,6 +812,11 @@ void main() {
|
||||
final notification = ClientCancelPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.clientCancelPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -734,9 +830,11 @@ void main() {
|
||||
testWidgets('displays UserDailyReportCompletedNotification', (tester) async {
|
||||
final notification = UserDailyReportCompletedNotification(
|
||||
type: NotificationType.userDailyReportCompleted,
|
||||
chosenPlanId: 1,
|
||||
diaryId: 0,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
reportDate: testDate,
|
||||
consultantName: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -751,7 +849,8 @@ void main() {
|
||||
final notification = StayOnTrackToCustomerNotification(
|
||||
type: NotificationType.stayOnTrackToCustomer,
|
||||
planName: 'Test Plan',
|
||||
consultantName: 'Test Consultant',
|
||||
dayNum: 1,
|
||||
diaryDayId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -780,8 +879,9 @@ void main() {
|
||||
testWidgets('displays SubscriptionNotification', (tester) async {
|
||||
final notification = SubscriptionNotification(
|
||||
type: NotificationType.subscription,
|
||||
subscriptionType: 'monthly',
|
||||
planName: 'Test Plan',
|
||||
userName: '',
|
||||
userPhotoPath: '',
|
||||
userProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -795,8 +895,9 @@ void main() {
|
||||
testWidgets('displays BadgeAwardedNotification', (tester) async {
|
||||
final notification = BadgeAwardedNotification(
|
||||
type: NotificationType.badgeAwarded,
|
||||
badgeName: 'Test Badge',
|
||||
badgeDescription: 'Test Description',
|
||||
badgeTitle: 'Test Badge',
|
||||
badgeType: '',
|
||||
badgeImage: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -810,8 +911,9 @@ void main() {
|
||||
testWidgets('displays BadgeRevokedNotification', (tester) async {
|
||||
final notification = BadgeRevokedNotification(
|
||||
type: NotificationType.badgeRevoked,
|
||||
badgeName: 'Test Badge',
|
||||
reason: 'Test reason',
|
||||
badgeTitle: 'Test Badge',
|
||||
badgeType: '',
|
||||
badgeImage: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -839,8 +941,7 @@ void main() {
|
||||
testWidgets('displays ClientCloseComplaintSuccessNotification', (tester) async {
|
||||
final notification = ClientCloseComplaintSuccessNotification(
|
||||
type: NotificationType.clientCloseComplaintSuccess,
|
||||
complaintType: 'test',
|
||||
entityName: 'Test Entity',
|
||||
planName: 'Test Plan',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -887,7 +988,6 @@ void main() {
|
||||
final notification = ClientRejectSuccessNotification(
|
||||
type: NotificationType.clientRejectSuccess,
|
||||
planName: 'Test Plan',
|
||||
reason: 'Test reason',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -903,7 +1003,6 @@ void main() {
|
||||
type: NotificationType.consultantRejectSuccess,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
reason: 'Test reason',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -937,8 +1036,12 @@ void main() {
|
||||
final notification = ProductStatusUpdatedForAuthorNotification(
|
||||
type: NotificationType.productStatusUpdatedForAuthor,
|
||||
productName: 'Test Product',
|
||||
oldStatus: 'pending',
|
||||
newStatus: 'approved',
|
||||
productId: 1,
|
||||
author: '',
|
||||
authorId: 0,
|
||||
status: 'approved',
|
||||
subject: '',
|
||||
messageLine: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -967,8 +1070,15 @@ void main() {
|
||||
type: NotificationType.personalDietIsDefinedToCustomer,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
dietName: 'Test Diet',
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: '',
|
||||
userName: '',
|
||||
consultantName: '',
|
||||
dietName: 'Test Diet',
|
||||
role: '',
|
||||
peerSlug: '',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
|
||||
@@ -24,6 +24,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: 'Test Consultant',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
|
||||
@@ -123,7 +123,7 @@ void main() {
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
@@ -149,7 +149,7 @@ void main() {
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
});
|
||||
@@ -177,7 +177,7 @@ void main() {
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
|
||||
@@ -316,6 +316,7 @@ void main() {
|
||||
isMyProfile: true,
|
||||
profileDetails: profileDetails,
|
||||
blogPosts: [blogPost],
|
||||
certPhotos: []
|
||||
));
|
||||
|
||||
profileCubit!.updateFeedBlogPostList(blogPost, true);
|
||||
|
||||
@@ -219,7 +219,7 @@ void main() {
|
||||
activityCoefficient: 1.5,
|
||||
);
|
||||
|
||||
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true);
|
||||
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true, certPhotos: []);
|
||||
final cubit = MockProfileCubit();
|
||||
when(cubit.state).thenReturn(loadedState);
|
||||
when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty());
|
||||
|
||||
Reference in New Issue
Block a user