From df5917060b1df91f359b9de31c0df331d9a646c7 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 17:40:20 +0530 Subject: [PATCH 01/15] Copy change --- mobile/apps/photos/lib/l10n/intl_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index 825cbe44fb..bf02a76f70 100644 --- a/mobile/apps/photos/lib/l10n/intl_en.arb +++ b/mobile/apps/photos/lib/l10n/intl_en.arb @@ -1941,5 +1941,5 @@ "findingSimilarImages": "Finding similar images", "almostDone": "Almost done", "processingLocally": "Processing locally", - "useMLToFindSimilarImages": "Use ML to find images that look similar to each other." + "useMLToFindSimilarImages": "Find and remove images that look similar to each other." } From 18262581612d0cd285ac82c28d7a3859dbdb6b99 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 17:43:58 +0530 Subject: [PATCH 02/15] Copy --- mobile/apps/photos/lib/l10n/intl_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index bf02a76f70..259f78ea12 100644 --- a/mobile/apps/photos/lib/l10n/intl_en.arb +++ b/mobile/apps/photos/lib/l10n/intl_en.arb @@ -1941,5 +1941,5 @@ "findingSimilarImages": "Finding similar images", "almostDone": "Almost done", "processingLocally": "Processing locally", - "useMLToFindSimilarImages": "Find and remove images that look similar to each other." + "useMLToFindSimilarImages": "Review and remove images that look similar to each other." } From 01aab41c25b0e1a4e9bcc54ee389411860ea251f Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 17:46:53 +0530 Subject: [PATCH 03/15] Select all by default --- mobile/apps/photos/lib/ui/tools/similar_images_page.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 791518e343..f6613f48f6 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -505,6 +505,14 @@ class _SimilarImagesPageState extends State { _pageState = SimilarImagesPageState.results; _sortSimilarFiles(); + for (final group in _similarFilesList) { + if (group.files.length > 1) { + for (int i = 1; i < group.files.length; i++) { + _selectedFiles.toggleSelection(group.files[i]); + } + } + } + if (_isDisposed) return; setState(() {}); From 31652894834dee4189cf7045310fe4794ed26340 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 17:48:39 +0530 Subject: [PATCH 04/15] Remove header --- .../lib/ui/tools/similar_images_page.dart | 50 +------------------ 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index f6613f48f6..c85a593945 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -321,55 +321,9 @@ class _SimilarImagesPageState extends State { Expanded( child: ListView.builder( cacheExtent: 400, - itemCount: _similarFilesList.length + 1, // +1 for header + itemCount: _similarFilesList.length, itemBuilder: (context, index) { - if (index == 0) { - return RepaintBoundary( - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: crossAxisSpacing, - vertical: 12, - ), - padding: const EdgeInsets.all(crossAxisSpacing), - decoration: BoxDecoration( - color: colorScheme.fillFaint, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon( - Icons.photo_library_outlined, - size: 20, - color: colorScheme.textMuted, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context).similarGroupsFound( - count: _similarFilesList.length, - ), - style: textTheme.bodyBold, - ), - const SizedBox(height: 4), - Text( - AppLocalizations.of(context) - .reviewAndRemoveSimilarImages, - style: textTheme.miniMuted, - ), - ], - ), - ), - ], - ), - ), - ); - } - - // Similar files groups (index - 1 because first item is header) - final similarFiles = _similarFilesList[index - 1]; + final similarFiles = _similarFilesList[index]; return RepaintBoundary( child: _buildSimilarFilesGroup(similarFiles), ); From 47313a74ff706ca004c0426c9f21195b3a7d646a Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 21:28:21 +0530 Subject: [PATCH 05/15] Tab bar filter --- .../lib/ui/tools/similar_images_page.dart | 117 +++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index c85a593945..9bc17ee03a 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -12,7 +12,9 @@ import "package:photos/models/similar_files.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/services/machine_learning/similar_images_service.dart"; +import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; +import "package:photos/theme/text_style.dart"; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/models/button_type.dart"; @@ -37,6 +39,12 @@ enum SortKey { count, } +enum TabFilter { + all, + similar, + identical, +} + class SimilarImagesPage extends StatefulWidget { final bool debugScreen; @@ -49,6 +57,8 @@ class SimilarImagesPage extends StatefulWidget { class _SimilarImagesPageState extends State { static const crossAxisCount = 3; static const crossAxisSpacing = 12.0; + static const double _similarThreshold = 0.02; + static const double _identicalThreshold = 0.0; final _logger = Logger("SimilarImagesPage"); bool _isDisposed = false; @@ -56,14 +66,30 @@ class _SimilarImagesPageState extends State { SimilarImagesPageState _pageState = SimilarImagesPageState.setup; double _distanceThreshold = 0.04; // Default value List _similarFilesList = []; + SortKey _sortKey = SortKey.distanceAsc; bool _exactSearch = false; bool _fullRefresh = false; bool _isSelectionSheetOpen = false; + TabFilter _selectedTab = TabFilter.all; late SelectedFiles _selectedFiles; late ValueNotifier _deleteProgress; + List get _filteredGroups { + if (_selectedTab == TabFilter.all) { + return _similarFilesList; + } + + final threshold = _selectedTab == TabFilter.similar + ? _similarThreshold + : _identicalThreshold; + + return _similarFilesList.where((group) { + return group.furthestDistance <= threshold; + }).toList(); + } + @override void initState() { super.initState(); @@ -318,14 +344,20 @@ class _SimilarImagesPageState extends State { return Column( children: [ + _buildTabBar(), Expanded( child: ListView.builder( cacheExtent: 400, - itemCount: _similarFilesList.length, + itemCount: _filteredGroups.length, itemBuilder: (context, index) { - final similarFiles = _similarFilesList[index]; + final similarFiles = _filteredGroups[index]; return RepaintBoundary( - child: _buildSimilarFilesGroup(similarFiles), + child: Column( + children: [ + SizedBox(height: index == 0 ? 0 : 16), + _buildSimilarFilesGroup(similarFiles), + ], + ), ); }, ), @@ -335,6 +367,85 @@ class _SimilarImagesPageState extends State { ); } + Widget _buildTabBar() { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + _buildTabButton( + TabFilter.all, + 'All', // TODO: lau: extract string + colorScheme, + textTheme, + ), + const SizedBox(width: 8), + _buildTabButton( + TabFilter.similar, + 'Similar', // TODO: lau: extract string + colorScheme, + textTheme, + ), + const SizedBox(width: 8), + _buildTabButton( + TabFilter.identical, + 'Identical', // TODO: lau: extract string + colorScheme, + textTheme, + ), + ], + ), + ); + } + + Widget _buildTabButton( + TabFilter tab, + String label, + EnteColorScheme colorScheme, + EnteTextTheme textTheme, + ) { + final isSelected = _selectedTab == tab; + + return GestureDetector( + onTap: () => _onTabChanged(tab), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? colorScheme.primary700 : colorScheme.fillFaint, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: isSelected ? textTheme.bodyMuted : textTheme.bodyBold, + ), + ), + ); + } + + void _onTabChanged(TabFilter newTab) { + final hadSelections = _selectedFiles.files.isNotEmpty; + + setState(() { + _selectedTab = newTab; + + if (hadSelections) { + // Select all files in the newly filtered groups + final newSelection = {}; + for (final group in _filteredGroups) { + // Skip the first file in each group (the reference file) + for (int i = 1; i < group.files.length; i++) { + newSelection.add(group.files[i]); + } + } + _selectedFiles.clearAll(); + _selectedFiles.selectAll(newSelection); + } + // If no selections before, keep everything unselected + }); + } + Widget _getBottomActionButtons() { return ListenableBuilder( listenable: _selectedFiles, From 7c2a719ba802351f80efe67e9e5cdb846a7d9fef Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 22:32:11 +0530 Subject: [PATCH 06/15] (un)select all --- .../lib/ui/tools/similar_images_page.dart | 181 +++++------------- 1 file changed, 50 insertions(+), 131 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 9bc17ee03a..111b1e971f 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -15,7 +15,6 @@ import "package:photos/services/machine_learning/similar_images_service.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; import "package:photos/theme/text_style.dart"; -import 'package:photos/ui/components/action_sheet_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/toggle_switch_widget.dart"; @@ -70,7 +69,6 @@ class _SimilarImagesPageState extends State { SortKey _sortKey = SortKey.distanceAsc; bool _exactSearch = false; bool _fullRefresh = false; - bool _isSelectionSheetOpen = false; TabFilter _selectedTab = TabFilter.all; late SelectedFiles _selectedFiles; @@ -453,6 +451,19 @@ class _SimilarImagesPageState extends State { final selectedCount = _selectedFiles.files.length; final hasSelectedFiles = selectedCount > 0; + int totalFilteredFiles = 0; + int selectedFilteredFiles = 0; + for (final group in _filteredGroups) { + for (int i = 1; i < group.files.length; i++) { + totalFilteredFiles++; + if (_selectedFiles.isFileSelected(group.files[i])) { + selectedFilteredFiles++; + } + } + } + final allFilteredSelected = totalFilteredFiles > 0 && + selectedFilteredFiles == totalFilteredFiles; + int totalSize = 0; for (final file in _selectedFiles.files) { totalSize += file.fileSize ?? 0; @@ -489,7 +500,7 @@ class _SimilarImagesPageState extends State { ), ); }, - child: hasSelectedFiles && !_isSelectionSheetOpen + child: hasSelectedFiles ? Column( key: const ValueKey('delete_section'), children: [ @@ -518,27 +529,20 @@ class _SimilarImagesPageState extends State { ) : const SizedBox.shrink(key: ValueKey('no_delete')), ), - if (!_isSelectionSheetOpen) - SizedBox( - width: double.infinity, - child: ButtonWidget( - labelText: AppLocalizations.of(context).selectionOptions, - buttonType: ButtonType.secondary, - shouldSurfaceExecutionStates: false, - shouldShowSuccessConfirmation: false, - onTap: () async { - setState(() { - _isSelectionSheetOpen = true; - }); - await _showSelectionOptionsSheet(); - if (mounted) { - setState(() { - _isSelectionSheetOpen = false; - }); - } - }, - ), + SizedBox( + width: double.infinity, + child: ButtonWidget( + labelText: allFilteredSelected + ? AppLocalizations.of(context).unselectAll + : AppLocalizations.of(context).selectAll, + buttonType: ButtonType.secondary, + shouldSurfaceExecutionStates: false, + shouldShowSuccessConfirmation: false, + onTap: () async { + _toggleSelectAll(); + }, ), + ), ], ), ), @@ -547,6 +551,29 @@ class _SimilarImagesPageState extends State { ); } + void _toggleSelectAll() { + final filesToToggle = {}; + for (final group in _filteredGroups) { + for (int i = 1; i < group.files.length; i++) { + filesToToggle.add(group.files[i]); + } + } + + bool allSelected = true; + for (final file in filesToToggle) { + if (!_selectedFiles.isFileSelected(file)) { + allSelected = false; + break; + } + } + + if (allSelected) { + _selectedFiles.unSelectAll(filesToToggle); + } else { + _selectedFiles.selectAll(filesToToggle); + } + } + Future _findSimilarImages() async { if (_isDisposed) return; setState(() { @@ -630,114 +657,6 @@ class _SimilarImagesPageState extends State { setState(() {}); } - void _selectFilesByThreshold(double threshold) { - final filesToSelect = {}; - - for (final similarFilesGroup in _similarFilesList) { - if (similarFilesGroup.furthestDistance <= threshold) { - for (int i = 1; i < similarFilesGroup.files.length; i++) { - filesToSelect.add(similarFilesGroup.files[i]); - } - } - } - - if (filesToSelect.isNotEmpty) { - _selectedFiles.clearAll(fireEvent: false); - _selectedFiles.selectAll(filesToSelect); - } else { - _selectedFiles.clearAll(fireEvent: false); - } - } - - Future _showSelectionOptionsSheet() async { - // Calculate how many files fall into each category - int exactFiles = 0; - int similarFiles = 0; - int allFiles = 0; - - for (final group in _similarFilesList) { - final duplicateCount = group.files.length - 1; // Exclude the first file - allFiles += duplicateCount; - - if (group.furthestDistance <= 0.0) { - exactFiles += duplicateCount; - similarFiles += duplicateCount; - } else if (group.furthestDistance <= 0.02) { - similarFiles += duplicateCount; - } - } - - // Always show counts, even when 0 - final String exactLabel = - AppLocalizations.of(context).selectExactWithCount(count: exactFiles); - - final String similarLabel = AppLocalizations.of(context) - .selectSimilarWithCount(count: similarFiles); - - final String allLabel = - AppLocalizations.of(context).selectAllWithCount(count: allFiles); - - await showActionSheet( - context: context, - title: AppLocalizations.of(context).selectSimilarImagesTitle, - body: AppLocalizations.of(context).chooseSimilarImagesToSelect, - buttons: [ - ButtonWidget( - labelText: exactLabel, - buttonType: ButtonType.neutral, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - isInAlert: true, - buttonAction: ButtonAction.first, - shouldSurfaceExecutionStates: false, - isDisabled: exactFiles == 0, - onTap: () async { - _selectFilesByThreshold(0.0); - }, - ), - ButtonWidget( - labelText: similarLabel, - buttonType: ButtonType.neutral, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - isInAlert: true, - buttonAction: ButtonAction.second, - shouldSurfaceExecutionStates: false, - isDisabled: similarFiles == 0, - onTap: () async { - _selectFilesByThreshold(0.02); - }, - ), - ButtonWidget( - labelText: allLabel, - buttonType: ButtonType.neutral, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - isInAlert: true, - buttonAction: ButtonAction.third, - shouldSurfaceExecutionStates: false, - isDisabled: allFiles == 0, - onTap: () async { - _selectFilesByThreshold(0.05); - }, - ), - ButtonWidget( - labelText: AppLocalizations.of(context).clearSelection, - buttonType: ButtonType.secondary, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - isInAlert: true, - buttonAction: ButtonAction.cancel, - shouldSurfaceExecutionStates: false, - onTap: () async { - _selectedFiles.clearAll(fireEvent: false); - }, - ), - ], - actionSheetType: ActionSheetType.defaultActionSheet, - ); - } - Widget _buildSimilarFilesGroup(SimilarFiles similarFiles) { final textTheme = getEnteTextTheme(context); return Padding( From 18d5aa61b0e8fc0f3b85706f8e4fcb27783256b4 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 22:34:59 +0530 Subject: [PATCH 07/15] Extract string --- mobile/apps/photos/lib/l10n/intl_en.arb | 5 ++++- mobile/apps/photos/lib/ui/tools/similar_images_page.dart | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index 259f78ea12..e98c18e64d 100644 --- a/mobile/apps/photos/lib/l10n/intl_en.arb +++ b/mobile/apps/photos/lib/l10n/intl_en.arb @@ -1941,5 +1941,8 @@ "findingSimilarImages": "Finding similar images", "almostDone": "Almost done", "processingLocally": "Processing locally", - "useMLToFindSimilarImages": "Review and remove images that look similar to each other." + "useMLToFindSimilarImages": "Review and remove images that look similar to each other.", + "all": "All", + "similar": "Similar", + "identical": "Identical" } diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 111b1e971f..fca862d031 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -375,21 +375,21 @@ class _SimilarImagesPageState extends State { children: [ _buildTabButton( TabFilter.all, - 'All', // TODO: lau: extract string + AppLocalizations.of(context).all, colorScheme, textTheme, ), const SizedBox(width: 8), _buildTabButton( TabFilter.similar, - 'Similar', // TODO: lau: extract string + AppLocalizations.of(context).similar, colorScheme, textTheme, ), const SizedBox(width: 8), _buildTabButton( TabFilter.identical, - 'Identical', // TODO: lau: extract string + AppLocalizations.of(context).identical, colorScheme, textTheme, ), From 7e8368268627f45c37f05fa2fe2777af3461d5b9 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 22:42:04 +0530 Subject: [PATCH 08/15] tiny margin in threshold --- mobile/apps/photos/lib/ui/tools/similar_images_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index fca862d031..13218c0511 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -57,7 +57,7 @@ class _SimilarImagesPageState extends State { static const crossAxisCount = 3; static const crossAxisSpacing = 12.0; static const double _similarThreshold = 0.02; - static const double _identicalThreshold = 0.0; + static const double _identicalThreshold = 0.0001; final _logger = Logger("SimilarImagesPage"); bool _isDisposed = false; From 6d21b73367d9c197d363f3e232d83bca4ab003cc Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 22:59:30 +0530 Subject: [PATCH 09/15] faster select --- .../lib/ui/tools/similar_images_page.dart | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 13218c0511..523411a468 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -429,10 +429,8 @@ class _SimilarImagesPageState extends State { _selectedTab = newTab; if (hadSelections) { - // Select all files in the newly filtered groups final newSelection = {}; for (final group in _filteredGroups) { - // Skip the first file in each group (the reference file) for (int i = 1; i < group.files.length; i++) { newSelection.add(group.files[i]); } @@ -440,7 +438,6 @@ class _SimilarImagesPageState extends State { _selectedFiles.clearAll(); _selectedFiles.selectAll(newSelection); } - // If no selections before, keep everything unselected }); } @@ -448,24 +445,24 @@ class _SimilarImagesPageState extends State { return ListenableBuilder( listenable: _selectedFiles, builder: (context, _) { - final selectedCount = _selectedFiles.files.length; + final selectedFiles = _selectedFiles.files; + final selectedCount = selectedFiles.length; final hasSelectedFiles = selectedCount > 0; - int totalFilteredFiles = 0; - int selectedFilteredFiles = 0; + final eligibleFilteredFiles = {}; for (final group in _filteredGroups) { for (int i = 1; i < group.files.length; i++) { - totalFilteredFiles++; - if (_selectedFiles.isFileSelected(group.files[i])) { - selectedFilteredFiles++; - } + eligibleFilteredFiles.add(group.files[i]); } } - final allFilteredSelected = totalFilteredFiles > 0 && - selectedFilteredFiles == totalFilteredFiles; + + final selectedFilteredFiles = + selectedFiles.intersection(eligibleFilteredFiles); + final allFilteredSelected = eligibleFilteredFiles.isNotEmpty && + selectedFilteredFiles.length == eligibleFilteredFiles.length; int totalSize = 0; - for (final file in _selectedFiles.files) { + for (final file in selectedFilteredFiles) { totalSize += file.fileSize ?? 0; } @@ -509,7 +506,7 @@ class _SimilarImagesPageState extends State { child: ButtonWidget( labelText: AppLocalizations.of(context) .deletePhotosWithSize( - count: selectedCount, + count: selectedFilteredFiles.length, size: formatBytes(totalSize), ), buttonType: ButtonType.critical, @@ -517,7 +514,7 @@ class _SimilarImagesPageState extends State { shouldShowSuccessConfirmation: false, onTap: () async { await _deleteFiles( - _selectedFiles.files, + selectedFilteredFiles, showDialog: true, showUIFeedback: true, ); @@ -552,25 +549,21 @@ class _SimilarImagesPageState extends State { } void _toggleSelectAll() { - final filesToToggle = {}; + final eligibleFiles = {}; for (final group in _filteredGroups) { for (int i = 1; i < group.files.length; i++) { - filesToToggle.add(group.files[i]); + eligibleFiles.add(group.files[i]); } } - bool allSelected = true; - for (final file in filesToToggle) { - if (!_selectedFiles.isFileSelected(file)) { - allSelected = false; - break; - } - } + final currentSelected = _selectedFiles.files.intersection(eligibleFiles); + final allSelected = eligibleFiles.isNotEmpty && + currentSelected.length == eligibleFiles.length; if (allSelected) { - _selectedFiles.unSelectAll(filesToToggle); + _selectedFiles.unSelectAll(eligibleFiles); } else { - _selectedFiles.selectAll(filesToToggle); + _selectedFiles.selectAll(eligibleFiles); } } @@ -581,7 +574,6 @@ class _SimilarImagesPageState extends State { }); try { - // You can use _toggleValue here for advanced mode features _logger.info("exact mode: $_exactSearch"); final similarFiles = await SimilarImagesService.instance.getSimilarFiles( From b740d1af0507b56ed4ed750b0f272656dbf11ffa Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 23:01:30 +0530 Subject: [PATCH 10/15] Show modal on 100+ deleted files only --- mobile/apps/photos/lib/ui/tools/similar_images_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 523411a468..770e3d9b20 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -997,7 +997,7 @@ class _SimilarImagesPageState extends State { await deleteFilesFromRemoteOnly(context, allDeleteFiles.toList()); // Show congratulations popup - if (allDeleteFiles.isNotEmpty && mounted && showUIFeedback) { + if (allDeleteFiles.length > 100 && mounted && showUIFeedback) { final int totalSize = allDeleteFiles.fold( 0, (sum, file) => sum + (file.fileSize ?? 0), From 56cc7309a50d04f4a7478ec9bb9e1dfd2d624f3d Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 23:05:04 +0530 Subject: [PATCH 11/15] Show progress only for multiple albums symlinking --- mobile/apps/photos/lib/ui/tools/similar_images_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 770e3d9b20..740813f056 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -967,7 +967,7 @@ class _SimilarImagesPageState extends State { if (!mounted) { return; } - if (collectionCnt > 0 && showUIFeedback) { + if (collectionCnt > 2 && showUIFeedback) { progress++; // calculate progress percentage upto 2 decimal places final double percentage = (progress / collectionCnt) * 100; @@ -988,7 +988,7 @@ class _SimilarImagesPageState extends State { } } } - if (showUIFeedback) { + if (collectionCnt > 2 && showUIFeedback) { _deleteProgress.value = ""; } From eca0e5943dae3cd8422e25f2998cd085fe3bd290 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 23:24:37 +0530 Subject: [PATCH 12/15] tab button look --- .../apps/photos/lib/ui/tools/similar_images_page.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 740813f056..f66afb1cda 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -379,14 +379,14 @@ class _SimilarImagesPageState extends State { colorScheme, textTheme, ), - const SizedBox(width: 8), + const SizedBox(width: crossAxisSpacing), _buildTabButton( TabFilter.similar, AppLocalizations.of(context).similar, colorScheme, textTheme, ), - const SizedBox(width: 8), + const SizedBox(width: crossAxisSpacing), _buildTabButton( TabFilter.identical, AppLocalizations.of(context).identical, @@ -409,14 +409,16 @@ class _SimilarImagesPageState extends State { return GestureDetector( onTap: () => _onTabChanged(tab), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( color: isSelected ? colorScheme.primary700 : colorScheme.fillFaint, borderRadius: BorderRadius.circular(20), ), child: Text( label, - style: isSelected ? textTheme.bodyMuted : textTheme.bodyBold, + style: isSelected + ? textTheme.smallBold.copyWith(color: Colors.white) + : textTheme.smallBold, ), ), ); From 4fd797338b7d21ccbe36f30a685be986810a4261 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 23:26:51 +0530 Subject: [PATCH 13/15] Empty state --- .../lib/ui/tools/similar_images_page.dart | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index f66afb1cda..f652e72b29 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -20,6 +20,7 @@ import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/toggle_switch_widget.dart"; import "package:photos/ui/viewer/file/detail_page.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; +import "package:photos/ui/viewer/gallery/empty_state.dart"; import "package:photos/utils/delete_file_util.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; @@ -344,23 +345,20 @@ class _SimilarImagesPageState extends State { children: [ _buildTabBar(), Expanded( - child: ListView.builder( - cacheExtent: 400, - itemCount: _filteredGroups.length, - itemBuilder: (context, index) { - final similarFiles = _filteredGroups[index]; - return RepaintBoundary( - child: Column( - children: [ - SizedBox(height: index == 0 ? 0 : 16), - _buildSimilarFilesGroup(similarFiles), - ], + child: _filteredGroups.isEmpty + ? const EmptyState() + : ListView.builder( + cacheExtent: 400, + itemCount: _filteredGroups.length, + itemBuilder: (context, index) { + final similarFiles = _filteredGroups[index]; + return RepaintBoundary( + child: _buildSimilarFilesGroup(similarFiles), + ); + }, ), - ); - }, - ), ), - _getBottomActionButtons(), + if (_filteredGroups.isNotEmpty) _getBottomActionButtons(), ], ); } @@ -961,9 +959,9 @@ class _SimilarImagesPageState extends State { _similarFilesList.remove(group); } + final int collectionCnt = collectionToFilesToAddMap.keys.length; if (createSymlink) { final userID = Configuration.instance.getUserID(); - final int collectionCnt = collectionToFilesToAddMap.keys.length; int progress = 0; for (final collectionID in collectionToFilesToAddMap.keys) { if (!mounted) { From c1ff02df14c3326a755de89386bbd602d4dff58e Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 23:39:21 +0530 Subject: [PATCH 14/15] Always select all on tab change --- .../photos/lib/ui/tools/similar_images_page.dart | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index f652e72b29..6dddb61ffb 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -423,21 +423,17 @@ class _SimilarImagesPageState extends State { } void _onTabChanged(TabFilter newTab) { - final hadSelections = _selectedFiles.files.isNotEmpty; - setState(() { _selectedTab = newTab; - if (hadSelections) { - final newSelection = {}; - for (final group in _filteredGroups) { - for (int i = 1; i < group.files.length; i++) { - newSelection.add(group.files[i]); - } + final newSelection = {}; + for (final group in _filteredGroups) { + for (int i = 1; i < group.files.length; i++) { + newSelection.add(group.files[i]); } - _selectedFiles.clearAll(); - _selectedFiles.selectAll(newSelection); } + _selectedFiles.clearAll(); + _selectedFiles.selectAll(newSelection); }); } From 21aac29020e7480fdb59311eb3e0e4e12f1dc3ab Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 26 Aug 2025 23:48:31 +0530 Subject: [PATCH 15/15] format count properly --- mobile/apps/photos/lib/l10n/intl_en.arb | 4 ++-- mobile/apps/photos/lib/ui/tools/similar_images_page.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index 13729f27c8..84391ae647 100644 --- a/mobile/apps/photos/lib/l10n/intl_en.arb +++ b/mobile/apps/photos/lib/l10n/intl_en.arb @@ -1866,7 +1866,7 @@ "@deletePhotosWithSize": { "placeholders": { "count": { - "type": "int" + "type": "String" }, "size": { "type": "String" @@ -1945,4 +1945,4 @@ "all": "All", "similar": "Similar", "identical": "Identical" -} \ No newline at end of file +} diff --git a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart index 6dddb61ffb..0f9e43a593 100644 --- a/mobile/apps/photos/lib/ui/tools/similar_images_page.dart +++ b/mobile/apps/photos/lib/ui/tools/similar_images_page.dart @@ -2,6 +2,7 @@ import "dart:async"; import "package:flutter/foundation.dart" show kDebugMode; import 'package:flutter/material.dart'; +import "package:intl/intl.dart"; import 'package:logging/logging.dart'; import "package:photos/core/configuration.dart"; import 'package:photos/core/constants.dart'; @@ -502,7 +503,7 @@ class _SimilarImagesPageState extends State { child: ButtonWidget( labelText: AppLocalizations.of(context) .deletePhotosWithSize( - count: selectedFilteredFiles.length, + count: NumberFormat().format(selectedFilteredFiles.length), size: formatBytes(totalSize), ), buttonType: ButtonType.critical,