Merge remote-tracking branch 'upstream/main' into issue-701

This commit is contained in:
misha
2025-09-25 15:14:25 +03:00
43 changed files with 898 additions and 736 deletions

View File

@@ -2,6 +2,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
}
}

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -539,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -578,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

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

View File

@@ -6,13 +6,24 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:huawei_push/huawei_push.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future<String?> getToken() async {
await _firebaseMessaging.requestPermission();
return await _firebaseMessaging.getToken();
String? token = await _firebaseMessaging.getToken();
if (token == null && Platform.isAndroid) {
final isHuawei = await _isHuaweiDevice();
if (isHuawei) {
token = await _getHuaweiPushToken();
}
}
return token;
}
Future<void> registerPushToken() async {
@@ -32,4 +43,38 @@ class FirebaseApi {
}
}
}
Future<bool> _isHuaweiDevice() async {
if (!Platform.isAndroid) return false;
try {
final android = await DeviceInfoPlugin().androidInfo;
final brand = android.brand.toLowerCase();
final manufacturer = android.manufacturer.toLowerCase();
return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor');
} catch (_) {
return false;
}
}
Future<String?> _getHuaweiPushToken() async {
try {
await Push.turnOnPush();
String? resolvedToken;
final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {});
Push.getToken('');
final started = DateTime.now();
while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
await sub.cancel();
return resolvedToken;
} catch (e, st) {
log('Huawei Push token error: $e', stackTrace: st);
return null;
}
}
}

View File

@@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
@@ -179,6 +182,7 @@ Future<void> init() async {
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -212,6 +216,7 @@ Future<void> init() async {
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -247,6 +252,7 @@ Future<void> init() async {
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -204,6 +204,8 @@ class PrimaryColors {
Color get commentBg => const Color(0XFFFFFDD8);
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
Color get grey7B => const Color(0xFF7B7B7B);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -0,0 +1,128 @@
import 'dart:async';
import 'package:eatvio_app/core/views/cubit/views_state.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ViewsCubit extends Cubit<ViewsCubitState> {
final ViewsRepository viewsRepository;
ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) {
_listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews);
}
late final AppLifecycleListener _listener;
final Set<int> postsIds = <int>{};
final Set<int> plansIds = <int>{};
final Set<int> dietsIds = <int>{};
final Set<int> recipesIds = <int>{};
final Set<int> _sentPosts = <int>{};
final Set<int> _sentPlans = <int>{};
final Set<int> _sentDiets = <int>{};
final Set<int> _sentRecipes = <int>{};
final Map<ViewableContentType, Map<int, Timer>> _visibilityTimers = {
ViewableContentType.posts: <int, Timer>{},
ViewableContentType.plans: <int, Timer>{},
ViewableContentType.diets: <int, Timer>{},
ViewableContentType.recipes: <int, Timer>{},
};
Timer? _debounceTimer;
Set<int> _idsForType(ViewableContentType type) {
switch (type) {
case ViewableContentType.posts:
return postsIds;
case ViewableContentType.plans:
return plansIds;
case ViewableContentType.diets:
return dietsIds;
case ViewableContentType.recipes:
return recipesIds;
}
}
Set<int> _sentIdsForType(ViewableContentType type) {
switch (type) {
case ViewableContentType.posts:
return _sentPosts;
case ViewableContentType.plans:
return _sentPlans;
case ViewableContentType.diets:
return _sentDiets;
case ViewableContentType.recipes:
return _sentRecipes;
}
}
void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) {
final timers = _visibilityTimers[type]!;
final ids = _idsForType(type);
final sent = _sentIdsForType(type);
if (sent.contains(id)) {
timers[id]?.cancel();
timers.remove(id);
return;
}
if (ids.contains(id)) return;
if (visibleFraction >= 0.5) {
if (timers.containsKey(id)) return;
timers[id] = Timer(const Duration(seconds: 1), () {
ids.add(id);
timers.remove(id);
_scheduleDebouncedSend();
});
} else {
timers[id]?.cancel();
timers.remove(id);
}
}
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
await viewsRepository.viewContent(
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
}
void _scheduleDebouncedSend() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(seconds: 4), _sendViews);
}
void _send(Set<int> ids, Set<int> sent, ViewableContentType type) {
final newIds = ids.difference(sent);
if (newIds.isNotEmpty) {
_viewContent(type: type, ids: newIds.toList());
sent.addAll(newIds);
ids.removeAll(newIds);
}
}
void _sendViews() {
_send(postsIds, _sentPosts, ViewableContentType.posts);
_send(plansIds, _sentPlans, ViewableContentType.plans);
_send(dietsIds, _sentDiets, ViewableContentType.diets);
_send(recipesIds, _sentRecipes, ViewableContentType.recipes);
}
@override
Future<void> close() async {
for (final map in _visibilityTimers.values) {
for (final t in map.values) {
t.cancel();
}
map.clear();
}
_debounceTimer?.cancel();
_listener.dispose();
_sendViews();
await super.close();
}
}

View File

