123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- import "dart:developer" show log;
- import "dart:io" show Platform;
- import "dart:typed_data";
- import "package:flutter/cupertino.dart";
- import "package:flutter/foundation.dart" show kDebugMode;
- import "package:flutter/material.dart";
- import "package:photos/face/db.dart";
- import "package:photos/face/model/face.dart";
- import "package:photos/face/model/person.dart";
- import 'package:photos/models/file/file.dart';
- import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart";
- import "package:photos/services/search_service.dart";
- import "package:photos/theme/ente_theme.dart";
- import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
- import "package:photos/ui/viewer/people/cluster_page.dart";
- import "package:photos/ui/viewer/people/cropped_face_image_view.dart";
- import "package:photos/ui/viewer/people/people_page.dart";
- import "package:photos/utils/face/face_box_crop.dart";
- import "package:photos/utils/thumbnail_util.dart";
- // import "package:photos/utils/toast_util.dart";
- class FaceWidget extends StatefulWidget {
- final EnteFile file;
- final Face face;
- final Person? person;
- final int? clusterID;
- final bool highlight;
- final bool editMode;
- const FaceWidget(
- this.file,
- this.face, {
- this.person,
- this.clusterID,
- this.highlight = false,
- this.editMode = false,
- Key? key,
- }) : super(key: key);
- @override
- State<FaceWidget> createState() => _FaceWidgetState();
- }
- class _FaceWidgetState extends State<FaceWidget> {
- bool isJustRemoved = false;
- @override
- Widget build(BuildContext context) {
- if (Platform.isIOS || Platform.isAndroid) {
- return FutureBuilder<Uint8List?>(
- future: getFaceCrop(),
- builder: (context, snapshot) {
- if (snapshot.hasData) {
- final ImageProvider imageProvider = MemoryImage(snapshot.data!);
- return GestureDetector(
- onTap: () async {
- if (widget.editMode) return;
- log(
- "FaceWidget is tapped, with person ${widget.person} and clusterID ${widget.clusterID}",
- name: "FaceWidget",
- );
- if (widget.person == null && widget.clusterID == null) {
- return;
- }
- if (widget.person != null) {
- await Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => PeoplePage(
- person: widget.person!,
- ),
- ),
- );
- } else if (widget.clusterID != null) {
- final fileIdsToClusterIds =
- await FaceMLDataDB.instance.getFileIdToClusterIds();
- final files = await SearchService.instance.getAllFiles();
- final clusterFiles = files
- .where(
- (file) =>
- fileIdsToClusterIds[file.uploadedFileID]
- ?.contains(widget.clusterID) ??
- false,
- )
- .toList();
- await Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => ClusterPage(
- clusterFiles,
- clusterID: widget.clusterID!,
- ),
- ),
- );
- }
- },
- child: Column(
- children: [
- Stack(
- children: [
- Container(
- height: 60,
- width: 60,
- decoration: ShapeDecoration(
- shape: RoundedRectangleBorder(
- borderRadius: const BorderRadius.all(
- Radius.elliptical(16, 12),
- ),
- side: widget.highlight
- ? BorderSide(
- color:
- getEnteColorScheme(context).primary700,
- width: 1.0,
- )
- : BorderSide.none,
- ),
- ),
- child: ClipRRect(
- borderRadius:
- const BorderRadius.all(Radius.elliptical(16, 12)),
- child: SizedBox(
- width: 60,
- height: 60,
- child: Image(
- image: imageProvider,
- fit: BoxFit.cover,
- ),
- ),
- ),
- ),
- // TODO: the edges of the green line are still not properly rounded around ClipRRect
- if (widget.editMode)
- Positioned(
- right: 0,
- top: 0,
- child: GestureDetector(
- onTap: _cornerIconPressed,
- child: isJustRemoved
- ? const Icon(
- CupertinoIcons.add_circled_solid,
- color: Colors.green,
- )
- : const Icon(
- Icons.cancel,
- color: Colors.red,
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- if (widget.person != null)
- Text(
- widget.person!.attr.name.trim(),
- style: Theme.of(context).textTheme.bodySmall,
- overflow: TextOverflow.ellipsis,
- maxLines: 1,
- ),
- if (kDebugMode)
- Text(
- 'S: ${widget.face.score.toStringAsFixed(3)}',
- style: Theme.of(context).textTheme.bodySmall,
- maxLines: 1,
- ),
- if (kDebugMode)
- Text(
- 'B: ${widget.face.blur.toStringAsFixed(3)}',
- style: Theme.of(context).textTheme.bodySmall,
- maxLines: 1,
- ),
- if (kDebugMode)
- Text(
- 'V: ${widget.face.detection.getVisibilityScore()}',
- style: Theme.of(context).textTheme.bodySmall,
- maxLines: 1,
- ),
- // if (kDebugMode)
- // if (highlight)
- // const Text(
- // "Highlighted",
- // style: TextStyle(
- // color: Colors.red,
- // fontSize: 12,
- // ),
- // ),
- ],
- ),
- );
- } else {
- if (snapshot.connectionState == ConnectionState.waiting) {
- return const ClipRRect(
- borderRadius: BorderRadius.all(Radius.elliptical(16, 12)),
- child: SizedBox(
- width: 60, // Ensure consistent sizing
- height: 60,
- child: CircularProgressIndicator(),
- ),
- );
- }
- if (snapshot.hasError) {
- log('Error getting face: ${snapshot.error}');
- }
- return const ClipRRect(
- borderRadius: BorderRadius.all(Radius.elliptical(16, 12)),
- child: SizedBox(
- width: 60, // Ensure consistent sizing
- height: 60,
- child: NoThumbnailWidget(),
- ),
- );
- }
- },
- );
- } else {
- return Builder(
- builder: (context) {
- return GestureDetector(
- onTap: () async {
- log(
- "FaceWidget is tapped, with person ${widget.person} and clusterID ${widget.clusterID}",
- name: "FaceWidget",
- );
- if (widget.person == null && widget.clusterID == null) {
- return;
- }
- if (widget.person != null) {
- await Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => PeoplePage(
- person: widget.person!,
- ),
- ),
- );
- } else if (widget.clusterID != null) {
- final fileIdsToClusterIds =
- await FaceMLDataDB.instance.getFileIdToClusterIds();
- final files = await SearchService.instance.getAllFiles();
- final clusterFiles = files
- .where(
- (file) =>
- fileIdsToClusterIds[file.uploadedFileID]
- ?.contains(widget.clusterID) ??
- false,
- )
- .toList();
- await Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => ClusterPage(
- clusterFiles,
- clusterID: widget.clusterID!,
- ),
- ),
- );
- }
- },
- child: Column(
- children: [
- Container(
- height: 60,
- width: 60,
- decoration: ShapeDecoration(
- shape: RoundedRectangleBorder(
- borderRadius:
- const BorderRadius.all(Radius.elliptical(16, 12)),
- side: widget.highlight
- ? BorderSide(
- color: getEnteColorScheme(context).primary700,
- width: 2.0,
- )
- : BorderSide.none,
- ),
- ),
- child: ClipRRect(
- borderRadius:
- const BorderRadius.all(Radius.elliptical(16, 12)),
- child: SizedBox(
- width: 60,
- height: 60,
- child: CroppedFaceImageView(
- enteFile: widget.file,
- face: widget.face,
- ),
- ),
- ),
- ),
- const SizedBox(height: 8),
- if (widget.person != null)
- Text(
- widget.person!.attr.name.trim(),
- style: Theme.of(context).textTheme.bodySmall,
- overflow: TextOverflow.ellipsis,
- maxLines: 1,
- ),
- if (kDebugMode)
- Text(
- 'S: ${widget.face.score.toStringAsFixed(3)}',
- style: Theme.of(context).textTheme.bodySmall,
- maxLines: 1,
- ),
- ],
- ),
- );
- },
- );
- }
- }
- void _cornerIconPressed() async {
- log('face widget (file info) corner icon is pressed');
- try {
- if (isJustRemoved) {
- await ClusterFeedbackService.instance
- .addFilesToCluster([widget.face.faceID], widget.clusterID!);
- } else {
- await ClusterFeedbackService.instance
- .removeFilesFromCluster([widget.file], widget.clusterID!);
- }
- setState(() {
- isJustRemoved = !isJustRemoved;
- });
- } catch (e, s) {
- log("removing face/file from cluster from file info widget failed: $e, \n $s");
- }
- }
- Future<Uint8List?> getFaceCrop() async {
- try {
- final Uint8List? cachedFace = faceCropCache.get(widget.face.faceID);
- if (cachedFace != null) {
- return cachedFace;
- }
- final faceCropCacheFile = cachedFaceCropPath(widget.face.faceID);
- if ((await faceCropCacheFile.exists())) {
- final data = await faceCropCacheFile.readAsBytes();
- faceCropCache.put(widget.face.faceID, data);
- return data;
- }
- final result = await pool.withResource(
- () async => await getFaceCrops(
- widget.file,
- {
- widget.face.faceID: widget.face.detection.box,
- },
- ),
- );
- final Uint8List? computedCrop = result?[widget.face.faceID];
- if (computedCrop != null) {
- faceCropCache.put(widget.face.faceID, computedCrop);
- faceCropCacheFile.writeAsBytes(computedCrop).ignore();
- }
- return computedCrop;
- } catch (e, s) {
- log(
- "Error getting face for faceID: ${widget.face.faceID}",
- error: e,
- stackTrace: s,
- );
- return null;
- }
- }
- }
|