Merge pull request 'Issue-514 and Issue-497' (#516) from mso7nueh/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#516
This commit is contained in:
mso7nueh
2025-06-20 08:41:24 +00:00
19 changed files with 280 additions and 147 deletions

View File

@@ -5,6 +5,7 @@
<application
android:label="Eatvio"
android:usesCleartextTraffic="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>27</string>
<string>28</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -31,6 +31,8 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string>
<key>NSCameraUsageDescription</key>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSLocationWhenInUseUsageDescription</key>

View File

@@ -160,6 +160,7 @@ class AppRouter {
name: 'welcomeScreen',
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
@@ -168,22 +169,24 @@ class AppRouter {
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios': 'android',
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken,
},
);
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return '/feed-screen';
} else {
return '/auth-screen';
}
} catch (e) {
rethrow;
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
return '/feed-screen';
} else {
return null;
}
},
),
GoRoute(
@@ -191,14 +194,18 @@ class AppRouter {
name: 'updateScreen',
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
try {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
return '/welcome-screen';
}
} catch (e) {
return '/welcome-screen';
}
},

View File

@@ -52,15 +52,23 @@ class _AuthScreenState extends State<AuthScreen> {
}
Future<void> _reconnectWebSocket(BuildContext context) async {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
try {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
} catch (e) {
rethrow;
}
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
}

View File

@@ -82,7 +82,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl),
foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),

View File

@@ -21,15 +21,18 @@ 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:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget {
final String chatId;
final bool? sendInitialMessage;
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
@override
@@ -338,37 +341,7 @@ class _ChatScreenState extends State<ChatScreen> {
children: [
const SizedBox(width: 8),
InkWell(
onTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files);
}
},
onTap: () => showImagePicker(),
customBorder: const CircleBorder(),
child: Ink(
height: 40,
@@ -448,6 +421,78 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await chatRoomCubit.sendMessage(chatId: widget.chatId, files: [File(image.path)]);
}
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files);
}
},
child: const Row(
children: [
Icon(Icons.file_present_rounded),
SizedBox(width: 8),
Text('Файлы'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
}
_messageDate(DateTime date) {
return Center(
child: Padding(

View File

@@ -21,6 +21,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
import '../widgets/recipes.dart';
@@ -100,6 +101,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
@override
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return Scaffold(
floatingActionButton: _selectedChipIndex == 1
? FloatingActionButton.small(

View File

@@ -56,81 +56,85 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
authorName: state.feedBlogPostList[widget.index].author.authorName,
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon,
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
),
);
},
child: CircleAvatar(
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),
const SizedBox(width: 8),
// Имя и дата внутри Expanded
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.feedBlogPostList[widget.index].author.authorName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
fontSize: 16,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
authorName: state.feedBlogPostList[widget.index].author.authorName,
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
? state.feedBlogPostList[widget.index].author.authorProfileIcon
: baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
),
);
},
child: CircleAvatar(
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
? state.feedBlogPostList[widget.index].author.authorProfileIcon
: baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
),
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,
),
),
],
),
),
// Кнопка подписки (если нужно)
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
child: SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),
// Кнопка меню
CustomPopup(
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
id: state.feedBlogPostList[widget.index].id,
feedBlogPostModel: state.feedBlogPostList[widget.index],
),
],
const SizedBox(width: 8),
// Имя и дата внутри Expanded
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.feedBlogPostList[widget.index].author.authorName,
overflow: TextOverflow.ellipsis,
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,
),
),
],
),
),
// Кнопка подписки (если нужно)
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
child: SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
),
),
// Кнопка меню
CustomPopup(
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
id: state.feedBlogPostList[widget.index].id,
feedBlogPostModel: state.feedBlogPostList[widget.index],
),
],
),
),
),
if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[
if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),

View File

@@ -72,13 +72,15 @@ class _CommentsWidgetState extends State<CommentsWidget> {
extra: Author(
authorSlug: comment.authorSlug,
authorName: comment.authorName,
authorProfileIcon: comment.authorProfileIcon,
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false,
),
);
},
child: CircleAvatar(
foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon),
foregroundImage: NetworkImage(
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius,
),

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/svg.dart';
import 'package:http/http.dart' as http;
@@ -116,6 +117,7 @@ class NoInternetScreen extends StatelessWidget {
final Future<void> Function() onTryAgain;
@override
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return Scaffold(
body: Column(
children: [

View File

@@ -1085,7 +1085,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
tag: state.profile.profilePhotoUrl,
child: CircleAvatar(
radius: 32,
foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl),
foregroundImage: NetworkImage(
state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : baseUrl + state.profile.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),

View File

@@ -67,7 +67,9 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
state.blogPosts[widget.index].author.authorProfileIcon.contains('https')
? state.blogPosts[widget.index].author.authorProfileIcon
: baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
@@ -119,8 +121,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
),
],
if (state.blogPosts[widget.index].title.isEmpty) const SizedBox(height: 8),
if (state.blogPosts[widget.index].title.isNotEmpty && state.blogPosts[widget.index].description.isNotEmpty)
const SizedBox(height: 8),
if (state.blogPosts[widget.index].title.isNotEmpty && state.blogPosts[widget.index].description.isNotEmpty) const SizedBox(height: 8),
if (state.blogPosts[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),

View File

@@ -38,4 +38,36 @@ class SettingsRepository {
rethrow;
}
}
Future<void> sendEmailCodeForPasswordChange(String email) async {
try {
return await settingsService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
} catch (e) {
rethrow;
}
}
Future<void> confirmPasswordChange(
String code,
String password,
String passwordConfirmation,
String email,
) async {
try {
return await settingsService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
} catch (e) {
rethrow;
}
}
}

View File

@@ -21,4 +21,14 @@ abstract class SettingsService {
Future<ProfileDetailsModel> updateProfileDetails({
@Body() required Map<String, dynamic> data,
});
@POST('/api/send_password_reset_code')
Future<void> sendEmailCodeForPasswordChange({
@Body() required Map<String, dynamic> data,
});
@POST('/api/confirm_password_reset_code')
Future<void> confirmPasswordChange({
@Body() required Map<String, dynamic> data,
});
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -11,6 +12,7 @@ class UpdateScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return Scaffold(
body: Stack(
clipBehavior: Clip.none,

View File

@@ -1,9 +1,10 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/theme/theme_helper.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@@ -23,6 +24,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
@override
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return Scaffold(
body: Stack(
clipBehavior: Clip.none,
@@ -80,7 +82,11 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
onTap: () async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setBool('welcomeScreenWasShowed', true);
goRouter.go('/feed-screen');
if (await TokenStorage.haveTokenInStorage()) {
goRouter.go('/feed-screen');
} else {
goRouter.go('/auth-screen');
}
},
borderRadius: BorderRadius.circular(8),
child: Ink(

View File

@@ -20,15 +20,20 @@ import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await di.init();
await SentryFlutter.init((options) {
options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2';
options.tracesSampleRate = 1.0;
}, appRunner: () => runApp(const MyApp()));
FlutterNativeSplash.remove();
await SentryFlutter.init(
(options) {
options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2';
options.tracesSampleRate = 1.0;
options.debug = false;
},
appRunner: () => runApp(const MyApp()),
);
}
class MyApp extends StatelessWidget {

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.1.4+34
version: 1.1.5+35
publish_to: 'none'
environment: