create_collection_page.dart 12 KB

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