|
@@ -2,12 +2,14 @@ import "dart:developer" show log;
|
|
import "dart:io" show Platform;
|
|
import "dart:io" show Platform;
|
|
import "dart:typed_data";
|
|
import "dart:typed_data";
|
|
|
|
|
|
|
|
+import "package:flutter/cupertino.dart";
|
|
import "package:flutter/foundation.dart" show kDebugMode;
|
|
import "package:flutter/foundation.dart" show kDebugMode;
|
|
import "package:flutter/material.dart";
|
|
import "package:flutter/material.dart";
|
|
import "package:photos/face/db.dart";
|
|
import "package:photos/face/db.dart";
|
|
import "package:photos/face/model/face.dart";
|
|
import "package:photos/face/model/face.dart";
|
|
import "package:photos/face/model/person.dart";
|
|
import "package:photos/face/model/person.dart";
|
|
import 'package:photos/models/file/file.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/services/search_service.dart";
|
|
import "package:photos/theme/ente_theme.dart";
|
|
import "package:photos/theme/ente_theme.dart";
|
|
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
|
|
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
|
|
@@ -16,13 +18,15 @@ import "package:photos/ui/viewer/people/cropped_face_image_view.dart";
|
|
import "package:photos/ui/viewer/people/people_page.dart";
|
|
import "package:photos/ui/viewer/people/people_page.dart";
|
|
import "package:photos/utils/face/face_box_crop.dart";
|
|
import "package:photos/utils/face/face_box_crop.dart";
|
|
import "package:photos/utils/thumbnail_util.dart";
|
|
import "package:photos/utils/thumbnail_util.dart";
|
|
|
|
+// import "package:photos/utils/toast_util.dart";
|
|
|
|
|
|
-class FaceWidget extends StatelessWidget {
|
|
|
|
|
|
+class FaceWidget extends StatefulWidget {
|
|
final EnteFile file;
|
|
final EnteFile file;
|
|
final Face face;
|
|
final Face face;
|
|
final Person? person;
|
|
final Person? person;
|
|
final int? clusterID;
|
|
final int? clusterID;
|
|
final bool highlight;
|
|
final bool highlight;
|
|
|
|
+ final bool editMode;
|
|
|
|
|
|
const FaceWidget(
|
|
const FaceWidget(
|
|
this.file,
|
|
this.file,
|
|
@@ -30,9 +34,17 @@ class FaceWidget extends StatelessWidget {
|
|
this.person,
|
|
this.person,
|
|
this.clusterID,
|
|
this.clusterID,
|
|
this.highlight = false,
|
|
this.highlight = false,
|
|
|
|
+ this.editMode = false,
|
|
Key? key,
|
|
Key? key,
|
|
}) : super(key: key);
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
+ @override
|
|
|
|
+ State<FaceWidget> createState() => _FaceWidgetState();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+class _FaceWidgetState extends State<FaceWidget> {
|
|
|
|
+ bool isJustRemoved = false;
|
|
|
|
+
|
|
@override
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
if (Platform.isIOS || Platform.isAndroid) {
|
|
if (Platform.isIOS || Platform.isAndroid) {
|
|
@@ -43,22 +55,24 @@ class FaceWidget extends StatelessWidget {
|
|
final ImageProvider imageProvider = MemoryImage(snapshot.data!);
|
|
final ImageProvider imageProvider = MemoryImage(snapshot.data!);
|
|
return GestureDetector(
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
onTap: () async {
|
|
|
|
+ if (widget.editMode) return;
|
|
|
|
+
|
|
log(
|
|
log(
|
|
- "FaceWidget is tapped, with person $person and clusterID $clusterID",
|
|
|
|
|
|
+ "FaceWidget is tapped, with person ${widget.person} and clusterID ${widget.clusterID}",
|
|
name: "FaceWidget",
|
|
name: "FaceWidget",
|
|
);
|
|
);
|
|
- if (person == null && clusterID == null) {
|
|
|
|
|
|
+ if (widget.person == null && widget.clusterID == null) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
- if (person != null) {
|
|
|
|
|
|
+ if (widget.person != null) {
|
|
await Navigator.of(context).push(
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
MaterialPageRoute(
|
|
builder: (context) => PeoplePage(
|
|
builder: (context) => PeoplePage(
|
|
- person: person!,
|
|
|
|
|
|
+ person: widget.person!,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
- } else if (clusterID != null) {
|
|
|
|
|
|
+ } else if (widget.clusterID != null) {
|
|
final fileIdsToClusterIds =
|
|
final fileIdsToClusterIds =
|
|
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
|
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
|
final files = await SearchService.instance.getAllFiles();
|
|
final files = await SearchService.instance.getAllFiles();
|
|
@@ -66,7 +80,7 @@ class FaceWidget extends StatelessWidget {
|
|
.where(
|
|
.where(
|
|
(file) =>
|
|
(file) =>
|
|
fileIdsToClusterIds[file.uploadedFileID]
|
|
fileIdsToClusterIds[file.uploadedFileID]
|
|
- ?.contains(clusterID) ??
|
|
|
|
|
|
+ ?.contains(widget.clusterID) ??
|
|
false,
|
|
false,
|
|
)
|
|
)
|
|
.toList();
|
|
.toList();
|
|
@@ -74,7 +88,7 @@ class FaceWidget extends StatelessWidget {
|
|
MaterialPageRoute(
|
|
MaterialPageRoute(
|
|
builder: (context) => ClusterPage(
|
|
builder: (context) => ClusterPage(
|
|
clusterFiles,
|
|
clusterFiles,
|
|
- clusterID: clusterID!,
|
|
|
|
|
|
+ clusterID: widget.clusterID!,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
@@ -82,46 +96,87 @@ class FaceWidget extends StatelessWidget {
|
|
},
|
|
},
|
|
child: Column(
|
|
child: Column(
|
|
children: [
|
|
children: [
|
|
- // TODO: the edges of the green line are still not properly rounded around ClipRRect
|
|
|
|
- Container(
|
|
|
|
- height: 60,
|
|
|
|
- width: 60,
|
|
|
|
- decoration: ShapeDecoration(
|
|
|
|
- shape: RoundedRectangleBorder(
|
|
|
|
- borderRadius:
|
|
|
|
- const BorderRadius.all(Radius.elliptical(16, 12)),
|
|
|
|
- side: 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,
|
|
|
|
|
|
+ Stack(
|
|
|
|
+ children: [
|
|
|
|
+ Container(
|
|
height: 60,
|
|
height: 60,
|
|
- child: Image(
|
|
|
|
- image: imageProvider,
|
|
|
|
- fit: BoxFit.cover,
|
|
|
|
|
|
+ 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),
|
|
const SizedBox(height: 8),
|
|
- if (person != null)
|
|
|
|
|
|
+ if (widget.person != null)
|
|
Text(
|
|
Text(
|
|
- person!.attr.name.trim(),
|
|
|
|
|
|
+ widget.person!.attr.name.trim(),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
if (kDebugMode)
|
|
if (kDebugMode)
|
|
Text(
|
|
Text(
|
|
- 'S: ${face.score.toStringAsFixed(3)}',
|
|
|
|
|
|
+ '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)
|
|
|
|
+ Text(
|
|
|
|
+ 'A: ${widget.face.detection.getFaceArea(widget.file.width, widget.file.height)}',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
maxLines: 1,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
@@ -168,21 +223,21 @@ class FaceWidget extends StatelessWidget {
|
|
return GestureDetector(
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
onTap: () async {
|
|
log(
|
|
log(
|
|
- "FaceWidget is tapped, with person $person and clusterID $clusterID",
|
|
|
|
|
|
+ "FaceWidget is tapped, with person ${widget.person} and clusterID ${widget.clusterID}",
|
|
name: "FaceWidget",
|
|
name: "FaceWidget",
|
|
);
|
|
);
|
|
- if (person == null && clusterID == null) {
|
|
|
|
|
|
+ if (widget.person == null && widget.clusterID == null) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
- if (person != null) {
|
|
|
|
|
|
+ if (widget.person != null) {
|
|
await Navigator.of(context).push(
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
MaterialPageRoute(
|
|
builder: (context) => PeoplePage(
|
|
builder: (context) => PeoplePage(
|
|
- person: person!,
|
|
|
|
|
|
+ person: widget.person!,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
- } else if (clusterID != null) {
|
|
|
|
|
|
+ } else if (widget.clusterID != null) {
|
|
final fileIdsToClusterIds =
|
|
final fileIdsToClusterIds =
|
|
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
|
await FaceMLDataDB.instance.getFileIdToClusterIds();
|
|
final files = await SearchService.instance.getAllFiles();
|
|
final files = await SearchService.instance.getAllFiles();
|
|
@@ -190,7 +245,7 @@ class FaceWidget extends StatelessWidget {
|
|
.where(
|
|
.where(
|
|
(file) =>
|
|
(file) =>
|
|
fileIdsToClusterIds[file.uploadedFileID]
|
|
fileIdsToClusterIds[file.uploadedFileID]
|
|
- ?.contains(clusterID) ??
|
|
|
|
|
|
+ ?.contains(widget.clusterID) ??
|
|
false,
|
|
false,
|
|
)
|
|
)
|
|
.toList();
|
|
.toList();
|
|
@@ -198,7 +253,7 @@ class FaceWidget extends StatelessWidget {
|
|
MaterialPageRoute(
|
|
MaterialPageRoute(
|
|
builder: (context) => ClusterPage(
|
|
builder: (context) => ClusterPage(
|
|
clusterFiles,
|
|
clusterFiles,
|
|
- clusterID: clusterID!,
|
|
|
|
|
|
+ clusterID: widget.clusterID!,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
@@ -213,7 +268,7 @@ class FaceWidget extends StatelessWidget {
|
|
shape: RoundedRectangleBorder(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius:
|
|
borderRadius:
|
|
const BorderRadius.all(Radius.elliptical(16, 12)),
|
|
const BorderRadius.all(Radius.elliptical(16, 12)),
|
|
- side: highlight
|
|
|
|
|
|
+ side: widget.highlight
|
|
? BorderSide(
|
|
? BorderSide(
|
|
color: getEnteColorScheme(context).primary700,
|
|
color: getEnteColorScheme(context).primary700,
|
|
width: 2.0,
|
|
width: 2.0,
|
|
@@ -228,23 +283,23 @@ class FaceWidget extends StatelessWidget {
|
|
width: 60,
|
|
width: 60,
|
|
height: 60,
|
|
height: 60,
|
|
child: CroppedFaceImageView(
|
|
child: CroppedFaceImageView(
|
|
- enteFile: file,
|
|
|
|
- face: face,
|
|
|
|
|
|
+ enteFile: widget.file,
|
|
|
|
+ face: widget.face,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const SizedBox(height: 8),
|
|
- if (person != null)
|
|
|
|
|
|
+ if (widget.person != null)
|
|
Text(
|
|
Text(
|
|
- person!.attr.name.trim(),
|
|
|
|
|
|
+ widget.person!.attr.name.trim(),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
if (kDebugMode)
|
|
if (kDebugMode)
|
|
Text(
|
|
Text(
|
|
- 'S: ${face.score.toStringAsFixed(3)}',
|
|
|
|
|
|
+ 'S: ${widget.face.score.toStringAsFixed(3)}',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
maxLines: 1,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
@@ -256,36 +311,55 @@ class FaceWidget extends StatelessWidget {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ 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 {
|
|
Future<Uint8List?> getFaceCrop() async {
|
|
try {
|
|
try {
|
|
- final Uint8List? cachedFace = faceCropCache.get(face.faceID);
|
|
|
|
|
|
+ final Uint8List? cachedFace = faceCropCache.get(widget.face.faceID);
|
|
if (cachedFace != null) {
|
|
if (cachedFace != null) {
|
|
return cachedFace;
|
|
return cachedFace;
|
|
}
|
|
}
|
|
- final faceCropCacheFile = cachedFaceCropPath(face.faceID);
|
|
|
|
|
|
+ final faceCropCacheFile = cachedFaceCropPath(widget.face.faceID);
|
|
if ((await faceCropCacheFile.exists())) {
|
|
if ((await faceCropCacheFile.exists())) {
|
|
final data = await faceCropCacheFile.readAsBytes();
|
|
final data = await faceCropCacheFile.readAsBytes();
|
|
- faceCropCache.put(face.faceID, data);
|
|
|
|
|
|
+ faceCropCache.put(widget.face.faceID, data);
|
|
return data;
|
|
return data;
|
|
}
|
|
}
|
|
|
|
|
|
final result = await pool.withResource(
|
|
final result = await pool.withResource(
|
|
() async => await getFaceCrops(
|
|
() async => await getFaceCrops(
|
|
- file,
|
|
|
|
|
|
+ widget.file,
|
|
{
|
|
{
|
|
- face.faceID: face.detection.box,
|
|
|
|
|
|
+ widget.face.faceID: widget.face.detection.box,
|
|
},
|
|
},
|
|
),
|
|
),
|
|
);
|
|
);
|
|
- final Uint8List? computedCrop = result?[face.faceID];
|
|
|
|
|
|
+ final Uint8List? computedCrop = result?[widget.face.faceID];
|
|
if (computedCrop != null) {
|
|
if (computedCrop != null) {
|
|
- faceCropCache.put(face.faceID, computedCrop);
|
|
|
|
|
|
+ faceCropCache.put(widget.face.faceID, computedCrop);
|
|
faceCropCacheFile.writeAsBytes(computedCrop).ignore();
|
|
faceCropCacheFile.writeAsBytes(computedCrop).ignore();
|
|
}
|
|
}
|
|
return computedCrop;
|
|
return computedCrop;
|
|
} catch (e, s) {
|
|
} catch (e, s) {
|
|
log(
|
|
log(
|
|
- "Error getting face for faceID: ${face.faceID}",
|
|
|
|
|
|
+ "Error getting face for faceID: ${widget.face.faceID}",
|
|
error: e,
|
|
error: e,
|
|
stackTrace: s,
|
|
stackTrace: s,
|
|
);
|
|
);
|