file_app_bar.dart 10 KB

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