gallery_app_bar_widget.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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/magic_metadata.dart';
  13. import 'package:photos/models/selected_files.dart';
  14. import 'package:photos/services/collections_service.dart';
  15. import 'package:photos/ui/change_collection_name_dialog.dart';
  16. import 'package:photos/ui/create_collection_page.dart';
  17. import 'package:photos/ui/share_collection_widget.dart';
  18. import 'package:photos/utils/delete_file_util.dart';
  19. import 'package:photos/utils/dialog_util.dart';
  20. import 'package:photos/services/file_magic_service.dart';
  21. import 'package:photos/utils/share_util.dart';
  22. import 'package:photos/utils/toast_util.dart';
  23. enum GalleryAppBarType {
  24. homepage,
  25. archive,
  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. // skip add button for incoming collection till this feature is implemented
  243. if (Configuration.instance.hasConfiguredAccount() &&
  244. widget.type != GalleryAppBarType.shared_collection) {
  245. actions.add(
  246. Tooltip(
  247. message: "add",
  248. child: IconButton(
  249. icon: Icon(
  250. Platform.isAndroid ? Icons.add_outlined : CupertinoIcons.add),
  251. onPressed: () {
  252. _createAlbum();
  253. },
  254. ),
  255. ),
  256. );
  257. }
  258. if (Configuration.instance.hasConfiguredAccount() &&
  259. widget.type == GalleryAppBarType.owned_collection &&
  260. widget.collection.type != CollectionType.favorites) {
  261. actions.add(
  262. Tooltip(
  263. message: "move",
  264. child: IconButton(
  265. icon: Icon(Platform.isAndroid
  266. ? Icons.arrow_forward
  267. : CupertinoIcons.arrow_right),
  268. onPressed: () {
  269. _moveFiles();
  270. },
  271. ),
  272. ),
  273. );
  274. }
  275. actions.add(
  276. Tooltip(
  277. message: "share",
  278. child: IconButton(
  279. icon: Icon(
  280. Platform.isAndroid ? Icons.share_outlined : CupertinoIcons.share),
  281. onPressed: () {
  282. _shareSelected(context);
  283. },
  284. ),
  285. ),
  286. );
  287. if (widget.type == GalleryAppBarType.homepage ||
  288. widget.type == GalleryAppBarType.archive ||
  289. widget.type == GalleryAppBarType.local_folder) {
  290. actions.add(
  291. Tooltip(
  292. message: "delete",
  293. child: IconButton(
  294. icon: Icon(Platform.isAndroid
  295. ? Icons.delete_outline
  296. : CupertinoIcons.delete),
  297. onPressed: () {
  298. _showDeleteSheet(context);
  299. },
  300. ),
  301. ),
  302. );
  303. } else if (widget.type == GalleryAppBarType.owned_collection) {
  304. if (widget.collection.type == CollectionType.folder) {
  305. actions.add(
  306. Tooltip(
  307. message: "delete",
  308. child: IconButton(
  309. icon: Icon(Platform.isAndroid
  310. ? Icons.delete_outline
  311. : CupertinoIcons.delete),
  312. onPressed: () {
  313. _showDeleteSheet(context);
  314. },
  315. ),
  316. ),
  317. );
  318. } else {
  319. actions.add(
  320. Tooltip(
  321. message: "remove",
  322. child: IconButton(
  323. icon: Icon(Icons.remove_circle_outline_rounded),
  324. onPressed: () {
  325. _showRemoveFromCollectionSheet(context);
  326. },
  327. ),
  328. ),
  329. );
  330. }
  331. }
  332. if (widget.type == GalleryAppBarType.homepage ||
  333. widget.type == GalleryAppBarType.archive) {
  334. bool showArchive = widget.type == GalleryAppBarType.homepage;
  335. actions.add(Tooltip(
  336. message: showArchive ? "archive" : "unarchive",
  337. child: IconButton(
  338. icon: Icon(
  339. Platform.isAndroid
  340. ? (showArchive
  341. ? Icons.archive_outlined
  342. : Icons.unarchive_outlined)
  343. : CupertinoIcons.archivebox,
  344. ),
  345. onPressed: () {
  346. _handleVisibilityChangeRequest(
  347. context, showArchive ? kVisibilityArchive : kVisibilityVisible);
  348. },
  349. ),
  350. ));
  351. }
  352. return actions;
  353. }
  354. Future<void> _handleVisibilityChangeRequest(
  355. BuildContext context, int newVisibility) async {
  356. final dialog = createProgressDialog(
  357. context,
  358. newVisibility == kVisibilityArchive
  359. ? "archiving..."
  360. : "unarchiving...");
  361. await dialog.show();
  362. try {
  363. await FileMagicService.instance
  364. .changeVisibility(widget.selectedFiles.files.toList(), newVisibility);
  365. showToast(
  366. newVisibility == kVisibilityArchive
  367. ? "successfully archived"
  368. : "successfully unarchived",
  369. toastLength: Toast.LENGTH_SHORT);
  370. await dialog.hide();
  371. } catch (e, s) {
  372. _logger.severe("failed to update file visibility", e, s);
  373. await dialog.hide();
  374. await showGenericErrorDialog(context);
  375. } finally {
  376. _clearSelectedFiles();
  377. }
  378. }
  379. void _shareSelected(BuildContext context) {
  380. share(context, widget.selectedFiles.files.toList());
  381. }
  382. void _showRemoveFromCollectionSheet(BuildContext context) {
  383. final count = widget.selectedFiles.files.length;
  384. final action = CupertinoActionSheet(
  385. title: Text("remove " +
  386. count.toString() +
  387. " file" +
  388. (count == 1 ? "" : "s") +
  389. " from " +
  390. widget.collection.name +
  391. "?"),
  392. actions: <Widget>[
  393. CupertinoActionSheetAction(
  394. child: Text("remove"),
  395. isDestructiveAction: true,
  396. onPressed: () async {
  397. Navigator.of(context, rootNavigator: true).pop();
  398. final dialog = createProgressDialog(context, "removing files...");
  399. await dialog.show();
  400. try {
  401. await CollectionsService.instance.removeFromCollection(
  402. widget.collection.id, widget.selectedFiles.files.toList());
  403. await dialog.hide();
  404. widget.selectedFiles.clearAll();
  405. } catch (e, s) {
  406. _logger.severe(e, s);
  407. await dialog.hide();
  408. showGenericErrorDialog(context);
  409. }
  410. },
  411. ),
  412. ],
  413. cancelButton: CupertinoActionSheetAction(
  414. child: Text("cancel"),
  415. onPressed: () {
  416. Navigator.of(context, rootNavigator: true).pop();
  417. },
  418. ),
  419. );
  420. showCupertinoModalPopup(context: context, builder: (_) => action);
  421. }
  422. void _showDeleteSheet(BuildContext context) {
  423. final count = widget.selectedFiles.files.length;
  424. bool containsUploadedFile = false, containsLocalFile = false;
  425. for (final file in widget.selectedFiles.files) {
  426. if (file.uploadedFileID != null) {
  427. containsUploadedFile = true;
  428. }
  429. if (file.localID != null) {
  430. containsLocalFile = true;
  431. }
  432. }
  433. final actions = <Widget>[];
  434. if (containsUploadedFile && containsLocalFile) {
  435. actions.add(CupertinoActionSheetAction(
  436. child: Text("this device"),
  437. isDestructiveAction: true,
  438. onPressed: () async {
  439. Navigator.of(context, rootNavigator: true).pop();
  440. await deleteFilesOnDeviceOnly(
  441. context, widget.selectedFiles.files.toList());
  442. _clearSelectedFiles();
  443. showToast("files deleted from device");
  444. },
  445. ));
  446. actions.add(CupertinoActionSheetAction(
  447. child: Text("everywhere"),
  448. isDestructiveAction: true,
  449. onPressed: () async {
  450. Navigator.of(context, rootNavigator: true).pop();
  451. await deleteFilesFromEverywhere(
  452. context, widget.selectedFiles.files.toList());
  453. _clearSelectedFiles();
  454. },
  455. ));
  456. } else {
  457. actions.add(CupertinoActionSheetAction(
  458. child: Text("delete forever"),
  459. isDestructiveAction: true,
  460. onPressed: () async {
  461. Navigator.of(context, rootNavigator: true).pop();
  462. await deleteFilesFromEverywhere(
  463. context, widget.selectedFiles.files.toList());
  464. _clearSelectedFiles();
  465. },
  466. ));
  467. }
  468. final action = CupertinoActionSheet(
  469. title: Text("delete " +
  470. count.toString() +
  471. " file" +
  472. (count == 1 ? "" : "s") +
  473. (containsUploadedFile && containsLocalFile ? " from" : "?")),
  474. actions: actions,
  475. cancelButton: CupertinoActionSheetAction(
  476. child: Text("cancel"),
  477. onPressed: () {
  478. Navigator.of(context, rootNavigator: true).pop();
  479. },
  480. ),
  481. );
  482. showCupertinoModalPopup(context: context, builder: (_) => action);
  483. }
  484. void _clearSelectedFiles() {
  485. widget.selectedFiles.clearAll();
  486. }
  487. }