file_app_bar.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import 'dart:io';
  2. import 'package:flutter/material.dart';
  3. import 'package:logging/logging.dart';
  4. import 'package:media_extension/media_extension.dart';
  5. import "package:photos/generated/l10n.dart";
  6. import "package:photos/l10n/l10n.dart";
  7. import "package:photos/models/file/extensions/file_props.dart";
  8. import 'package:photos/models/file/file.dart';
  9. import 'package:photos/models/file/file_type.dart';
  10. import 'package:photos/models/file/trash_file.dart';
  11. import "package:photos/models/metadata/common_keys.dart";
  12. import 'package:photos/models/selected_files.dart';
  13. import "package:photos/service_locator.dart";
  14. import 'package:photos/services/collections_service.dart';
  15. import 'package:photos/services/hidden_service.dart';
  16. import 'package:photos/ui/collections/collection_action_sheet.dart';
  17. import 'package:photos/ui/viewer/file/custom_app_bar.dart';
  18. import "package:photos/ui/viewer/file_details/favorite_widget.dart";
  19. import "package:photos/ui/viewer/file_details/upload_icon_widget.dart";
  20. import 'package:photos/utils/dialog_util.dart';
  21. import "package:photos/utils/file_download_util.dart";
  22. import 'package:photos/utils/file_util.dart';
  23. import "package:photos/utils/magic_util.dart";
  24. import 'package:photos/utils/toast_util.dart';
  25. class FileAppBar extends StatefulWidget {
  26. final EnteFile file;
  27. final Function(EnteFile) onFileRemoved;
  28. final double height;
  29. final bool shouldShowActions;
  30. final ValueNotifier<bool> enableFullScreenNotifier;
  31. const FileAppBar(
  32. this.file,
  33. this.onFileRemoved,
  34. this.height,
  35. this.shouldShowActions, {
  36. required this.enableFullScreenNotifier,
  37. Key? key,
  38. }) : super(key: key);
  39. @override
  40. FileAppBarState createState() => FileAppBarState();
  41. }
  42. class FileAppBarState extends State<FileAppBar> {
  43. final _logger = Logger("FadingAppBar");
  44. final List<Widget> _actions = [];
  45. @override
  46. void didUpdateWidget(FileAppBar oldWidget) {
  47. super.didUpdateWidget(oldWidget);
  48. if (oldWidget.file.generatedID != widget.file.generatedID) {
  49. _getActions();
  50. }
  51. }
  52. @override
  53. Widget build(BuildContext context) {
  54. _logger.fine("building app bar ${widget.file.generatedID?.toString()}");
  55. //When the widget is initialized, the actions are not available.
  56. //Cannot call _getActions() in initState.
  57. if (_actions.isEmpty) {
  58. _getActions();
  59. }
  60. final isTrashedFile = widget.file is TrashFile;
  61. final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
  62. return CustomAppBar(
  63. ValueListenableBuilder(
  64. valueListenable: widget.enableFullScreenNotifier,
  65. builder: (context, bool isFullScreen, child) {
  66. return IgnorePointer(
  67. ignoring: isFullScreen,
  68. child: AnimatedOpacity(
  69. opacity: isFullScreen ? 0 : 1,
  70. duration: const Duration(milliseconds: 150),
  71. child: child,
  72. ),
  73. );
  74. },
  75. child: Container(
  76. decoration: BoxDecoration(
  77. gradient: LinearGradient(
  78. begin: Alignment.topCenter,
  79. end: Alignment.bottomCenter,
  80. colors: [
  81. Colors.black.withOpacity(0.72),
  82. Colors.black.withOpacity(0.6),
  83. Colors.transparent,
  84. ],
  85. stops: const [0, 0.2, 1],
  86. ),
  87. ),
  88. child: AppBar(
  89. iconTheme: const IconThemeData(
  90. color: Colors.white,
  91. ), //same for both themes
  92. actions: shouldShowActions ? _actions : [],
  93. elevation: 0,
  94. backgroundColor: const Color(0x00000000),
  95. ),
  96. ),
  97. ),
  98. Size.fromHeight(Platform.isAndroid ? 84 : 96),
  99. );
  100. }
  101. List<Widget> _getActions() {
  102. _actions.clear();
  103. final bool isOwnedByUser = widget.file.isOwner;
  104. final bool isFileUploaded = widget.file.isUploaded;
  105. bool isFileHidden = false;
  106. if (isOwnedByUser && isFileUploaded) {
  107. isFileHidden = CollectionsService.instance
  108. .getCollectionByID(widget.file.collectionID!)
  109. ?.isHidden() ??
  110. false;
  111. }
  112. if (widget.file.isLiveOrMotionPhoto) {
  113. _actions.add(
  114. IconButton(
  115. icon: const Icon(Icons.album_outlined),
  116. onPressed: () {
  117. showShortToast(
  118. context,
  119. S.of(context).pressAndHoldToPlayVideoDetailed,
  120. );
  121. },
  122. ),
  123. );
  124. }
  125. // only show fav option for files owned by the user
  126. if (!isFileHidden && isFileUploaded) {
  127. _actions.add(FavoriteWidget(widget.file));
  128. }
  129. if (!isFileUploaded) {
  130. _actions.add(
  131. UploadIconWidget(
  132. file: widget.file,
  133. key: ValueKey(widget.file.tag),
  134. ),
  135. );
  136. }
  137. final List<PopupMenuItem> items = [];
  138. if (widget.file.isRemoteFile) {
  139. items.add(
  140. PopupMenuItem(
  141. value: 1,
  142. child: Row(
  143. children: [
  144. Icon(
  145. Platform.isAndroid
  146. ? Icons.download
  147. : Icons.cloud_download_outlined,
  148. color: Theme.of(context).iconTheme.color,
  149. ),
  150. const Padding(
  151. padding: EdgeInsets.all(8),
  152. ),
  153. Text(S.of(context).download),
  154. ],
  155. ),
  156. ),
  157. );
  158. }
  159. // options for files owned by the user
  160. if (isOwnedByUser && !isFileHidden && isFileUploaded) {
  161. final bool isArchived =
  162. widget.file.magicMetadata.visibility == archiveVisibility;
  163. items.add(
  164. PopupMenuItem(
  165. value: 2,
  166. child: Row(
  167. children: [
  168. Icon(
  169. isArchived ? Icons.unarchive : Icons.archive_outlined,
  170. color: Theme.of(context).iconTheme.color,
  171. ),
  172. const Padding(
  173. padding: EdgeInsets.all(8),
  174. ),
  175. Text(
  176. isArchived ? S.of(context).unarchive : S.of(context).archive,
  177. ),
  178. ],
  179. ),
  180. ),
  181. );
  182. }
  183. if ((widget.file.fileType == FileType.image ||
  184. widget.file.fileType == FileType.livePhoto) &&
  185. Platform.isAndroid) {
  186. items.add(
  187. PopupMenuItem(
  188. value: 3,
  189. child: Row(
  190. children: [
  191. Icon(
  192. Icons.wallpaper_outlined,
  193. color: Theme.of(context).iconTheme.color,
  194. ),
  195. const Padding(
  196. padding: EdgeInsets.all(8),
  197. ),
  198. Text(S.of(context).setAs),
  199. ],
  200. ),
  201. ),
  202. );
  203. }
  204. if (isOwnedByUser && widget.file.isUploaded) {
  205. if (!isFileHidden) {
  206. items.add(
  207. PopupMenuItem(
  208. value: 4,
  209. child: Row(
  210. children: [
  211. Icon(
  212. Icons.visibility_off,
  213. color: Theme.of(context).iconTheme.color,
  214. ),
  215. const Padding(
  216. padding: EdgeInsets.all(8),
  217. ),
  218. Text(S.of(context).hide),
  219. ],
  220. ),
  221. ),
  222. );
  223. } else {
  224. items.add(
  225. PopupMenuItem(
  226. value: 5,
  227. child: Row(
  228. children: [
  229. Icon(
  230. Icons.visibility,
  231. color: Theme.of(context).iconTheme.color,
  232. ),
  233. const Padding(
  234. padding: EdgeInsets.all(8),
  235. ),
  236. Text(S.of(context).unhide),
  237. ],
  238. ),
  239. ),
  240. );
  241. }
  242. }
  243. if (items.isNotEmpty) {
  244. _actions.add(
  245. PopupMenuButton(
  246. itemBuilder: (context) {
  247. return items;
  248. },
  249. onSelected: (dynamic value) async {
  250. if (value == 1) {
  251. await _download(widget.file);
  252. } else if (value == 2) {
  253. await _toggleFileArchiveStatus(widget.file);
  254. } else if (value == 3) {
  255. await _setAs(widget.file);
  256. } else if (value == 4) {
  257. await _handleHideRequest(context);
  258. } else if (value == 5) {
  259. await _handleUnHideRequest(context);
  260. }
  261. },
  262. ),
  263. );
  264. }
  265. return _actions;
  266. }
  267. Future<void> _handleHideRequest(BuildContext context) async {
  268. try {
  269. final hideResult =
  270. await CollectionsService.instance.hideFiles(context, [widget.file]);
  271. if (hideResult) {
  272. widget.onFileRemoved(widget.file);
  273. }
  274. } catch (e, s) {
  275. _logger.severe("failed to update file visibility", e, s);
  276. await showGenericErrorDialog(context: context, error: e);
  277. }
  278. }
  279. Future<void> _handleUnHideRequest(BuildContext context) async {
  280. final selectedFiles = SelectedFiles();
  281. selectedFiles.files.add(widget.file);
  282. showCollectionActionSheet(
  283. context,
  284. selectedFiles: selectedFiles,
  285. actionType: CollectionActionType.unHide,
  286. );
  287. }
  288. Future<void> _toggleFileArchiveStatus(EnteFile file) async {
  289. final bool isArchived =
  290. widget.file.magicMetadata.visibility == archiveVisibility;
  291. await changeVisibility(
  292. context,
  293. [widget.file],
  294. isArchived ? visibleVisibility : archiveVisibility,
  295. );
  296. if (mounted) {
  297. setState(() {});
  298. }
  299. }
  300. Future<void> _download(EnteFile file) async {
  301. final dialog = createProgressDialog(
  302. context,
  303. context.l10n.downloading,
  304. isDismissible: true,
  305. );
  306. await dialog.show();
  307. try {
  308. await downloadToGallery(file);
  309. showToast(context, S.of(context).fileSavedToGallery);
  310. await dialog.hide();
  311. } catch (e) {
  312. _logger.warning("Failed to save file", e);
  313. await dialog.hide();
  314. await showGenericErrorDialog(context: context, error: e);
  315. }
  316. }
  317. Future<void> _setAs(EnteFile file) async {
  318. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  319. await dialog.show();
  320. try {
  321. final File? fileToSave = await (getFile(file));
  322. if (fileToSave == null) {
  323. throw Exception("Fail to get file for setAs operation");
  324. }
  325. final m = MediaExtension();
  326. final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
  327. if (result == false) {
  328. showShortToast(context, S.of(context).somethingWentWrong);
  329. }
  330. await dialog.hide();
  331. } catch (e) {
  332. await dialog.hide();
  333. _logger.severe("Failed to use as", e);
  334. await showGenericErrorDialog(context: context, error: e);
  335. }
  336. }
  337. }