diff --git a/mobile/lib/ui/sharing/album_share_info_widget.dart b/mobile/lib/ui/sharing/album_share_info_widget.dart index 7c1017ea2d..e856862146 100644 --- a/mobile/lib/ui/sharing/album_share_info_widget.dart +++ b/mobile/lib/ui/sharing/album_share_info_widget.dart @@ -12,6 +12,7 @@ class AlbumSharesIcons extends StatelessWidget { final bool removeBorder; final EdgeInsets padding; final Widget? trailingWidget; + final Alignment stackAlignment; const AlbumSharesIcons({ super.key, @@ -21,13 +22,14 @@ class AlbumSharesIcons extends StatelessWidget { this.removeBorder = true, this.trailingWidget, this.padding = const EdgeInsets.only(left: 10.0, top: 10, bottom: 10), + this.stackAlignment = Alignment.topLeft, }); @override Widget build(BuildContext context) { final displayCount = min(sharees.length, limitCountTo); final hasMore = sharees.length > limitCountTo; - final double overlapPadding = type == AvatarType.tiny ? 14.0 : 20.0; + final double overlapPadding = getOverlapPadding(type); final widgets = List.generate( displayCount, (index) => Positioned( @@ -46,9 +48,7 @@ class AlbumSharesIcons extends StatelessWidget { left: (overlapPadding * displayCount), child: MoreCountWidget( sharees.length - displayCount, - type: type == AvatarType.tiny - ? MoreCountType.tiny - : MoreCountType.mini, + type: moreCountTypeFromAvatarType(type), thumbnailView: removeBorder, ), ), @@ -67,9 +67,36 @@ class AlbumSharesIcons extends StatelessWidget { return Padding( padding: padding, child: Stack( + alignment: stackAlignment, clipBehavior: Clip.none, children: widgets, ), ); } } + +double getOverlapPadding(AvatarType type) { + switch (type) { + case AvatarType.extra: + return 14.0; + case AvatarType.tiny: + return 14.0; + case AvatarType.mini: + return 20.0; + case AvatarType.small: + return 28.0; + } +} + +MoreCountType moreCountTypeFromAvatarType(AvatarType type) { + switch (type) { + case AvatarType.extra: + return MoreCountType.extra; + case AvatarType.tiny: + return MoreCountType.tiny; + case AvatarType.mini: + return MoreCountType.mini; + case AvatarType.small: + return MoreCountType.small; + } +} diff --git a/mobile/lib/ui/sharing/more_count_badge.dart b/mobile/lib/ui/sharing/more_count_badge.dart index c828642bbc..0fa74ac9c1 100644 --- a/mobile/lib/ui/sharing/more_count_badge.dart +++ b/mobile/lib/ui/sharing/more_count_badge.dart @@ -67,7 +67,7 @@ class MoreCountWidget extends StatelessWidget { final enteTextTheme = getEnteTextTheme(context); switch (type) { case MoreCountType.small: - return Tuple2(36.0, enteTextTheme.small); + return Tuple2(32.0, enteTextTheme.small); case MoreCountType.mini: return Tuple2(24.0, enteTextTheme.mini); case MoreCountType.tiny: diff --git a/mobile/lib/ui/sharing/user_avator_widget.dart b/mobile/lib/ui/sharing/user_avator_widget.dart index 58974d116e..b6df9cbd5b 100644 --- a/mobile/lib/ui/sharing/user_avator_widget.dart +++ b/mobile/lib/ui/sharing/user_avator_widget.dart @@ -28,6 +28,7 @@ class UserAvatarWidget extends StatefulWidget { @override State createState() => _UserAvatarWidgetState(); + static const strokeWidth = 1.0; } class _UserAvatarWidgetState extends State { @@ -67,7 +68,7 @@ class _UserAvatarWidgetState extends State { color: widget.thumbnailView ? strokeMutedDark : getEnteColorScheme(context).strokeMuted, - width: 1, + width: UserAvatarWidget.strokeWidth, strokeAlign: BorderSide.strokeAlignOutside, ), ), @@ -114,21 +115,6 @@ class _UserAvatarWidgetState extends State { type: widget.type, ); } - - double getAvatarSize( - AvatarType type, - ) { - switch (type) { - case AvatarType.small: - return 36.0; - case AvatarType.mini: - return 24.0; - case AvatarType.tiny: - return 18.0; - case AvatarType.extra: - return 18.0; - } - } } class _FirstLetterAvatar extends StatefulWidget { @@ -178,7 +164,7 @@ class _FirstLetterAvatarState extends State<_FirstLetterAvatar> { color: widget.thumbnailView ? strokeMutedDark : getEnteColorScheme(context).strokeMuted, - width: 1.0, + width: UserAvatarWidget.strokeWidth, strokeAlign: BorderSide.strokeAlignOutside, ), ), @@ -204,7 +190,7 @@ class _FirstLetterAvatarState extends State<_FirstLetterAvatar> { final enteTextTheme = getEnteTextTheme(context); switch (type) { case AvatarType.small: - return Tuple2(36.0, enteTextTheme.small); + return Tuple2(32.0, enteTextTheme.small); case AvatarType.mini: return Tuple2(24.0, enteTextTheme.mini); case AvatarType.tiny: @@ -214,3 +200,18 @@ class _FirstLetterAvatarState extends State<_FirstLetterAvatar> { } } } + +double getAvatarSize( + AvatarType type, +) { + switch (type) { + case AvatarType.small: + return 32.0; + case AvatarType.mini: + return 24.0; + case AvatarType.tiny: + return 18.0; + case AvatarType.extra: + return 18.0; + } +} diff --git a/mobile/lib/ui/viewer/people/save_or_edit_person.dart b/mobile/lib/ui/viewer/people/save_or_edit_person.dart index a3563a6b4c..ec47cbfe87 100644 --- a/mobile/lib/ui/viewer/people/save_or_edit_person.dart +++ b/mobile/lib/ui/viewer/people/save_or_edit_person.dart @@ -16,18 +16,23 @@ import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/generated/protos/ente/common/vector.pb.dart"; import "package:photos/l10n/l10n.dart"; +import "package:photos/models/api/collection/user.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/ml/face/person.dart"; +import "package:photos/services/collections_service.dart"; import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/machine_learning/ml_result.dart"; import "package:photos/services/search_service.dart"; +import "package:photos/services/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/date_input.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/action_sheet_widget.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; +import "package:photos/ui/sharing/album_share_info_widget.dart"; +import "package:photos/ui/sharing/user_avator_widget.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/gallery/hooks/pick_person_avatar.dart"; import "package:photos/ui/viewer/people/link_email_screen.dart"; @@ -778,12 +783,14 @@ class _EmailSectionState extends State<_EmailSection> { String? _email; final _logger = Logger("_EmailSectionState"); bool _initialEmailIsUserEmail = false; + late final List _contacts; @override void initState() { super.initState(); _email = widget.email; _initialEmailIsUserEmail = Configuration.instance.getEmail() == _email; + _contacts = _getContacts(); } @override @@ -798,6 +805,9 @@ class _EmailSectionState extends State<_EmailSection> { @override Widget build(BuildContext context) { + const limitCountTo = 5; + final avatarSize = getAvatarSize(AvatarType.small); + final overlapPadding = getOverlapPadding(AvatarType.small); if (_email == null || _email!.isEmpty) { return AnimatedSize( duration: const Duration(milliseconds: 200), @@ -810,80 +820,108 @@ class _EmailSectionState extends State<_EmailSection> { color: getEnteColorScheme(context).fillFaint, borderRadius: BorderRadius.circular(12.0), ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FutureBuilder( - future: _isMeAssigned(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final isMeAssigned = snapshot.data!; - if (!isMeAssigned || _initialEmailIsUserEmail) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: ButtonWidget( - buttonType: ButtonType.secondary, - labelText: "This is me!", - onTap: () async { - _updateEmailField( - Configuration.instance.getEmail(), - ); - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: ButtonWidget( - buttonType: ButtonType.primary, - labelText: "Link email", - shouldSurfaceExecutionStates: false, - onTap: () async { - final newEmail = await routeToPage( - context, - LinkEmailScreen( - widget.personID, - isFromSaveOrEditPerson: true, - ), - ); - if (newEmail != null) { - _updateEmailField(newEmail as String); - } - }, - ), - ), - ], - ); - } else { - return ButtonWidget( - buttonType: ButtonType.primary, - labelText: "Link email", - shouldSurfaceExecutionStates: false, - onTap: () async { - final newEmail = await routeToPage( - context, - LinkEmailScreen( - widget.personID, - isFromSaveOrEditPerson: true, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_contacts.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + height: 32 + 2 * UserAvatarWidget.strokeWidth, + width: ((avatarSize) * (limitCountTo + 1)) - + (((avatarSize) - overlapPadding) * limitCountTo) + + (2 * UserAvatarWidget.strokeWidth), + child: AlbumSharesIcons( + sharees: _contacts, + limitCountTo: limitCountTo, + type: AvatarType.small, + padding: EdgeInsets.zero, + stackAlignment: Alignment.center, + ), + ), + ), + if (_contacts.isNotEmpty) const SizedBox(height: 38), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: FutureBuilder( + future: _isMeAssigned(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final isMeAssigned = snapshot.data!; + if (!isMeAssigned || _initialEmailIsUserEmail) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: ButtonWidget( + buttonType: ButtonType.secondary, + labelText: "This is me!", + onTap: () async { + _updateEmailField( + Configuration.instance.getEmail(), + ); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: ButtonWidget( + buttonType: ButtonType.primary, + labelText: "Link email", + shouldSurfaceExecutionStates: false, + onTap: () async { + final newEmail = await routeToPage( + context, + LinkEmailScreen( + widget.personID, + isFromSaveOrEditPerson: true, + ), + ); + if (newEmail != null) { + _updateEmailField(newEmail as String); + } + }, + ), + ), + ], ); - if (newEmail != null) { - _updateEmailField(newEmail as String); - } - }, - ); - } - } else if (snapshot.hasError) { - _logger.severe( - "Error getting isMeAssigned", - snapshot.error, - ); - return const RepaintBoundary(child: EnteLoadingWidget()); - } else { - return const RepaintBoundary(child: EnteLoadingWidget()); - } - }, - ), + } else { + return ButtonWidget( + buttonType: ButtonType.primary, + labelText: "Link email", + shouldSurfaceExecutionStates: false, + onTap: () async { + final newEmail = await routeToPage( + context, + LinkEmailScreen( + widget.personID, + isFromSaveOrEditPerson: true, + ), + ); + if (newEmail != null) { + _updateEmailField(newEmail as String); + } + }, + ); + } + } else if (snapshot.hasError) { + _logger.severe( + "Error getting isMeAssigned", + snapshot.error, + ); + return const RepaintBoundary( + child: EnteLoadingWidget(), + ); + } else { + return const RepaintBoundary( + child: EnteLoadingWidget(), + ); + } + }, + ), + ), + ], ), ).animate().fadeIn( duration: const Duration(milliseconds: 200), @@ -946,4 +984,42 @@ class _EmailSectionState extends State<_EmailSection> { saveOrEditPersonState._email = newEmail; }); } + + List _getContacts() { + final List suggestedUsers = []; + final int ownerID = Configuration.instance.getUserID()!; + final cachedEmailToPartialPersonDataMap = + PersonService.instance.emailToPartialPersonDataMapCache; + + for (final c in CollectionsService.instance.getActiveCollections()) { + if (c.owner?.id == ownerID) { + for (final User? u in c.sharees ?? []) { + if (u != null && u.id != null && u.email.isNotEmpty) { + if (!suggestedUsers.any((user) => user.email == u.email) && + cachedEmailToPartialPersonDataMap[u.email] == null) { + suggestedUsers.add(u); + } + } + } + } else if (c.owner?.id != null && c.owner!.email.isNotEmpty) { + if (!suggestedUsers.any((user) => user.email == c.owner!.email) && + cachedEmailToPartialPersonDataMap[c.owner!.email] == null) { + suggestedUsers.add(c.owner!); + } + } + } + final cachedUserDetails = UserService.instance.getCachedUserDetails(); + if (cachedUserDetails?.familyData?.members?.isNotEmpty ?? false) { + for (final member in cachedUserDetails!.familyData!.members!) { + if (!suggestedUsers.any((user) => user.email == member.email) && + cachedEmailToPartialPersonDataMap[member.email] == null) { + suggestedUsers.add(User(email: member.email)); + } + } + } + + suggestedUsers.sort((a, b) => a.email.compareTo(b.email)); + + return suggestedUsers; + } }