ente/lib/ui/collections_list_widget.dart
2023-04-07 10:27:54 +05:30

471 lines
14 KiB
Dart

import "dart:async";
import 'package:flutter/material.dart';
import "package:fluttertoast/fluttertoast.dart";
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import "package:photos/core/event_bus.dart";
import 'package:photos/db/files_db.dart';
import "package:photos/events/tab_changed_event.dart";
import "package:photos/generated/l10n.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/actions/collection/collection_sharing_actions.dart";
import "package:photos/ui/collection_action_sheet.dart";
import 'package:photos/ui/components/album_list_item_widget.dart';
import 'package:photos/ui/components/new_album_list_widget.dart';
import "package:photos/ui/sharing/share_collection_page.dart";
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import "package:photos/ui/viewer/gallery/empty_state.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';
class CollectionsListWidget extends StatelessWidget {
final List<CollectionWithThumbnail> collectionsWithThumbnail;
final CollectionActionType actionType;
final SelectedFiles? selectedFiles;
final List<SharedMediaFile>? sharedFiles;
final String searchQuery;
final bool shouldShowCreateAlbum;
CollectionsListWidget(
this.collectionsWithThumbnail,
this.actionType,
this.selectedFiles,
this.sharedFiles,
this.searchQuery,
this.shouldShowCreateAlbum, {
Key? key,
}) : super(key: key);
final _logger = Logger("CollectionsListWidgetState");
@override
Widget build(BuildContext context) {
final filesCount = sharedFiles != null
? sharedFiles!.length
: selectedFiles?.files.length ?? 0;
if (collectionsWithThumbnail.isEmpty) {
if (shouldShowCreateAlbum) {
return _getNewAlbumWidget(context, filesCount);
}
return const EmptyState();
}
return ListView.separated(
itemBuilder: (context, index) {
if (index == 0 && shouldShowCreateAlbum) {
return _getNewAlbumWidget(context, filesCount);
}
final item =
collectionsWithThumbnail[index - (shouldShowCreateAlbum ? 1 : 0)];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _albumListItemOnTap(context, item),
child: AlbumListItemWidget(
item,
),
);
},
separatorBuilder: (context, index) => const SizedBox(
height: 8,
),
itemCount:
collectionsWithThumbnail.length + (shouldShowCreateAlbum ? 1 : 0),
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
);
}
GestureDetector _getNewAlbumWidget(BuildContext context, int filesCount) {
return GestureDetector(
onTap: () async {
await _createNewAlbumOnTap(context, filesCount);
},
behavior: HitTestBehavior.opaque,
child: const NewAlbumListItemWidget(),
);
}
Future<void> _createNewAlbumOnTap(
BuildContext context,
int filesCount,
) async {
if (filesCount > 0) {
final result = await showTextInputDialog(
context,
title: S.of(context).albumTitle,
submitButtonLabel: S.of(context).ok,
hintText: S.of(context).enterAlbumName,
onSubmit: (name) {
return _nameAlbum(context, name);
},
showOnlyLoadingState: true,
textCapitalization: TextCapitalization.words,
);
if (result is Exception) {
showGenericErrorDialog(
context: context,
);
_logger.severe(
"Failed to name album",
result,
);
}
} else {
Navigator.pop(context);
await showToast(
context,
S.of(context).createAlbumActionHint,
toastLength: Toast.LENGTH_LONG,
);
Bus.instance.fire(
TabChangedEvent(
0,
TabChangedEventSource.collectionsPage,
),
);
}
}
Future<void> _nameAlbum(BuildContext context, String albumName) async {
if (albumName.isNotEmpty) {
final collection = await _createAlbum(albumName);
if (collection != null) {
if (await _runCollectionAction(
context,
collection,
showProgressDialog: false,
)) {
if (actionType == CollectionActionType.restoreFiles) {
showShortToast(
context,
'Restored files to album ' + albumName,
);
} else {
showShortToast(
context,
"Album '" + albumName + "' created.",
);
}
_navigateToCollection(context, collection);
}
}
}
}
Future<Collection?> _createAlbum(String albumName) async {
Collection? collection;
try {
collection = await CollectionsService.instance.createAlbum(albumName);
} catch (e, s) {
_logger.severe("Failed to create album", e, s);
rethrow;
}
return collection;
}
Future<void> _albumListItemOnTap(
BuildContext context,
CollectionWithThumbnail item,
) async {
if (await _runCollectionAction(context, item.collection)) {
late final String toastMessage;
bool shouldNavigateToCollection = false;
if (actionType == CollectionActionType.addFiles) {
toastMessage = S.of(context).addedSuccessfullyTo(item.collection.name!);
shouldNavigateToCollection = true;
} else if (actionType == CollectionActionType.moveFiles ||
actionType == CollectionActionType.restoreFiles ||
actionType == CollectionActionType.unHide) {
toastMessage = S.of(context).movedSuccessfullyTo(item.collection.name!);
shouldNavigateToCollection = true;
} else {
toastMessage = "";
}
if (toastMessage.isNotEmpty) {
showShortToast(
context,
toastMessage,
);
}
if (shouldNavigateToCollection) {
_navigateToCollection(
context,
item.collection,
);
}
}
}
Future<bool> _runCollectionAction(
BuildContext context,
Collection collection, {
bool showProgressDialog = true,
}) async {
switch (actionType) {
case CollectionActionType.addFiles:
return _addToCollection(
context,
collection.id,
showProgressDialog,
);
case CollectionActionType.moveFiles:
return _moveFilesToCollection(context, collection.id);
case CollectionActionType.unHide:
return _moveFilesToCollection(context, collection.id);
case CollectionActionType.restoreFiles:
return _restoreFilesToCollection(context, collection.id);
case CollectionActionType.shareCollection:
return _showShareCollectionPage(context, collection);
case CollectionActionType.collectPhotos:
return _createCollaborativeLink(context, collection);
}
}
void _navigateToCollection(BuildContext context, Collection collection) {
Navigator.pop(context);
routeToPage(
context,
CollectionPage(
CollectionWithThumbnail(collection, null),
),
);
}
Future<bool> _createCollaborativeLink(
BuildContext context,
Collection collection,
) async {
final CollectionActions collectionActions =
CollectionActions(CollectionsService.instance);
if (collection.hasLink) {
if (collection.publicURLs!.first!.enableCollect) {
if (Configuration.instance.getUserID() == collection.owner!.id) {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
}
showShortToast(
context, S.of(context).thisAlbumAlreadyHDACollaborativeLink);
return Future.value(false);
} else {
try {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
CollectionsService.instance
.updateShareUrl(collection, {'enableCollect': true}).then(
(value) => showShortToast(
context,
S.of(context).collaborativeLinkCreatedFor(collection.name!),
),
);
return true;
} catch (e) {
showGenericErrorDialog(context: context);
return false;
}
}
}
final bool result = await collectionActions.enableUrl(
context,
collection,
enableCollect: true,
);
if (result) {
showShortToast(
context,
S.of(context).collaborativeLinkCreatedFor(collection.name!),
);
if (Configuration.instance.getUserID() == collection.owner!.id) {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
} else {
showGenericErrorDialog(context: context);
_logger.severe("Cannot share collections owned by others");
}
}
return result;
}
Future<bool> _showShareCollectionPage(
BuildContext context,
Collection collection,
) {
if (Configuration.instance.getUserID() == collection.owner!.id) {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
} else {
showGenericErrorDialog(context: context);
_logger.severe("Cannot share collections owned by others");
}
return Future.value(true);
}
Future<bool> _addToCollection(
BuildContext context,
int collectionID,
bool showProgressDialog,
) async {
final dialog = showProgressDialog
? createProgressDialog(
context,
S.of(context).uploadingFilesToAlbum,
isDismissible: true,
)
: null;
await dialog?.show();
try {
final List<File> files = [];
final List<File> filesPendingUpload = [];
final int currentUserID = Configuration.instance.getUserID()!;
if (sharedFiles != null) {
filesPendingUpload.addAll(
await convertIncomingSharedMediaToFile(
sharedFiles!,
collectionID,
),
);
} else {
for (final file in selectedFiles!.files) {
File? currentFile;
if (file.uploadedFileID != null) {
currentFile = file;
} else if (file.generatedID != null) {
// when file is not uploaded, refresh the state from the db to
// ensure we have latest upload status for given file before
// queueing it up as pending upload
currentFile = await (FilesDB.instance.getFile(file.generatedID!));
} else if (file.generatedID == null) {
_logger.severe("generated id should not be null");
}
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) {
// Newly created collection might not be cached
final Collection? c =
CollectionsService.instance.getCollectionByID(collectionID);
if (c != null && c.owner!.id != currentUserID) {
showToast(context, S.of(context).canNotUploadToAlbumsOwnedByOthers);
await dialog?.hide();
return false;
} else {
// 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();
selectedFiles?.clearAll();
return true;
} catch (e, s) {
_logger.severe("Failed to add to album", e, s);
await dialog?.hide();
showGenericErrorDialog(context: context);
rethrow;
}
}
Future<bool> _moveFilesToCollection(
BuildContext context,
int toCollectionID,
) async {
final String message = actionType == CollectionActionType.moveFiles
? S.of(context).movingFilesToAlbum
: S.of(context).unhidingFilesToAlbum;
final dialog = createProgressDialog(context, message, isDismissible: true);
await dialog.show();
try {
final int fromCollectionID = selectedFiles!.files.first.collectionID!;
await CollectionsService.instance.move(
toCollectionID,
fromCollectionID,
selectedFiles!.files.toList(),
);
await dialog.hide();
RemoteSyncService.instance.sync(silently: true);
selectedFiles?.clearAll();
return true;
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, S.of(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(
BuildContext context,
int toCollectionID,
) async {
final dialog = createProgressDialog(
context,
S.of(context).restoringFiles,
isDismissible: true,
);
await dialog.show();
try {
await CollectionsService.instance
.restore(toCollectionID, selectedFiles!.files.toList());
RemoteSyncService.instance.sync(silently: true);
selectedFiles?.clearAll();
await dialog.hide();
return true;
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, S.of(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;
}
}
}