Merge remote-tracking branch 'origin/smart-album-nothingelse' into internal_photos_july25_2

This commit is contained in:
Prateek Sunal
2025-07-29 19:58:02 +05:30
10 changed files with 160 additions and 25 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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(

View File

@@ -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(

View File

@@ -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",

View File

@@ -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()!;

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;