create_collection_page.dart 12 KB

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