forked from eatvio/eatvio_app
Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user