@@ -0,0 +1,3 @@
class ViewsCubitState {
ViewsCubitState();
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
class ViewsRepository {
final ViewsService viewsService;
ViewsRepository({required this.viewsService});
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await viewsService.viewContent(viewModel: viewModel);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:retrofit/retrofit.dart';
part 'views_service.g.dart';
@RestApi()
abstract class ViewsService {
factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService;
@POST('/api/views')
Future<void> viewContent({
@Body() required ViewModel viewModel,
});
}

View File

@@ -0,0 +1,12 @@
class SocialExchangeRequest {
final String provider;
final String accessToken;
SocialExchangeRequest({required this.provider, required this.accessToken});
Map<String, dynamic> toJson() {
return {'provider': provider, 'access_token': accessToken};
}
}

View File

@@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository authRepository;
AuthCubit({
required this.authRepository,
}) : super(const AuthState());
AuthCubit({required this.authRepository}) : super(const AuthState());
Future<bool> getTokenByCredentials(String email, String password) async {
emit(const AuthState.loading());
try {
final token = (await authRepository.getTokenByCredentials(
email,
password,
))
.token;
emit(
AuthState.loaded(
token: token,
),
);
await TokenStorage.saveTokenInStorage(token);
final token = (await authRepository.getTokenByCredentials(email, password)).token;
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
log(e.toString());
@@ -36,4 +25,19 @@ class AuthCubit extends Cubit<AuthState> {
return false;
}
}
Future<bool> getTokenBySocial({required String provider, required String accessToken}) async {
emit(const AuthState.loading());
try {
final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token;
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
log(e.toString());
showErrorToast('Не удалось войти через $provider');
emit(AuthState.error(errorMessage: '$e'));
return false;
}
}
}

View File

@@ -1,3 +1,5 @@
// ignore_for_file: unused_element, duplicate_ignore
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
@@ -24,6 +26,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@@ -165,6 +168,52 @@ class _AuthScreenState extends State<AuthScreen> {
),
],
),
// const SizedBox(height: 16),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// _SocialButton(
// onTap: _onVkPressed,
// backgroundColor: const Color(0xFF2787F5),
// tooltip: 'VK ID',
// child: SvgPicture.asset(
// 'assets/icons/vk.svg',
// height: 24,
// width: 24,
// colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
// ),
// ),
// _SocialButton(
// onTap: _onOkPressed,
// backgroundColor: const Color(0xFFF58220),
// tooltip: 'Одноклассники',
// child: SvgPicture.asset(
// 'assets/icons/ok.svg',
// height: 24,
// width: 24,
// colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
// ),
// ),
// const _SocialButton(
// onTap: _onMailRuPressed,
// backgroundColor: Color(0xFF005FF9),
// tooltip: 'Mail.ru',
// child: Text(
// 'Mail',
// style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
// ),
// ),
// const _SocialButton(
// onTap: _onYandexPressed,
// backgroundColor: Color(0xFFFFCC00),
// tooltip: 'Яндекс ID',
// child: Text(
// 'Yandex',
// style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700),
// ),
// ),
// ],
// ),
const Spacer(),
Center(
child: RichText(
@@ -317,3 +366,88 @@ class _AuthScreenState extends State<AuthScreen> {
);
}
}
// ignore: unused_element
class _SocialButton extends StatelessWidget {
const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip});
final VoidCallback onTap;
final Widget child;
final Color backgroundColor;
final String tooltip;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Ink(
height: 48,
width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4,
decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)),
child: Center(child: child),
),
),
);
}
}
// ignore: unused_element
void _onVkPressed() async {
final accessToken = await SocialAuthFacade.signInWithVk();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'vk', accessToken: accessToken);
if (isAuthenticated) {
if (AppRouter.navigatorKey.currentContext != null) {
// Reuse success flow
// ignore: use_build_context_synchronously
await _reconnectAfterAuth();
}
}
}
void _onOkPressed() async {
final accessToken = await SocialAuthFacade.signInWithOk();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'ok', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onMailRuPressed() async {
final accessToken = await SocialAuthFacade.signInWithMailRu();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'mailru', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onYandexPressed() async {
final accessToken = await SocialAuthFacade.signInWithYandex();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'yandex', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
Future<void> _reconnectAfterAuth() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
try {
await sl<WsManager>().clear();
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 (_) {}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
class AuthRepository {
final AuthService authService;
@@ -22,4 +23,17 @@ class AuthRepository {
rethrow;
}
}
Future<TokenModel> getTokenBySocial({
required String provider,
required String accessToken,
}) async {
try {
return await authService.getTokenBySocial(
body: SocialExchangeRequest(provider: provider, accessToken: accessToken),
);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,25 @@
import 'dart:async';
class SocialAuthFacade {
static Future<String?> signInWithVk() async {
// TODO: Integrate official VK ID SDK and return access token
return null;
}
static Future<String?> signInWithOk() async {
// TODO: Integrate official OK SDK and return access token
return null;
}
static Future<String?> signInWithMailRu() async {
// TODO: Integrate official Mail.ru SDK and return access token
return null;
}
static Future<String?> signInWithYandex() async {
// TODO: Integrate official Yandex ID SDK and return access token
return null;
}
}

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
import 'package:retrofit/retrofit.dart';
part 'auth_service.g.dart';
@@ -13,4 +14,9 @@ abstract class AuthService {
Future<TokenModel> getTokenByCredentials({
@Body() required CredentialsModel credentials,
});
@POST('/api/social/login')
Future<TokenModel> getTokenBySocial({
@Body() required SocialExchangeRequest body,
});
}

View File

@@ -18,4 +18,26 @@ class BlogPostCubit extends Cubit<BlogPostState> {
emit(state.copyWith(isLoading: false));
}
}
Future<void> toggleLike() async {
final current = state.blogPostModel;
if (current == null) return;
// optimistic update
final wasLiked = current.wasLikedByMe;
final updated = current.copyWith(wasLikedByMe: !wasLiked, totalLikes: wasLiked ? current.totalLikes - 1 : current.totalLikes + 1);
emit(state.copyWith(blogPostModel: updated));
try {
if (wasLiked) {
await feedRepository.unlikedBlogPost(id: current.id);
} else {
await feedRepository.likeBlogPost(id: current.id);
}
} catch (e, st) {
// rollback on error
log(e.toString(), stackTrace: st);
emit(state.copyWith(blogPostModel: current));
}
}
}

View File

@@ -32,8 +32,6 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>();
final textEditingController = TextEditingController();
int? replyId;
bool isLiked = false;
int totalLikes = 0;
@override
void initState() {
@@ -60,26 +58,30 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
builder: (BuildContext context, CommentsState state) {
if (blogPostState.isLoading) {
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
body: const Loader());
),
body: const Loader(),
);
}
isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
final bool isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
final int totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -114,24 +116,14 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -196,7 +188,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
id: widget.id,
feedBlogPostModel: blogPostState.blogPostModel,
userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
),
),
const SizedBox(width: 16),
],
),
@@ -215,9 +207,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''),
),
foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
@@ -242,30 +232,32 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'), style: {"*": Style(color: Colors.black87)}),
child: Html(
data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'),
style: {"*": Style(color: Colors.black87)},
),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}',
),
CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
icon: Icons.favorite_border_rounded,
icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked ? await context.read<FeedCubit>().likePost(widget.id) : await context.read<FeedCubit>().unlikedPost(widget.id);
sl<FeedCubit>().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked);
await context.read<BlogPostCubit>().toggleLike();
// ignore: use_build_context_synchronously
final updated = context.read<BlogPostCubit>().state.blogPostModel;
if (updated != null) {
// ignore: use_build_context_synchronously
context.read<FeedCubit>().updateFeedBlogPostList(updated, updated.wasLikedByMe);
}
},
),
const SizedBox(width: 8),
@@ -284,8 +276,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
const Spacer(),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(blogPostState.blogPostModel?.totalViews.toString() ?? '',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
Text(
blogPostState.blogPostModel?.totalViews.toString() ?? '',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),
),
@@ -332,7 +326,11 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
children: [
Icon(icon, size: 20, color: iconColor ?? appTheme.gray600),
if (text != null) const SizedBox(width: 8),
if (text != null) Text(text, style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500)),
if (text != null)
Text(
text,
style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500),
),
],
),
),

View File

@@ -1,11 +1,13 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/const.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/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -63,7 +65,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
Row(
children: [
CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)),
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -284,6 +286,19 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
),
],
),
// Кнопка получения выплаты
if (widget.consultationsModel.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
const SizedBox(height: 16),
CustomOutlinedButton(
text: s.collectCharge,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () async {
GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.consultationsModel.id);
},
),
],
],
),
),

View File

@@ -23,9 +23,12 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
final selectedIndex = state.selectedMeal - 1;
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].mood.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
final meals = state.diary.meals;
if (selectedIndex >= 0 && selectedIndex < meals.length) {
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
}
}
return Padding(
padding: const EdgeInsets.only(top: 24),
@@ -35,21 +38,17 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
Text(
state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
),
style: const TextStyle(fontSize: 16),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
'assets/icons/mood_1.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32),
RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
initialRating: state.diary.meals[selectedIndex].mood.toDouble() + 1,
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
? state.diary.meals[selectedIndex].mood.toDouble() + 1
: 3,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
@@ -63,20 +62,11 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1
? Border.all(
width: 2,
color: const Color(0xFFAA3333),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 1
? const BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: const BorderRadius.horizontal(
left: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
: const BorderRadius.horizontal(left: Radius.circular(8)),
),
);
case 1:
@@ -87,11 +77,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2
? Border.all(
width: 2,
color: const Color(0xFF550000),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
),
@@ -104,11 +90,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3
? Border.all(
width: 2,
color: const Color(0xFF8C5D00),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
),
@@ -121,11 +103,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4
? Border.all(
width: 2,
color: const Color(0xFFB5911B),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
),
@@ -138,11 +116,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5
? Border.all(
width: 2,
color: const Color(0xFF276918),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
),
@@ -155,20 +129,11 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6
? Border.all(
width: 2,
color: const Color(0xFF184D12),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 6
? const BorderRadius.horizontal(
left: Radius.circular(2),
right: Radius.circular(8),
)
: const BorderRadius.horizontal(
right: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
: const BorderRadius.horizontal(right: Radius.circular(8)),
),
);
}
@@ -177,20 +142,12 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
setState(() {
currentSelectedRating[selectedIndex] = value;
});
EasyDebounce.debounce(
'change-satiety',
const Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
},
);
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
});
},
),
SvgPicture.asset(
'assets/icons/mood_2.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32),
],
),
],

