|
@@ -0,0 +1,339 @@
|
|
|
+import 'package:collection/collection.dart';
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:logging/logging.dart';
|
|
|
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
|
|
+import 'package:photos/db/files_db.dart';
|
|
|
+import 'package:photos/models/collection.dart';
|
|
|
+import 'package:photos/models/collection_items.dart';
|
|
|
+import 'package:photos/models/file.dart';
|
|
|
+import 'package:photos/models/selected_files.dart';
|
|
|
+import 'package:photos/services/collections_service.dart';
|
|
|
+import 'package:photos/services/ignored_files_service.dart';
|
|
|
+import 'package:photos/services/remote_sync_service.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/title_bar_title_widget.dart';
|
|
|
+import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
|
|
|
+import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
|
|
|
+import 'package:photos/ui/viewer/gallery/collection_page.dart';
|
|
|
+import 'package:photos/utils/dialog_util.dart';
|
|
|
+import 'package:photos/utils/navigation_util.dart';
|
|
|
+import 'package:photos/utils/share_util.dart';
|
|
|
+import 'package:photos/utils/toast_util.dart';
|
|
|
+import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|
|
+
|
|
|
+enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
|
|
|
+
|
|
|
+String _actionName(CollectionActionType type, bool plural) {
|
|
|
+ final titleSuffix = (plural ? "s" : "");
|
|
|
+ String text = "";
|
|
|
+ switch (type) {
|
|
|
+ case CollectionActionType.addFiles:
|
|
|
+ text = "Add item";
|
|
|
+ break;
|
|
|
+ case CollectionActionType.moveFiles:
|
|
|
+ text = "Move item";
|
|
|
+ break;
|
|
|
+ case CollectionActionType.restoreFiles:
|
|
|
+ text = "Restore item";
|
|
|
+ break;
|
|
|
+ case CollectionActionType.unHide:
|
|
|
+ text = "Unhide item";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return text + titleSuffix;
|
|
|
+}
|
|
|
+
|
|
|
+void createCollectionSheet(
|
|
|
+ SelectedFiles? selectedFiles,
|
|
|
+ List<SharedMediaFile>? sharedFiles,
|
|
|
+ BuildContext context, {
|
|
|
+ CollectionActionType actionType = CollectionActionType.addFiles,
|
|
|
+}) {
|
|
|
+ showBarModalBottomSheet(
|
|
|
+ context: context,
|
|
|
+ builder: (context) {
|
|
|
+ return CreateCollectionSheet(
|
|
|
+ selectedFiles: selectedFiles,
|
|
|
+ sharedFiles: sharedFiles,
|
|
|
+ actionType: actionType,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ shape: const RoundedRectangleBorder(
|
|
|
+ side: BorderSide(width: 0),
|
|
|
+ borderRadius: BorderRadius.only(
|
|
|
+ topLeft: Radius.circular(5),
|
|
|
+ topRight: Radius.circular(5),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ topControl: const SizedBox.shrink(),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+class CreateCollectionSheet extends StatefulWidget {
|
|
|
+ final SelectedFiles? selectedFiles;
|
|
|
+ final List<SharedMediaFile>? sharedFiles;
|
|
|
+ final CollectionActionType actionType;
|
|
|
+ const CreateCollectionSheet({
|
|
|
+ required this.selectedFiles,
|
|
|
+ required this.sharedFiles,
|
|
|
+ required this.actionType,
|
|
|
+ super.key,
|
|
|
+ });
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<CreateCollectionSheet> createState() => _CreateCollectionSheetState();
|
|
|
+}
|
|
|
+
|
|
|
+class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
|
|
|
+ final _logger = Logger((_CreateCollectionSheetState).toString());
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ final filesCount = widget.sharedFiles != null
|
|
|
+ ? widget.sharedFiles!.length
|
|
|
+ : widget.selectedFiles!.files.length;
|
|
|
+ return Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ Expanded(
|
|
|
+ child: Padding(
|
|
|
+ padding: const EdgeInsets.fromLTRB(0, 32, 0, 12),
|
|
|
+ child: Column(
|
|
|
+ children: [
|
|
|
+ BottomOfTitleBarWidget(
|
|
|
+ title: TitleBarTitleWidget(
|
|
|
+ title: _actionName(widget.actionType, filesCount > 1),
|
|
|
+ ),
|
|
|
+ caption: "Create or select album",
|
|
|
+ ),
|
|
|
+ Expanded(
|
|
|
+ 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<CollectionWithThumbnail>;
|
|
|
+ return ListView.builder(
|
|
|
+ itemBuilder: (context, index) {
|
|
|
+ return _buildCollectionItem(
|
|
|
+ collectionsWithThumbnail[index],
|
|
|
+ );
|
|
|
+ },
|
|
|
+ itemCount: collectionsWithThumbnail.length,
|
|
|
+ shrinkWrap: true,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ return const EnteLoadingWidget();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildCollectionItem(CollectionWithThumbnail item) {
|
|
|
+ return Container(
|
|
|
+ padding: const EdgeInsets.only(left: 24, bottom: 16),
|
|
|
+ child: GestureDetector(
|
|
|
+ behavior: HitTestBehavior.translucent,
|
|
|
+ child: Row(
|
|
|
+ children: <Widget>[
|
|
|
+ ClipRRect(
|
|
|
+ borderRadius: BorderRadius.circular(2.0),
|
|
|
+ child: SizedBox(
|
|
|
+ height: 64,
|
|
|
+ width: 64,
|
|
|
+ key: Key("collection_item:" + (item.thumbnail?.tag ?? "")),
|
|
|
+ child: item.thumbnail != null
|
|
|
+ ? ThumbnailWidget(
|
|
|
+ item.thumbnail,
|
|
|
+ showFavForAlbumOnly: true,
|
|
|
+ )
|
|
|
+ : const NoThumbnailWidget(),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const Padding(padding: EdgeInsets.all(8)),
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ item.collection.name!,
|
|
|
+ style: const TextStyle(
|
|
|
+ fontSize: 16,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ onTap: () async {
|
|
|
+ if (await _runCollectionAction(item.collection.id)) {
|
|
|
+ showShortToast(
|
|
|
+ context,
|
|
|
+ widget.actionType == CollectionActionType.addFiles
|
|
|
+ ? "Added successfully to " + item.collection.name!
|
|
|
+ : "Moved successfully to " + item.collection.name!,
|
|
|
+ );
|
|
|
+ _navigateToCollection(context, item.collection);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
|
|
|
+ final List<CollectionWithThumbnail> 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 _navigateToCollection(BuildContext context, Collection collection) {
|
|
|
+ Navigator.pop(context);
|
|
|
+ routeToPage(
|
|
|
+ context,
|
|
|
+ CollectionPage(
|
|
|
+ CollectionWithThumbnail(collection, null),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<bool> _runCollectionAction(int collectionID) async {
|
|
|
+ switch (widget.actionType) {
|
|
|
+ case CollectionActionType.addFiles:
|
|
|
+ return _addToCollection(collectionID);
|
|
|
+ case CollectionActionType.moveFiles:
|
|
|
+ return _moveFilesToCollection(collectionID);
|
|
|
+ case CollectionActionType.unHide:
|
|
|
+ return _moveFilesToCollection(collectionID);
|
|
|
+ case CollectionActionType.restoreFiles:
|
|
|
+ return _restoreFilesToCollection(collectionID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<bool> _addToCollection(int collectionID) async {
|
|
|
+ final dialog = createProgressDialog(context, "Uploading files to album...");
|
|
|
+ await dialog.show();
|
|
|
+ try {
|
|
|
+ final List<File> files = [];
|
|
|
+ final List<File> filesPendingUpload = [];
|
|
|
+ if (widget.sharedFiles != null) {
|
|
|
+ filesPendingUpload.addAll(
|
|
|
+ await convertIncomingSharedMediaToFile(
|
|
|
+ widget.sharedFiles!,
|
|
|
+ collectionID,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ for (final file in widget.selectedFiles!.files) {
|
|
|
+ final File? currentFile =
|
|
|
+ await (FilesDB.instance.getFile(file.generatedID!));
|
|
|
+ if (currentFile == null) {
|
|
|
+ _logger.severe("Failed to find fileBy genID");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (currentFile.uploadedFileID == null) {
|
|
|
+ currentFile.collectionID = collectionID;
|
|
|
+ filesPendingUpload.add(currentFile);
|
|
|
+ } else {
|
|
|
+ files.add(currentFile);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (filesPendingUpload.isNotEmpty) {
|
|
|
+ // filesPendingUpload might be getting ignored during auto-upload
|
|
|
+ // because the user deleted these files from ente in the past.
|
|
|
+ await IgnoredFilesService.instance
|
|
|
+ .removeIgnoredMappings(filesPendingUpload);
|
|
|
+ await FilesDB.instance.insertMultiple(filesPendingUpload);
|
|
|
+ }
|
|
|
+ if (files.isNotEmpty) {
|
|
|
+ await CollectionsService.instance.addToCollection(collectionID, files);
|
|
|
+ }
|
|
|
+ RemoteSyncService.instance.sync(silently: true);
|
|
|
+ await dialog.hide();
|
|
|
+ widget.selectedFiles?.clearAll();
|
|
|
+ return true;
|
|
|
+ } catch (e, s) {
|
|
|
+ _logger.severe("Could not add to album", e, s);
|
|
|
+ await dialog.hide();
|
|
|
+ showGenericErrorDialog(context: context);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<bool> _moveFilesToCollection(int toCollectionID) async {
|
|
|
+ final String message = widget.actionType == CollectionActionType.moveFiles
|
|
|
+ ? "Moving files to album..."
|
|
|
+ : "Unhiding files to album";
|
|
|
+ final dialog = createProgressDialog(context, message);
|
|
|
+ await dialog.show();
|
|
|
+ try {
|
|
|
+ final int fromCollectionID =
|
|
|
+ widget.selectedFiles!.files.first.collectionID!;
|
|
|
+ await CollectionsService.instance.move(
|
|
|
+ toCollectionID,
|
|
|
+ fromCollectionID,
|
|
|
+ widget.selectedFiles!.files.toList(),
|
|
|
+ );
|
|
|
+ await dialog.hide();
|
|
|
+ RemoteSyncService.instance.sync(silently: true);
|
|
|
+ widget.selectedFiles?.clearAll();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } on AssertionError catch (e) {
|
|
|
+ await dialog.hide();
|
|
|
+ showErrorDialog(context, "Oops", e.message as String?);
|
|
|
+ return false;
|
|
|
+ } catch (e, s) {
|
|
|
+ _logger.severe("Could not move to album", e, s);
|
|
|
+ await dialog.hide();
|
|
|
+ showGenericErrorDialog(context: context);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<bool> _restoreFilesToCollection(int toCollectionID) async {
|
|
|
+ final dialog = createProgressDialog(context, "Restoring files...");
|
|
|
+ await dialog.show();
|
|
|
+ try {
|
|
|
+ await CollectionsService.instance
|
|
|
+ .restore(toCollectionID, widget.selectedFiles!.files.toList());
|
|
|
+ RemoteSyncService.instance.sync(silently: true);
|
|
|
+ widget.selectedFiles?.clearAll();
|
|
|
+ await dialog.hide();
|
|
|
+ return true;
|
|
|
+ } on AssertionError catch (e) {
|
|
|
+ await dialog.hide();
|
|
|
+ showErrorDialog(context, "Oops", e.message as String?);
|
|
|
+ return false;
|
|
|
+ } catch (e, s) {
|
|
|
+ _logger.severe("Could not move to album", e, s);
|
|
|
+ await dialog.hide();
|
|
|
+ showGenericErrorDialog(context: context);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|