create_collection_sheet.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import 'dart:math';
  2. import 'package:collection/collection.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
  6. import 'package:photos/db/files_db.dart';
  7. import 'package:photos/models/collection.dart';
  8. import 'package:photos/models/collection_items.dart';
  9. import 'package:photos/models/file.dart';
  10. import 'package:photos/models/selected_files.dart';
  11. import 'package:photos/services/collections_service.dart';
  12. import 'package:photos/services/ignored_files_service.dart';
  13. import 'package:photos/services/remote_sync_service.dart';
  14. import 'package:photos/theme/colors.dart';
  15. import 'package:photos/theme/ente_theme.dart';
  16. import 'package:photos/ui/common/loading_widget.dart';
  17. import 'package:photos/ui/components/album_list_item_widget.dart';
  18. import 'package:photos/ui/components/bottom_of_title_bar_widget.dart';
  19. import 'package:photos/ui/components/button_widget.dart';
  20. import 'package:photos/ui/components/models/button_type.dart';
  21. import 'package:photos/ui/components/title_bar_title_widget.dart';
  22. import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
  23. import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
  24. import 'package:photos/ui/viewer/gallery/collection_page.dart';
  25. import 'package:photos/utils/dialog_util.dart';
  26. import 'package:photos/utils/navigation_util.dart';
  27. import 'package:photos/utils/share_util.dart';
  28. import 'package:photos/utils/toast_util.dart';
  29. import 'package:receive_sharing_intent/receive_sharing_intent.dart';
  30. enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
  31. String _actionName(CollectionActionType type, bool plural) {
  32. final titleSuffix = (plural ? "s" : "");
  33. String text = "";
  34. switch (type) {
  35. case CollectionActionType.addFiles:
  36. text = "Add item";
  37. break;
  38. case CollectionActionType.moveFiles:
  39. text = "Move item";
  40. break;
  41. case CollectionActionType.restoreFiles:
  42. text = "Restore item";
  43. break;
  44. case CollectionActionType.unHide:
  45. text = "Unhide item";
  46. break;
  47. }
  48. return text + titleSuffix;
  49. }
  50. void createCollectionSheet(
  51. SelectedFiles? selectedFiles,
  52. List<SharedMediaFile>? sharedFiles,
  53. BuildContext context, {
  54. CollectionActionType actionType = CollectionActionType.addFiles,
  55. }) {
  56. showBarModalBottomSheet(
  57. context: context,
  58. builder: (context) {
  59. return CreateCollectionSheet(
  60. selectedFiles: selectedFiles,
  61. sharedFiles: sharedFiles,
  62. actionType: actionType,
  63. );
  64. },
  65. shape: const RoundedRectangleBorder(
  66. side: BorderSide(width: 0),
  67. borderRadius: BorderRadius.vertical(
  68. top: Radius.circular(5),
  69. ),
  70. ),
  71. topControl: const SizedBox.shrink(),
  72. backgroundColor: getEnteColorScheme(context).backgroundElevated,
  73. barrierColor: backdropFaintDark,
  74. enableDrag: false,
  75. );
  76. }
  77. class CreateCollectionSheet extends StatefulWidget {
  78. final SelectedFiles? selectedFiles;
  79. final List<SharedMediaFile>? sharedFiles;
  80. final CollectionActionType actionType;
  81. const CreateCollectionSheet({
  82. required this.selectedFiles,
  83. required this.sharedFiles,
  84. required this.actionType,
  85. super.key,
  86. });
  87. @override
  88. State<CreateCollectionSheet> createState() => _CreateCollectionSheetState();
  89. }
  90. class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
  91. final _logger = Logger((_CreateCollectionSheetState).toString());
  92. @override
  93. Widget build(BuildContext context) {
  94. final filesCount = widget.sharedFiles != null
  95. ? widget.sharedFiles!.length
  96. : widget.selectedFiles!.files.length;
  97. return Row(
  98. mainAxisAlignment: MainAxisAlignment.center,
  99. children: [
  100. ConstrainedBox(
  101. constraints: BoxConstraints(
  102. maxWidth: min(428, MediaQuery.of(context).size.width),
  103. ),
  104. child: Padding(
  105. padding: const EdgeInsets.fromLTRB(0, 32, 0, 12),
  106. child: Column(
  107. mainAxisSize: MainAxisSize.min,
  108. children: [
  109. BottomOfTitleBarWidget(
  110. title: TitleBarTitleWidget(
  111. title: _actionName(widget.actionType, filesCount > 1),
  112. ),
  113. caption: "Create or select album",
  114. ),
  115. Flexible(
  116. child: Column(
  117. mainAxisSize: MainAxisSize.min,
  118. children: [
  119. Flexible(
  120. child: Padding(
  121. padding: const EdgeInsets.fromLTRB(16, 24, 4, 0),
  122. child: Scrollbar(
  123. radius: const Radius.circular(2),
  124. child: Padding(
  125. padding: const EdgeInsets.only(right: 8),
  126. child: FutureBuilder(
  127. future: _getCollectionsWithThumbnail(),
  128. builder: (context, snapshot) {
  129. if (snapshot.hasError) {
  130. //Need to show an error on the UI here
  131. return const SizedBox.shrink();
  132. } else if (snapshot.hasData) {
  133. final collectionsWithThumbnail = snapshot
  134. .data as List<CollectionWithThumbnail>;
  135. return ListView.separated(
  136. itemBuilder: (context, index) {
  137. final item =
  138. collectionsWithThumbnail[index];
  139. return GestureDetector(
  140. behavior: HitTestBehavior.opaque,
  141. onTap: () =>
  142. _albumListItemOnTap(item),
  143. child: AlbumListItemWidget(
  144. item: item,
  145. ),
  146. );
  147. // return _buildCollectionItem(
  148. // collectionsWithThumbnail[index],
  149. // );
  150. },
  151. separatorBuilder: (context, index) =>
  152. const SizedBox(
  153. height: 8,
  154. ),
  155. itemCount:
  156. collectionsWithThumbnail.length,
  157. shrinkWrap: true,
  158. );
  159. } else {
  160. return const EnteLoadingWidget();
  161. }
  162. },
  163. ),
  164. ),
  165. ),
  166. ),
  167. ),
  168. const Padding(
  169. padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
  170. child: ButtonWidget(
  171. buttonType: ButtonType.secondary,
  172. buttonAction: ButtonAction.cancel,
  173. isInAlert: true,
  174. labelText: "Cancel",
  175. ),
  176. )
  177. ],
  178. ),
  179. ),
  180. ],
  181. ),
  182. ),
  183. ),
  184. ],
  185. );
  186. }
  187. Future<void> _albumListItemOnTap(CollectionWithThumbnail item) async {
  188. if (await _runCollectionAction(
  189. item.collection.id,
  190. )) {
  191. showShortToast(
  192. context,
  193. widget.actionType == CollectionActionType.addFiles
  194. ? "Added successfully to " + item.collection.name!
  195. : "Moved successfully to " + item.collection.name!,
  196. );
  197. _navigateToCollection(
  198. context,
  199. item.collection,
  200. );
  201. }
  202. }
  203. Widget _buildCollectionItem(CollectionWithThumbnail item) {
  204. return Container(
  205. padding: const EdgeInsets.only(left: 24, bottom: 16),
  206. child: GestureDetector(
  207. behavior: HitTestBehavior.translucent,
  208. child: Row(
  209. children: <Widget>[
  210. ClipRRect(
  211. borderRadius: BorderRadius.circular(2.0),
  212. child: SizedBox(
  213. height: 64,
  214. width: 64,
  215. key: Key("collection_item:" + (item.thumbnail?.tag ?? "")),
  216. child: item.thumbnail != null
  217. ? ThumbnailWidget(
  218. item.thumbnail,
  219. showFavForAlbumOnly: true,
  220. )
  221. : const NoThumbnailWidget(),
  222. ),
  223. ),
  224. const Padding(padding: EdgeInsets.all(8)),
  225. Expanded(
  226. child: Text(
  227. item.collection.name!,
  228. style: const TextStyle(
  229. fontSize: 16,
  230. ),
  231. ),
  232. ),
  233. ],
  234. ),
  235. onTap: () async {
  236. if (await _runCollectionAction(item.collection.id)) {
  237. showShortToast(
  238. context,
  239. widget.actionType == CollectionActionType.addFiles
  240. ? "Added successfully to " + item.collection.name!
  241. : "Moved successfully to " + item.collection.name!,
  242. );
  243. _navigateToCollection(context, item.collection);
  244. }
  245. },
  246. ),
  247. );
  248. }
  249. Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
  250. final List<CollectionWithThumbnail> collectionsWithThumbnail =
  251. await CollectionsService.instance.getCollectionsWithThumbnails(
  252. // in collections where user is a collaborator, only addTo and remove
  253. // action can to be performed
  254. includeCollabCollections:
  255. widget.actionType == CollectionActionType.addFiles,
  256. );
  257. collectionsWithThumbnail.removeWhere(
  258. (element) => (element.collection.type == CollectionType.favorites ||
  259. element.collection.type == CollectionType.uncategorized ||
  260. element.collection.isSharedFilesCollection()),
  261. );
  262. collectionsWithThumbnail.sort((first, second) {
  263. return compareAsciiLowerCaseNatural(
  264. first.collection.name ?? "",
  265. second.collection.name ?? "",
  266. );
  267. });
  268. return collectionsWithThumbnail;
  269. }
  270. void _navigateToCollection(BuildContext context, Collection collection) {
  271. Navigator.pop(context);
  272. routeToPage(
  273. context,
  274. CollectionPage(
  275. CollectionWithThumbnail(collection, null),
  276. ),
  277. );
  278. }
  279. Future<bool> _runCollectionAction(int collectionID) async {
  280. switch (widget.actionType) {
  281. case CollectionActionType.addFiles:
  282. return _addToCollection(collectionID);
  283. case CollectionActionType.moveFiles:
  284. return _moveFilesToCollection(collectionID);
  285. case CollectionActionType.unHide:
  286. return _moveFilesToCollection(collectionID);
  287. case CollectionActionType.restoreFiles:
  288. return _restoreFilesToCollection(collectionID);
  289. }
  290. }
  291. Future<bool> _addToCollection(int collectionID) async {
  292. final dialog = createProgressDialog(context, "Uploading files to album...");
  293. await dialog.show();
  294. try {
  295. final List<File> files = [];
  296. final List<File> filesPendingUpload = [];
  297. if (widget.sharedFiles != null) {
  298. filesPendingUpload.addAll(
  299. await convertIncomingSharedMediaToFile(
  300. widget.sharedFiles!,
  301. collectionID,
  302. ),
  303. );
  304. } else {
  305. for (final file in widget.selectedFiles!.files) {
  306. final File? currentFile =
  307. await (FilesDB.instance.getFile(file.generatedID!));
  308. if (currentFile == null) {
  309. _logger.severe("Failed to find fileBy genID");
  310. continue;
  311. }
  312. if (currentFile.uploadedFileID == null) {
  313. currentFile.collectionID = collectionID;
  314. filesPendingUpload.add(currentFile);
  315. } else {
  316. files.add(currentFile);
  317. }
  318. }
  319. }
  320. if (filesPendingUpload.isNotEmpty) {
  321. // filesPendingUpload might be getting ignored during auto-upload
  322. // because the user deleted these files from ente in the past.
  323. await IgnoredFilesService.instance
  324. .removeIgnoredMappings(filesPendingUpload);
  325. await FilesDB.instance.insertMultiple(filesPendingUpload);
  326. }
  327. if (files.isNotEmpty) {
  328. await CollectionsService.instance.addToCollection(collectionID, files);
  329. }
  330. RemoteSyncService.instance.sync(silently: true);
  331. await dialog.hide();
  332. widget.selectedFiles?.clearAll();
  333. return true;
  334. } catch (e, s) {
  335. _logger.severe("Could not add to album", e, s);
  336. await dialog.hide();
  337. showGenericErrorDialog(context: context);
  338. }
  339. return false;
  340. }
  341. Future<bool> _moveFilesToCollection(int toCollectionID) async {
  342. final String message = widget.actionType == CollectionActionType.moveFiles
  343. ? "Moving files to album..."
  344. : "Unhiding files to album";
  345. final dialog = createProgressDialog(context, message);
  346. await dialog.show();
  347. try {
  348. final int fromCollectionID =
  349. widget.selectedFiles!.files.first.collectionID!;
  350. await CollectionsService.instance.move(
  351. toCollectionID,
  352. fromCollectionID,
  353. widget.selectedFiles!.files.toList(),
  354. );
  355. await dialog.hide();
  356. RemoteSyncService.instance.sync(silently: true);
  357. widget.selectedFiles?.clearAll();
  358. return true;
  359. } on AssertionError catch (e) {
  360. await dialog.hide();
  361. showErrorDialog(context, "Oops", e.message as String?);
  362. return false;
  363. } catch (e, s) {
  364. _logger.severe("Could not move to album", e, s);
  365. await dialog.hide();
  366. showGenericErrorDialog(context: context);
  367. return false;
  368. }
  369. }
  370. Future<bool> _restoreFilesToCollection(int toCollectionID) async {
  371. final dialog = createProgressDialog(context, "Restoring files...");
  372. await dialog.show();
  373. try {
  374. await CollectionsService.instance
  375. .restore(toCollectionID, widget.selectedFiles!.files.toList());
  376. RemoteSyncService.instance.sync(silently: true);
  377. widget.selectedFiles?.clearAll();
  378. await dialog.hide();
  379. return true;
  380. } on AssertionError catch (e) {
  381. await dialog.hide();
  382. showErrorDialog(context, "Oops", e.message as String?);
  383. return false;
  384. } catch (e, s) {
  385. _logger.severe("Could not move to album", e, s);
  386. await dialog.hide();
  387. showGenericErrorDialog(context: context);
  388. return false;
  389. }
  390. }
  391. }