diff --git a/mobile/apps/locker/lib/l10n/app_en.arb b/mobile/apps/locker/lib/l10n/app_en.arb index c2c94289afe..2812f19a29a 100644 --- a/mobile/apps/locker/lib/l10n/app_en.arb +++ b/mobile/apps/locker/lib/l10n/app_en.arb @@ -95,6 +95,8 @@ } }, "collectionCannotBeDeleted": "This collection cannot be deleted", + "collectionCannotBeEdited": "This collection cannot be edited", + "collectionCannotBeShared": "This collection cannot be shared", "deleteCollection": "Delete collection", "deleteCollectionConfirmation": "Are you sure you want to delete \"{collectionName}\"?", "@deleteCollectionConfirmation": { diff --git a/mobile/apps/locker/lib/l10n/app_localizations.dart b/mobile/apps/locker/lib/l10n/app_localizations.dart index d5d4e74b7f7..b57c302e4da 100644 --- a/mobile/apps/locker/lib/l10n/app_localizations.dart +++ b/mobile/apps/locker/lib/l10n/app_localizations.dart @@ -292,6 +292,18 @@ abstract class AppLocalizations { /// **'This collection cannot be deleted'** String get collectionCannotBeDeleted; + /// No description provided for @collectionCannotBeEdited. + /// + /// In en, this message translates to: + /// **'This collection cannot be edited'** + String get collectionCannotBeEdited; + + /// No description provided for @collectionCannotBeShared. + /// + /// In en, this message translates to: + /// **'This collection cannot be shared'** + String get collectionCannotBeShared; + /// No description provided for @deleteCollection. /// /// In en, this message translates to: diff --git a/mobile/apps/locker/lib/l10n/app_localizations_en.dart b/mobile/apps/locker/lib/l10n/app_localizations_en.dart index d88bdd914f1..eb172e4f5c6 100644 --- a/mobile/apps/locker/lib/l10n/app_localizations_en.dart +++ b/mobile/apps/locker/lib/l10n/app_localizations_en.dart @@ -123,6 +123,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get collectionCannotBeDeleted => 'This collection cannot be deleted'; + @override + String get collectionCannotBeEdited => 'This collection cannot be edited'; + + @override + String get collectionCannotBeShared => 'This collection cannot be shared'; + @override String get deleteCollection => 'Delete collection'; diff --git a/mobile/apps/locker/lib/services/collections/models/collection.dart b/mobile/apps/locker/lib/services/collections/models/collection.dart index 3146a71da59..92515fac7e2 100644 --- a/mobile/apps/locker/lib/services/collections/models/collection.dart +++ b/mobile/apps/locker/lib/services/collections/models/collection.dart @@ -297,6 +297,12 @@ String typeToString(CollectionType type) { extension CollectionTypeExtn on CollectionType { bool get canDelete => this != CollectionType.favorites && this != CollectionType.uncategorized; + + bool get canEdit => + this != CollectionType.favorites && this != CollectionType.uncategorized; + + bool get canShare => + this != CollectionType.favorites && this != CollectionType.uncategorized; } enum CollectionParticipantRole { diff --git a/mobile/apps/locker/lib/ui/components/collection_list_widget.dart b/mobile/apps/locker/lib/ui/components/collection_list_widget.dart index 3c15573ac10..ec472368a65 100644 --- a/mobile/apps/locker/lib/ui/components/collection_list_widget.dart +++ b/mobile/apps/locker/lib/ui/components/collection_list_widget.dart @@ -10,14 +10,11 @@ import "package:locker/models/selected_collections.dart"; import "package:locker/services/collections/collections_service.dart"; import "package:locker/services/collections/models/collection.dart"; import "package:locker/services/configuration.dart"; -import "package:locker/ui/components/collection_popup_menu_widget.dart"; -import "package:locker/ui/components/item_list_view.dart"; import "package:locker/ui/pages/collection_page.dart"; import "package:locker/ui/sharing/album_share_info_widget.dart"; class CollectionListWidget extends StatelessWidget { final Collection collection; - final List? overflowActions; final bool isLastItem; final SelectedCollections? selectedCollections; final void Function(Collection)? onTapCallback; @@ -26,7 +23,6 @@ class CollectionListWidget extends StatelessWidget { const CollectionListWidget({ super.key, required this.collection, - this.overflowActions, this.isLastItem = false, this.selectedCollections, this.onTapCallback, @@ -150,7 +146,7 @@ class CollectionListWidget extends StatelessWidget { final bool isSelected = selectedCollections?.isCollectionSelected(collection) ?? false; return AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 300), curve: Curves.easeOut, decoration: BoxDecoration( border: Border.all( @@ -166,58 +162,30 @@ class CollectionListWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ collectionRowWidget, - isFavourite - ? const SizedBox.shrink() - : AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeIn, - child: isSelected - ? IconButtonWidget( - key: const ValueKey("selected"), - icon: Icons.check_circle_rounded, - iconButtonType: IconButtonType.secondary, - iconColor: colorScheme.primary700, - ) - : showSharingIndicator - ? Row( - key: const ValueKey("shared"), - mainAxisSize: MainAxisSize.min, - children: [ - if (hasSharees) - _buildShareesAvatars( - collection.sharees - .whereType() - .toList(), - ), - CollectionPopupMenuWidget( - collection: collection, - overflowActions: overflowActions, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: HugeIcon( - icon: HugeIcons - .strokeRoundedMoreVertical, - color: colorScheme.textBase, - ), - ), - ), - ], - ) - : CollectionPopupMenuWidget( - key: const ValueKey("menu"), - collection: collection, - overflowActions: overflowActions, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: HugeIcon( - icon: - HugeIcons.strokeRoundedMoreVertical, - color: colorScheme.textBase, - ), - ), - ), - ), + if (!isFavourite) + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: isSelected + ? IconButtonWidget( + key: const ValueKey("selected"), + icon: Icons.check_circle_rounded, + iconButtonType: IconButtonType.secondary, + iconColor: colorScheme.primary700, + ) + : (showSharingIndicator && hasSharees) + ? _buildShareesAvatars( + collection.sharees.whereType().toList(), + ) + : const SizedBox( + key: ValueKey("unselected"), + width: 12, + ), + ), + ), ], ), ); diff --git a/mobile/apps/locker/lib/ui/components/delete_confirmation_sheet.dart b/mobile/apps/locker/lib/ui/components/delete_confirmation_sheet.dart index 6db65b748f2..f354b147954 100644 --- a/mobile/apps/locker/lib/ui/components/delete_confirmation_sheet.dart +++ b/mobile/apps/locker/lib/ui/components/delete_confirmation_sheet.dart @@ -1,6 +1,6 @@ -import "package:ente_ui/components/base_bottom_sheet.dart"; import "package:ente_ui/components/buttons/button_widget.dart"; import "package:ente_ui/components/buttons/models/button_result.dart"; +import "package:ente_ui/components/close_icon_button.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:flutter/material.dart"; import "package:locker/l10n/l10n.dart"; @@ -24,12 +24,13 @@ Future showDeleteConfirmationSheet( required String assetPath, bool showDeleteFromAllCollectionsOption = false, }) { - return showBaseBottomSheet( - context, - title: title, - headerSpacing: 20, - crossAxisAlignment: CrossAxisAlignment.center, - child: DeleteConfirmationSheet( + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + backgroundColor: Colors.transparent, + builder: (context) => DeleteConfirmationSheet( + title: title, body: body, deleteButtonLabel: deleteButtonLabel, assetPath: assetPath, @@ -39,6 +40,7 @@ Future showDeleteConfirmationSheet( } class DeleteConfirmationSheet extends StatefulWidget { + final String title; final String body; final String deleteButtonLabel; final String assetPath; @@ -46,6 +48,7 @@ class DeleteConfirmationSheet extends StatefulWidget { const DeleteConfirmationSheet({ super.key, + required this.title, required this.body, required this.deleteButtonLabel, required this.assetPath, @@ -65,87 +68,114 @@ class _DeleteConfirmationSheetState extends State { final textTheme = getEnteTextTheme(context); final colorScheme = getEnteColorScheme(context); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center(child: Image.asset(widget.assetPath)), - const SizedBox(height: 16), - Text( - widget.body, - style: textTheme.body.copyWith( - color: colorScheme.textMuted, - ), - textAlign: TextAlign.center, + return Container( + decoration: BoxDecoration( + color: colorScheme.backgroundElevated2, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), ), - const SizedBox(height: 20), - if (widget.showDeleteFromAllCollectionsOption) ...[ - GestureDetector( - onTap: () { - setState(() { - _deleteFromAllCollections = !_deleteFromAllCollections; - }); - }, - child: SizedBox( - width: double.infinity, - child: Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 12, - runSpacing: 8, + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - border: Border.all( - color: _deleteFromAllCollections - ? colorScheme.primary700 - : colorScheme.strokeMuted, - width: 2, - ), - borderRadius: BorderRadius.circular(6), - color: _deleteFromAllCollections - ? colorScheme.primary700 - : Colors.transparent, - ), - alignment: Alignment.center, - child: _deleteFromAllCollections - ? const Icon( - Icons.check, - size: 12, - color: Colors.white, - ) - : null, - ), - Text( - context.l10n.deleteCollectionFromEverywhere, - style: textTheme.small, - textAlign: TextAlign.center, - ), + CloseIconButton(), ], ), - ), - ), - const SizedBox(height: 20), - ], - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: GradientButton( - onTap: () { - Navigator.of(context).pop( - DeleteConfirmationResult( - buttonResult: ButtonResult(ButtonAction.first), - deleteFromAllCollections: _deleteFromAllCollections, + const SizedBox(height: 12), + Center(child: Image.asset(widget.assetPath)), + const SizedBox(height: 20), + Text( + widget.title, + style: textTheme.h3Bold, + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + widget.body, + style: textTheme.body.copyWith( + color: colorScheme.textMuted, ), - ); - }, - text: widget.deleteButtonLabel, - backgroundColor: colorScheme.warning400, + textAlign: TextAlign.center, + ), + if (widget.showDeleteFromAllCollectionsOption) ...[ + const SizedBox(height: 20), + GestureDetector( + onTap: () { + setState(() { + _deleteFromAllCollections = !_deleteFromAllCollections; + }); + }, + child: SizedBox( + width: double.infinity, + child: Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 12, + runSpacing: 8, + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + border: Border.all( + color: _deleteFromAllCollections + ? colorScheme.primary700 + : colorScheme.strokeMuted, + width: 2, + ), + borderRadius: BorderRadius.circular(6), + color: _deleteFromAllCollections + ? colorScheme.primary700 + : Colors.transparent, + ), + alignment: Alignment.center, + child: _deleteFromAllCollections + ? const Icon( + Icons.check, + size: 12, + color: Colors.white, + ) + : null, + ), + Text( + context.l10n.deleteCollectionFromEverywhere, + style: textTheme.small, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: GradientButton( + onTap: () { + Navigator.of(context).pop( + DeleteConfirmationResult( + buttonResult: ButtonResult(ButtonAction.first), + deleteFromAllCollections: _deleteFromAllCollections, + ), + ); + }, + text: widget.deleteButtonLabel, + backgroundColor: colorScheme.warning400, + ), + ), + ], ), ), - ], + ), ); } } diff --git a/mobile/apps/locker/lib/ui/components/file_list_widget.dart b/mobile/apps/locker/lib/ui/components/file_list_widget.dart index 8bc03f7bf96..62251ea86c5 100644 --- a/mobile/apps/locker/lib/ui/components/file_list_widget.dart +++ b/mobile/apps/locker/lib/ui/components/file_list_widget.dart @@ -1,20 +1,21 @@ +import "package:ente_sharing/models/user.dart"; +import "package:ente_sharing/user_avator_widget.dart"; import "package:ente_ui/components/buttons/icon_button_widget.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:flutter/material.dart"; import "package:hugeicons/hugeicons.dart"; import "package:locker/models/selected_files.dart"; +import "package:locker/services/collections/collections_service.dart"; import "package:locker/services/configuration.dart"; import "package:locker/services/files/sync/models/file.dart"; import "package:locker/services/info_file_service.dart"; -import "package:locker/ui/components/file_popup_menu_widget.dart"; -import "package:locker/ui/components/item_list_view.dart"; +import "package:locker/ui/sharing/album_share_info_widget.dart"; import "package:locker/utils/file_icon_utils.dart"; import "package:locker/utils/file_util.dart"; import "package:locker/utils/info_item_utils.dart"; class FileListWidget extends StatelessWidget { final EnteFile file; - final List? overflowActions; final bool isLastItem; final SelectedFiles? selectedFiles; final void Function(EnteFile)? onTapCallback; @@ -23,32 +24,66 @@ class FileListWidget extends StatelessWidget { const FileListWidget({ super.key, required this.file, - this.overflowActions, this.isLastItem = false, this.selectedFiles, this.onTapCallback, this.onLongPressCallback, }); - // TODO: Re-enable file popup menu for shared files when file actions are ready - bool get _isFileOwnedByUser { - final currentUserID = Configuration.instance.getUserID(); - return file.ownerID == currentUserID; - } - @override Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); + final collection = file.collectionID != null + ? CollectionService.instance.getFromCache(file.collectionID!) + : null; + + final int? currentUserID = Configuration.instance.getUserID(); + final bool isOwner = collection != null && + currentUserID != null && + collection.isOwner(currentUserID); + final List sharees = + collection?.sharees.whereType().toList() ?? const []; + final bool hasSharees = sharees.isNotEmpty; + final bool isOutgoing = isOwner && hasSharees; + final bool isIncoming = collection != null && !isOwner; + final bool showSharingIndicator = isOutgoing || isIncoming; + final fileRowWidget = Flexible( flex: 6, child: Row( children: [ - Container( - padding: const EdgeInsets.all(10.0), + SizedBox( height: 60, width: 60, - child: _buildFileIcon(), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: _buildFileIcon(), + ), + if (showSharingIndicator) + Positioned( + right: 1, + bottom: 10, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorScheme.backdropBase, + ), + padding: const EdgeInsets.all(1.0), + child: HugeIcon( + icon: isOutgoing + ? HugeIcons.strokeRoundedCircleArrowUpRight + : HugeIcons.strokeRoundedCircleArrowDownLeft, + strokeWidth: 2.0, + color: colorScheme.primary700, + size: 16.0, + ), + ), + ), + ], + ), ), const SizedBox(width: 12), Flexible( @@ -103,10 +138,10 @@ class FileListWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ fileRowWidget, - Flexible( - flex: 1, + Padding( + padding: const EdgeInsets.only(right: 12.0), child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 300), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeIn, child: isSelected @@ -116,19 +151,12 @@ class FileListWidget extends StatelessWidget { iconButtonType: IconButtonType.secondary, iconColor: colorScheme.primary700, ) - : _isFileOwnedByUser - ? FilePopupMenuWidget( - file: file, - overflowActions: overflowActions, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: HugeIcon( - icon: HugeIcons.strokeRoundedMoreVertical, - color: getEnteColorScheme(context).textBase, - ), - ), - ) - : const SizedBox(width: 48), + : (showSharingIndicator && hasSharees) + ? _buildShareesAvatars(sharees) + : const SizedBox( + key: ValueKey("unselected"), + width: 12, + ), ), ), ], @@ -140,7 +168,6 @@ class FileListWidget extends StatelessWidget { } Widget _buildFileIcon() { - // Check if this is an info file if (InfoFileService.instance.isInfoFile(file)) { try { final infoItem = InfoFileService.instance.extractInfoFromFile(file); @@ -152,7 +179,33 @@ class FileListWidget extends StatelessWidget { } } - // For non-info files or if extraction fails, use the original logic return FileIconUtils.getFileIcon(file.displayName, showBackground: true); } + + Widget _buildShareesAvatars(List sharees) { + if (sharees.isEmpty) { + return const SizedBox.shrink(); + } + + const int limitCountTo = 1; + const double avatarSize = 24.0; + const double overlapPadding = 20.0; + + final hasMore = sharees.length > limitCountTo; + + final double totalWidth = + hasMore ? avatarSize + overlapPadding : avatarSize; + + return SizedBox( + height: avatarSize, + width: totalWidth, + child: AlbumSharesIcons( + sharees: sharees, + padding: EdgeInsets.zero, + limitCountTo: limitCountTo, + type: AvatarType.mini, + removeBorder: true, + ), + ); + } } diff --git a/mobile/apps/locker/lib/ui/components/file_popup_menu_widget.dart b/mobile/apps/locker/lib/ui/components/file_popup_menu_widget.dart index a133adecdcf..8ffd2976829 100644 --- a/mobile/apps/locker/lib/ui/components/file_popup_menu_widget.dart +++ b/mobile/apps/locker/lib/ui/components/file_popup_menu_widget.dart @@ -178,7 +178,7 @@ class FilePopupMenuWidget extends StatelessWidget { _editFile(context); break; case 'toggle_important': - _toggleImportant(context); + _markImportant(context); break; case 'download': _downloadFile(context); @@ -214,8 +214,8 @@ class FilePopupMenuWidget extends StatelessWidget { await FileActions.editFile(context, file); } - Future _toggleImportant(BuildContext context) async { - await FileActions.toggleImportant( + Future _markImportant(BuildContext context) async { + await FileActions.markImportant( context, file, ); diff --git a/mobile/apps/locker/lib/ui/components/item_list_view.dart b/mobile/apps/locker/lib/ui/components/item_list_view.dart index d545a4288d7..246d1a5cea8 100644 --- a/mobile/apps/locker/lib/ui/components/item_list_view.dart +++ b/mobile/apps/locker/lib/ui/components/item_list_view.dart @@ -1,9 +1,10 @@ -import 'package:ente_ui/theme/ente_theme.dart'; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/utils/toast_util.dart"; import "package:ente_utils/ente_utils.dart"; -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:locker/extensions/collection_extension.dart"; -import 'package:locker/l10n/l10n.dart'; +import "package:locker/l10n/l10n.dart"; import 'package:locker/models/file_type.dart'; import 'package:locker/models/selected_collections.dart'; import 'package:locker/models/selected_files.dart'; @@ -37,25 +38,27 @@ class OverflowMenuAction { } class ItemListView extends StatefulWidget { + static const double _selectionOverlayPadding = 200.0; + final List files; final List collections; final Widget? emptyStateWidget; - final List? fileOverflowActions; - final List? collectionOverflowActions; final SelectedCollections? selectedCollections; final SelectedFiles? selectedFiles; final ScrollPhysics? physics; + final ScrollController? scrollController; + final bool selectionEnabled; const ItemListView({ super.key, this.files = const [], this.collections = const [], this.emptyStateWidget, - this.fileOverflowActions, - this.collectionOverflowActions, this.selectedCollections, this.selectedFiles, this.physics = const NeverScrollableScrollPhysics(), + this.scrollController, + this.selectionEnabled = true, }); @override @@ -95,13 +98,15 @@ class _ItemListViewState extends State { } void _toggleCollectionSelection(Collection collection) { - // TODO(aman): Re-enable collection multi-select when bulk actions return. - return; - // HapticFeedback.lightImpact(); - // widget.selectedCollections!.toggleSelection(collection); + HapticFeedback.lightImpact(); + widget.selectedCollections!.toggleSelection(collection); } void _toggleFileSelection(EnteFile file) { + if (!widget.selectionEnabled) { + showToast(context, "Multi-select for shared files coming soon"); + return; + } HapticFeedback.lightImpact(); widget.selectedFiles!.toggleSelection(file); } @@ -116,10 +121,23 @@ class _ItemListViewState extends State { return _buildDefaultEmptyState(context); } + final hasFileSelection = + widget.selectedFiles != null && widget.selectionEnabled; + final hasCollectionSelection = widget.selectedCollections != null; + final bottomPadding = (hasFileSelection || hasCollectionSelection) + ? ItemListView._selectionOverlayPadding + : 0.0; + + return _buildListView(bottomPadding: bottomPadding); + } + + Widget _buildListView({double bottomPadding = 0}) { return ListView.separated( + controller: widget.scrollController, separatorBuilder: (context, index) => const SizedBox(height: 8), shrinkWrap: true, physics: widget.physics, + padding: EdgeInsets.only(bottom: bottomPadding), itemCount: _sortedItems.length, itemBuilder: (context, index) => _buildItem(index), ); @@ -196,7 +214,6 @@ class _ItemListViewState extends State { }) { return CollectionListWidget( collection: collection, - overflowActions: widget.collectionOverflowActions, isLastItem: isLastItem, selectedCollections: widget.selectedCollections, onTapCallback: onTap, @@ -212,7 +229,6 @@ class _ItemListViewState extends State { }) { return FileListWidget( file: file, - overflowActions: widget.fileOverflowActions, isLastItem: isLastItem, selectedFiles: widget.selectedFiles, onTapCallback: onTap, diff --git a/mobile/apps/locker/lib/ui/components/recents_section_widget.dart b/mobile/apps/locker/lib/ui/components/recents_section_widget.dart index e11f767e31f..e4720f24b7b 100644 --- a/mobile/apps/locker/lib/ui/components/recents_section_widget.dart +++ b/mobile/apps/locker/lib/ui/components/recents_section_widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import "package:hugeicons/hugeicons.dart"; import "package:locker/extensions/collection_extension.dart"; import 'package:locker/l10n/l10n.dart'; +import "package:locker/models/selected_files.dart"; import 'package:locker/services/collections/collections_service.dart'; import 'package:locker/services/collections/models/collection.dart'; import 'package:locker/services/files/sync/models/file.dart'; @@ -17,11 +18,15 @@ import 'package:locker/ui/pages/all_collections_page.dart'; class RecentsSectionWidget extends StatefulWidget { final List collections; final List recentFiles; + final SelectedFiles? selectedFiles; + final ValueNotifier>? displayedFilesNotifier; const RecentsSectionWidget({ super.key, required this.collections, required this.recentFiles, + this.selectedFiles, + this.displayedFilesNotifier, }); @override @@ -43,6 +48,15 @@ class _RecentsSectionWidgetState extends State { super.initState(); _originalCollectionOrder = List.from(widget.collections); _availableCollections = List.from(widget.collections); + // Update notifier with initial displayed files after first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateDisplayedFilesNotifier(); + }); + } + + void _updateDisplayedFilesNotifier() { + if (!mounted) return; + widget.displayedFilesNotifier?.value = _displayedFiles; } @override @@ -60,6 +74,12 @@ class _RecentsSectionWidgetState extends State { } }); _updateFilteredFiles(); + // Update notifier when source files change and no filters are active + if (!_hasActiveFilters) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateDisplayedFilesNotifier(); + }); + } } } @@ -183,6 +203,7 @@ class _RecentsSectionWidgetState extends State { : ItemListView( key: const ValueKey('items_list'), files: _displayedFiles, + selectedFiles: widget.selectedFiles, ), ); } @@ -246,6 +267,11 @@ class _RecentsSectionWidgetState extends State { _filteredFiles = []; _availableCollections = List.from(widget.collections); }); + + // Update notifier when filters are cleared + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateDisplayedFilesNotifier(); + }); return; } @@ -298,6 +324,11 @@ class _RecentsSectionWidgetState extends State { _filteredFiles = filteredFiles; _availableCollections = availableCollections; }); + + // Update notifier with the new displayed files + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateDisplayedFilesNotifier(); + }); } Future>> _ensureCollectionsForFiles( diff --git a/mobile/apps/locker/lib/ui/components/selection_action_button_widget.dart b/mobile/apps/locker/lib/ui/components/selection_action_button_widget.dart index aefc028828d..6e96551b094 100644 --- a/mobile/apps/locker/lib/ui/components/selection_action_button_widget.dart +++ b/mobile/apps/locker/lib/ui/components/selection_action_button_widget.dart @@ -33,24 +33,16 @@ class SelectionActionButton extends StatelessWidget { color: colorScheme.backgroundElevated2, borderRadius: BorderRadius.circular(24.0), ), - padding: const EdgeInsets.symmetric( - vertical: 18.0, - horizontal: 12.0, - ), + padding: const EdgeInsets.symmetric(vertical: 16.0), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - hugeIcon ?? - Icon( - icon!, - color: color, - size: 24, - ), + hugeIcon ?? Icon(icon!, color: color, size: 24), const SizedBox(height: 8), Text( label, - style: textTheme.body.copyWith( + style: textTheme.small.copyWith( color: color, ), textAlign: TextAlign.center, diff --git a/mobile/apps/locker/lib/ui/pages/all_collections_page.dart b/mobile/apps/locker/lib/ui/pages/all_collections_page.dart index 2b84e7425a8..2c5332324b0 100644 --- a/mobile/apps/locker/lib/ui/pages/all_collections_page.dart +++ b/mobile/apps/locker/lib/ui/pages/all_collections_page.dart @@ -14,18 +14,17 @@ import 'package:locker/services/collections/models/collection.dart'; import "package:locker/ui/components/empty_state_widget.dart"; import 'package:locker/ui/components/item_list_view.dart'; import 'package:locker/ui/pages/collection_page.dart'; +import "package:locker/ui/viewer/actions/collection_selection_overlay_bar.dart"; import "package:locker/utils/collection_actions.dart"; import 'package:locker/utils/collection_sort_util.dart'; import 'package:logging/logging.dart'; class AllCollectionsPage extends StatefulWidget { final UISectionType viewType; - final SelectedCollections? selectedCollections; const AllCollectionsPage({ super.key, this.viewType = UISectionType.homeCollections, - this.selectedCollections, }); @override @@ -42,6 +41,9 @@ class _AllCollectionsPageState extends State { final _logger = Logger("AllCollectionsPage"); StreamSubscription? _collectionsUpdatedSub; + final _selectedCollections = SelectedCollections(); + final _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -59,6 +61,7 @@ class _AllCollectionsPageState extends State { @override void dispose() { _collectionsUpdatedSub?.cancel(); + _scrollController.dispose(); super.dispose(); } @@ -154,12 +157,10 @@ class _AllCollectionsPageState extends State { body: Stack( children: [ _buildBody(context), - // TODO(aman): Uncomment when multi-select actions are restored. - // CollectionSelectionOverlayBar( - // collection: _sortedCollections, - // selectedCollections: widget.selectedCollections!, - // viewType: widget.viewType, - // ), + CollectionSelectionOverlayBar( + collections: _sortedCollections, + selectedCollections: _selectedCollections, + ), ], ), ); @@ -276,8 +277,8 @@ class _AllCollectionsPageState extends State { Expanded( child: ItemListView( collections: _sortedCollections, - // TODO(aman): pass selectedCollections when multi-select returns. - selectedCollections: null, + selectedCollections: _selectedCollections, + scrollController: _scrollController, physics: const BouncingScrollPhysics(), ), ), diff --git a/mobile/apps/locker/lib/ui/pages/collection_page.dart b/mobile/apps/locker/lib/ui/pages/collection_page.dart index f16d9808a53..ec56e8fdd20 100644 --- a/mobile/apps/locker/lib/ui/pages/collection_page.dart +++ b/mobile/apps/locker/lib/ui/pages/collection_page.dart @@ -25,6 +25,7 @@ import 'package:locker/ui/mixins/search_mixin.dart'; import 'package:locker/ui/pages/home_page.dart'; import 'package:locker/ui/pages/uploader_page.dart'; import "package:locker/ui/sharing/share_collection_bottom_sheet.dart"; +import "package:locker/ui/viewer/actions/file_selection_overlay_bar.dart"; import 'package:locker/utils/collection_actions.dart'; import "package:logging/logging.dart"; @@ -59,6 +60,7 @@ class _CollectionPageState extends UploaderPageState bool isFavorite = false; final _selectedFiles = SelectedFiles(); + final _scrollController = ScrollController(); @override void onFileUploadComplete() { @@ -103,12 +105,19 @@ class _CollectionPageState extends UploaderPageState @override void dispose() { _collectionUpdateSubscription.cancel(); + _scrollController.dispose(); super.dispose(); } List get _displayedFiles => isSearchActive ? _filteredFiles : _files; + bool get _isSelectionEnabled => + collectionViewType != CollectionViewType.sharedCollectionViewer && + collectionViewType != CollectionViewType.sharedCollectionCollaborator && + collectionViewType != CollectionViewType.quickLink && + collectionViewType != CollectionViewType.favorite; + @override void initState() { super.initState(); @@ -256,11 +265,12 @@ class _CollectionPageState extends UploaderPageState alignment: Alignment.bottomCenter, children: [ _buildBody(colorScheme, textTheme), - // TODO(aman): Re-enable file multi-select overlay when bulk actions return. - // FileSelectionOverlayBar( - // files: _displayedFiles, - // selectedFiles: _selectedFiles, - // ), + FileSelectionOverlayBar( + files: _displayedFiles, + selectedFiles: _selectedFiles, + collectionViewType: collectionViewType, + scrollController: _scrollController, + ), ], ), ), @@ -476,9 +486,10 @@ class _CollectionPageState extends UploaderPageState : ItemListView( key: ValueKey(_displayedFiles.length), files: _displayedFiles, - // TODO(aman): pass selectedFiles when multi-select returns. - selectedFiles: null, + selectedFiles: _selectedFiles, + scrollController: _scrollController, physics: const BouncingScrollPhysics(), + selectionEnabled: _isSelectionEnabled, ), ), ), diff --git a/mobile/apps/locker/lib/ui/pages/home_page.dart b/mobile/apps/locker/lib/ui/pages/home_page.dart index 04f97ca8748..6e70c3723f9 100644 --- a/mobile/apps/locker/lib/ui/pages/home_page.dart +++ b/mobile/apps/locker/lib/ui/pages/home_page.dart @@ -13,6 +13,7 @@ import 'package:listen_sharing_intent/listen_sharing_intent.dart'; import 'package:locker/events/collections_updated_event.dart'; import 'package:locker/events/trigger_logout_event.dart'; import 'package:locker/l10n/l10n.dart'; +import 'package:locker/models/selected_files.dart'; import 'package:locker/services/collections/collections_service.dart'; import 'package:locker/services/collections/models/collection.dart'; import 'package:locker/services/configuration.dart'; @@ -26,6 +27,7 @@ import "package:locker/ui/drawer/drawer_page.dart"; import 'package:locker/ui/mixins/search_mixin.dart'; import 'package:locker/ui/pages/save_page.dart'; import 'package:locker/ui/pages/uploader_page.dart'; +import "package:locker/ui/viewer/actions/file_selection_overlay_bar.dart"; import 'package:locker/utils/collection_sort_util.dart'; import 'package:logging/logging.dart'; @@ -214,6 +216,8 @@ class _HomePageState extends UploaderPageState ); final scaffoldKey = GlobalKey(); final _searchFocusNode = FocusNode(); + final _selectedFiles = SelectedFiles(); + final _scrollController = ScrollController(); bool _isLoading = true; bool _hasCompletedInitialLoad = false; bool _isSettingsOpen = false; @@ -222,6 +226,8 @@ class _HomePageState extends UploaderPageState List _filteredCollections = []; List _recentFiles = []; List _filteredFiles = []; + final ValueNotifier> _displayedFilesNotifier = + ValueNotifier([]); String? _error; final _logger = Logger('HomePage'); @@ -318,6 +324,8 @@ class _HomePageState extends UploaderPageState @override void dispose() { _searchFocusNode.dispose(); + _scrollController.dispose(); + _displayedFilesNotifier.dispose(); _deepLinkSubscription?.cancel(); _triggerLogoutSubscription?.cancel(); disposeSharing(); @@ -586,61 +594,95 @@ class _HomePageState extends UploaderPageState Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); return UserDetailsStateWidget( - child: PopScope( - canPop: !isSearchActive && !_isSettingsOpen, - onPopInvokedWithResult: (didPop, result) async { - if (didPop) { - return; - } - - if (isSearchActive) { - _handleClearSearch(); - return; - } - - if (_isSettingsOpen) { - scaffoldKey.currentState!.closeDrawer(); - return; - } - }, - child: KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: handleKeyEvent, - child: Scaffold( - key: scaffoldKey, - backgroundColor: colorScheme.backgroundBase, - drawer: Drawer( - width: 428, - backgroundColor: colorScheme.backgroundBase, - child: _settingsPage, - ), - drawerEnableOpenDragGesture: !Platform.isAndroid, - onDrawerChanged: (isOpened) => _isSettingsOpen = isOpened, - appBar: CustomLockerAppBar( - scaffoldKey: scaffoldKey, - isSearchActive: isSearchActive, - isSyncing: !_hasCompletedInitialLoad || _isLoading, - searchController: searchController, - searchFocusNode: _searchFocusNode, - onSearchFocused: _handleSearchFocused, - onClearSearch: _handleClearSearch, - onSearchChanged: _handleSearchChange, - ), - body: _buildBody(), - floatingActionButton: isSearchActive - ? null - : FloatingActionButton( - onPressed: _openSavePage, - shape: const CircleBorder(), - backgroundColor: colorScheme.primary700, - elevation: 0, - child: const HugeIcon( - icon: HugeIcons.strokeRoundedPlusSign, - color: Colors.white, + child: ListenableBuilder( + listenable: _selectedFiles, + builder: (context, _) { + final hasSelection = _selectedFiles.files.isNotEmpty; + return PopScope( + canPop: !isSearchActive && !_isSettingsOpen && !hasSelection, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) { + return; + } + + if (hasSelection) { + _selectedFiles.clearAll(); + return; + } + + if (isSearchActive) { + _handleClearSearch(); + return; + } + + if (_isSettingsOpen) { + scaffoldKey.currentState!.closeDrawer(); + return; + } + }, + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: handleKeyEvent, + child: Scaffold( + key: scaffoldKey, + backgroundColor: colorScheme.backgroundBase, + drawer: Drawer( + width: 428, + backgroundColor: colorScheme.backgroundBase, + child: _settingsPage, + ), + drawerEnableOpenDragGesture: !Platform.isAndroid, + onDrawerChanged: (isOpened) => _isSettingsOpen = isOpened, + appBar: CustomLockerAppBar( + scaffoldKey: scaffoldKey, + isSearchActive: isSearchActive, + searchController: searchController, + searchFocusNode: _searchFocusNode, + onSearchFocused: _handleSearchFocused, + onClearSearch: _handleClearSearch, + onSearchChanged: _handleSearchChange, + ), + body: Stack( + children: [ + _buildBody(), + ValueListenableBuilder>( + valueListenable: _displayedFilesNotifier, + builder: (context, displayedFiles, _) { + return FileSelectionOverlayBar( + selectedFiles: _selectedFiles, + files: displayedFiles.isNotEmpty + ? displayedFiles + : _recentFiles, + scrollController: _scrollController, + ); + }, ), - ), - ), - ), + ], + ), + floatingActionButton: isSearchActive + ? null + : ListenableBuilder( + listenable: _selectedFiles, + builder: (context, _) { + if (_selectedFiles.files.isNotEmpty) { + return const SizedBox.shrink(); + } + return FloatingActionButton( + onPressed: _openSavePage, + shape: const CircleBorder(), + backgroundColor: colorScheme.primary700, + elevation: 0, + child: const HugeIcon( + icon: HugeIcons.strokeRoundedPlusSign, + color: Colors.white, + ), + ); + }, + ), + ), + ), + ); + }, ), ); } @@ -711,6 +753,7 @@ class _HomePageState extends UploaderPageState ), ) : SingleChildScrollView( + controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only( left: 16.0, @@ -724,6 +767,8 @@ class _HomePageState extends UploaderPageState RecentsSectionWidget( collections: _filterOutUncategorized(_collections), recentFiles: _recentFiles, + selectedFiles: _selectedFiles, + displayedFilesNotifier: _displayedFilesNotifier, ), ], ), diff --git a/mobile/apps/locker/lib/ui/pages/trash_page.dart b/mobile/apps/locker/lib/ui/pages/trash_page.dart index 8298fc0bae1..222e8ada830 100644 --- a/mobile/apps/locker/lib/ui/pages/trash_page.dart +++ b/mobile/apps/locker/lib/ui/pages/trash_page.dart @@ -6,7 +6,6 @@ import "package:ente_ui/theme/text_style.dart"; import 'package:ente_ui/utils/dialog_util.dart'; import 'package:ente_ui/utils/toast_util.dart'; import 'package:flutter/material.dart'; -import "package:hugeicons/hugeicons.dart"; import 'package:locker/l10n/l10n.dart'; import 'package:locker/services/collections/collections_service.dart'; import 'package:locker/services/collections/models/collection.dart'; @@ -42,37 +41,6 @@ class _TrashPageState extends State { _sortedTrashFiles = List.from(widget.trashFiles); } - List _getFileOverflowActions() { - final colorScheme = getEnteColorScheme(context); - return [ - OverflowMenuAction( - id: 'restore', - label: context.l10n.restore, - icon: Icon( - Icons.restore, - color: colorScheme.textBase, - size: 20, - ), - onTap: (context, file, collection) { - _restoreFile(context, file!); - }, - ), - OverflowMenuAction( - id: 'delete', - label: context.l10n.delete, - icon: HugeIcon( - icon: HugeIcons.strokeRoundedDelete01, - color: colorScheme.warning500, - size: 20, - ), - onTap: (context, file, collection) { - _deleteFilePermanently(context, file!); - }, - isWarning: true, - ), - ]; - } - void _restoreFile(BuildContext context, EnteFile file) async { final collections = await CollectionService.instance.getCollectionsForUI(); @@ -268,7 +236,6 @@ class _TrashPageState extends State { : Expanded( child: ItemListView( files: _sortedTrashFiles.cast(), - fileOverflowActions: _getFileOverflowActions(), physics: const BouncingScrollPhysics(), ), ), diff --git a/mobile/apps/locker/lib/ui/viewer/actions/collection_selection_overlay_bar.dart b/mobile/apps/locker/lib/ui/viewer/actions/collection_selection_overlay_bar.dart index 29bcc1ff56c..39dee57da15 100644 --- a/mobile/apps/locker/lib/ui/viewer/actions/collection_selection_overlay_bar.dart +++ b/mobile/apps/locker/lib/ui/viewer/actions/collection_selection_overlay_bar.dart @@ -1,14 +1,11 @@ -import "package:ente_events/event_bus.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:ente_ui/utils/dialog_util.dart"; +import "package:ente_ui/utils/toast_util.dart"; import "package:flutter/material.dart"; import "package:hugeicons/hugeicons.dart"; -import "package:locker/events/collections_updated_event.dart"; import "package:locker/l10n/l10n.dart"; import "package:locker/models/selected_collections.dart"; -import "package:locker/models/ui_section_type.dart"; import "package:locker/services/collections/models/collection.dart"; -import "package:locker/services/collections/models/collection_view_type.dart"; import "package:locker/services/configuration.dart"; import "package:locker/ui/components/selection_action_button_widget.dart"; import "package:locker/ui/sharing/share_collection_bottom_sheet.dart"; @@ -17,12 +14,11 @@ import "package:logging/logging.dart"; class CollectionSelectionOverlayBar extends StatefulWidget { final SelectedCollections selectedCollections; - final List collection; - final UISectionType viewType; + final List collections; + const CollectionSelectionOverlayBar({ required this.selectedCollections, - required this.collection, - this.viewType = UISectionType.homeCollections, + required this.collections, super.key, }); @@ -35,6 +31,27 @@ class _CollectionSelectionOverlayBarState extends State { static final _logger = Logger('CollectionSelectionOverlayBar'); + int get _currentUserID => Configuration.instance.getUserID()!; + + List _getOwnedCollections(List collections) { + final ownedCollections = + collections.where((c) => c.isOwner(_currentUserID)).toList(); + + final sharedCount = collections.length - ownedCollections.length; + if (sharedCount > 0 && mounted) { + showToast( + context, + "Skipped $sharedCount shared collection${sharedCount > 1 ? 's' : ''} - you can only leave shared collections", + ); + } + + return ownedCollections; + } + + List _getSharedIncomingCollections(List collections) { + return collections.where((c) => !c.isOwner(_currentUserID)).toList(); + } + @override void initState() { super.initState(); @@ -48,9 +65,8 @@ class _CollectionSelectionOverlayBarState } void _onSelectionChanged() { - if (mounted) { - setState(() {}); - } + if (!mounted) return; + setState(() {}); } @override @@ -75,7 +91,7 @@ class _CollectionSelectionOverlayBarState child: hasSelection ? Container( decoration: BoxDecoration( - color: colorScheme.backdropBase, + color: colorScheme.backdropBase.withValues(alpha: 1.0), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -85,8 +101,7 @@ class _CollectionSelectionOverlayBarState ), ), child: Padding( - padding: - EdgeInsets.fromLTRB(16, 16, 16, 28 + bottomPadding), + padding: EdgeInsets.fromLTRB(16, 16, 16, bottomPadding), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -97,7 +112,7 @@ class _CollectionSelectionOverlayBarState builder: (context, child) { final isAllSelected = widget.selectedCollections.count == - widget.collection.length; + widget.collections.length; final buttonText = isAllSelected ? context.l10n.deselectAll : context.l10n.selectAll; @@ -111,7 +126,7 @@ class _CollectionSelectionOverlayBarState widget.selectedCollections.clearAll(); } else { widget.selectedCollections.select( - widget.collection.toSet(), + widget.collections.toSet(), ); } }, @@ -121,21 +136,20 @@ class _CollectionSelectionOverlayBarState borderRadius: BorderRadius.circular(50), ), padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 14.0, + horizontal: 12.0, + vertical: 10.0, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( buttonText, - style: textTheme.body, + style: textTheme.small, ), const SizedBox(width: 6), Icon( iconData, - color: getEnteColorScheme(context) - .textBase, + color: colorScheme.textBase, size: 20, ), ], @@ -164,21 +178,20 @@ class _CollectionSelectionOverlayBarState borderRadius: BorderRadius.circular(50), ), padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 14.0, + horizontal: 12.0, + vertical: 10.0, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( countText, - style: textTheme.body, + style: textTheme.small, ), const SizedBox(width: 6), Icon( Icons.close, - color: getEnteColorScheme(context) - .textBase, + color: colorScheme.textBase, size: 20, ), ], @@ -189,8 +202,8 @@ class _CollectionSelectionOverlayBarState ), ], ), - const SizedBox(height: 20), - _buildActionButtons(), + const SizedBox(height: 12), + _buildPrimaryActionButtons(), ], ), ), @@ -202,41 +215,78 @@ class _CollectionSelectionOverlayBarState ); } - Widget _buildActionButtons() { - return ListenableBuilder( - listenable: widget.selectedCollections, - builder: (context, child) { - final selectedCollections = widget.selectedCollections.collections; - if (selectedCollections.isEmpty) { - return const SizedBox.shrink(); - } + Widget _buildPrimaryActionButtons() { + final colorScheme = getEnteColorScheme(context); + final selectedCollections = widget.selectedCollections.collections; + if (selectedCollections.isEmpty) { + return const SizedBox.shrink(); + } + + final isSingleSelection = selectedCollections.length == 1; + final collection = isSingleSelection ? selectedCollections.first : null; - final actions = _getActionsForSelection(selectedCollections); + final ownedCollections = + selectedCollections.where((c) => c.isOwner(_currentUserID)).toList(); + final sharedIncomingCollections = _getSharedIncomingCollections( + selectedCollections.toList(), + ); - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - child: child, - ), - ); + final hasOwnedCollections = ownedCollections.isNotEmpty; + final hasSharedIncoming = sharedIncomingCollections.isNotEmpty; + + final primaryActions = [ + if (isSingleSelection && hasOwnedCollections) + SelectionActionButton( + hugeIcon: const HugeIcon( + icon: HugeIcons.strokeRoundedNavigation06, + ), + label: context.l10n.share, + onTap: () => _shareCollection(collection!), + ), + if (isSingleSelection && hasOwnedCollections) + SelectionActionButton( + hugeIcon: const HugeIcon( + icon: HugeIcons.strokeRoundedPencilEdit02, + ), + label: context.l10n.edit, + onTap: () => _editCollection(collection!), + ), + if (hasSharedIncoming) + SelectionActionButton( + hugeIcon: HugeIcon( + icon: HugeIcons.strokeRoundedLogout02, + color: colorScheme.warning500, + ), + label: context.l10n.leaveCollection, + onTap: () => _leaveCollections(sharedIncomingCollections), + isDestructive: true, + ), + if (hasOwnedCollections) + SelectionActionButton( + hugeIcon: HugeIcon( + icon: HugeIcons.strokeRoundedDelete02, + color: colorScheme.warning500, + ), + label: context.l10n.delete, + onTap: () { + if (isSingleSelection) { + _deleteCollection(collection!); + } else { + _deleteMultipleCollections(selectedCollections); + } }, - child: actions.length > 1 - ? Row( - key: const ValueKey('multi_action'), - children: _buildActionRow(actions), - ) - : Row( - key: const ValueKey('single_action'), - children: [Expanded(child: actions.first)], - ), - ); - }, + isDestructive: true, + ), + ]; + + return Container( + decoration: BoxDecoration( + color: colorScheme.backgroundElevated2, + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: _buildActionRow(primaryActions), + ), ); } @@ -251,159 +301,101 @@ class _CollectionSelectionOverlayBarState return children; } - List _getActionsForSelection( - Set selectedCollections, - ) { - final colorScheme = getEnteColorScheme(context); - final isSingleSelection = selectedCollections.length == 1; - final collection = isSingleSelection ? selectedCollections.first : null; - final actions = []; - final viewType = widget.viewType; - - if (viewType == UISectionType.homeCollections || - viewType == UISectionType.outgoingCollections) { - if (isSingleSelection) { - actions.addAll([ - SelectionActionButton( - hugeIcon: const HugeIcon( - icon: HugeIcons.strokeRoundedNavigation06, - ), - label: context.l10n.share, - onTap: () => _shareCollection(collection!), - ), - SelectionActionButton( - hugeIcon: const HugeIcon( - icon: HugeIcons.strokeRoundedPencilEdit02, - ), - label: context.l10n.edit, - onTap: () { - _editCollection(collection!); - }, - ), - SelectionActionButton( - hugeIcon: HugeIcon( - icon: HugeIcons.strokeRoundedDelete02, - color: colorScheme.warning500, - ), - label: context.l10n.delete, - onTap: () { - _deleteCollection(collection!); - }, - isDestructive: true, - ), - ]); - } else { - actions.addAll([ - SelectionActionButton( - hugeIcon: HugeIcon( - icon: HugeIcons.strokeRoundedDelete02, - color: colorScheme.warning500, - ), - label: context.l10n.delete, - onTap: () { - _deleteMultipleCollections( - widget.selectedCollections.collections, - ); - }, - isDestructive: true, - ), - ]); - } - } else { - actions.add( - SelectionActionButton( - hugeIcon: const HugeIcon( - icon: HugeIcons.strokeRoundedNavigation06, - ), - label: context.l10n.share, - onTap: _leaveCollection, - ), - ); + Future _editCollection(Collection collection) async { + final ownedCollections = _getOwnedCollections([collection]); + if (ownedCollections.isEmpty) { + widget.selectedCollections.clearAll(); + return; } - return actions; - } - - Future _editCollection(Collection collection) async { try { - await CollectionActions.editCollection( - context, - collection, - ); - widget.selectedCollections.clearAll(); + await CollectionActions.editCollection(context, collection); } catch (e, s) { _logger.severe(e, s); await showGenericErrorDialog(context: context, error: e); } - widget.selectedCollections.clearAll(); } Future _deleteCollection(Collection collection) async { + final ownedCollections = _getOwnedCollections([collection]); + if (ownedCollections.isEmpty) { + widget.selectedCollections.clearAll(); + return; + } + try { await CollectionActions.deleteCollection(context, collection); - widget.selectedCollections.clearAll(); } catch (e, s) { _logger.severe(e, s); await showGenericErrorDialog(context: context, error: e); } - widget.selectedCollections.clearAll(); } - Future _deleteMultipleCollections( - Set collections, - ) async { + Future _deleteMultipleCollections(Set collections) async { + final ownedCollections = _getOwnedCollections(collections.toList()); + if (ownedCollections.isEmpty) { + widget.selectedCollections.clearAll(); + return; + } + try { await CollectionActions.deleteMultipleCollections( context, - collections.toList(), + ownedCollections, ); - widget.selectedCollections.clearAll(); } catch (e, s) { _logger.severe(e, s); await showGenericErrorDialog(context: context, error: e); } - widget.selectedCollections.clearAll(); } Future _shareCollection(Collection collection) async { - final collectionViewType = getCollectionViewType( - collection, - Configuration.instance.getUserID()!, - ); - try { - if ((collectionViewType != CollectionViewType.ownedCollection && - collectionViewType != CollectionViewType.sharedCollectionViewer && - collectionViewType != - CollectionViewType.sharedCollectionCollaborator && - collectionViewType != CollectionViewType.hiddenOwnedCollection && - collectionViewType != CollectionViewType.favorite)) { - throw Exception( - "Cannot share collection of type $collectionViewType", - ); - } + final ownedCollections = _getOwnedCollections([collection]); + if (ownedCollections.isEmpty) { + widget.selectedCollections.clearAll(); + return; + } + + if (!collection.type.canShare) { + showToast(context, context.l10n.collectionCannotBeShared); + widget.selectedCollections.clearAll(); + return; + } + try { await showShareCollectionSheet(context, collection: collection); } catch (e, s) { _logger.severe(e, s); await showGenericErrorDialog(context: context, error: e); } - widget.selectedCollections.clearAll(); } - Future _leaveCollection() async { - await CollectionActions.leaveMultipleCollection( - context, - widget.selectedCollections.collections.toList(), - onSuccess: () { - Bus.instance.fire( - CollectionsUpdatedEvent("leave_collection"), + Future _leaveCollections(List collections) async { + if (collections.isEmpty) { + widget.selectedCollections.clearAll(); + return; + } + + try { + if (collections.length == 1) { + await CollectionActions.leaveCollection( + context, + collections.first, ); - }, - ); + } else { + await CollectionActions.leaveMultipleCollection( + context, + collections, + ); + } + } catch (e, s) { + _logger.severe(e, s); + await showGenericErrorDialog(context: context, error: e); + } widget.selectedCollections.clearAll(); } } diff --git a/mobile/apps/locker/lib/ui/viewer/actions/file_selection_overlay_bar.dart b/mobile/apps/locker/lib/ui/viewer/actions/file_selection_overlay_bar.dart index 5d9fb7d51e3..0b6c4557439 100644 --- a/mobile/apps/locker/lib/ui/viewer/actions/file_selection_overlay_bar.dart +++ b/mobile/apps/locker/lib/ui/viewer/actions/file_selection_overlay_bar.dart @@ -10,6 +10,9 @@ import "package:locker/l10n/l10n.dart"; import "package:locker/models/selected_files.dart"; import "package:locker/services/collections/collections_service.dart"; import "package:locker/services/collections/models/collection.dart"; +import "package:locker/services/collections/models/collection_view_type.dart"; +import "package:locker/services/configuration.dart"; +import "package:locker/services/favorites_service.dart"; import "package:locker/services/files/sync/models/file.dart"; import "package:locker/ui/components/add_to_collection_dialog.dart"; import "package:locker/ui/components/delete_confirmation_sheet.dart"; @@ -22,9 +25,14 @@ import "package:logging/logging.dart"; class FileSelectionOverlayBar extends StatefulWidget { final SelectedFiles selectedFiles; final List files; + final CollectionViewType? collectionViewType; + final ScrollController? scrollController; + const FileSelectionOverlayBar({ required this.selectedFiles, required this.files, + this.collectionViewType, + this.scrollController, super.key, }); @@ -36,22 +44,89 @@ class FileSelectionOverlayBar extends StatefulWidget { class _FileSelectionOverlayBarState extends State { static final Logger _logger = Logger("FileSelectionOverlayBar"); + static const double _scrollThreshold = 10.0; + + bool _isExpanded = true; + double _lastScrollPosition = 0; + int _previousSelectionCount = 0; + + bool get _hasSelection => widget.selectedFiles.files.isNotEmpty; + + List _getOwnedFiles(List files) { + final currentUserID = Configuration.instance.getUserID(); + final ownedFiles = + files.where((file) => file.ownerID == currentUserID).toList(); + + final sharedCount = files.length - ownedFiles.length; + if (sharedCount > 0 && mounted) { + showToast( + context, + "Action is not supported for $sharedCount shared file${sharedCount > 1 ? 's' : ''}", + ); + } + + return ownedFiles; + } + @override void initState() { super.initState(); widget.selectedFiles.addListener(_onSelectionChanged); + widget.scrollController?.addListener(_onScroll); } @override void dispose() { widget.selectedFiles.removeListener(_onSelectionChanged); + widget.scrollController?.removeListener(_onScroll); super.dispose(); } void _onSelectionChanged() { - if (mounted) { + if (!mounted) return; + + final currentCount = widget.selectedFiles.files.length; + final isFirstSelection = _previousSelectionCount == 0 && currentCount > 0; + + if (isFirstSelection) { + setState(() => _isExpanded = true); + } else { setState(() {}); } + + _previousSelectionCount = currentCount; + } + + void _onScroll() { + final controller = widget.scrollController; + if (!mounted || controller == null || !_hasSelection) return; + + final position = controller.position; + final current = position.pixels; + + if (current < 0 || current > position.maxScrollExtent) return; + + final delta = current - _lastScrollPosition; + if (delta.abs() < _scrollThreshold) return; + + _lastScrollPosition = current; + + final shouldCollapse = delta > 0 && _isExpanded; + final shouldExpand = delta < 0 && !_isExpanded; + + if (shouldCollapse || shouldExpand) { + setState(() => _isExpanded = !_isExpanded); + } + } + + bool get showFileSelectionOverlayBar { + final viewType = widget.collectionViewType; + if (viewType == null) return true; + + return viewType != CollectionViewType.sharedCollectionViewer && + viewType != CollectionViewType.sharedCollectionCollaborator && + viewType != CollectionViewType.quickLink && + viewType != CollectionViewType.favorite; } @override @@ -76,10 +151,18 @@ class _FileSelectionOverlayBarState extends State { child: hasSelection ? GestureDetector( behavior: HitTestBehavior.opaque, - onVerticalDragUpdate: (_) {}, + onVerticalDragUpdate: (details) { + if (details.primaryDelta != null) { + if (details.primaryDelta! < -5 && !_isExpanded) { + setState(() => _isExpanded = true); + } else if (details.primaryDelta! > 5 && _isExpanded) { + setState(() => _isExpanded = false); + } + } + }, child: Container( decoration: BoxDecoration( - color: colorScheme.backdropBase, + color: colorScheme.backdropBase.withValues(alpha: 1.0), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -90,8 +173,7 @@ class _FileSelectionOverlayBarState extends State { ), margin: EdgeInsets.zero, child: Padding( - padding: - EdgeInsets.fromLTRB(16, 16, 16, 28 + bottomPadding), + padding: EdgeInsets.fromLTRB(16, 16, 16, bottomPadding), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -100,9 +182,14 @@ class _FileSelectionOverlayBarState extends State { ListenableBuilder( listenable: widget.selectedFiles, builder: (context, child) { + final selectedSet = + widget.selectedFiles.files; final isAllSelected = - widget.selectedFiles.count == - widget.files.length; + widget.files.isNotEmpty && + widget.files.every( + (file) => + selectedSet.contains(file), + ); final buttonText = isAllSelected ? context.l10n.deselectAll : context.l10n.selectAll; @@ -127,21 +214,20 @@ class _FileSelectionOverlayBarState extends State { BorderRadius.circular(50), ), padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 14.0, + horizontal: 12.0, + vertical: 10.0, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( buttonText, - style: textTheme.body, + style: textTheme.small, ), const SizedBox(width: 6), Icon( iconData, - color: getEnteColorScheme(context) - .textBase, + color: colorScheme.textBase, size: 20, ), ], @@ -171,21 +257,20 @@ class _FileSelectionOverlayBarState extends State { BorderRadius.circular(50), ), padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 14.0, + horizontal: 12.0, + vertical: 10.0, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( countText, - style: textTheme.body, + style: textTheme.small, ), const SizedBox(width: 6), Icon( Icons.close, - color: getEnteColorScheme(context) - .textBase, + color: colorScheme.textBase, size: 20, ), ], @@ -196,7 +281,7 @@ class _FileSelectionOverlayBarState extends State { ), ], ), - const SizedBox(height: 20), + const SizedBox(height: 12), _buildActionButtons(), ], ), @@ -226,8 +311,19 @@ class _FileSelectionOverlayBarState extends State { mainAxisSize: MainAxisSize.min, children: [ _buildPrimaryActionRow(selectedFiles), - const SizedBox(height: 12), - _buildSecondaryActionRow(selectedFiles), + AnimatedCrossFade( + duration: const Duration(milliseconds: 250), + crossFadeState: _isExpanded + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Column( + children: [ + const SizedBox(height: 12), + _buildSecondaryActionRow(selectedFiles), + ], + ), + secondChild: const SizedBox.shrink(), + ), ], ), ); @@ -241,6 +337,9 @@ class _FileSelectionOverlayBarState extends State { final file = isSingleSelection ? files.first : null; final colorScheme = getEnteColorScheme(context); + final isImportant = + isSingleSelection && FavoritesService.instance.isFavoriteCache(file!); + return Row( children: [ Expanded( @@ -248,7 +347,7 @@ class _FileSelectionOverlayBarState extends State { hugeIcon: const HugeIcon( icon: HugeIcons.strokeRoundedDownload01, ), - label: "Download", + label: context.l10n.save, onTap: () => isSingleSelection ? _downloadFile(context, file!) : _downloadMultipleFiles(context, files), @@ -257,13 +356,12 @@ class _FileSelectionOverlayBarState extends State { const SizedBox(width: 12), Expanded( child: SelectionActionButton( - hugeIcon: const HugeIcon( - icon: HugeIcons.strokeRoundedNavigation06, - ), - label: context.l10n.share, + icon: isImportant ? Icons.star_rounded : Icons.star_border_rounded, + label: + isImportant ? context.l10n.unimportant : context.l10n.important, onTap: () => isSingleSelection - ? _shareFileLink(context, file!) - : _shareMultipleFiles(context), + ? _markImportant(context, file!) + : _markMultipleImportant(context, files), ), ), const SizedBox(width: 12), @@ -276,7 +374,7 @@ class _FileSelectionOverlayBarState extends State { label: context.l10n.delete, onTap: () => isSingleSelection ? _deleteFile(context, file!) - : _deleteMultipleFile(context, files), + : _deleteMultipleFiles(context, files), isDestructive: true, ), ), @@ -293,29 +391,8 @@ class _FileSelectionOverlayBarState extends State { color: colorScheme.backgroundElevated2, borderRadius: BorderRadius.circular(24), ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - layoutBuilder: (Widget? currentChild, List previousChildren) { - return Stack( - alignment: Alignment.center, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ); - }, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: child, - ); - }, - child: Row( - key: ValueKey('secondary_${selectedFiles.length}'), - children: _buildActionRow(actions), - ), + child: Row( + children: _buildActionRow(actions), ), ); } @@ -349,6 +426,18 @@ class _FileSelectionOverlayBarState extends State { ); } + if (isSingleSelection) { + actions.add( + SelectionActionButton( + hugeIcon: const HugeIcon( + icon: HugeIcons.strokeRoundedNavigation06, + ), + label: context.l10n.share, + onTap: () => _shareFileLink(context, file!), + ), + ); + } + actions.add( SelectionActionButton( hugeIcon: const HugeIcon( @@ -362,12 +451,39 @@ class _FileSelectionOverlayBarState extends State { return actions; } + Future _downloadFile(BuildContext context, EnteFile file) async { + final currentUserID = Configuration.instance.getUserID(); + if (file.ownerID != currentUserID) { + showToast(context, "Download is not supported for shared files"); + return; + } + try { + final success = await FileUtil.downloadFile(context, file); + if (success) { + widget.selectedFiles.clearAll(); + } + } catch (e, stackTrace) { + _logger.severe("Failed to download file: $e", e, stackTrace); + if (context.mounted) { + showToast( + context, + context.l10n.failedToDownloadOrDecrypt, + ); + } + } + } + Future _downloadMultipleFiles( BuildContext context, List files, ) async { + final ownedFiles = _getOwnedFiles(files); + if (ownedFiles.isEmpty) { + return; + } + try { - final success = await FileUtil.downloadFiles(context, files); + final success = await FileUtil.downloadFiles(context, ownedFiles); if (success) { widget.selectedFiles.clearAll(); } @@ -382,31 +498,36 @@ class _FileSelectionOverlayBarState extends State { } } - void _shareMultipleFiles(BuildContext context) { - showToast( - context, - "Sharing multiple files is coming soon", - ); - } - Future _shareFileLink(BuildContext context, EnteFile file) async { + final currentUserID = Configuration.instance.getUserID(); + if (file.ownerID != currentUserID) { + showToast(context, "Share is not supported for shared files"); + return; + } await FileActions.shareFileLink(context, file); } Future _editFile(BuildContext context, EnteFile file) async { + final currentUserID = Configuration.instance.getUserID(); + if (file.ownerID != currentUserID) { + showToast(context, "Edit is not supported for shared files"); + return; + } await FileActions.editFile(context, file); + widget.selectedFiles.clearAll(); } Future _showAddToDialog( BuildContext context, List files, ) async { - if (files.isEmpty) { + final ownedFiles = _getOwnedFiles(files); + if (ownedFiles.isEmpty) { return; } _logger.info( - 'Opening add-to dialog for ${files.length} file(s); fetching collections.', + 'Opening add-to dialog for ${ownedFiles.length} file(s); fetching collections.', ); final allCollections = @@ -439,7 +560,7 @@ class _FileSelectionOverlayBarState extends State { try { final addFutures = >[]; - for (final file in files) { + for (final file in ownedFiles) { _logger.fine( 'Processing file ${file.uploadedFileID} for add-to operation', ); @@ -477,6 +598,7 @@ class _FileSelectionOverlayBarState extends State { if (addFutures.isEmpty) { await dialog.hide(); + widget.selectedFiles.clearAll(); showToast( context, context.l10n.noChangesWereMade, @@ -487,7 +609,7 @@ class _FileSelectionOverlayBarState extends State { await Future.wait(addFutures); await CollectionService.instance.sync(); _logger.info( - 'Completed add-to operation for ${files.length} file(s).', + 'Completed add-to operation for ${ownedFiles.length} file(s).', ); await dialog.hide(); @@ -513,6 +635,11 @@ class _FileSelectionOverlayBarState extends State { } Future _deleteFile(BuildContext context, EnteFile file) async { + final currentUserID = Configuration.instance.getUserID(); + if (file.ownerID != currentUserID) { + showToast(context, "Delete is not supported for shared files"); + return; + } await FileActions.deleteFile( context, file, @@ -523,19 +650,20 @@ class _FileSelectionOverlayBarState extends State { ); } - Future _deleteMultipleFile( + Future _deleteMultipleFiles( BuildContext context, List files, ) async { - if (files.isEmpty) { + final ownedFiles = _getOwnedFiles(files); + if (ownedFiles.isEmpty) { return; } final confirmation = await showDeleteConfirmationSheet( context, title: context.l10n.areYouSure, - body: context.l10n.deleteMultipleFilesDialogBody(files.length), - deleteButtonLabel: context.l10n.yesDeleteFiles(files.length), + body: context.l10n.deleteMultipleFilesDialogBody(ownedFiles.length), + deleteButtonLabel: context.l10n.yesDeleteFiles(ownedFiles.length), assetPath: "assets/file_delete_icon.png", ); @@ -552,7 +680,7 @@ class _FileSelectionOverlayBarState extends State { try { await dialog.show(); - for (final file in files) { + for (final file in ownedFiles) { final collections = await CollectionService.instance.getCollectionsForFile(file); @@ -587,20 +715,38 @@ class _FileSelectionOverlayBarState extends State { } } - Future _downloadFile(BuildContext context, EnteFile file) async { - try { - final success = await FileUtil.downloadFile(context, file); - if (success) { + Future _markImportant(BuildContext context, EnteFile file) async { + final currentUserID = Configuration.instance.getUserID(); + if (file.ownerID != currentUserID) { + showToast(context, "Important is not supported for shared files"); + return; + } + await FileActions.markImportant( + context, + file, + onSuccess: () { widget.selectedFiles.clearAll(); - } - } catch (e, stackTrace) { - _logger.severe("Failed to download file: $e", e, stackTrace); - if (context.mounted) { - showToast( - context, - context.l10n.failedToDownloadOrDecrypt, - ); - } + Bus.instance.fire(CollectionsUpdatedEvent('file_important_toggled')); + }, + ); + } + + Future _markMultipleImportant( + BuildContext context, + List files, + ) async { + final ownedFiles = _getOwnedFiles(files); + if (ownedFiles.isEmpty) { + return; } + + await FileActions.markMultipleImportant( + context, + ownedFiles, + onSuccess: () { + widget.selectedFiles.clearAll(); + Bus.instance.fire(CollectionsUpdatedEvent('files_marked_important')); + }, + ); } } diff --git a/mobile/apps/locker/lib/utils/collection_actions.dart b/mobile/apps/locker/lib/utils/collection_actions.dart index 9a8d2abcc2c..b0e8f5f9e00 100644 --- a/mobile/apps/locker/lib/utils/collection_actions.dart +++ b/mobile/apps/locker/lib/utils/collection_actions.dart @@ -77,6 +77,11 @@ class CollectionActions { Collection collection, { VoidCallback? onSuccess, }) async { + if (!collection.type.canEdit) { + showToast(context, context.l10n.collectionCannotBeEdited); + return; + } + await showInputSheet( context, title: context.l10n.renameCollection, @@ -371,46 +376,40 @@ class CollectionActions { List collections, { VoidCallback? onSuccess, }) async { - final actionResult = await showActionSheet( - context: context, + final confirmed = await showAlertBottomSheet( + context, + title: context.l10n.leaveCollection, + message: context.l10n.filesAddedByYouWillBeRemovedFromTheCollection, + assetPath: "assets/warning-grey.png", buttons: [ - ButtonWidget( - buttonType: ButtonType.critical, - isInAlert: true, - shouldStickToDarkTheme: true, - buttonAction: ButtonAction.first, - shouldSurfaceExecutionStates: true, - labelText: context.l10n.leaveCollection, - onTap: () async { - for (final col in collections) { - await CollectionApiClient.instance.leaveCollection(col); - } - }, - ), - ButtonWidget( - buttonType: ButtonType.secondary, - buttonAction: ButtonAction.cancel, - isInAlert: true, - shouldStickToDarkTheme: true, - labelText: context.l10n.cancel, + SizedBox( + child: GradientButton( + text: context.l10n.leaveCollection, + onTap: () => Navigator.of(context).pop(true), + ), ), ], - title: context.l10n.leaveCollection, - body: context.l10n.filesAddedByYouWillBeRemovedFromTheCollection, ); - if (actionResult?.action != null && context.mounted) { - if (actionResult!.action == ButtonAction.error) { - await showGenericErrorDialog( - context: context, - error: actionResult.exception, - ); - } else if (actionResult.action == ButtonAction.first) { - onSuccess?.call(); - Navigator.of(context).pop(); - showToast( - context, - "Leave collection successfully", - ); + if (confirmed == true) { + try { + for (final col in collections) { + await CollectionApiClient.instance.leaveCollection(col); + } + if (context.mounted) { + onSuccess?.call(); + showToast( + context, + "Left ${collections.length} collection${collections.length > 1 ? 's' : ''} successfully", + ); + } + } catch (e) { + _logger.severe("Failed to leave collections", e); + if (context.mounted) { + showToast( + context, + context.l10n.somethingWentWrong, + ); + } } } } diff --git a/mobile/apps/locker/lib/utils/file_actions.dart b/mobile/apps/locker/lib/utils/file_actions.dart index 400e428ce5f..e39651d037e 100644 --- a/mobile/apps/locker/lib/utils/file_actions.dart +++ b/mobile/apps/locker/lib/utils/file_actions.dart @@ -417,7 +417,7 @@ class FileActions { } /// Toggles important status of a single file - static Future toggleImportant( + static Future markImportant( BuildContext context, EnteFile file, { VoidCallback? onSuccess, @@ -466,7 +466,7 @@ class FileActions { } /// Marks multiple files as important - static Future markMultipleAsImportant( + static Future markMultipleImportant( BuildContext context, List files, { VoidCallback? onSuccess, @@ -480,7 +480,6 @@ class FileActions { try { await dialog.show(); - // Use sync cache for better performance final filesToMark = files .where((file) => !FavoritesService.instance.isFavoriteCache(file)) .toList(); diff --git a/mobile/apps/locker/lib/utils/file_icon_utils.dart b/mobile/apps/locker/lib/utils/file_icon_utils.dart index cd6284c47b3..948091023e6 100644 --- a/mobile/apps/locker/lib/utils/file_icon_utils.dart +++ b/mobile/apps/locker/lib/utils/file_icon_utils.dart @@ -73,7 +73,7 @@ class FileIconUtils { static Widget getFileIcon( String fileName, { bool showBackground = true, - double size = 20, + double size = 24, }) { final config = _getFileConfig(fileName); diff --git a/mobile/apps/locker/lib/utils/info_item_utils.dart b/mobile/apps/locker/lib/utils/info_item_utils.dart index b15516c99b2..a34d3b782af 100644 --- a/mobile/apps/locker/lib/utils/info_item_utils.dart +++ b/mobile/apps/locker/lib/utils/info_item_utils.dart @@ -46,7 +46,7 @@ class InfoItemUtils { static Widget getInfoIcon( InfoType type, { bool showBackground = true, - double size = 20, + double size = 24, }) { final config = _getInfoConfig(type); diff --git a/mobile/packages/ui/lib/components/buttons/icon_button_widget.dart b/mobile/packages/ui/lib/components/buttons/icon_button_widget.dart index 922e69e7f01..af7768043e3 100644 --- a/mobile/packages/ui/lib/components/buttons/icon_button_widget.dart +++ b/mobile/packages/ui/lib/components/buttons/icon_button_widget.dart @@ -62,23 +62,19 @@ class _IconButtonWidgetState extends State { } Widget _iconButton(EnteColorScheme colorTheme) { - return Padding( - padding: const EdgeInsets.all(4.0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 20), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: iconStateColor, - ), - child: Icon( - widget.icon, - color: widget.iconColor ?? - (widget.iconButtonType == IconButtonType.secondary - ? colorTheme.strokeMuted - : colorTheme.strokeBase), - size: 24, - ), + return AnimatedContainer( + duration: const Duration(milliseconds: 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: iconStateColor, + ), + child: Icon( + widget.icon, + color: widget.iconColor ?? + (widget.iconButtonType == IconButtonType.secondary + ? colorTheme.strokeMuted + : colorTheme.strokeBase), + size: 24, ), ); }