gallery_app_bar_widget.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:fluttertoast/fluttertoast.dart';
  6. import 'package:intl/intl.dart';
  7. import 'package:logging/logging.dart';
  8. import 'package:page_transition/page_transition.dart';
  9. import 'package:photos/core/configuration.dart';
  10. import 'package:photos/core/event_bus.dart';
  11. import 'package:photos/events/subscription_purchased_event.dart';
  12. import 'package:photos/models/collection.dart';
  13. import 'package:photos/models/magic_metadata.dart';
  14. import 'package:photos/models/selected_files.dart';
  15. import 'package:photos/services/collections_service.dart';
  16. import 'package:photos/ui/change_collection_name_dialog.dart';
  17. import 'package:photos/ui/create_collection_page.dart';
  18. import 'package:photos/ui/password_entry_page.dart';
  19. import 'package:photos/ui/share_collection_widget.dart';
  20. import 'package:photos/utils/delete_file_util.dart';
  21. import 'package:photos/utils/dialog_util.dart';
  22. import 'package:photos/services/file_magic_service.dart';
  23. import 'package:photos/utils/share_util.dart';
  24. import 'package:photos/utils/toast_util.dart';
  25. enum GalleryAppBarType {
  26. homepage,
  27. archive,
  28. local_folder,
  29. // indicator for gallery view of collections shared with the user
  30. shared_collection,
  31. owned_collection,
  32. search_results
  33. }
  34. class GalleryAppBarWidget extends StatefulWidget {
  35. final GalleryAppBarType type;
  36. final String title;
  37. final SelectedFiles selectedFiles;
  38. final String path;
  39. final Collection collection;
  40. GalleryAppBarWidget(
  41. this.type,
  42. this.title,
  43. this.selectedFiles, {
  44. this.path,
  45. this.collection,
  46. });
  47. @override
  48. _GalleryAppBarWidgetState createState() => _GalleryAppBarWidgetState();
  49. }
  50. class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
  51. final _logger = Logger("GalleryAppBar");
  52. StreamSubscription _userAuthEventSubscription;
  53. Function() _selectedFilesListener;
  54. String appBarTitle;
  55. @override
  56. void initState() {
  57. _selectedFilesListener = () {
  58. setState(() {});
  59. };
  60. widget.selectedFiles.addListener(_selectedFilesListener);
  61. _userAuthEventSubscription =
  62. Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
  63. setState(() {});
  64. });
  65. appBarTitle = widget.title;
  66. super.initState();
  67. }
  68. @override
  69. void dispose() {
  70. _userAuthEventSubscription.cancel();
  71. widget.selectedFiles.removeListener(_selectedFilesListener);
  72. super.dispose();
  73. }
  74. @override
  75. Widget build(BuildContext context) {
  76. if (widget.selectedFiles.files.isEmpty) {
  77. return AppBar(
  78. backgroundColor: widget.type == GalleryAppBarType.homepage
  79. ? Color(0x00000000)
  80. : null,
  81. elevation: 0,
  82. title: widget.type == GalleryAppBarType.homepage
  83. ? Container()
  84. : TextButton(
  85. child: Text(
  86. appBarTitle,
  87. style: TextStyle(
  88. color: Colors.white.withOpacity(0.80),
  89. ),
  90. ),
  91. onPressed: () => _renameAlbum(context),
  92. ),
  93. actions: _getDefaultActions(context),
  94. );
  95. }
  96. return AppBar(
  97. leading: IconButton(
  98. icon: Icon(Platform.isAndroid ? Icons.clear : CupertinoIcons.clear),
  99. onPressed: () {
  100. _clearSelectedFiles();
  101. },
  102. ),
  103. title: Text(widget.selectedFiles.files.length.toString()),
  104. actions: _getActions(context),
  105. );
  106. }
  107. Future<dynamic> _renameAlbum(BuildContext context) async {
  108. if (widget.type != GalleryAppBarType.owned_collection) {
  109. return;
  110. }
  111. var result = await showDialog(
  112. context: context,
  113. builder: (BuildContext context) {
  114. return ChangeCollectionNameDialog(name: appBarTitle);
  115. },
  116. barrierColor: Colors.black.withOpacity(0.85),
  117. );
  118. if (result == null) {
  119. return;
  120. }
  121. final dialog = createProgressDialog(context, "changing name...");
  122. await dialog.show();
  123. try {
  124. await CollectionsService.instance
  125. .rename(widget.collection, result);
  126. await dialog.hide();
  127. if (mounted) {
  128. appBarTitle = result;
  129. setState(() {});
  130. }
  131. } catch (e) {
  132. await dialog.hide();
  133. showGenericErrorDialog(context);
  134. }
  135. }
  136. List<Widget> _getDefaultActions(BuildContext context) {
  137. List<Widget> actions = <Widget>[];
  138. if (Configuration.instance.hasConfiguredAccount() &&
  139. (widget.type == GalleryAppBarType.local_folder ||
  140. widget.type == GalleryAppBarType.owned_collection)) {
  141. actions.add(IconButton(
  142. icon: Icon(Icons.person_add),
  143. onPressed: () {
  144. _showShareCollectionDialog();
  145. },
  146. ));
  147. }
  148. return actions;
  149. }
  150. Future<void> _showShareCollectionDialog() async {
  151. var collection = widget.collection;
  152. final dialog = createProgressDialog(context, "please wait...");
  153. await dialog.show();
  154. if (collection == null) {
  155. if (widget.type == GalleryAppBarType.local_folder) {
  156. collection =
  157. CollectionsService.instance.getCollectionForPath(widget.path);
  158. if (collection == null) {
  159. try {
  160. collection = await CollectionsService.instance
  161. .getOrCreateForPath(widget.path);
  162. } catch (e, s) {
  163. _logger.severe(e, s);
  164. await dialog.hide();
  165. showGenericErrorDialog(context);
  166. }
  167. }
  168. } else {
  169. throw Exception(
  170. "Cannot create a collection of type" + widget.type.toString());
  171. }
  172. } else {
  173. final sharees =
  174. await CollectionsService.instance.getSharees(collection.id);
  175. collection = collection.copyWith(sharees: sharees);
  176. }
  177. await dialog.hide();
  178. return showDialog<void>(
  179. context: context,
  180. builder: (BuildContext context) {
  181. return SharingDialog(collection);
  182. },
  183. );
  184. }
  185. Future<void> _createAlbum() async {
  186. Navigator.push(
  187. context,
  188. PageTransition(
  189. type: PageTransitionType.bottomToTop,
  190. child: CreateCollectionPage(
  191. widget.selectedFiles,
  192. null,
  193. )));
  194. }
  195. Future<void> _moveFiles() async {
  196. Navigator.push(
  197. context,
  198. PageTransition(
  199. type: PageTransitionType.bottomToTop,
  200. child: CreateCollectionPage(
  201. widget.selectedFiles,
  202. null,
  203. actionType: CollectionActionType.moveFiles,
  204. )));
  205. }
  206. List<Widget> _getActions(BuildContext context) {
  207. List<Widget> actions = <Widget>[];
  208. // skip add button for incoming collection till this feature is implemented
  209. if (Configuration.instance.hasConfiguredAccount() &&
  210. widget.type != GalleryAppBarType.shared_collection) {
  211. actions.add(IconButton(
  212. icon:
  213. Icon(Platform.isAndroid ? Icons.add_outlined : CupertinoIcons.add),
  214. onPressed: () {
  215. _createAlbum();
  216. },
  217. ));
  218. }
  219. if (Configuration.instance.hasConfiguredAccount() &&
  220. widget.type == GalleryAppBarType.owned_collection) {
  221. actions.add(IconButton(
  222. icon: Icon(Platform.isAndroid
  223. ? Icons.arrow_right_alt_rounded
  224. : CupertinoIcons.arrow_right),
  225. onPressed: () {
  226. _moveFiles();
  227. },
  228. ));
  229. }
  230. actions.add(IconButton(
  231. icon: Icon(
  232. Platform.isAndroid ? Icons.share_outlined : CupertinoIcons.share),
  233. onPressed: () {
  234. _shareSelected(context);
  235. },
  236. ));
  237. if (widget.type == GalleryAppBarType.homepage ||
  238. widget.type == GalleryAppBarType.archive ||
  239. widget.type == GalleryAppBarType.local_folder) {
  240. actions.add(IconButton(
  241. icon: Icon(
  242. Platform.isAndroid ? Icons.delete_outline : CupertinoIcons.delete),
  243. onPressed: () {
  244. _showDeleteSheet(context);
  245. },
  246. ));
  247. } else if (widget.type == GalleryAppBarType.owned_collection) {
  248. if (widget.collection.type == CollectionType.folder) {
  249. actions.add(IconButton(
  250. icon: Icon(Platform.isAndroid
  251. ? Icons.delete_outline
  252. : CupertinoIcons.delete),
  253. onPressed: () {
  254. _showDeleteSheet(context);
  255. },
  256. ));
  257. } else {
  258. actions.add(IconButton(
  259. icon: Icon(Icons.remove_circle_outline_rounded),
  260. onPressed: () {
  261. _showRemoveFromCollectionSheet(context);
  262. },
  263. ));
  264. }
  265. }
  266. if (widget.type == GalleryAppBarType.homepage ||
  267. widget.type == GalleryAppBarType.archive) {
  268. bool showArchive = widget.type == GalleryAppBarType.homepage;
  269. actions.add(PopupMenuButton(
  270. itemBuilder: (context) {
  271. final List<PopupMenuItem> items = [];
  272. items.add(
  273. PopupMenuItem(
  274. value: 1,
  275. child: Row(
  276. children: [
  277. Icon(Platform.isAndroid
  278. ? Icons.archive_outlined
  279. : CupertinoIcons.archivebox),
  280. Padding(
  281. padding: EdgeInsets.all(8),
  282. ),
  283. Text(showArchive ? "archive" : "unarchive"),
  284. ],
  285. ),
  286. ),
  287. );
  288. return items;
  289. },
  290. onSelected: (value) async {
  291. if (value == 1) {
  292. await _handleVisibilityChangeRequest(
  293. context, showArchive ? kVisibilityArchive : kVisibilityVisible);
  294. }
  295. },
  296. ));
  297. }
  298. return actions;
  299. }
  300. Future<void> _handleVisibilityChangeRequest(
  301. BuildContext context, int newVisibility) async {
  302. final dialog = createProgressDialog(context, "please wait...");
  303. await dialog.show();
  304. try {
  305. await FileMagicService.instance
  306. .changeVisibility(widget.selectedFiles.files.toList(), newVisibility);
  307. showToast(
  308. newVisibility == kVisibilityArchive
  309. ? "successfully archived"
  310. : "successfully unarchived",
  311. toastLength: Toast.LENGTH_SHORT);
  312. await dialog.hide();
  313. } catch (e, s) {
  314. _logger.severe("failed to update file visibility", e, s);
  315. await dialog.hide();
  316. await showGenericErrorDialog(context);
  317. } finally {
  318. _clearSelectedFiles();
  319. }
  320. }
  321. void _shareSelected(BuildContext context) {
  322. share(context, widget.selectedFiles.files.toList());
  323. }
  324. void _showRemoveFromCollectionSheet(BuildContext context) {
  325. final count = widget.selectedFiles.files.length;
  326. final action = CupertinoActionSheet(
  327. title: Text("remove " +
  328. count.toString() +
  329. " file" +
  330. (count == 1 ? "" : "s") +
  331. " from " +
  332. widget.collection.name +
  333. "?"),
  334. actions: <Widget>[
  335. CupertinoActionSheetAction(
  336. child: Text("remove"),
  337. isDestructiveAction: true,
  338. onPressed: () async {
  339. Navigator.of(context, rootNavigator: true).pop();
  340. final dialog = createProgressDialog(context, "removing files...");
  341. await dialog.show();
  342. try {
  343. await CollectionsService.instance.removeFromCollection(
  344. widget.collection.id, widget.selectedFiles.files.toList());
  345. await dialog.hide();
  346. widget.selectedFiles.clearAll();
  347. } catch (e, s) {
  348. _logger.severe(e, s);
  349. await dialog.hide();
  350. showGenericErrorDialog(context);
  351. }
  352. },
  353. ),
  354. ],
  355. cancelButton: CupertinoActionSheetAction(
  356. child: Text("cancel"),
  357. onPressed: () {
  358. Navigator.of(context, rootNavigator: true).pop();
  359. },
  360. ),
  361. );
  362. showCupertinoModalPopup(context: context, builder: (_) => action);
  363. }
  364. void _showDeleteSheet(BuildContext context) {
  365. final count = widget.selectedFiles.files.length;
  366. bool containsUploadedFile = false, containsLocalFile = false;
  367. for (final file in widget.selectedFiles.files) {
  368. if (file.uploadedFileID != null) {
  369. containsUploadedFile = true;
  370. }
  371. if (file.localID != null) {
  372. containsLocalFile = true;
  373. }
  374. }
  375. final actions = <Widget>[];
  376. if (containsUploadedFile && containsLocalFile) {
  377. actions.add(CupertinoActionSheetAction(
  378. child: Text("this device"),
  379. isDestructiveAction: true,
  380. onPressed: () async {
  381. Navigator.of(context, rootNavigator: true).pop();
  382. await deleteFilesOnDeviceOnly(
  383. context, widget.selectedFiles.files.toList());
  384. _clearSelectedFiles();
  385. showToast("files deleted from device");
  386. },
  387. ));
  388. actions.add(CupertinoActionSheetAction(
  389. child: Text("everywhere"),
  390. isDestructiveAction: true,
  391. onPressed: () async {
  392. Navigator.of(context, rootNavigator: true).pop();
  393. await deleteFilesFromEverywhere(
  394. context, widget.selectedFiles.files.toList());
  395. _clearSelectedFiles();
  396. },
  397. ));
  398. } else {
  399. actions.add(CupertinoActionSheetAction(
  400. child: Text("delete forever"),
  401. isDestructiveAction: true,
  402. onPressed: () async {
  403. Navigator.of(context, rootNavigator: true).pop();
  404. await deleteFilesFromEverywhere(
  405. context, widget.selectedFiles.files.toList());
  406. _clearSelectedFiles();
  407. },
  408. ));
  409. }
  410. final action = CupertinoActionSheet(
  411. title: Text("delete " +
  412. count.toString() +
  413. " file" +
  414. (count == 1 ? "" : "s") +
  415. (containsUploadedFile && containsLocalFile ? " from" : "?")),
  416. actions: actions,
  417. cancelButton: CupertinoActionSheetAction(
  418. child: Text("cancel"),
  419. onPressed: () {
  420. Navigator.of(context, rootNavigator: true).pop();
  421. },
  422. ),
  423. );
  424. showCupertinoModalPopup(context: context, builder: (_) => action);
  425. }
  426. void _clearSelectedFiles() {
  427. widget.selectedFiles.clearAll();
  428. }
  429. }