create_collection_page.dart 13 KB

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