forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main' into issue-701
This commit is contained in:
@@ -2,6 +2,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://developer.huawei.com/repo/' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
128
lib/core/views/cubit/views_cubit.dart
Normal file
128
lib/core/views/cubit/views_cubit.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
3
lib/core/views/cubit/views_state.dart
Normal file
3
lib/core/views/cubit/views_state.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class ViewsCubitState {
|
||||
ViewsCubitState();
|
||||
}
|
||||
16
lib/core/views/repository/views_repository.dart
Normal file
16
lib/core/views/repository/views_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/core/views/service/views_service.dart
Normal file
15
lib/core/views/service/views_service.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
12
lib/features/auth/models/social_exchange_request.dart
Normal file
12
lib/features/auth/models/social_exchange_request.dart
Normal 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};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
lib/features/auth/sdk/social_auth_facade.dart
Normal file
25
lib/features/auth/sdk/social_auth_facade.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
16
lib/features/feed/models/view_model.dart
Normal file
16
lib/features/feed/models/view_model.dart
Normal 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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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>())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 ? 'да' : 'нет')),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user