create_collection_page.dart 11 KB

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