View File

@@ -23,9 +23,12 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
final selectedIndex = state.selectedMeal - 1;
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].satiety.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
final meals = state.diary.meals;
if (selectedIndex >= 0 && selectedIndex < meals.length) {
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
}
}
return Padding(
padding: const EdgeInsets.only(top: 24),
@@ -35,21 +38,17 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
Text(
state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
),
style: const TextStyle(fontSize: 16),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
'assets/icons/satiety_emoji_left.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32),
RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
initialRating: state.diary.meals[selectedIndex].satiety.toDouble() + 1,
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
? state.diary.meals[selectedIndex].satiety.toDouble() + 1
: 3,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
@@ -63,20 +62,11 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1
? Border.all(
width: 2,
color: const Color(0xFFAA3333),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 1
? const BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: const BorderRadius.horizontal(
left: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
: const BorderRadius.horizontal(left: Radius.circular(8)),
),
);
case 1:
@@ -87,11 +77,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2
? Border.all(
width: 2,
color: const Color(0xFF550000),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
),
@@ -104,11 +90,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3
? Border.all(
width: 2,
color: const Color(0xFF8C5D00),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
),
@@ -121,11 +103,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4
? Border.all(
width: 2,
color: const Color(0xFFB5911B),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
),
@@ -138,11 +116,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5
? Border.all(
width: 2,
color: const Color(0xFF276918),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
),
@@ -155,20 +129,11 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6
? Border.all(
width: 2,
color: const Color(0xFF184D12),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 6
? const BorderRadius.horizontal(
left: Radius.circular(2),
right: Radius.circular(8),
)
: const BorderRadius.horizontal(
right: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
: const BorderRadius.horizontal(right: Radius.circular(8)),
),
);
}
@@ -178,20 +143,12 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
setState(() {
currentSelectedRating[selectedIndex] = value;
});
EasyDebounce.debounce(
'change-satiety',
const Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
},
);
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
});
},
),
SvgPicture.asset(
'assets/icons/satiety_emoji_right.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32),
],
),
],

View File

@@ -75,7 +75,53 @@ class DiaryRepository {
}
Future<DiaryDayModel> getDiaryDay(int diaryId) async {
return (await diaryService.getDiaryDay(diary: diaryId)).diary;
// Align with getDiaryByDate: enrich portions with glossary nutritional info
final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
final DiaryDayModel diaryDay = diaryWrapper.diary;
Set<int> ids = {};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
}
Future<NutritionDataModel> getNutritionData(int index) async {

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'view_model.freezed.dart';
part 'view_model.g.dart';
@freezed
abstract class ViewModel with _$ViewModel {
factory ViewModel({
required String type,
List<int>? ids,
List<String>? slugs,
}) = _ViewModel;
factory ViewModel.fromJson(Map<String, dynamic> json) => _$ViewModelFromJson(json);
}

View File

@@ -15,13 +15,12 @@ class FeedCubit extends Cubit<FeedState> {
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
if (feedBlogPostModel == null || isClosed) return;
List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexOf(feedBlogPostModel);
feedBlogPostList[index] = feedBlogPostModel.copyWith(
wasLikedByMe: wasLikedByMe,
totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1,
);
if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return;
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexWhere((e) => e.id == feedBlogPostModel.id);
if (index == -1) return;
final current = feedBlogPostList[index];
feedBlogPostList[index] = current.copyWith(wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe ? current.totalLikes + 1 : current.totalLikes - 1);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
}

View File

@@ -1,5 +1,6 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
@@ -35,7 +36,6 @@ class FeedScreen extends StatelessWidget {
providers: [
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
BlocProvider<FeedProfileCubit>(create: (context) => sl<FeedProfileCubit>()..getProfile()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<DietsCubit>(create: (context) => sl<DietsCubit>()..loadDiets()),
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
@@ -66,12 +66,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final dietsScrollController = ScrollController();
final plansScrollController = ScrollController();
final consultantsScrollController = ScrollController();
@override
void initState() {
// Initialize chipsLabelList based on hidePlans setting
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants);
super.initState();
}
@@ -228,10 +225,10 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
];
},
body: switch (_selectedChipIndex) {
0 => const BlogPosts(),
1 => const Recipes(),
2 => const Diets(),
3 when !hidePlans => const Plans(),
0 => BlogPosts(handleVisibility: context.read<ViewsCubit>().handleVisibility),
1 => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
2 => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
3 when !hidePlans => Plans(handleVisibility: context.read<ViewsCubit>().handleVisibility),
3 when hidePlans => const Consultants(),
4 when !hidePlans => const Consultants(),
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
@@ -240,3 +237,20 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
);
}
}
enum ViewableContentType {
plans,
posts,
diets,
recipes;
String getStringifiedVersion() => switch (this) {
ViewableContentType.plans => 'plans',
ViewableContentType.posts => 'blog_posts',
ViewableContentType.diets => 'diets',
ViewableContentType.recipes => 'recipes',
};
}

