gallery_app_bar_widget.dart 16 KB

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