create_collection_page.dart 12 KB

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