View File

@@ -21,10 +21,7 @@ import 'package:share_plus/share_plus.dart';
class BlogPostWidget extends StatefulWidget {
final int index;
const BlogPostWidget({
super.key,
required this.index,
});
const BlogPostWidget({super.key, required this.index});
@override
State<BlogPostWidget> createState() => _BlogPostWidgetState();
@@ -44,10 +41,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
totalLikes = state.feedBlogPostList[widget.index].totalLikes;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -95,21 +89,12 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
Text(
state.feedBlogPostList[widget.index].author.authorName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
fontSize: 16,
),
style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
height: 1,
color: Colors.black45,
fontSize: 14,
),
style: const TextStyle(height: 1, color: Colors.black45, fontSize: 14),
),
],
),
@@ -119,9 +104,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
child: SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
),
child: SubscribeButtonWidget(slug: state.feedBlogPostList[widget.index].author.authorSlug),
),
// Кнопка меню
CustomPopup(
@@ -141,11 +124,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.feedBlogPostList[widget.index].title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87),
),
),
],
@@ -156,15 +135,12 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
data: (state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description).replaceAll('\n', '<br>'),
style: {
"*": Style(
fontSize: FontSize(16),
color: Colors.black87,
),
},
data:
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description)
.replaceAll('\n', '<br>'),
style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
),
),
if (state.feedBlogPostList[widget.index].description.length > 200)
@@ -172,12 +148,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty)
@@ -186,10 +157,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'],
index: 0,
),
PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0),
);
},
child: Hero(
@@ -218,11 +186,14 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(state.feedBlogPostList[widget.index].id)
: await context.read<FeedCubit>().unlikedPost(state.feedBlogPostList[widget.index].id);
if (!context.mounted) return;
await context.read<FeedCubit>().loadFeedBlogPostList();
// Update feed list immediately by ID (no reload)
context.read<FeedCubit>().updateFeedBlogPostList(state.feedBlogPostList[widget.index], isLiked);
// Fire network call without introducing UI delay
if (isLiked) {
await context.read<FeedCubit>().likePost(state.feedBlogPostList[widget.index].id);
} else {
await context.read<FeedCubit>().unlikedPost(state.feedBlogPostList[widget.index].id);
}
},
),
const SizedBox(width: 8),
@@ -231,16 +202,10 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
'assets/icons/comment.svg',
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
appTheme.gray600,
BlendMode.srcIn,
),
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
text: state.feedBlogPostList[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -254,28 +219,18 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse(
'$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',
),
uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
),
const Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
state.feedBlogPostList[widget.index].totalViews.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),

View File

@@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class BlogPosts extends StatelessWidget {
const BlogPosts({super.key});
const BlogPosts({super.key, required this.handleVisibility});
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget {
if (index == state.feedBlogPostList.length) {
return const Loader();
}
return BlogPostWidget(index: index);
return VisibilityDetector(
onVisibilityChanged: (info) {
final id = state.feedBlogPostList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts);
},
key: ValueKey(index),
child: BlogPostWidget(index: index),
);
},
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
),
);
} else if (state is FeedStateError) {
return DataErrorWidget(
onReload: () => context.read<FeedCubit>().loadFeedBlogPostList(),
);
return DataErrorWidget(onReload: () => context.read<FeedCubit>().loadFeedBlogPostList());
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
}
},
),
);
}
}

View File

@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Diets extends StatelessWidget {
const Diets({super.key});
const Diets({super.key, required this.handleVisibility});
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -19,10 +21,7 @@ class Diets extends StatelessWidget {
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
} else if (state.errorMessage != null) {
return DataErrorWidget(onReload: () => context.read<DietsCubit>().loadDiets());
} else {
@@ -39,7 +38,14 @@ class Diets extends StatelessWidget {
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.dietsList[index], index: index);
return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.dietsList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets);
},
child: DietsWidget(diet: state.dietsList[index], index: index),
);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 8);
@@ -50,11 +56,7 @@ class Diets extends StatelessWidget {
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: DietFilterBottomSheet(
dietsCubit: context.read<DietsCubit>(),
),
),
child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read<DietsCubit>())),
),
],
);

View File

@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Plans extends StatelessWidget {
const Plans({super.key});
const Plans({super.key, required this.handleVisibility});
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -19,10 +21,7 @@ class Plans extends StatelessWidget {
child: BlocBuilder<PlansCubit, PlansState>(
builder: (BuildContext context, PlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
} else if (state.errorMessage != null) {
return DataErrorWidget(onReload: () => context.read<PlansCubit>().loadPlans());
}
@@ -39,9 +38,13 @@ class Plans extends StatelessWidget {
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return PlanWidget(
plan: state.plans[index],
index: index,
return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.plans[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans);
},
child: PlanWidget(plan: state.plans[index], index: index),
);
},
separatorBuilder: (context, index) {
@@ -53,11 +56,7 @@ class Plans extends StatelessWidget {
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: PlansFilterBottomSheet(
plansCubit: context.read<PlansCubit>(),
),
),
child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read<PlansCubit>())),
),
],
);

