gallery_app_bar_widget.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:collection/collection.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photos/core/configuration.dart';
  7. import 'package:photos/core/event_bus.dart';
  8. import 'package:photos/events/subscription_purchased_event.dart';
  9. import 'package:photos/models/backup_status.dart';
  10. import 'package:photos/models/collection.dart';
  11. import 'package:photos/models/device_collection.dart';
  12. import 'package:photos/models/gallery_type.dart';
  13. import 'package:photos/models/magic_metadata.dart';
  14. import 'package:photos/models/selected_files.dart';
  15. import 'package:photos/services/collections_service.dart';
  16. import 'package:photos/services/sync_service.dart';
  17. import 'package:photos/services/update_service.dart';
  18. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  19. import 'package:photos/ui/components/action_sheet_widget.dart';
  20. import 'package:photos/ui/components/button_widget.dart';
  21. import 'package:photos/ui/components/dialog_widget.dart';
  22. import 'package:photos/ui/components/models/button_type.dart';
  23. import 'package:photos/ui/sharing/album_participants_page.dart';
  24. import 'package:photos/ui/sharing/share_collection_page.dart';
  25. import 'package:photos/ui/tools/free_space_page.dart';
  26. import 'package:photos/utils/data_util.dart';
  27. import 'package:photos/utils/dialog_util.dart';
  28. import 'package:photos/utils/magic_util.dart';
  29. import 'package:photos/utils/navigation_util.dart';
  30. import 'package:photos/utils/toast_util.dart';
  31. class GalleryAppBarWidget extends StatefulWidget {
  32. final GalleryType type;
  33. final String? title;
  34. final SelectedFiles selectedFiles;
  35. final DeviceCollection? deviceCollection;
  36. final Collection? collection;
  37. const GalleryAppBarWidget(
  38. this.type,
  39. this.title,
  40. this.selectedFiles, {
  41. Key? key,
  42. this.deviceCollection,
  43. this.collection,
  44. }) : super(key: key);
  45. @override
  46. State<GalleryAppBarWidget> createState() => _GalleryAppBarWidgetState();
  47. }
  48. class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
  49. final _logger = Logger("GalleryAppBar");
  50. late StreamSubscription _userAuthEventSubscription;
  51. late Function() _selectedFilesListener;
  52. String? _appBarTitle;
  53. late CollectionActions collectionActions;
  54. final GlobalKey shareButtonKey = GlobalKey();
  55. @override
  56. void initState() {
  57. _selectedFilesListener = () {
  58. setState(() {});
  59. };
  60. collectionActions = CollectionActions(CollectionsService.instance);
  61. widget.selectedFiles.addListener(_selectedFilesListener);
  62. _userAuthEventSubscription =
  63. Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
  64. setState(() {});
  65. });
  66. _appBarTitle = widget.title;
  67. super.initState();
  68. }
  69. @override
  70. void dispose() {
  71. _userAuthEventSubscription.cancel();
  72. widget.selectedFiles.removeListener(_selectedFilesListener);
  73. super.dispose();
  74. }
  75. @override
  76. Widget build(BuildContext context) {
  77. return widget.type == GalleryType.homepage
  78. ? const SizedBox.shrink()
  79. : AppBar(
  80. backgroundColor: widget.type == GalleryType.homepage
  81. ? const Color(0x00000000)
  82. : null,
  83. elevation: 0,
  84. centerTitle: false,
  85. title: widget.type == GalleryType.homepage
  86. ? const SizedBox.shrink()
  87. : TextButton(
  88. child: Text(
  89. _appBarTitle!,
  90. style: Theme.of(context)
  91. .textTheme
  92. .headline5!
  93. .copyWith(fontSize: 16),
  94. ),
  95. onPressed: () => _renameAlbum(context),
  96. ),
  97. actions: _getDefaultActions(context),
  98. );
  99. }
  100. Future<dynamic> _renameAlbum(BuildContext context) async {
  101. if (widget.type != GalleryType.ownedCollection) {
  102. return;
  103. }
  104. final result = await showTextInputDialog(
  105. context,
  106. title: "Rename album",
  107. submitButtonLabel: "Rename",
  108. hintText: "Enter album name",
  109. alwaysShowSuccessState: true,
  110. textCapitalization: TextCapitalization.words,
  111. onSubmit: (String text) async {
  112. // indicates user cancelled the rename request
  113. if (text == "" || text.trim() == _appBarTitle!.trim()) {
  114. return;
  115. }
  116. try {
  117. await CollectionsService.instance.rename(widget.collection!, text);
  118. if (mounted) {
  119. _appBarTitle = text;
  120. setState(() {});
  121. }
  122. } catch (e, s) {
  123. _logger.severe("Failed to rename album", e, s);
  124. rethrow;
  125. }
  126. },
  127. );
  128. if (result is Exception) {
  129. showGenericErrorDialog(context: context);
  130. }
  131. }
  132. Future<dynamic> _leaveAlbum(BuildContext context) async {
  133. final actionResult = await showActionSheet(
  134. context: context,
  135. buttons: [
  136. ButtonWidget(
  137. buttonType: ButtonType.critical,
  138. isInAlert: true,
  139. shouldStickToDarkTheme: true,
  140. buttonAction: ButtonAction.first,
  141. shouldSurfaceExecutionStates: true,
  142. labelText: "Leave album",
  143. onTap: () async {
  144. await CollectionsService.instance.leaveAlbum(widget.collection!);
  145. },
  146. ),
  147. const ButtonWidget(
  148. buttonType: ButtonType.secondary,
  149. buttonAction: ButtonAction.cancel,
  150. isInAlert: true,
  151. shouldStickToDarkTheme: true,
  152. labelText: "Cancel",
  153. )
  154. ],
  155. title: "Leave shared album?",
  156. body: "Photos added by you will be removed from the album",
  157. );
  158. if (actionResult?.action != null && mounted) {
  159. if (actionResult!.action == ButtonAction.error) {
  160. showGenericErrorDialog(context: context);
  161. } else if (actionResult.action == ButtonAction.first) {
  162. Navigator.of(context).pop();
  163. }
  164. }
  165. }
  166. // todo: In the new design, clicking on free up space will directly open
  167. // the free up space page and show loading indicator while calculating
  168. // the space which can be claimed up. This code duplication should be removed
  169. // whenever we move to the new design for free up space.
  170. Future<dynamic> _deleteBackedUpFiles(BuildContext context) async {
  171. final dialog = createProgressDialog(context, "Calculating...");
  172. await dialog.show();
  173. BackupStatus status;
  174. try {
  175. status = await SyncService.instance
  176. .getBackupStatus(pathID: widget.deviceCollection!.id);
  177. } catch (e) {
  178. await dialog.hide();
  179. showGenericErrorDialog(context: context);
  180. return;
  181. }
  182. await dialog.hide();
  183. if (status.localIDs.isEmpty) {
  184. showErrorDialog(
  185. context,
  186. "✨ All clear",
  187. "You've no files in this album that can be deleted",
  188. );
  189. } else {
  190. final bool? result = await routeToPage(
  191. context,
  192. FreeSpacePage(status, clearSpaceForFolder: true),
  193. );
  194. if (result == true) {
  195. _showSpaceFreedDialog(status);
  196. }
  197. }
  198. }
  199. void _showSpaceFreedDialog(BackupStatus status) {
  200. final DialogWidget dialog = choiceDialog(
  201. title: "Success",
  202. body: "You have successfully freed up " + formatBytes(status.size) + "!",
  203. firstButtonLabel: "Rate us",
  204. firstButtonOnTap: () async {
  205. UpdateService.instance.launchReviewUrl();
  206. },
  207. firstButtonType: ButtonType.primary,
  208. secondButtonLabel: "OK",
  209. secondButtonOnTap: () async {
  210. if (Platform.isIOS) {
  211. showToast(
  212. context,
  213. "Also empty \"Recently Deleted\" from \"Settings\" -> \"Storage\" to claim the freed space",
  214. );
  215. }
  216. },
  217. );
  218. showConfettiDialog(
  219. context: context,
  220. dialogBuilder: (BuildContext context) {
  221. return dialog;
  222. },
  223. barrierColor: Colors.black87,
  224. confettiAlignment: Alignment.topCenter,
  225. useRootNavigator: true,
  226. );
  227. }
  228. List<Widget> _getDefaultActions(BuildContext context) {
  229. final List<Widget> actions = <Widget>[];
  230. if (Configuration.instance.hasConfiguredAccount() &&
  231. widget.selectedFiles.files.isEmpty &&
  232. (widget.type == GalleryType.ownedCollection ||
  233. widget.type == GalleryType.sharedCollection) &&
  234. widget.collection?.type != CollectionType.favorites) {
  235. actions.add(
  236. Tooltip(
  237. message: "Share",
  238. child: IconButton(
  239. icon: const Icon(Icons.people_outlined),
  240. onPressed: () async {
  241. await _showShareCollectionDialog();
  242. },
  243. ),
  244. ),
  245. );
  246. }
  247. final List<PopupMenuItem> items = [];
  248. if (widget.type == GalleryType.ownedCollection) {
  249. if (widget.collection!.type != CollectionType.favorites) {
  250. items.add(
  251. PopupMenuItem(
  252. value: 1,
  253. child: Row(
  254. children: const [
  255. Icon(Icons.edit),
  256. Padding(
  257. padding: EdgeInsets.all(8),
  258. ),
  259. Text("Rename album"),
  260. ],
  261. ),
  262. ),
  263. );
  264. }
  265. final bool isArchived = widget.collection!.isArchived();
  266. // Do not show archive option for favorite collection. If collection is
  267. // already archived, allow user to unarchive that collection.
  268. if (isArchived || widget.collection!.type != CollectionType.favorites) {
  269. items.add(
  270. PopupMenuItem(
  271. value: 2,
  272. child: Row(
  273. children: [
  274. Icon(isArchived ? Icons.unarchive : Icons.archive_outlined),
  275. const Padding(
  276. padding: EdgeInsets.all(8),
  277. ),
  278. Text(isArchived ? "Unarchive album" : "Archive album"),
  279. ],
  280. ),
  281. ),
  282. );
  283. }
  284. if (widget.collection!.type != CollectionType.favorites) {
  285. items.add(
  286. PopupMenuItem(
  287. value: 3,
  288. child: Row(
  289. children: const [
  290. Icon(Icons.delete_outline),
  291. Padding(
  292. padding: EdgeInsets.all(8),
  293. ),
  294. Text("Delete album"),
  295. ],
  296. ),
  297. ),
  298. );
  299. }
  300. } // ownedCollection open ends
  301. if (widget.type == GalleryType.sharedCollection) {
  302. items.add(
  303. PopupMenuItem(
  304. value: 4,
  305. child: Row(
  306. children: const [
  307. Icon(Icons.logout),
  308. Padding(
  309. padding: EdgeInsets.all(8),
  310. ),
  311. Text("Leave album"),
  312. ],
  313. ),
  314. ),
  315. );
  316. }
  317. if (widget.type == GalleryType.localFolder) {
  318. items.add(
  319. PopupMenuItem(
  320. value: 5,
  321. child: Row(
  322. children: const [
  323. Icon(Icons.delete_sweep_outlined),
  324. Padding(
  325. padding: EdgeInsets.all(8),
  326. ),
  327. Text("Free up device space"),
  328. ],
  329. ),
  330. ),
  331. );
  332. }
  333. if (items.isNotEmpty) {
  334. actions.add(
  335. PopupMenuButton(
  336. itemBuilder: (context) {
  337. return items;
  338. },
  339. onSelected: (dynamic value) async {
  340. if (value == 1) {
  341. await _renameAlbum(context);
  342. } else if (value == 2) {
  343. await changeCollectionVisibility(
  344. context,
  345. widget.collection!,
  346. widget.collection!.isArchived()
  347. ? visibilityVisible
  348. : visibilityArchive,
  349. );
  350. } else if (value == 3) {
  351. await _trashCollection();
  352. } else if (value == 4) {
  353. await _leaveAlbum(context);
  354. } else if (value == 5) {
  355. await _deleteBackedUpFiles(context);
  356. } else {
  357. showToast(context, "Something went wrong");
  358. }
  359. },
  360. ),
  361. );
  362. }
  363. return actions;
  364. }
  365. Future<void> _trashCollection() async {
  366. final collectionWithThumbnail =
  367. await CollectionsService.instance.getCollectionsWithThumbnails();
  368. final bool isEmptyCollection = collectionWithThumbnail
  369. .firstWhereOrNull(
  370. (element) => element.collection.id == widget.collection!.id,
  371. )
  372. ?.thumbnail ==
  373. null;
  374. if (isEmptyCollection) {
  375. final dialog = createProgressDialog(
  376. context,
  377. "Please wait, deleting album",
  378. );
  379. await dialog.show();
  380. try {
  381. await CollectionsService.instance
  382. .trashEmptyCollection(widget.collection!);
  383. await dialog.hide();
  384. Navigator.of(context).pop();
  385. } catch (e, s) {
  386. _logger.severe("failed to trash collection", e, s);
  387. await dialog.hide();
  388. showGenericErrorDialog(context: context);
  389. }
  390. } else {
  391. final bool result = await collectionActions.deleteCollectionSheet(
  392. context,
  393. widget.collection!,
  394. );
  395. if (result == true) {
  396. Navigator.of(context).pop();
  397. } else {
  398. debugPrint("No pop");
  399. }
  400. }
  401. }
  402. Future<void> _showShareCollectionDialog() async {
  403. final collection = widget.collection;
  404. try {
  405. if (collection == null ||
  406. (widget.type != GalleryType.ownedCollection &&
  407. widget.type != GalleryType.sharedCollection)) {
  408. throw Exception(
  409. "Cannot share empty collection of typex ${widget.type}",
  410. );
  411. }
  412. if (Configuration.instance.getUserID() == widget.collection!.owner!.id) {
  413. unawaited(
  414. routeToPage(
  415. context,
  416. ShareCollectionPage(collection),
  417. ),
  418. );
  419. } else {
  420. unawaited(
  421. routeToPage(
  422. context,
  423. AlbumParticipantsPage(collection),
  424. ),
  425. );
  426. }
  427. } catch (e, s) {
  428. _logger.severe(e, s);
  429. showGenericErrorDialog(context: context);
  430. }
  431. }
  432. }