gallery_app_bar_widget.dart 17 KB

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