gallery_app_bar_widget.dart 17 KB

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