Merge remote-tracking branch 'origin/smart-album-nothingelse' into internal_photos_july25_2
This commit is contained in:
@@ -127,6 +127,9 @@ PODS:
|
||||
- libwebp/sharpyuv (1.5.0)
|
||||
- libwebp/webp (1.5.0):
|
||||
- libwebp/sharpyuv
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- local_auth_ios (0.0.1):
|
||||
- Flutter
|
||||
- Mantle (2.2.0):
|
||||
@@ -266,6 +269,7 @@ DEPENDENCIES:
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- launcher_icon_switcher (from `.symlinks/plugins/launcher_icon_switcher/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||
- maps_launcher (from `.symlinks/plugins/maps_launcher/ios`)
|
||||
- media_extension (from `.symlinks/plugins/media_extension/ios`)
|
||||
@@ -373,6 +377,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
launcher_icon_switcher:
|
||||
:path: ".symlinks/plugins/launcher_icon_switcher/ios"
|
||||
local_auth_darwin:
|
||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||
local_auth_ios:
|
||||
:path: ".symlinks/plugins/local_auth_ios/ios"
|
||||
maps_launcher:
|
||||
@@ -473,6 +479,7 @@ SPEC CHECKSUMS:
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
|
||||
@@ -548,6 +548,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/launcher_icon_switcher/launcher_icon_switcher.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
|
||||
@@ -643,6 +644,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/launcher_icon_switcher.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
|
||||
|
||||
@@ -548,6 +548,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Authentication successful!"),
|
||||
"autoAddPeople":
|
||||
MessageLookupByLibrary.simpleMessage("Auto-add people"),
|
||||
"autoAddToAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Auto-add to album"),
|
||||
"autoCastDialogBody": MessageLookupByLibrary.simpleMessage(
|
||||
"You\'ll see available Cast devices here."),
|
||||
"autoCastiOSPermission": MessageLookupByLibrary.simpleMessage(
|
||||
|
||||
10
mobile/apps/photos/lib/generated/l10n.dart
generated
10
mobile/apps/photos/lib/generated/l10n.dart
generated
@@ -12386,6 +12386,16 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Auto-add to album`
|
||||
String get autoAddToAlbum {
|
||||
return Intl.message(
|
||||
'Auto-add to album',
|
||||
name: 'autoAddToAlbum',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Should the files related to the person that were previously selected in smart albums be removed?`
|
||||
String get shouldRemoveFilesSmartAlbumsDesc {
|
||||
return Intl.message(
|
||||
|
||||
@@ -1798,6 +1798,7 @@
|
||||
"fileAnalysisFailed": "Unable to analyze file",
|
||||
"editAutoAddPeople": "Edit auto-add people",
|
||||
"autoAddPeople": "Auto-add people",
|
||||
"autoAddToAlbum": "Auto-add to album",
|
||||
"shouldRemoveFilesSmartAlbumsDesc": "Should the files related to the person that were previously selected in smart albums be removed?",
|
||||
"addingPhotos": "Adding photos",
|
||||
"gettingReady": "Getting ready",
|
||||
|
||||
@@ -126,8 +126,8 @@ class SmartAlbumsService {
|
||||
for (final personId in config.personIDs) {
|
||||
// compares current updateAt with last added file's updatedAt
|
||||
if (updatedAtMap[personId] == null ||
|
||||
infoMap[personId] == null ||
|
||||
(updatedAtMap[personId]! <= infoMap[personId]!.updatedAt)) {
|
||||
infoMap[personId] != null &&
|
||||
(updatedAtMap[personId]! <= infoMap[personId]!.updatedAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -173,6 +173,36 @@ class SmartAlbumsService {
|
||||
Bus.instance.fire(SmartAlbumSyncingEvent());
|
||||
}
|
||||
|
||||
Future<SmartAlbumConfig> addPeopleToSmartAlbum(
|
||||
int collectionId,
|
||||
List<String> personIDs,
|
||||
) async {
|
||||
final cachedConfigs = await getSmartConfigs();
|
||||
|
||||
late SmartAlbumConfig newConfig;
|
||||
|
||||
final config = cachedConfigs[collectionId];
|
||||
final infoMap = Map<String, PersonInfo>.from(config?.infoMap ?? {});
|
||||
|
||||
for (final personId in personIDs) {
|
||||
// skip if personId already exists in infoMap
|
||||
// only relevant when config exists before
|
||||
if (infoMap.containsKey(personId)) continue;
|
||||
infoMap[personId] = (updatedAt: 0, addedFiles: {});
|
||||
}
|
||||
|
||||
newConfig = SmartAlbumConfig(
|
||||
id: config?.id,
|
||||
collectionId: collectionId,
|
||||
personIDs: {...?config?.personIDs, ...personIDs},
|
||||
infoMap: infoMap,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
await saveConfig(newConfig);
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
Future<void> saveConfig(SmartAlbumConfig config) async {
|
||||
final userId = Configuration.instance.getUserID()!;
|
||||
|
||||
|
||||
@@ -37,12 +37,14 @@ class AlbumVerticalListWidget extends StatefulWidget {
|
||||
final bool enableSelection;
|
||||
final List<Collection> selectedCollections;
|
||||
final Function()? onSelectionChanged;
|
||||
final List<String>? selectedPeople;
|
||||
|
||||
const AlbumVerticalListWidget(
|
||||
this.collections,
|
||||
this.actionType,
|
||||
this.selectedFiles,
|
||||
this.sharedFiles,
|
||||
this.selectedPeople,
|
||||
this.searchQuery,
|
||||
this.shouldShowCreateAlbum, {
|
||||
required this.selectedCollections,
|
||||
@@ -66,7 +68,9 @@ class _AlbumVerticalListWidgetState extends State<AlbumVerticalListWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final filesCount = widget.sharedFiles != null
|
||||
? widget.sharedFiles!.length
|
||||
: widget.selectedFiles?.files.length ?? 0;
|
||||
: widget.selectedPeople != null
|
||||
? widget.selectedPeople!.length
|
||||
: widget.selectedFiles?.files.length ?? 0;
|
||||
|
||||
if (widget.collections.isEmpty) {
|
||||
if (widget.shouldShowCreateAlbum) {
|
||||
@@ -282,6 +286,8 @@ class _AlbumVerticalListWidgetState extends State<AlbumVerticalListWidget> {
|
||||
}) async {
|
||||
switch (widget.actionType) {
|
||||
case CollectionActionType.addFiles:
|
||||
case CollectionActionType.addToHiddenAlbum:
|
||||
case CollectionActionType.autoAddPeople:
|
||||
return _addToCollection(
|
||||
context,
|
||||
collection.id,
|
||||
@@ -297,8 +303,6 @@ class _AlbumVerticalListWidgetState extends State<AlbumVerticalListWidget> {
|
||||
return _showShareCollectionPage(context, collection);
|
||||
case CollectionActionType.moveToHiddenCollection:
|
||||
return _moveFilesToCollection(context, collection.id);
|
||||
case CollectionActionType.addToHiddenAlbum:
|
||||
return _addToCollection(context, collection.id, showProgressDialog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
@@ -10,6 +11,7 @@ import "package:photos/events/create_new_album_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
@@ -17,12 +19,14 @@ import "package:photos/ui/actions/collection/collection_file_actions.dart";
|
||||
import "package:photos/ui/actions/collection/collection_sharing_actions.dart";
|
||||
import 'package:photos/ui/collections/album/vertical_list.dart';
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import "package:photos/ui/common/progress_dialog.dart";
|
||||
import 'package:photos/ui/components/bottom_of_title_bar_widget.dart';
|
||||
import 'package:photos/ui/components/buttons/button_widget.dart';
|
||||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
import "package:photos/ui/components/text_input_widget.dart";
|
||||
import 'package:photos/ui/components/title_bar_title_widget.dart';
|
||||
import "package:photos/ui/notification/toast.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/separators_util.dart";
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
@@ -34,6 +38,7 @@ enum CollectionActionType {
|
||||
shareCollection,
|
||||
addToHiddenAlbum,
|
||||
moveToHiddenCollection,
|
||||
autoAddPeople;
|
||||
}
|
||||
|
||||
extension CollectionActionTypeExtension on CollectionActionType {
|
||||
@@ -70,6 +75,9 @@ String _actionName(
|
||||
case CollectionActionType.moveToHiddenCollection:
|
||||
text = S.of(context).moveToHiddenAlbum;
|
||||
break;
|
||||
case CollectionActionType.autoAddPeople:
|
||||
text = S.of(context).autoAddToAlbum;
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -80,6 +88,7 @@ void showCollectionActionSheet(
|
||||
List<SharedMediaFile>? sharedFiles,
|
||||
CollectionActionType actionType = CollectionActionType.addFiles,
|
||||
bool showOptionToCreateNewAlbum = true,
|
||||
List<String>? selectedPeople,
|
||||
}) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
@@ -89,6 +98,7 @@ void showCollectionActionSheet(
|
||||
sharedFiles: sharedFiles,
|
||||
actionType: actionType,
|
||||
showOptionToCreateNewAlbum: showOptionToCreateNewAlbum,
|
||||
selectedPeople: selectedPeople,
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
@@ -107,6 +117,7 @@ void showCollectionActionSheet(
|
||||
class CollectionActionSheet extends StatefulWidget {
|
||||
final SelectedFiles? selectedFiles;
|
||||
final List<SharedMediaFile>? sharedFiles;
|
||||
final List<String>? selectedPeople;
|
||||
final CollectionActionType actionType;
|
||||
final bool showOptionToCreateNewAlbum;
|
||||
const CollectionActionSheet({
|
||||
@@ -114,6 +125,7 @@ class CollectionActionSheet extends StatefulWidget {
|
||||
required this.sharedFiles,
|
||||
required this.actionType,
|
||||
required this.showOptionToCreateNewAlbum,
|
||||
this.selectedPeople,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -129,14 +141,18 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
||||
final _selectedCollections = <Collection>[];
|
||||
final _recentlyCreatedCollections = <Collection>[];
|
||||
late StreamSubscription<CreateNewAlbumEvent> _createNewAlbumSubscription;
|
||||
final _logger = Logger("CollectionActionSheet");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showOnlyHiddenCollections = widget.actionType.isHiddenAction;
|
||||
_enableSelection = (widget.actionType == CollectionActionType.addFiles ||
|
||||
widget.actionType == CollectionActionType.addToHiddenAlbum) &&
|
||||
(widget.sharedFiles == null || widget.sharedFiles!.isEmpty);
|
||||
_enableSelection = (widget.actionType ==
|
||||
CollectionActionType.autoAddPeople &&
|
||||
widget.selectedPeople != null) ||
|
||||
((widget.actionType == CollectionActionType.addFiles ||
|
||||
widget.actionType == CollectionActionType.addToHiddenAlbum) &&
|
||||
(widget.sharedFiles == null || widget.sharedFiles!.isEmpty));
|
||||
_createNewAlbumSubscription =
|
||||
Bus.instance.on<CreateNewAlbumEvent>().listen((event) {
|
||||
setState(() {
|
||||
@@ -156,7 +172,9 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
final filesCount = widget.sharedFiles != null
|
||||
? widget.sharedFiles!.length
|
||||
: widget.selectedFiles?.files.length ?? 0;
|
||||
: widget.selectedPeople != null
|
||||
? widget.selectedPeople!.length
|
||||
: widget.selectedFiles?.files.length ?? 0;
|
||||
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
|
||||
final isKeyboardUp = bottomInset > 100;
|
||||
final double bottomPadding =
|
||||
@@ -256,6 +274,31 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
||||
shouldSurfaceExecutionStates: false,
|
||||
isDisabled: _selectedCollections.isEmpty,
|
||||
onTap: () async {
|
||||
if (widget.selectedPeople != null) {
|
||||
final ProgressDialog? dialog = createProgressDialog(
|
||||
context,
|
||||
S.of(context).uploadingFilesToAlbum,
|
||||
isDismissible: true,
|
||||
);
|
||||
await dialog?.show();
|
||||
for (final collection in _selectedCollections) {
|
||||
try {
|
||||
await smartAlbumsService.addPeopleToSmartAlbum(
|
||||
collection.id,
|
||||
widget.selectedPeople!,
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe(
|
||||
"Error while adding people to smart album",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
unawaited(smartAlbumsService.syncSmartAlbums());
|
||||
await dialog?.hide();
|
||||
return;
|
||||
}
|
||||
final CollectionActions collectionActions =
|
||||
CollectionActions(CollectionsService.instance);
|
||||
final result = await collectionActions.addToMultipleCollections(
|
||||
@@ -319,6 +362,7 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
||||
widget.actionType,
|
||||
widget.selectedFiles,
|
||||
widget.sharedFiles,
|
||||
widget.selectedPeople,
|
||||
_searchQuery,
|
||||
shouldShowCreateAlbum,
|
||||
enableSelection: _enableSelection,
|
||||
|
||||
@@ -8,6 +8,7 @@ import "package:photos/theme/ente_theme.dart";
|
||||
class SelectionActionButton extends StatelessWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final Widget? iconWidget;
|
||||
final String? svgAssetPath;
|
||||
final VoidCallback? onTap;
|
||||
final bool shouldShow;
|
||||
@@ -17,13 +18,14 @@ class SelectionActionButton extends StatelessWidget {
|
||||
required this.onTap,
|
||||
this.icon,
|
||||
this.svgAssetPath,
|
||||
this.iconWidget,
|
||||
this.shouldShow = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(icon != null || svgAssetPath != null);
|
||||
assert(icon != null || iconWidget != null || svgAssetPath != null);
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOutCirc,
|
||||
@@ -35,6 +37,7 @@ class SelectionActionButton extends StatelessWidget {
|
||||
icon: icon,
|
||||
onTap: onTap,
|
||||
svgAssetPath: svgAssetPath,
|
||||
iconWidget: iconWidget,
|
||||
)
|
||||
: const SizedBox(
|
||||
height: 60,
|
||||
@@ -48,12 +51,14 @@ class _Body extends StatefulWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final String? svgAssetPath;
|
||||
final Widget? iconWidget;
|
||||
final VoidCallback? onTap;
|
||||
const _Body({
|
||||
required this.labelText,
|
||||
required this.onTap,
|
||||
this.icon,
|
||||
this.svgAssetPath,
|
||||
this.iconWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -128,22 +133,24 @@ class __BodyState extends State<_Body> {
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (widget.svgAssetPath != null)
|
||||
SvgPicture.asset(
|
||||
widget.svgAssetPath!,
|
||||
colorFilter: ColorFilter.mode(
|
||||
getEnteColorScheme(context).textMuted,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
width: 24,
|
||||
height: 24,
|
||||
)
|
||||
else if (widget.iconWidget != null)
|
||||
widget.iconWidget!
|
||||
else
|
||||
widget.svgAssetPath != null
|
||||
? SvgPicture.asset(
|
||||
widget.svgAssetPath!,
|
||||
colorFilter: ColorFilter.mode(
|
||||
getEnteColorScheme(context).textMuted,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
width: 24,
|
||||
height: 24,
|
||||
)
|
||||
: Icon(
|
||||
widget.icon,
|
||||
size: 24,
|
||||
color: getEnteColorScheme(context).textMuted,
|
||||
),
|
||||
Icon(
|
||||
widget.icon,
|
||||
size: 24,
|
||||
color: getEnteColorScheme(context).textMuted,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.labelText,
|
||||
|
||||
@@ -7,6 +7,8 @@ import "package:photos/models/ml/face/person.dart";
|
||||
import "package:photos/models/selected_people.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/theme/ente_theme.dart";
|
||||
import "package:photos/ui/collections/collection_action_sheet.dart";
|
||||
import "package:photos/ui/components/bottom_action_bar/selection_action_button_widget.dart";
|
||||
import "package:photos/ui/viewer/people/person_cluster_suggestion.dart";
|
||||
import "package:photos/ui/viewer/people/save_or_edit_person.dart";
|
||||
@@ -73,6 +75,8 @@ class _PeopleSelectionActionWidgetState
|
||||
final selectedClusterIds = _getSelectedClusterIds();
|
||||
final onlyOnePerson =
|
||||
selectedPersonIds.length == 1 && selectedClusterIds.isEmpty;
|
||||
final onlyPersonSelected =
|
||||
selectedPersonIds.isNotEmpty && selectedClusterIds.isEmpty;
|
||||
final onePersonAndClusters =
|
||||
selectedPersonIds.length == 1 && selectedClusterIds.isNotEmpty;
|
||||
final anythingSelected =
|
||||
@@ -118,6 +122,19 @@ class _PeopleSelectionActionWidgetState
|
||||
shouldShow: onlyOnePerson,
|
||||
),
|
||||
);
|
||||
items.add(
|
||||
SelectionActionButton(
|
||||
labelText: S.of(context).autoAddToAlbum,
|
||||
iconWidget: Image.asset(
|
||||
"assets/auto-add-people.png",
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: EnteTheme.isDark(context) ? Colors.white : Colors.black,
|
||||
),
|
||||
onTap: _autoAddToAlbum,
|
||||
shouldShow: onlyPersonSelected,
|
||||
),
|
||||
);
|
||||
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).removePadding(removeBottom: true),
|
||||
@@ -182,6 +199,17 @@ class _PeopleSelectionActionWidgetState
|
||||
widget.selectedPeople.clearAll();
|
||||
}
|
||||
|
||||
Future<void> _autoAddToAlbum() async {
|
||||
final selectedPersonIds = _getSelectedPersonIds();
|
||||
if (selectedPersonIds.isEmpty) return;
|
||||
showCollectionActionSheet(
|
||||
context,
|
||||
selectedPeople: selectedPersonIds,
|
||||
actionType: CollectionActionType.autoAddPeople,
|
||||
);
|
||||
widget.selectedPeople.clearAll();
|
||||
}
|
||||
|
||||
Future<void> _onResetPerson() async {
|
||||
final selectedPersonIds = _getSelectedPersonIds();
|
||||
if (selectedPersonIds.length != 1) return;
|
||||
|
||||
Reference in New Issue
Block a user