gallery_app_bar_widget.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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/create_collection_page.dart';
  15. import 'package:photos/ui/rename_dialog.dart';
  16. import 'package:photos/ui/share_collection_widget.dart';
  17. import 'package:photos/utils/delete_file_util.dart';
  18. import 'package:photos/utils/dialog_util.dart';
  19. import 'package:photos/utils/magic_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<String>(
  115. context: context,
  116. builder: (BuildContext context) {
  117. return RenameDialog(_appBarTitle, 'album');
  118. },
  119. barrierColor: Colors.black.withOpacity(0.85),
  120. );
  121. // indicates user cancelled the rename request
  122. if (result == null || result.trim() == _appBarTitle.trim()) {
  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. actions.add(PopupMenuButton(
  158. itemBuilder: (context) {
  159. final List<PopupMenuItem> items = [];
  160. if (widget.collection.type == CollectionType.album) {
  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. }
  176. bool isArchived = widget.collection.isArchived();
  177. items.add(
  178. PopupMenuItem(
  179. value: 2,
  180. child: Row(
  181. children: [
  182. Icon(isArchived ? Icons.unarchive : Icons.archive),
  183. Padding(
  184. padding: EdgeInsets.all(8),
  185. ),
  186. Text(isArchived ? "unarchive" : "archive"),
  187. ],
  188. ),
  189. ),
  190. );
  191. return items;
  192. },
  193. onSelected: (value) async {
  194. if (value == 1) {
  195. await _renameAlbum(context);
  196. }
  197. if (value == 2) {
  198. await changeCollectionVisibility(
  199. context,
  200. widget.collection,
  201. widget.collection.isArchived()
  202. ? kVisibilityVisible
  203. : kVisibilityArchive);
  204. }
  205. },
  206. ));
  207. }
  208. if (widget.type == GalleryAppBarType.trash) {
  209. actions.add(
  210. Tooltip(
  211. message: "empty trash",
  212. child: IconButton(
  213. icon: Icon(Icons.delete_forever),
  214. onPressed: () async {
  215. await emptyTrash(context);
  216. },
  217. ),
  218. ),
  219. );
  220. }
  221. return actions;
  222. }
  223. Future<void> _showShareCollectionDialog() async {
  224. var collection = widget.collection;
  225. final dialog = createProgressDialog(context, "please wait...");
  226. await dialog.show();
  227. try {
  228. if (collection == null) {
  229. if (widget.type == GalleryAppBarType.local_folder) {
  230. collection =
  231. await CollectionsService.instance.getOrCreateForPath(widget.path);
  232. } else {
  233. throw Exception(
  234. "Cannot create a collection of type" + widget.type.toString());
  235. }
  236. } else {
  237. final sharees =
  238. await CollectionsService.instance.getSharees(collection.id);
  239. collection = collection.copyWith(sharees: sharees);
  240. }
  241. await dialog.hide();
  242. return showDialog<void>(
  243. context: context,
  244. builder: (BuildContext context) {
  245. return SharingDialog(collection);
  246. },
  247. );
  248. } catch (e, s) {
  249. _logger.severe(e, s);
  250. await dialog.hide();
  251. showGenericErrorDialog(context);
  252. }
  253. }
  254. Future<void> _createAlbum() async {
  255. Navigator.push(
  256. context,
  257. PageTransition(
  258. type: PageTransitionType.bottomToTop,
  259. child: CreateCollectionPage(
  260. widget.selectedFiles,
  261. null,
  262. )));
  263. }
  264. Future<void> _moveFiles() async {
  265. Navigator.push(
  266. context,
  267. PageTransition(
  268. type: PageTransitionType.bottomToTop,
  269. child: CreateCollectionPage(
  270. widget.selectedFiles,
  271. null,
  272. actionType: CollectionActionType.moveFiles,
  273. )));
  274. }
  275. List<Widget> _getActions(BuildContext context) {
  276. List<Widget> actions = <Widget>[];
  277. if (widget.type == GalleryAppBarType.trash) {
  278. _addTrashAction(actions);
  279. return actions;
  280. }
  281. // skip add button for incoming collection till this feature is implemented
  282. if (Configuration.instance.hasConfiguredAccount() &&
  283. widget.type != GalleryAppBarType.shared_collection) {
  284. String msg = "add";
  285. IconData iconData =
  286. Platform.isAndroid ? Icons.add_outlined : CupertinoIcons.add;
  287. // show upload icon instead of add for files selected in local gallery
  288. if (widget.type == GalleryAppBarType.local_folder) {
  289. msg = "upload";
  290. iconData = Platform.isAndroid
  291. ? Icons.cloud_upload
  292. : CupertinoIcons.cloud_upload;
  293. }
  294. actions.add(
  295. Tooltip(
  296. message: msg,
  297. child: IconButton(
  298. icon: Icon(iconData),
  299. onPressed: () {
  300. _createAlbum();
  301. },
  302. ),
  303. ),
  304. );
  305. }
  306. if (Configuration.instance.hasConfiguredAccount() &&
  307. widget.type == GalleryAppBarType.owned_collection &&
  308. widget.collection.type != CollectionType.favorites) {
  309. actions.add(
  310. Tooltip(
  311. message: "move",
  312. child: IconButton(
  313. icon: Icon(Platform.isAndroid
  314. ? Icons.arrow_forward
  315. : CupertinoIcons.arrow_right),
  316. onPressed: () {
  317. _moveFiles();
  318. },
  319. ),
  320. ),
  321. );
  322. }
  323. actions.add(
  324. Tooltip(
  325. message: "share",
  326. child: IconButton(
  327. icon: Icon(
  328. Platform.isAndroid ? Icons.share_outlined : CupertinoIcons.share),
  329. onPressed: () {
  330. _shareSelected(context);
  331. },
  332. ),
  333. ),
  334. );
  335. if (widget.type == GalleryAppBarType.homepage ||
  336. widget.type == GalleryAppBarType.archive ||
  337. widget.type == GalleryAppBarType.local_folder) {
  338. actions.add(
  339. Tooltip(
  340. message: "delete",
  341. child: IconButton(
  342. icon: Icon(Platform.isAndroid
  343. ? Icons.delete_outline
  344. : CupertinoIcons.delete),
  345. onPressed: () {
  346. _showDeleteSheet(context);
  347. },
  348. ),
  349. ),
  350. );
  351. } else if (widget.type == GalleryAppBarType.owned_collection) {
  352. if (widget.collection.type == CollectionType.folder) {
  353. actions.add(
  354. Tooltip(
  355. message: "delete",
  356. child: IconButton(
  357. icon: Icon(Platform.isAndroid
  358. ? Icons.delete_outline
  359. : CupertinoIcons.delete),
  360. onPressed: () {
  361. _showDeleteSheet(context);
  362. },
  363. ),
  364. ),
  365. );
  366. } else {
  367. actions.add(
  368. Tooltip(
  369. message: "remove",
  370. child: IconButton(
  371. icon: Icon(Icons.remove_circle_outline_rounded),
  372. onPressed: () {
  373. _showRemoveFromCollectionSheet(context);
  374. },
  375. ),
  376. ),
  377. );
  378. }
  379. }
  380. if (widget.type == GalleryAppBarType.homepage ||
  381. widget.type == GalleryAppBarType.archive) {
  382. bool showArchive = widget.type == GalleryAppBarType.homepage;
  383. actions.add(Tooltip(
  384. message: showArchive ? "archive" : "unarchive",
  385. child: IconButton(
  386. icon: Icon(
  387. Platform.isAndroid
  388. ? (showArchive
  389. ? Icons.archive_outlined
  390. : Icons.unarchive_outlined)
  391. : (showArchive
  392. ? CupertinoIcons.archivebox
  393. : CupertinoIcons.archivebox_fill),
  394. ),
  395. onPressed: () {
  396. _handleVisibilityChangeRequest(
  397. context, showArchive ? kVisibilityArchive : kVisibilityVisible);
  398. },
  399. ),
  400. ));
  401. }
  402. return actions;
  403. }
  404. void _addTrashAction(List<Widget> actions) {
  405. actions.add(Tooltip(
  406. message: "restore",
  407. child: IconButton(
  408. icon: Icon(Icons.restore_outlined),
  409. onPressed: () {
  410. Navigator.push(
  411. context,
  412. PageTransition(
  413. type: PageTransitionType.bottomToTop,
  414. child: CreateCollectionPage(
  415. widget.selectedFiles,
  416. null,
  417. actionType: CollectionActionType.restoreFiles,
  418. )));
  419. },
  420. ),
  421. ));
  422. actions.add(
  423. Tooltip(
  424. message: "delete permanently",
  425. child: IconButton(
  426. icon: Icon(Icons.delete_forever_outlined),
  427. onPressed: () async {
  428. if (await deleteFromTrash(
  429. context, widget.selectedFiles.files.toList())) {
  430. _clearSelectedFiles();
  431. }
  432. },
  433. ),
  434. ),
  435. );
  436. }
  437. Future<void> _handleVisibilityChangeRequest(
  438. BuildContext context, int newVisibility) async {
  439. try {
  440. await changeVisibility(
  441. context, widget.selectedFiles.files.toList(), newVisibility);
  442. } catch (e, s) {
  443. _logger.severe("failed to update file visibility", e, s);
  444. await showGenericErrorDialog(context);
  445. } finally {
  446. _clearSelectedFiles();
  447. }
  448. }
  449. void _shareSelected(BuildContext context) {
  450. share(context, widget.selectedFiles.files.toList());
  451. }
  452. void _showRemoveFromCollectionSheet(BuildContext context) {
  453. final count = widget.selectedFiles.files.length;
  454. final action = CupertinoActionSheet(
  455. title: Text("remove " +
  456. count.toString() +
  457. " file" +
  458. (count == 1 ? "" : "s") +
  459. " from " +
  460. widget.collection.name +
  461. "?"),
  462. actions: <Widget>[
  463. CupertinoActionSheetAction(
  464. child: Text("remove"),
  465. isDestructiveAction: true,
  466. onPressed: () async {
  467. Navigator.of(context, rootNavigator: true).pop();
  468. final dialog = createProgressDialog(context, "removing files...");
  469. await dialog.show();
  470. try {
  471. await CollectionsService.instance.removeFromCollection(
  472. widget.collection.id, widget.selectedFiles.files.toList());
  473. await dialog.hide();
  474. widget.selectedFiles.clearAll();
  475. } catch (e, s) {
  476. _logger.severe(e, s);
  477. await dialog.hide();
  478. showGenericErrorDialog(context);
  479. }
  480. },
  481. ),
  482. ],
  483. cancelButton: CupertinoActionSheetAction(
  484. child: Text("cancel"),
  485. onPressed: () {
  486. Navigator.of(context, rootNavigator: true).pop();
  487. },
  488. ),
  489. );
  490. showCupertinoModalPopup(context: context, builder: (_) => action);
  491. }
  492. void _showDeleteSheet(BuildContext context) {
  493. final count = widget.selectedFiles.files.length;
  494. bool containsUploadedFile = false, containsLocalFile = false;
  495. for (final file in widget.selectedFiles.files) {
  496. if (file.uploadedFileID != null) {
  497. containsUploadedFile = true;
  498. }
  499. if (file.localID != null) {
  500. containsLocalFile = true;
  501. }
  502. }
  503. final actions = <Widget>[];
  504. if (containsUploadedFile && containsLocalFile) {
  505. actions.add(CupertinoActionSheetAction(
  506. child: Text("device"),
  507. isDestructiveAction: true,
  508. onPressed: () async {
  509. Navigator.of(context, rootNavigator: true).pop();
  510. await deleteFilesOnDeviceOnly(
  511. context, widget.selectedFiles.files.toList());
  512. _clearSelectedFiles();
  513. showToast("files deleted from device");
  514. },
  515. ));
  516. actions.add(CupertinoActionSheetAction(
  517. child: Text("ente"),
  518. isDestructiveAction: true,
  519. onPressed: () async {
  520. Navigator.of(context, rootNavigator: true).pop();
  521. await deleteFilesFromRemoteOnly(
  522. context, widget.selectedFiles.files.toList());
  523. _clearSelectedFiles();
  524. showShortToast("moved to trash");
  525. },
  526. ));
  527. actions.add(CupertinoActionSheetAction(
  528. child: Text("everywhere"),
  529. isDestructiveAction: true,
  530. onPressed: () async {
  531. Navigator.of(context, rootNavigator: true).pop();
  532. await deleteFilesFromEverywhere(
  533. context, widget.selectedFiles.files.toList());
  534. _clearSelectedFiles();
  535. },
  536. ));
  537. } else {
  538. actions.add(CupertinoActionSheetAction(
  539. child: Text("delete"),
  540. isDestructiveAction: true,
  541. onPressed: () async {
  542. Navigator.of(context, rootNavigator: true).pop();
  543. await deleteFilesFromEverywhere(
  544. context, widget.selectedFiles.files.toList());
  545. _clearSelectedFiles();
  546. },
  547. ));
  548. }
  549. final action = CupertinoActionSheet(
  550. title: Text("delete " +
  551. count.toString() +
  552. " file" +
  553. (count == 1 ? "" : "s") +
  554. (containsUploadedFile && containsLocalFile ? " from" : "?")),
  555. actions: actions,
  556. cancelButton: CupertinoActionSheetAction(
  557. child: Text("cancel"),
  558. onPressed: () {
  559. Navigator.of(context, rootNavigator: true).pop();
  560. },
  561. ),
  562. );
  563. showCupertinoModalPopup(context: context, builder: (_) => action);
  564. }
  565. void _clearSelectedFiles() {
  566. widget.selectedFiles.clearAll();
  567. }
  568. }