import "dart:async"; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import "package:photos/core/configuration.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection.dart'; import 'package:photos/models/collection_items.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/collections_list_widget.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/bottom_of_title_bar_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/components/text_input_widget.dart"; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide, shareCollection, collectPhotos, } String _actionName( BuildContext context, CollectionActionType type, int fileCount, ) { String text = ""; switch (type) { case CollectionActionType.addFiles: text = S.of(context).addItem(fileCount); break; case CollectionActionType.moveFiles: text = S.of(context).moveItem(fileCount); break; case CollectionActionType.restoreFiles: text = S.of(context).restoreToAlbum; break; case CollectionActionType.unHide: text = S.of(context).unhideToAlbum; break; case CollectionActionType.shareCollection: text = S.of(context).share; break; case CollectionActionType.collectPhotos: text = S.of(context).share; break; } return text; } void showCollectionActionSheet( BuildContext context, { SelectedFiles? selectedFiles, List? sharedFiles, CollectionActionType actionType = CollectionActionType.addFiles, bool showOptionToCreateNewAlbum = true, }) { showBarModalBottomSheet( context: context, builder: (context) { return CollectionActionSheet( selectedFiles: selectedFiles, sharedFiles: sharedFiles, actionType: actionType, showOptionToCreateNewAlbum: showOptionToCreateNewAlbum, ); }, shape: const RoundedRectangleBorder( side: BorderSide(width: 0), borderRadius: BorderRadius.vertical( top: Radius.circular(5), ), ), topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, enableDrag: false, ); } class CollectionActionSheet extends StatefulWidget { final SelectedFiles? selectedFiles; final List? sharedFiles; final CollectionActionType actionType; final bool showOptionToCreateNewAlbum; const CollectionActionSheet({ required this.selectedFiles, required this.sharedFiles, required this.actionType, required this.showOptionToCreateNewAlbum, super.key, }); @override State createState() => _CollectionActionSheetState(); } class _CollectionActionSheetState extends State { static const int cancelButtonSize = 80; String _searchQuery = ""; @override Widget build(BuildContext context) { final filesCount = widget.sharedFiles != null ? widget.sharedFiles!.length : widget.selectedFiles?.files.length ?? 0; final bottomInset = MediaQuery.of(context).viewInsets.bottom; final isKeyboardUp = bottomInset > 100; return Padding( padding: EdgeInsets.only( bottom: isKeyboardUp ? bottomInset - cancelButtonSize : 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ConstrainedBox( constraints: BoxConstraints( maxWidth: min(428, MediaQuery.of(context).size.width), ), child: Padding( padding: const EdgeInsets.fromLTRB(0, 32, 0, 8), child: Column( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Column( children: [ BottomOfTitleBarWidget( title: TitleBarTitleWidget( title: _actionName( context, widget.actionType, filesCount), ), caption: widget.showOptionToCreateNewAlbum ? S.of(context).createOrSelectAlbum : S.of(context).selectAlbum, ), Padding( padding: const EdgeInsets.only( top: 16, left: 16, right: 16, ), child: TextInputWidget( hintText: S.of(context).searchByAlbumNameHint, prefixIcon: Icons.search_rounded, onChange: (value) { setState(() { _searchQuery = value; }); }, cancellable: true, shouldUnfocusOnCancelOrSubmit: true, ), ), _getCollectionItems(filesCount), ], ), ), SafeArea( child: Container( //inner stroke of 1pt + 15 pts of top padding = 16 pts padding: const EdgeInsets.fromLTRB(16, 15, 16, 8), decoration: BoxDecoration( border: Border( top: BorderSide( color: getEnteColorScheme(context).strokeFaint, ), ), ), child: ButtonWidget( buttonType: ButtonType.secondary, buttonAction: ButtonAction.cancel, isInAlert: true, labelText: S.of(context).cancel, ), ), ) ], ), ), ), ], ), ); } Flexible _getCollectionItems(int filesCount) { return Flexible( child: Padding( padding: const EdgeInsets.fromLTRB(16, 24, 4, 0), child: FutureBuilder( future: _getCollectionsWithThumbnail(), builder: (context, snapshot) { if (snapshot.hasError) { //Need to show an error on the UI here return const SizedBox.shrink(); } else if (snapshot.hasData) { final collectionsWithThumbnail = snapshot.data as List; _removeIncomingCollections(collectionsWithThumbnail); final shouldShowCreateAlbum = widget.showOptionToCreateNewAlbum && _searchQuery.isEmpty; final searchResults = _searchQuery.isNotEmpty ? collectionsWithThumbnail .where( (element) => element.collection.name! .toLowerCase() .contains(_searchQuery), ) .toList() : collectionsWithThumbnail; return Scrollbar( thumbVisibility: true, radius: const Radius.circular(2), child: Padding( padding: const EdgeInsets.only(right: 12), child: CollectionsListWidget( searchResults, widget.actionType, widget.selectedFiles, widget.sharedFiles, _searchQuery, shouldShowCreateAlbum, ), ), ); } else { return const EnteLoadingWidget(); } }, ), ), ); } Future> _getCollectionsWithThumbnail() async { final List collectionsWithThumbnail = await CollectionsService.instance.getCollectionsWithThumbnails( // in collections where user is a collaborator, only addTo and remove // action can to be performed includeCollabCollections: widget.actionType == CollectionActionType.addFiles, ); collectionsWithThumbnail.removeWhere( (element) => (element.collection.type == CollectionType.favorites || element.collection.type == CollectionType.uncategorized || element.collection.isSharedFilesCollection()), ); collectionsWithThumbnail.sort((first, second) { return compareAsciiLowerCaseNatural( first.collection.name ?? "", second.collection.name ?? "", ); }); return collectionsWithThumbnail; } void _removeIncomingCollections(List items) { if (widget.actionType == CollectionActionType.shareCollection || widget.actionType == CollectionActionType.collectPhotos) { final ownerID = Configuration.instance.getUserID(); items.removeWhere( (e) => !e.collection.isOwner(ownerID!), ); } } }