mirror of
https://github.com/ente-io/ente.git
synced 2025-04-30 11:35:46 +00:00
324 lines
10 KiB
Dart
324 lines
10 KiB
Dart
import "dart:async";
|
|
|
|
import "package:collection/collection.dart";
|
|
import 'package:flutter/material.dart';
|
|
import "package:logging/logging.dart";
|
|
import "package:photos/core/configuration.dart";
|
|
import "package:photos/core/event_bus.dart";
|
|
import "package:photos/events/people_changed_event.dart";
|
|
import "package:photos/extensions/user_extension.dart";
|
|
import "package:photos/models/api/collection/user.dart";
|
|
import "package:photos/models/file/file.dart";
|
|
import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
|
|
import "package:photos/theme/colors.dart";
|
|
import 'package:photos/theme/ente_theme.dart';
|
|
import "package:photos/ui/viewer/search/result/person_face_widget.dart";
|
|
import "package:photos/utils/standalone/debouncer.dart";
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
enum AvatarType { small, mini, tiny, extra }
|
|
|
|
class UserAvatarWidget extends StatefulWidget {
|
|
final User user;
|
|
final AvatarType type;
|
|
final int currentUserID;
|
|
final bool thumbnailView;
|
|
|
|
const UserAvatarWidget(
|
|
this.user, {
|
|
super.key,
|
|
this.currentUserID = -1,
|
|
this.type = AvatarType.mini,
|
|
this.thumbnailView = false,
|
|
});
|
|
|
|
@override
|
|
State<UserAvatarWidget> createState() => _UserAvatarWidgetState();
|
|
static const strokeWidth = 1.0;
|
|
}
|
|
|
|
class _UserAvatarWidgetState extends State<UserAvatarWidget> {
|
|
Future<String?>? _personID;
|
|
EnteFile? _faceThumbnail;
|
|
final _logger = Logger("_UserAvatarWidgetState");
|
|
late final StreamSubscription<PeopleChangedEvent> _peopleChangedSubscription;
|
|
final _debouncer = Debouncer(
|
|
const Duration(milliseconds: 250),
|
|
executionInterval: const Duration(seconds: 20),
|
|
leading: true,
|
|
);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_reload();
|
|
_peopleChangedSubscription =
|
|
Bus.instance.on<PeopleChangedEvent>().listen((event) {
|
|
if (event.type == PeopleEventType.saveOrEditPerson ||
|
|
event.type == PeopleEventType.syncDone) {
|
|
_reload();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_peopleChangedSubscription.cancel();
|
|
_debouncer.cancelDebounceTimer();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _reload() async {
|
|
_debouncer.run(() async {
|
|
setState(() {
|
|
if (PersonService
|
|
.instance.emailToPartialPersonDataMapCache[widget.user.email] !=
|
|
null) {
|
|
_personID = PersonService.instance.getPersons().then((people) async {
|
|
final person = people.firstWhereOrNull(
|
|
(person) => person.data.email == widget.user.email,
|
|
);
|
|
if (person != null) {
|
|
_faceThumbnail =
|
|
await PersonService.instance.getThumbnailFileOfPerson(person);
|
|
}
|
|
return person?.remoteID;
|
|
});
|
|
} else {
|
|
_personID = null;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final double size = getAvatarSize(widget.type);
|
|
|
|
return _personID != null
|
|
? Container(
|
|
padding: const EdgeInsets.all(0.5),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: widget.thumbnailView
|
|
? strokeMutedDark
|
|
: getEnteColorScheme(context).strokeMuted,
|
|
width: UserAvatarWidget.strokeWidth,
|
|
strokeAlign: BorderSide.strokeAlignOutside,
|
|
),
|
|
),
|
|
child: SizedBox(
|
|
height: size,
|
|
width: size,
|
|
child: FutureBuilder(
|
|
future: _personID,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData) {
|
|
final personID = snapshot.data as String;
|
|
return ClipOval(
|
|
child: _faceThumbnail == null
|
|
? _FirstLetterCircularAvatar(
|
|
user: widget.user,
|
|
currentUserID: widget.currentUserID,
|
|
thumbnailView: widget.thumbnailView,
|
|
type: widget.type,
|
|
)
|
|
: PersonFaceWidget(
|
|
_faceThumbnail!,
|
|
personId: personID,
|
|
onErrorCallback: () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_personID = null;
|
|
_faceThumbnail = null;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
);
|
|
} else if (snapshot.hasError) {
|
|
_logger.severe("Error loading personID", snapshot.error);
|
|
return _FirstLetterCircularAvatar(
|
|
user: widget.user,
|
|
currentUserID: widget.currentUserID,
|
|
thumbnailView: widget.thumbnailView,
|
|
type: widget.type,
|
|
);
|
|
} else if (snapshot.connectionState == ConnectionState.done &&
|
|
snapshot.data == null) {
|
|
return _FirstLetterCircularAvatar(
|
|
user: widget.user,
|
|
currentUserID: widget.currentUserID,
|
|
thumbnailView: widget.thumbnailView,
|
|
type: widget.type,
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
)
|
|
: _FirstLetterCircularAvatar(
|
|
user: widget.user,
|
|
currentUserID: widget.currentUserID,
|
|
thumbnailView: widget.thumbnailView,
|
|
type: widget.type,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FirstLetterCircularAvatar extends StatefulWidget {
|
|
final User user;
|
|
final int currentUserID;
|
|
final bool thumbnailView;
|
|
final AvatarType type;
|
|
const _FirstLetterCircularAvatar({
|
|
required this.user,
|
|
required this.currentUserID,
|
|
required this.thumbnailView,
|
|
required this.type,
|
|
});
|
|
|
|
@override
|
|
State<_FirstLetterCircularAvatar> createState() =>
|
|
_FirstLetterCircularAvatarState();
|
|
}
|
|
|
|
class _FirstLetterCircularAvatarState
|
|
extends State<_FirstLetterCircularAvatar> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = getEnteColorScheme(context);
|
|
final displayChar =
|
|
(widget.user.displayName == null || widget.user.displayName!.isEmpty)
|
|
? ((widget.user.email.isEmpty)
|
|
? " "
|
|
: widget.user.email.substring(0, 1))
|
|
: widget.user.displayName!.substring(0, 1);
|
|
Color decorationColor;
|
|
if ((widget.user.id != null && widget.user.id! < 0) ||
|
|
widget.user.email == Configuration.instance.getEmail()) {
|
|
decorationColor = Colors.black;
|
|
} else {
|
|
decorationColor = colorScheme.avatarColors[(widget.user.email.length)
|
|
.remainder(colorScheme.avatarColors.length)];
|
|
}
|
|
|
|
final avatarStyle = getAvatarStyle(context, widget.type);
|
|
final double size = avatarStyle.item1;
|
|
final TextStyle textStyle = avatarStyle.item2;
|
|
return Container(
|
|
padding: const EdgeInsets.all(0.5),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: widget.thumbnailView
|
|
? strokeMutedDark
|
|
: getEnteColorScheme(context).strokeMuted,
|
|
width: UserAvatarWidget.strokeWidth,
|
|
strokeAlign: BorderSide.strokeAlignOutside,
|
|
),
|
|
),
|
|
child: SizedBox(
|
|
height: size,
|
|
width: size,
|
|
child: CircleAvatar(
|
|
backgroundColor: decorationColor,
|
|
child: Text(
|
|
displayChar.toUpperCase(),
|
|
// fixed color
|
|
style: textStyle.copyWith(color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Tuple2<double, TextStyle> getAvatarStyle(
|
|
BuildContext context,
|
|
AvatarType type,
|
|
) {
|
|
final enteTextTheme = getEnteTextTheme(context);
|
|
switch (type) {
|
|
case AvatarType.small:
|
|
return Tuple2(32.0, enteTextTheme.small);
|
|
case AvatarType.mini:
|
|
return Tuple2(24.0, enteTextTheme.mini);
|
|
case AvatarType.tiny:
|
|
return Tuple2(18.0, enteTextTheme.tiny);
|
|
case AvatarType.extra:
|
|
return Tuple2(18.0, enteTextTheme.tiny);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
class FirstLetterUserAvatar extends StatefulWidget {
|
|
final User user;
|
|
const FirstLetterUserAvatar(this.user, {super.key});
|
|
|
|
@override
|
|
State<FirstLetterUserAvatar> createState() => _FirstLetterUserAvatarState();
|
|
}
|
|
|
|
class _FirstLetterUserAvatarState extends State<FirstLetterUserAvatar> {
|
|
final currentUserEmail = Configuration.instance.getEmail();
|
|
late User user;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
user = widget.user;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant FirstLetterUserAvatar oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.user != widget.user) {
|
|
setState(() {
|
|
user = widget.user;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = getEnteColorScheme(context);
|
|
final displayChar = (user.displayName == null || user.displayName!.isEmpty)
|
|
? ((user.email.isEmpty) ? " " : user.email.substring(0, 1))
|
|
: user.displayName!.substring(0, 1);
|
|
Color decorationColor;
|
|
if ((widget.user.id != null && widget.user.id! < 0) ||
|
|
user.email == currentUserEmail) {
|
|
decorationColor = Colors.black;
|
|
} else {
|
|
decorationColor = colorScheme.avatarColors[
|
|
(user.email.length).remainder(colorScheme.avatarColors.length)];
|
|
}
|
|
return Container(
|
|
color: decorationColor,
|
|
child: Center(
|
|
child: Text(
|
|
displayChar.toUpperCase(),
|
|
style: getEnteTextTheme(context).small.copyWith(color: Colors.white),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|