ente/mobile/lib/ui/viewer/people/cluster_app_bar.dart
2025-01-29 16:36:06 +05:30

291 lines
9.3 KiB
Dart

import 'dart:async';
import "package:flutter/foundation.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/db/files_db.dart";
import 'package:photos/db/ml/base.dart';
import "package:photos/db/ml/db.dart";
import "package:photos/events/people_changed_event.dart";
import 'package:photos/events/subscription_purchased_event.dart';
import "package:photos/models/file/file.dart";
import 'package:photos/models/gallery_type.dart';
import "package:photos/models/ml/face/person.dart";
import 'package:photos/models/selected_files.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/ml_result.dart";
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
import "package:photos/ui/common/popup_item.dart";
import "package:photos/ui/viewer/people/cluster_breakup_page.dart";
import "package:photos/ui/viewer/people/cluster_page.dart";
import "package:photos/utils/dialog_util.dart";
class ClusterAppBar extends StatefulWidget {
final GalleryType type;
final String? title;
final SelectedFiles selectedFiles;
final String clusterID;
final PersonEntity? person;
const ClusterAppBar(
this.type,
this.title,
this.selectedFiles,
this.clusterID, {
this.person,
super.key,
});
@override
State<ClusterAppBar> createState() => _AppBarWidgetState();
}
enum ClusterPopupAction {
setCover,
breakupCluster,
breakupClusterDebug,
ignore,
}
class _AppBarWidgetState extends State<ClusterAppBar> {
final _logger = Logger("_AppBarWidgetState");
late StreamSubscription _userAuthEventSubscription;
late Function() _selectedFilesListener;
String? _appBarTitle;
late CollectionActions collectionActions;
final GlobalKey shareButtonKey = GlobalKey();
bool isQuickLink = false;
late GalleryType galleryType;
late final IMLDataDB mlDataDB = MLDataDB.instance;
@override
void initState() {
super.initState();
_selectedFilesListener = () {
setState(() {});
};
collectionActions = CollectionActions(CollectionsService.instance);
widget.selectedFiles.addListener(_selectedFilesListener);
_userAuthEventSubscription =
Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
setState(() {});
});
_appBarTitle = widget.title;
galleryType = widget.type;
}
@override
void dispose() {
_userAuthEventSubscription.cancel();
widget.selectedFiles.removeListener(_selectedFilesListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppBar(
elevation: 0,
centerTitle: false,
title: Text(
_appBarTitle!,
style:
Theme.of(context).textTheme.headlineSmall!.copyWith(fontSize: 16),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
actions: _getDefaultActions(context),
scrolledUnderElevation: 4,
shadowColor: Colors.black.withOpacity(0.15),
surfaceTintColor: Colors.transparent,
);
}
List<Widget> _getDefaultActions(BuildContext context) {
final List<Widget> actions = <Widget>[];
// If the user has selected files, don't show any actions
if (widget.selectedFiles.files.isNotEmpty ||
!Configuration.instance.hasConfiguredAccount()) {
return actions;
}
final List<EntePopupMenuItem<ClusterPopupAction>> items = [];
items.addAll(
[
EntePopupMenuItem(
"Ignore person",
value: ClusterPopupAction.ignore,
icon: Icons.hide_image_outlined,
),
EntePopupMenuItem(
"Mixed grouping?",
value: ClusterPopupAction.breakupCluster,
icon: Icons.analytics_outlined,
),
],
);
if (kDebugMode) {
items.add(
EntePopupMenuItem(
"Debug mixed grouping",
value: ClusterPopupAction.breakupClusterDebug,
icon: Icons.analytics_outlined,
),
);
}
if (items.isNotEmpty) {
actions.add(
PopupMenuButton(
itemBuilder: (context) {
return items;
},
onSelected: (ClusterPopupAction value) async {
if (value == ClusterPopupAction.breakupCluster) {
// ignore: unawaited_futures
await _breakUpCluster(context);
} else if (value == ClusterPopupAction.ignore) {
await _onIgnoredClusterClicked(context);
} else if (value == ClusterPopupAction.breakupClusterDebug) {
await _breakUpClusterDebug(context);
}
// else if (value == ClusterPopupAction.setCover) {
// await setCoverPhoto(context);
},
),
);
}
return actions;
}
Future<void> _onIgnoredClusterClicked(BuildContext context) async {
await showChoiceDialog(
context,
title: "Are you sure you want to ignore this person?",
body:
"The person grouping will not be displayed in the discovery tap anymore. Photos will remain untouched.",
firstButtonLabel: "Yes, confirm",
firstButtonOnTap: () async {
try {
await ClusterFeedbackService.instance.ignoreCluster(widget.clusterID);
Navigator.of(context).pop(); // Close the cluster page
} catch (e, s) {
_logger.severe('Ignoring a cluster failed', e, s);
// await showGenericErrorDialog(context: context, error: e);
}
},
);
}
Future<void> _breakUpCluster(BuildContext context) async {
bool userConfirmed = false;
List<EnteFile> biggestClusterFiles = [];
String biggestClusterID = '';
await showChoiceDialog(
context,
title: "Does this grouping contain multiple people?",
body:
"We will automatically analyze the grouping to determine if there are multiple people present, and separate them out again. This may take a few seconds.",
firstButtonLabel: "Yes, confirm",
firstButtonOnTap: () async {
try {
final breakupResult = await ClusterFeedbackService.instance
.breakUpCluster(widget.clusterID);
final Map<String, List<String>> newClusterIDToFaceIDs =
breakupResult.newClusterIdToFaceIds;
final Map<String, String> newFaceIdToClusterID =
breakupResult.newFaceIdToCluster;
// Update to delete the old clusters and save the new clusters
await mlDataDB.deleteClusterSummary(widget.clusterID);
await MLDataDB.instance
.clusterSummaryUpdate(breakupResult.newClusterSummaries);
await mlDataDB.updateFaceIdToClusterId(newFaceIdToClusterID);
// Find the biggest cluster
biggestClusterID = '';
int biggestClusterSize = 0;
for (final MapEntry<String, List<String>> clusterToFaces
in newClusterIDToFaceIDs.entries) {
if (clusterToFaces.value.length > biggestClusterSize) {
biggestClusterSize = clusterToFaces.value.length;
biggestClusterID = clusterToFaces.key;
}
}
// Get the files for the biggest new cluster
final biggestClusterFileIDs = newClusterIDToFaceIDs[biggestClusterID]!
.map((e) => getFileIdFromFaceId<int>(e))
.toList();
biggestClusterFiles = await FilesDB.instance
.getFileIDToFileFromIDs(
biggestClusterFileIDs,
)
.then((mapping) => mapping.values.toList());
// Sort the files to prevent issues with the order of the files in gallery
biggestClusterFiles
.sort((a, b) => b.creationTime!.compareTo(a.creationTime!));
userConfirmed = true;
} catch (e, s) {
_logger.severe('Breakup cluster failed', e, s);
// await showGenericErrorDialog(context: context, error: e);
}
},
);
if (userConfirmed) {
// Close the old cluster page
Navigator.of(context).pop();
// Push the new cluster page
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ClusterPage(
biggestClusterFiles,
clusterID: biggestClusterID,
),
),
);
Bus.instance.fire(PeopleChangedEvent());
}
}
Future<void> _breakUpClusterDebug(BuildContext context) async {
final breakupResult =
await ClusterFeedbackService.instance.breakUpCluster(widget.clusterID);
final Map<String, List<String>> newClusterIDToFaceIDs =
breakupResult.newClusterIdToFaceIds;
final allFileIDs = newClusterIDToFaceIDs.values
.expand((e) => e)
.map((e) => getFileIdFromFaceId<int>(e))
.toList();
final fileIDtoFile = await FilesDB.instance.getFileIDToFileFromIDs(
allFileIDs,
);
final newClusterIDToFiles = newClusterIDToFaceIDs.map(
(key, value) => MapEntry(
key,
value
.map((faceId) => fileIDtoFile[getFileIdFromFaceId<int>(faceId)]!)
.toList(),
),
);
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ClusterBreakupPage(
newClusterIDToFiles,
"(Analysis)",
),
),
);
}
}