[mob][photos] Show user avatars in email section of save or edit person screen to match figma design

This commit is contained in:
ashilkn 2025-01-31 12:45:28 +05:30
parent 96e8b09555
commit ba53da4a69
4 changed files with 200 additions and 96 deletions

View File

@ -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<Widget>.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;
}
}

View File

@ -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:

View File

@ -28,6 +28,7 @@ class UserAvatarWidget extends StatefulWidget {
@override
State<UserAvatarWidget> createState() => _UserAvatarWidgetState();
static const strokeWidth = 1.0;
}
class _UserAvatarWidgetState extends State<UserAvatarWidget> {
@ -67,7 +68,7 @@ class _UserAvatarWidgetState extends State<UserAvatarWidget> {
color: widget.thumbnailView
? strokeMutedDark
: getEnteColorScheme(context).strokeMuted,
width: 1,
width: UserAvatarWidget.strokeWidth,
strokeAlign: BorderSide.strokeAlignOutside,
),
),
@ -114,21 +115,6 @@ class _UserAvatarWidgetState extends State<UserAvatarWidget> {
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;
}
}

View File

@ -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<User> _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<bool>(
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<bool>(
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<User> _getContacts() {
final List<User> 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;
}
}