View File

@@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Recipes extends StatelessWidget {
const Recipes({super.key});
const Recipes({super.key, required this.handleVisibility});
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -40,9 +42,16 @@ class Recipes extends StatelessWidget {
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return RecipesWidget(
recipe: state.recipesList[index],
index: index,
return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.recipesList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes);
},
child: RecipesWidget(
recipe: state.recipesList[index],
index: index,
),
);
},
separatorBuilder: (context, index) {

View File

@@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
class FeedRepository {
final FeedService feedService;
FeedRepository({
required this.feedService,
});
FeedRepository({required this.feedService});
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({
String? query,
int page = 1,
}) async {
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({String? query, int page = 1}) async {
try {
return (await feedService.getBlogPostList(
page: page,
query: query,
));
return (await feedService.getBlogPostList(page: page, query: query));
} catch (e) {
rethrow;
}
}
Future<FeedBlogPostModel> getBlogPost({
required int id
}) async {
Future<FeedBlogPostModel> getBlogPost({required int id}) async {
try {
return await feedService.getBlogPost(
postId: id
);
return await feedService.getBlogPost(postId: id);
} catch (e) {
rethrow;
}
}
Future<List<Recipe>> getRecipes({
String? query,
List<int>? tags,
int page = 1,
}) async {
Future<List<Recipe>> getRecipes({String? query, List<int>? tags, int page = 1}) async {
try {
return (await feedService.getRecipes(
query: query,
tags: tags,
page: page,
))
.recipesList;
return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList;
} catch (e) {
rethrow;
}
}
Future<DietsPagination> getDiets({
int page = 1,
String? query,
String? types,
int? duration,
String? sorting,
}) async {
Future<DietsPagination> getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async {
try {
return (await feedService.getDiets(
page: page,
query: query,
types: types,
duration: duration,
sorting: sorting,
));
return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting));
} catch (e) {
rethrow;
}
}
Future<PlanPaginationModel> getPlans({
int page = 1,
String? query,
String? types,
int? duration,
int? minPrice,
int? maxPrice,
}) async {
Future<PlanPaginationModel> getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async {
try {
return (await feedService.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
));
return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice));
} catch (e) {
rethrow;
}
}
Future<CommentModel> getBlogPostComments({
required String id,
}) async {
Future<CommentModel> getBlogPostComments({required String id}) async {
try {
return (await feedService.getBlogPostComments(id: id));
} catch (e) {
@@ -109,25 +61,15 @@ class FeedRepository {
}
}
Future<void> sendBlogPostComment({
required String id,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendBlogPostComment(
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<CommentModel> getDietsComments({
required String id,
}) async {
Future<CommentModel> getDietsComments({required String id}) async {
try {
return (await feedService.getDietsComments(id: id));
} catch (e) {
@@ -135,25 +77,15 @@ class FeedRepository {
}
}
Future<void> sendDietsComment({
required String id,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendDietsComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendDietsComment(
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<CommentModel> getRecipesComments({
required String id,
}) async {
Future<CommentModel> getRecipesComments({required String id}) async {
try {
return (await feedService.getRecipesComments(id: id));
} catch (e) {
@@ -161,25 +93,15 @@ class FeedRepository {
}
}
Future<void> sendRecipesComment({
required String id,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendRecipesComment(
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<CommentModel> getPlansComments({
required String slug,
}) async {
Future<CommentModel> getPlansComments({required String slug}) async {
try {
return (await feedService.getPlansComments(slug: slug));
} catch (e) {
@@ -187,25 +109,15 @@ class FeedRepository {
}
}
Future<void> sendPlansComment({
required String slug,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendPlansComment(
slug: slug,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<void> likeBlogPost({
required int id,
}) async {
Future<void> likeBlogPost({required int id}) async {
try {
await feedService.likeBlogPost(id: id);
} catch (e) {
@@ -213,9 +125,7 @@ class FeedRepository {
}
}
Future<void> unlikedBlogPost({
required int id,
}) async {
Future<void> unlikedBlogPost({required int id}) async {
try {
await feedService.unlikedBlogPost(id: id);
} catch (e) {
@@ -262,4 +172,12 @@ class FeedRepository {
rethrow;
}
}
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await feedService.viewContent(viewModel: viewModel);
} catch (e) {
rethrow;
}
}
}

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:retrofit/retrofit.dart';
part 'feed_service.g.dart';
@@ -131,4 +132,9 @@ abstract class FeedService {
Future<void> viewPlan({
@Path() required String slug,
});
@POST('/api/views')
Future<void> viewContent({
@Body() required ViewModel viewModel,
});
}

View File

@@ -12,7 +12,6 @@ import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_botto
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -64,9 +63,7 @@ class MyPlansScreenContent extends StatelessWidget {
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
backgroundColor: appTheme.gray100,
foregroundColor: appTheme.gray100,
automaticallyImplyLeading: false,
@@ -76,21 +73,12 @@ class MyPlansScreenContent extends StatelessWidget {
snap: true,
scrolledUnderElevation: 0,
bottom: PreferredSize(
preferredSize: Size(
MediaQuery.sizeOf(context).width,
64,
),
preferredSize: Size(MediaQuery.sizeOf(context).width, 32),
child: Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: MyPlansTab(),
),
const SizedBox(height: 8),
Row(
children: [
const SizedBox(width: 4),
@@ -104,20 +92,14 @@ class MyPlansScreenContent extends StatelessWidget {
_ => activePlansSearchController,
},
onChanged: (query) {
EasyDebounce.debounce(
'searchPlans',
const Duration(milliseconds: 500),
() {
context.read<MyPlansCubit>().loadPlans(query: query == '' ? null : query);
},
);
EasyDebounce.debounce('searchPlans', const Duration(milliseconds: 500), () {
context.read<MyPlansCubit>().loadPlans(query: query == '' ? null : query);
});
},
),
),
const SizedBox(width: 12),
AddItemButton(
onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name),
),
AddItemButton(onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name)),
const SizedBox(width: 4),
],
),
@@ -134,10 +116,7 @@ class MyPlansScreenContent extends StatelessWidget {
child: BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (BuildContext context, MyPlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
} else if (state.error != null) {
return DataErrorWidget(onReload: () => context.read<MyPlansCubit>().loadPlans());
} else {
@@ -162,11 +141,7 @@ class MyPlansScreenContent extends StatelessWidget {
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: PlansFilterBottomSheet(
myPlansCubit: context.read<MyPlansCubit>(),
),
),
child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(myPlansCubit: context.read<MyPlansCubit>())),
),
],
);

View File

@@ -14,16 +14,13 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
// Removed SVG icon usage after switching to text buttons
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MyPlanWidget extends StatefulWidget {
final PlanModel plan;
const MyPlanWidget({
super.key,
required this.plan,
});
const MyPlanWidget({super.key, required this.plan});
@override
State<MyPlanWidget> createState() => _MyPlanWidgetState();
@@ -34,10 +31,7 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -48,23 +42,14 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.plan.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
),
),
Positioned(
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.plan,
),
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
),
),
],
@@ -75,19 +60,16 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.plan.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.plan.details.replaceAll('\n', '<br>'),
data: widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
style: {
"*": Style(
margin: Margins.zero,
@@ -116,26 +98,61 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Buttons aligned with MyRecipeWidget: Delete (gray) and Share (blue)
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: switch (state.selectedStateIndex) {
1 => s.publish,
_ => s.edit,
onPressed: () async {
final result = await context.read<MyPlansCubit>().deletePlan(widget.plan.slug);
if (result) {
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
const SizedBox(height: 20),
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(widget.plan.name, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
Text(s.deleted, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
const SizedBox(height: 20),
],
),
);
});
}
},
text: 'Удалить',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
if (state.selectedStateIndex == 0) ...[
const SizedBox(width: 6),
CustomOutlinedButton(
const SizedBox(width: 8),
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
@@ -146,109 +163,14 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
);
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/share2.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn),
),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: '',
text: s.share,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
],
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: switch (state.selectedStateIndex) {
0 => s.removeFromActive,
_ => s.edit,
},
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
const SizedBox(width: 6),
CustomOutlinedButton(
onPressed: () async {
final result = await context.read<MyPlansCubit>().deletePlan(widget.plan.slug);
if (result) {
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
const SizedBox(
height: 20,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text(
widget.plan.name,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
s.deleted,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
const SizedBox(
height: 20,
),
],
),
);
});
}
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/trash_can.svg',
width: 20,
height: 20,
),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
],
),

View File

@@ -1,71 +1,3 @@
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/animated_option_button.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// File intentionally left minimal after removing tabs for My Plans
class MyPlansTab extends StatefulWidget {
const MyPlansTab({super.key, this.additionalOnTap});
final void Function()? additionalOnTap;
@override
State<MyPlansTab> createState() => _MyPlansTabState();
}
class _MyPlansTabState extends State<MyPlansTab> {
final List<String> tabTitles = [];
@override
void didChangeDependencies() {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Container(
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
...List.generate(
2,
(index) => Expanded(
child: AnimatedOptionButton(
textStyle: TextStyles.textSMedium,
isSelected: state.selectedStateIndex == index,
text: tabTitles[index],
onTap: () {
widget.additionalOnTap?.call();
context.read<MyPlansCubit>().changeSelectedStateIndex(index);
},
),
),
),
],
),
),
),
],
),
);
},
);
}
}
// Drafts/published tab UI removed per requirements

View File

@@ -307,7 +307,7 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () {
GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId);
GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId);
},
),
],

