gallery_app_bar_widget.dart 13 KB

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