create_collection_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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/collection_page.dart';
  13. import 'package:photos/ui/common/gradientButton.dart';
  14. import 'package:photos/ui/loading_widget.dart';
  15. import 'package:photos/ui/thumbnail_widget.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. _CreateCollectionPageState 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. child: Row(
  83. mainAxisAlignment: MainAxisAlignment.center,
  84. crossAxisAlignment: CrossAxisAlignment.center,
  85. //mainAxisSize: MainAxisSize.min,
  86. children: [
  87. Icon(
  88. Icons.create_new_folder_outlined,
  89. color: Colors.white,
  90. ),
  91. Padding(padding: EdgeInsets.all(6)),
  92. Text(
  93. "To a new album",
  94. style: gradientButtonTextTheme(),
  95. ),
  96. ],
  97. ),
  98. linearGradientColors: const [
  99. Color(0xFF2CD267),
  100. Color(0xFF1DB954),
  101. ],
  102. onTap: () async {
  103. _showNameAlbumDialog();
  104. },
  105. ),
  106. ),
  107. ),
  108. ],
  109. ),
  110. Padding(
  111. padding: const EdgeInsets.fromLTRB(40, 24, 40, 20),
  112. child: Align(
  113. alignment: Alignment.centerLeft,
  114. child: Text(
  115. "To an existing album",
  116. style: TextStyle(
  117. fontWeight: FontWeight.bold,
  118. // color: Theme.of(context).primaryColorLight.withOpacity(0.8),
  119. ),
  120. ),
  121. ),
  122. ),
  123. Padding(
  124. padding: const EdgeInsets.fromLTRB(20, 4, 20, 0),
  125. child: _getExistingCollectionsWidget(),
  126. ),
  127. ],
  128. ),
  129. );
  130. }
  131. Widget _getExistingCollectionsWidget() {
  132. return FutureBuilder<List<CollectionWithThumbnail>>(
  133. future: _getCollectionsWithThumbnail(),
  134. builder: (context, snapshot) {
  135. if (snapshot.hasError) {
  136. return Text(snapshot.error.toString());
  137. } else if (snapshot.hasData) {
  138. return ListView.builder(
  139. itemBuilder: (context, index) {
  140. return _buildCollectionItem(snapshot.data[index]);
  141. },
  142. itemCount: snapshot.data.length,
  143. shrinkWrap: true,
  144. physics: NeverScrollableScrollPhysics(),
  145. );
  146. } else {
  147. return loadWidget;
  148. }
  149. },
  150. );
  151. }
  152. Widget _buildCollectionItem(CollectionWithThumbnail item) {
  153. return Container(
  154. padding: EdgeInsets.only(left: 24, bottom: 16),
  155. child: GestureDetector(
  156. behavior: HitTestBehavior.translucent,
  157. child: Row(
  158. children: <Widget>[
  159. ClipRRect(
  160. borderRadius: BorderRadius.circular(2.0),
  161. child: SizedBox(
  162. child: ThumbnailWidget(item.thumbnail),
  163. height: 64,
  164. width: 64,
  165. key: Key("collection_item:" + item.thumbnail.tag()),
  166. ),
  167. ),
  168. Padding(padding: EdgeInsets.all(8)),
  169. Expanded(
  170. child: Text(
  171. item.collection.name,
  172. style: TextStyle(
  173. fontSize: 16,
  174. ),
  175. ),
  176. ),
  177. ],
  178. ),
  179. onTap: () async {
  180. if (await _runCollectionAction(item.collection.id)) {
  181. showShortToast(
  182. context,
  183. widget.actionType == CollectionActionType.addFiles
  184. ? "Added successfully to " + item.collection.name
  185. : "Moved successfully to " + item.collection.name,
  186. );
  187. _navigateToCollection(item.collection);
  188. }
  189. },
  190. ),
  191. );
  192. }
  193. Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
  194. final List<CollectionWithThumbnail> collectionsWithThumbnail = [];
  195. final latestCollectionFiles =
  196. await CollectionsService.instance.getLatestCollectionFiles();
  197. for (final file in latestCollectionFiles) {
  198. final c =
  199. CollectionsService.instance.getCollectionByID(file.collectionID);
  200. if (c.owner.id == Configuration.instance.getUserID()) {
  201. collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
  202. }
  203. }
  204. collectionsWithThumbnail.sort((first, second) {
  205. return (first.collection.name ?? "")
  206. .compareTo((second.collection.name ?? ""));
  207. });
  208. return collectionsWithThumbnail;
  209. }
  210. void _showNameAlbumDialog() async {
  211. AlertDialog alert = AlertDialog(
  212. title: Text("Album title"),
  213. content: TextFormField(
  214. decoration: InputDecoration(
  215. hintText: "Christmas 2020 / Dinner at Alice's",
  216. contentPadding: EdgeInsets.all(8),
  217. ),
  218. onChanged: (value) {
  219. setState(() {
  220. _albumName = value;
  221. });
  222. },
  223. autofocus: true,
  224. keyboardType: TextInputType.text,
  225. textCapitalization: TextCapitalization.words,
  226. ),
  227. actions: [
  228. TextButton(
  229. child: Text(
  230. "Ok",
  231. style: TextStyle(
  232. color: Theme.of(context).buttonColor,
  233. ),
  234. ),
  235. onPressed: () async {
  236. Navigator.of(context, rootNavigator: true).pop('dialog');
  237. final collection = await _createAlbum(_albumName);
  238. if (collection != null) {
  239. if (await _runCollectionAction(collection.id)) {
  240. if (widget.actionType == CollectionActionType.restoreFiles) {
  241. showShortToast(
  242. context,
  243. 'Restored files to album ' + _albumName,
  244. );
  245. } else {
  246. showShortToast(
  247. context,
  248. "Album '" + _albumName + "' created.",
  249. );
  250. }
  251. _navigateToCollection(collection);
  252. }
  253. }
  254. },
  255. ),
  256. ],
  257. );
  258. showDialog(
  259. context: context,
  260. builder: (BuildContext context) {
  261. return alert;
  262. },
  263. );
  264. }
  265. void _navigateToCollection(Collection collection) {
  266. Navigator.pop(context);
  267. routeToPage(
  268. context,
  269. CollectionPage(
  270. CollectionWithThumbnail(collection, null),
  271. ),
  272. );
  273. }
  274. Future<bool> _runCollectionAction(int collectionID) async {
  275. switch (widget.actionType) {
  276. case CollectionActionType.addFiles:
  277. return _addToCollection(collectionID);
  278. case CollectionActionType.moveFiles:
  279. return _moveFilesToCollection(collectionID);
  280. case CollectionActionType.restoreFiles:
  281. return _restoreFilesToCollection(collectionID);
  282. }
  283. throw AssertionError("unexpected actionType ${widget.actionType}");
  284. }
  285. Future<bool> _moveFilesToCollection(int toCollectionID) async {
  286. final dialog = createProgressDialog(context, "Moving files to album...");
  287. await dialog.show();
  288. try {
  289. int fromCollectionID = widget.selectedFiles.files?.first?.collectionID;
  290. await CollectionsService.instance.move(
  291. toCollectionID,
  292. fromCollectionID,
  293. widget.selectedFiles.files?.toList(),
  294. );
  295. await dialog.hide();
  296. RemoteSyncService.instance.sync(silently: true);
  297. widget.selectedFiles?.clearAll();
  298. return true;
  299. } on AssertionError catch (e, s) {
  300. await dialog.hide();
  301. showErrorDialog(context, "Oops", e.message);
  302. return false;
  303. } catch (e, s) {
  304. _logger.severe("Could not move to album", e, s);
  305. await dialog.hide();
  306. showGenericErrorDialog(context);
  307. return false;
  308. }
  309. }
  310. Future<bool> _restoreFilesToCollection(int toCollectionID) async {
  311. final dialog = createProgressDialog(context, "Restoring files...");
  312. await dialog.show();
  313. try {
  314. await CollectionsService.instance
  315. .restore(toCollectionID, widget.selectedFiles.files?.toList());
  316. RemoteSyncService.instance.sync(silently: true);
  317. widget.selectedFiles?.clearAll();
  318. await dialog.hide();
  319. return true;
  320. } on AssertionError catch (e, s) {
  321. await dialog.hide();
  322. showErrorDialog(context, "Oops", e.message);
  323. return false;
  324. } catch (e, s) {
  325. _logger.severe("Could not move to album", e, s);
  326. await dialog.hide();
  327. showGenericErrorDialog(context);
  328. return false;
  329. }
  330. }
  331. Future<bool> _addToCollection(int collectionID) async {
  332. final dialog = createProgressDialog(context, "Uploading files to album...");
  333. await dialog.show();
  334. try {
  335. final List<File> files = [];
  336. final List<File> filesPendingUpload = [];
  337. if (widget.sharedFiles != null) {
  338. filesPendingUpload.addAll(
  339. await convertIncomingSharedMediaToFile(
  340. widget.sharedFiles,
  341. collectionID,
  342. ),
  343. );
  344. } else {
  345. final List<File> filesPendingUpload = [];
  346. for (final file in widget.selectedFiles.files) {
  347. final currentFile = await FilesDB.instance.getFile(file.generatedID);
  348. if (currentFile.uploadedFileID == null) {
  349. currentFile.collectionID = collectionID;
  350. filesPendingUpload.add(currentFile);
  351. } else {
  352. files.add(currentFile);
  353. }
  354. }
  355. await FilesDB.instance.insertMultiple(filesPendingUpload);
  356. await CollectionsService.instance.addToCollection(collectionID, files);
  357. }
  358. RemoteSyncService.instance.sync(silently: true);
  359. await dialog.hide();
  360. widget.selectedFiles?.clearAll();
  361. return true;
  362. } catch (e, s) {
  363. _logger.severe("Could not add to album", e, s);
  364. await dialog.hide();
  365. showGenericErrorDialog(context);
  366. }
  367. return false;
  368. }
  369. Future<Collection> _createAlbum(String albumName) async {
  370. Collection collection;
  371. final dialog = createProgressDialog(context, "Creating album...");
  372. await dialog.show();
  373. try {
  374. collection = await CollectionsService.instance.createAlbum(albumName);
  375. } catch (e, s) {
  376. _logger.severe(e, s);
  377. await dialog.hide();
  378. showGenericErrorDialog(context);
  379. } finally {
  380. await dialog.hide();
  381. }
  382. return collection;
  383. }
  384. }