create_collection_page.dart 12 KB

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