Merge pull request 'feat(profile): Issue-248' (#621) from mso7nueh/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#621
This commit is contained in:
mso7nueh
2025-08-08 08:11:24 +00:00
3 changed files with 210 additions and 103 deletions

View File

@@ -42,9 +42,16 @@ jobs:
env:
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Flutter build
- name: Setup API key
run: |
echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8
chmod 600 ios/AuthKey.p8
ls -la ios/AuthKey.p8
env:
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
- name: Flutter build
run: |
flutter clean
flutter pub get
flutter pub upgrade

View File

@@ -18,8 +18,16 @@ default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
UI.message "Checking API key file..."
if File.exist?("./AuthKey.p8")
UI.message "API key file exists"
else
UI.user_error!("API key file not found at ./AuthKey.p8")
end
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
@@ -27,7 +35,29 @@ platform :ios do
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true"
)
app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8")
upload_to_testflight
app_store_connect_api_key(
key_id: "325JS9JCZH",
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
key_filepath: "./AuthKey.p8",
duration: 1200,
in_house: false
)
begin
upload_to_testflight(
api_key_path: "./AuthKey.p8",
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
verbose: true
)
rescue => ex
UI.error "Upload to TestFlight failed: #{ex.message}"
UI.error "Full error: #{ex.backtrace.join("\n")}"
raise ex
end
end
end

View File

@@ -760,76 +760,92 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: Row(
children: [
PopupMenuButton(
onSelected: (value) async {
if (value == 'view') {
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0));
}
if (value == 'replace') {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Colors.white,
constraints: const BoxConstraints(minWidth: 260),
surfaceTintColor: Colors.transparent,
elevation: 2,
itemBuilder: (context) => [
_buildCompactItem(
icon: 'assets/icons/eye.svg',
text: 'Посмотреть',
onTap: () async {
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0));
},
),
if (widget.author == null || widget.author!.authorIsMe) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/edit_paint.svg',
text: 'Заменить',
onTap: () async {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
context: context,
);
},
context: context,
);
},
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
AppRouter.navigatorKey.currentContext?.pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
await profileCubit.uploadPhoto(File(image.path));
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
context: context,
);
},
context: context,
);
},
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
);
},
);
},
);
}
if (value == 'delete') {
await profileCubit.deletePhoto();
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 'view', child: Text('Посмотреть')),
if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'replace', child: Text('Заменить')),
if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'delete', child: Text('Удалить')),
),
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/red_trash.svg',
text: 'Удалить',
onTap: () async {
await profileCubit.deletePhoto();
await sl<ProfileAvatarCubit>().getMyProfile();
},
),
],
],
child: Hero(
tag: state.profile.profilePhotoUrl,
@@ -899,41 +915,68 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const Spacer(),
PopupMenuButton(
onSelected: (value) {
if (value == 'share') {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
);
} else if (value == 'block') {
isFirstFetch = true;
sl<BlockedUsersCubit>().blockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
} else if (value == 'unblock') {
isFirstFetch = true;
sl<BlockedUsersCubit>().unblockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
} else {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug),
);
}
},
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Colors.white,
constraints: const BoxConstraints(minWidth: 260),
surfaceTintColor: Colors.transparent,
elevation: 2,
itemBuilder: (context) => [
const PopupMenuItem(value: 'share', child: Text('Поделиться')),
if (widget.author != null) const PopupMenuItem(value: 'complain', child: Text('Пожаловаться')),
if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem(value: 'block', child: Text('Заблокировать')),
if (widget.author != null && state.profile.isBlockedByMe) const PopupMenuItem(value: 'unblock', child: Text('Разблокировать')),
_buildCompactItem(
icon: 'assets/icons/share2.svg',
text: 'Поделиться',
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
);
},
),
if (widget.author != null) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/complain.svg',
text: 'Пожаловаться',
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug),
);
},
),
],
if (widget.author != null && !state.profile.isBlockedByMe) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/complain.svg',
text: 'Заблокировать',
onTap: () {
isFirstFetch = true;
sl<BlockedUsersCubit>().blockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
},
),
],
if (widget.author != null && state.profile.isBlockedByMe) ...[
_buildDivider(),
_buildCompactItem(
icon: 'assets/icons/complain.svg',
text: 'Разблокировать',
onTap: () {
isFirstFetch = true;
sl<BlockedUsersCubit>().unblockUser(state.profile.slug);
widget.author == null || widget.author!.authorIsMe ? profileCubit.getMyProfile() : profileCubit.getProfileBySlug(widget.author!.authorSlug);
if (state.profile.roles.contains('consultant')) {
plansCubit.getPlans(state.profile.slug);
}
isFirstFetch = false;
},
),
],
],
child: const Icon(Icons.more_horiz_rounded),
),
@@ -942,6 +985,33 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
PopupMenuItem _buildCompactItem({required String icon, required String text, required Function() onTap}) {
return PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
height: 36,
onTap: onTap,
child: SizedBox(
width: double.infinity,
child: Row(
children: [
Text(text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Spacer(),
SvgPicture.asset(icon),
],
),
),
);
}
PopupMenuItem _buildDivider() {
return PopupMenuItem(
enabled: false,
height: 1,
padding: EdgeInsets.zero,
child: Container(margin: const EdgeInsets.symmetric(horizontal: 12), height: 1, color: appTheme.dividerColor),
);
}
Row _additional(String title, String value) {
return Row(
children: [