View File

@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
@JsonKey(name: "is_blocked_by_author", fromJson: parseBool) required bool isBlockedByAuthor,
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug,

View File

@@ -18,11 +18,7 @@ class ProfileCubit extends Cubit<ProfileState> {
final ConsultantRepository consultantRepository;
String _currentPeriod = '1';
ProfileCubit({
required this.profileRepository,
required this.chatRepository,
required this.consultantRepository,
}) : super(const ProfileState.loading());
ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading());
Future<void> getProfileBySlug(String slug) async {
emit(const ProfileState.loading());
@@ -42,9 +38,7 @@ class ProfileCubit extends Cubit<ProfileState> {
}
}
Future<void> getMyProfile({
String period = '1',
}) async {
Future<void> getMyProfile({String period = '1'}) async {
emit(const ProfileState.loading());
_currentPeriod = period;
try {
@@ -77,15 +71,14 @@ class ProfileCubit extends Cubit<ProfileState> {
profile: (state as ProfileStateLoaded).profile,
profileDetails: (state as ProfileStateLoaded).profileDetails,
blogPosts: feedBlogPostList,
weightPoints: (state as ProfileStateLoaded).weightPoints,
circumferencePoints: (state as ProfileStateLoaded).circumferencePoints,
consultantInfo: (state as ProfileStateLoaded).consultantInfo,
),
);
}
Future<void> addCircumference({
required int chestCircumference,
required int waistCircumference,
required int hipCircumference,
}) async {
Future<void> addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async {
try {
await profileRepository.addCircumference(
chestCircumference: chestCircumference,
@@ -98,9 +91,7 @@ class ProfileCubit extends Cubit<ProfileState> {
}
}
Future<void> addWeight({
required double weight,
}) async {
Future<void> addWeight({required double weight}) async {
try {
await profileRepository.addWeight(weight: weight);
await getMyProfile(period: _currentPeriod);
@@ -109,25 +100,15 @@ class ProfileCubit extends Cubit<ProfileState> {
}
}
Future<String?> getChatRoomIdBySlug({
required Author author,
}) async {
Future<String?> getChatRoomIdBySlug({required Author author}) async {
try {
final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug);
if (chatIdOrError == '404') {
AppRouter.navigatorKey.currentContext?.pushNamed(
'emptyChatScreen',
extra: author,
);
AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: author);
} else if (chatIdOrError == 'error') {
showErrorToast('Ошибка создания чата');
} else {
AppRouter.navigatorKey.currentContext?.pushNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError});
return chatIdOrError;
}
} catch (e, st) {

View File

@@ -1,11 +1,11 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
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/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -200,11 +200,34 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state),
if (state.blogPosts.isNotEmpty)
if (state.profile.isBlockedByAuthor)
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.block, size: 48, color: appTheme.grey7B),
const SizedBox(height: 16),
Center(
child: Text(
'Пользователь ограничил доступ\nк своей странице',
textAlign: TextAlign.center,
style: TextStyles.textMSemibold.copyWith(color: appTheme.grey7B),
),
),
],
),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
_socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
_education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
_diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') &&
state.consultantInfo != null &&
!hidePlans &&
!state.profile.isBlockedByMe &&
!state.profile.isBlockedByAuthor)
_activePlans(state),
if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe)
const Padding(
padding: EdgeInsets.only(left: 8, top: 16, bottom: 8),
child: Text(
@@ -212,7 +235,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600),
),
),
_blogPosts(state),
if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) _blogPosts(state),
],
),
),

View File

@@ -231,7 +231,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
children: [
Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo ? 'да' : 'нет')),
],
),
),
@@ -242,7 +242,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
children: [
Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper ? 'да' : 'нет')),
],
),
),

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
@@ -67,7 +68,10 @@ class MyApp extends StatelessWidget {
child: MaterialApp.router(
theme: ThemeHelper().themeData(),
routerConfig: AppRouter.appRouter,
builder: (context, child) => InternetConnectionCheckerScreen(child: child!),
builder: (context, child) => BlocProvider(
create: (context) => sl<ViewsCubit>(),
child: InternetConnectionCheckerScreen(child: child!),
),
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
S.delegate,

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.12+72
version: 1.2.14+74
publish_to: 'none'
environment:
@@ -74,6 +74,7 @@ dependencies:
flutter_dotenv: ^5.2.1
uuid: ^4.5.1
appmetrica_plugin: ^3.3.0
huawei_push: ^6.11.0+300
dev_dependencies:
build_runner: ^2.7.0

View File

@@ -30,6 +30,7 @@ void main() {
badges: [],
subscriptions: [],
subscribers: [],
isBlockedByAuthor: false,
),
),
);
@@ -72,23 +73,17 @@ void main() {
});
test('getBlogPostByAuthorSlug', () async {
when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer(
(_) async => [],
);
when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA<List<FeedBlogPostModel>>());
});
test('getWeight', () async {
when(mockProfileService.getWeight(period: '')).thenAnswer(
(_) async => [],
);
when(mockProfileService.getWeight(period: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getWeight(period: ''), isA<List<WeightModel>>());
});
test('getCircumferences', () async {
when(mockProfileService.getCircumferences(period: '')).thenAnswer(
(_) async => [],
);
when(mockProfileService.getCircumferences(period: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getCircumferences(period: ''), isA<List<CircumferenceModel>>());
});
});