fading_app_bar.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. // @dart=2.9
  2. import 'dart:io';
  3. import 'dart:io' as io;
  4. import 'package:flutter/cupertino.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:like_button/like_button.dart';
  7. import 'package:logging/logging.dart';
  8. import 'package:media_extension/media_extension.dart';
  9. import 'package:page_transition/page_transition.dart';
  10. import 'package:path/path.dart' as file_path;
  11. import 'package:photo_manager/photo_manager.dart';
  12. import 'package:photos/core/event_bus.dart';
  13. import 'package:photos/db/files_db.dart';
  14. import 'package:photos/events/local_photos_updated_event.dart';
  15. import 'package:photos/models/file.dart';
  16. import 'package:photos/models/file_type.dart';
  17. import 'package:photos/models/ignored_file.dart';
  18. import 'package:photos/models/selected_files.dart';
  19. import 'package:photos/models/trash_file.dart';
  20. import 'package:photos/services/collections_service.dart';
  21. import 'package:photos/services/favorites_service.dart';
  22. import 'package:photos/services/hidden_service.dart';
  23. import 'package:photos/services/ignored_files_service.dart';
  24. import 'package:photos/services/local_sync_service.dart';
  25. import 'package:photos/ui/common/progress_dialog.dart';
  26. import 'package:photos/ui/create_collection_page.dart';
  27. import 'package:photos/ui/viewer/file/custom_app_bar.dart';
  28. import 'package:photos/utils/delete_file_util.dart';
  29. import 'package:photos/utils/dialog_util.dart';
  30. import 'package:photos/utils/file_util.dart';
  31. import 'package:photos/utils/toast_util.dart';
  32. class FadingAppBar extends StatefulWidget implements PreferredSizeWidget {
  33. final File file;
  34. final Function(File) onFileDeleted;
  35. final double height;
  36. final bool shouldShowActions;
  37. final int userID;
  38. const FadingAppBar(
  39. this.file,
  40. this.onFileDeleted,
  41. this.userID,
  42. this.height,
  43. this.shouldShowActions, {
  44. Key key,
  45. }) : super(key: key);
  46. @override
  47. Size get preferredSize => Size.fromHeight(height);
  48. @override
  49. FadingAppBarState createState() => FadingAppBarState();
  50. }
  51. class FadingAppBarState extends State<FadingAppBar> {
  52. final _logger = Logger("FadingAppBar");
  53. bool _shouldHide = false;
  54. @override
  55. Widget build(BuildContext context) {
  56. return CustomAppBar(
  57. IgnorePointer(
  58. ignoring: _shouldHide,
  59. child: AnimatedOpacity(
  60. opacity: _shouldHide ? 0 : 1,
  61. duration: const Duration(milliseconds: 150),
  62. child: Container(
  63. decoration: BoxDecoration(
  64. gradient: LinearGradient(
  65. begin: Alignment.topCenter,
  66. end: Alignment.bottomCenter,
  67. colors: [
  68. Colors.black.withOpacity(0.72),
  69. Colors.black.withOpacity(0.6),
  70. Colors.transparent,
  71. ],
  72. stops: const [0, 0.2, 1],
  73. ),
  74. ),
  75. child: _buildAppBar(),
  76. ),
  77. ),
  78. ),
  79. height: Platform.isAndroid ? 80 : 96,
  80. );
  81. }
  82. void hide() {
  83. setState(() {
  84. _shouldHide = true;
  85. });
  86. }
  87. void show() {
  88. if (mounted) {
  89. setState(() {
  90. _shouldHide = false;
  91. });
  92. }
  93. }
  94. AppBar _buildAppBar() {
  95. debugPrint("building app bar");
  96. final Set<int> hiddenCollections =
  97. CollectionsService.instance.getHiddenCollections();
  98. final List<Widget> actions = [];
  99. final isTrashedFile = widget.file is TrashFile;
  100. final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
  101. // only show fav option for files owned by the user
  102. if (widget.file.ownerID == null || widget.file.ownerID == widget.userID) {
  103. actions.add(_getFavoriteButton());
  104. }
  105. actions.add(
  106. PopupMenuButton(
  107. itemBuilder: (context) {
  108. final List<PopupMenuItem> items = [];
  109. if (widget.file.isRemoteFile) {
  110. items.add(
  111. PopupMenuItem(
  112. value: 1,
  113. child: Row(
  114. children: [
  115. Icon(
  116. Platform.isAndroid
  117. ? Icons.download
  118. : CupertinoIcons.cloud_download,
  119. color: Theme.of(context).iconTheme.color,
  120. ),
  121. const Padding(
  122. padding: EdgeInsets.all(8),
  123. ),
  124. const Text("Download"),
  125. ],
  126. ),
  127. ),
  128. );
  129. }
  130. // options for files owned by the user
  131. if (widget.file.ownerID == null ||
  132. widget.file.ownerID == widget.userID) {
  133. items.add(
  134. PopupMenuItem(
  135. value: 2,
  136. child: Row(
  137. children: [
  138. Icon(
  139. Platform.isAndroid
  140. ? Icons.delete_outline
  141. : CupertinoIcons.delete,
  142. color: Theme.of(context).iconTheme.color,
  143. ),
  144. const Padding(
  145. padding: EdgeInsets.all(8),
  146. ),
  147. const Text("Delete"),
  148. ],
  149. ),
  150. ),
  151. );
  152. }
  153. if ((widget.file.fileType == FileType.image ||
  154. widget.file.fileType == FileType.livePhoto) &&
  155. Platform.isAndroid) {
  156. items.add(
  157. PopupMenuItem(
  158. value: 3,
  159. child: Row(
  160. children: [
  161. Icon(
  162. Icons.wallpaper_outlined,
  163. color: Theme.of(context).iconTheme.color,
  164. ),
  165. const Padding(
  166. padding: EdgeInsets.all(8),
  167. ),
  168. const Text("Set as"),
  169. ],
  170. ),
  171. ),
  172. );
  173. }
  174. if (widget.file.ownerID != null &&
  175. widget.file.ownerID == widget.userID) {
  176. final bool isAlreadyHidden =
  177. hiddenCollections.contains(widget.file.collectionID);
  178. if (!isAlreadyHidden) {
  179. items.add(
  180. PopupMenuItem(
  181. value: 4,
  182. child: Row(
  183. children: [
  184. Icon(
  185. Icons.visibility_off,
  186. color: Theme.of(context).iconTheme.color,
  187. ),
  188. const Padding(
  189. padding: EdgeInsets.all(8),
  190. ),
  191. const Text("Hide"),
  192. ],
  193. ),
  194. ),
  195. );
  196. } else {
  197. items.add(
  198. PopupMenuItem(
  199. value: 5,
  200. child: Row(
  201. children: [
  202. Icon(
  203. Icons.visibility,
  204. color: Theme.of(context).iconTheme.color,
  205. ),
  206. const Padding(
  207. padding: EdgeInsets.all(8),
  208. ),
  209. const Text("Unhide"),
  210. ],
  211. ),
  212. ),
  213. );
  214. }
  215. }
  216. return items;
  217. },
  218. onSelected: (value) {
  219. if (value == 1) {
  220. _download(widget.file);
  221. } else if (value == 2) {
  222. _showDeleteSheet(widget.file);
  223. } else if (value == 3) {
  224. _setAs(widget.file);
  225. } else if (value == 4) {
  226. _handleHideRequest(context);
  227. } else if (value == 5) {
  228. _handleUnHideRequest(context);
  229. }
  230. },
  231. ),
  232. );
  233. return AppBar(
  234. iconTheme:
  235. const IconThemeData(color: Colors.white), //same for both themes
  236. actions: shouldShowActions ? actions : [],
  237. elevation: 0,
  238. backgroundColor: const Color(0x00000000),
  239. );
  240. }
  241. Future<void> _handleHideRequest(BuildContext context) async {
  242. try {
  243. final hideResult =
  244. await CollectionsService.instance.hideFiles(context, [widget.file]);
  245. if (hideResult) {
  246. // Navigator.of(context, rootNavigator: true).pop();
  247. }
  248. } catch (e, s) {
  249. _logger.severe("failed to update file visibility", e, s);
  250. await showGenericErrorDialog(context);
  251. }
  252. }
  253. Future<void> _handleUnHideRequest(BuildContext context) async {
  254. final s = SelectedFiles();
  255. s.files.add(widget.file);
  256. Navigator.push(
  257. context,
  258. PageTransition(
  259. type: PageTransitionType.bottomToTop,
  260. child: CreateCollectionPage(
  261. s,
  262. null,
  263. actionType: CollectionActionType.unHide,
  264. ),
  265. ),
  266. );
  267. }
  268. Widget _getFavoriteButton() {
  269. return FutureBuilder(
  270. future: FavoritesService.instance.isFavorite(widget.file),
  271. builder: (context, snapshot) {
  272. if (snapshot.hasData) {
  273. return _getLikeButton(widget.file, snapshot.data);
  274. } else {
  275. return _getLikeButton(widget.file, false);
  276. }
  277. },
  278. );
  279. }
  280. Widget _getLikeButton(File file, bool isLiked) {
  281. return LikeButton(
  282. isLiked: isLiked,
  283. onTap: (oldValue) async {
  284. final isLiked = !oldValue;
  285. bool hasError = false;
  286. if (isLiked) {
  287. final shouldBlockUser = file.uploadedFileID == null;
  288. ProgressDialog dialog;
  289. if (shouldBlockUser) {
  290. dialog = createProgressDialog(context, "Adding to favorites...");
  291. await dialog.show();
  292. }
  293. try {
  294. await FavoritesService.instance.addToFavorites(file);
  295. } catch (e, s) {
  296. _logger.severe(e, s);
  297. hasError = true;
  298. showToast(context, "Sorry, could not add this to favorites!");
  299. } finally {
  300. if (shouldBlockUser) {
  301. await dialog.hide();
  302. }
  303. }
  304. } else {
  305. try {
  306. await FavoritesService.instance.removeFromFavorites(file);
  307. } catch (e, s) {
  308. _logger.severe(e, s);
  309. hasError = true;
  310. showToast(context, "Sorry, could not remove this from favorites!");
  311. }
  312. }
  313. return hasError ? oldValue : isLiked;
  314. },
  315. likeBuilder: (isLiked) {
  316. return Icon(
  317. isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
  318. color:
  319. isLiked ? Colors.pinkAccent : Colors.white, //same for both themes
  320. size: 24,
  321. );
  322. },
  323. );
  324. }
  325. void _showDeleteSheet(File file) {
  326. final List<Widget> actions = [];
  327. if (file.uploadedFileID == null || file.localID == null) {
  328. actions.add(
  329. CupertinoActionSheetAction(
  330. isDestructiveAction: true,
  331. onPressed: () async {
  332. await deleteFilesFromEverywhere(context, [file]);
  333. Navigator.of(context, rootNavigator: true).pop();
  334. widget.onFileDeleted(file);
  335. },
  336. child: const Text("Everywhere"),
  337. ),
  338. );
  339. } else {
  340. // uploaded file which is present locally too
  341. actions.add(
  342. CupertinoActionSheetAction(
  343. isDestructiveAction: true,
  344. onPressed: () async {
  345. await deleteFilesOnDeviceOnly(context, [file]);
  346. showToast(context, "File deleted from device");
  347. Navigator.of(context, rootNavigator: true).pop();
  348. // TODO: Fix behavior when inside a device folder
  349. },
  350. child: const Text("Device"),
  351. ),
  352. );
  353. actions.add(
  354. CupertinoActionSheetAction(
  355. isDestructiveAction: true,
  356. onPressed: () async {
  357. await deleteFilesFromRemoteOnly(context, [file]);
  358. showShortToast(context, "Moved to trash");
  359. Navigator.of(context, rootNavigator: true).pop();
  360. // TODO: Fix behavior when inside a collection
  361. },
  362. child: const Text("ente"),
  363. ),
  364. );
  365. actions.add(
  366. CupertinoActionSheetAction(
  367. isDestructiveAction: true,
  368. onPressed: () async {
  369. await deleteFilesFromEverywhere(context, [file]);
  370. Navigator.of(context, rootNavigator: true).pop();
  371. widget.onFileDeleted(file);
  372. },
  373. child: const Text("Everywhere"),
  374. ),
  375. );
  376. }
  377. final action = CupertinoActionSheet(
  378. title: const Text("Delete file?"),
  379. actions: actions,
  380. cancelButton: CupertinoActionSheetAction(
  381. child: const Text("Cancel"),
  382. onPressed: () {
  383. Navigator.of(context, rootNavigator: true).pop();
  384. },
  385. ),
  386. );
  387. showCupertinoModalPopup(context: context, builder: (_) => action);
  388. }
  389. Future<void> _download(File file) async {
  390. final dialog = createProgressDialog(context, "Downloading...");
  391. await dialog.show();
  392. final FileType type = file.fileType;
  393. // save and track image for livePhoto/image and video for FileType.video
  394. final io.File fileToSave = await getFile(file);
  395. final savedAsset = type == FileType.video
  396. ? (await PhotoManager.editor.saveVideo(fileToSave, title: file.title))
  397. : (await PhotoManager.editor
  398. .saveImageWithPath(fileToSave.path, title: file.title));
  399. // immediately track assetID to avoid duplicate upload
  400. await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
  401. file.localID = savedAsset.id;
  402. await FilesDB.instance.insert(file);
  403. if (type == FileType.livePhoto) {
  404. final io.File liveVideo = await getFileFromServer(file, liveVideo: true);
  405. if (liveVideo == null) {
  406. _logger.warning("Failed to find live video" + file.tag);
  407. } else {
  408. final videoTitle = file_path.basenameWithoutExtension(file.title) +
  409. file_path.extension(liveVideo.path);
  410. final savedAsset = (await PhotoManager.editor.saveVideo(
  411. liveVideo,
  412. title: videoTitle,
  413. ));
  414. final ignoreVideoFile = IgnoredFile(
  415. savedAsset.id,
  416. savedAsset.title ?? videoTitle,
  417. savedAsset.relativePath ?? 'remoteDownload',
  418. "remoteDownload",
  419. );
  420. debugPrint("IgnoreFile for auto-upload ${ignoreVideoFile.toString()}");
  421. await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
  422. }
  423. }
  424. Bus.instance.fire(LocalPhotosUpdatedEvent([file]));
  425. await dialog.hide();
  426. if (file.fileType == FileType.livePhoto) {
  427. showToast(context, "Photo and video saved to gallery");
  428. } else {
  429. showToast(context, "File saved to gallery");
  430. }
  431. }
  432. Future<void> _setAs(File file) async {
  433. final dialog = createProgressDialog(context, "Please wait...");
  434. await dialog.show();
  435. try {
  436. final io.File fileToSave = await getFile(file);
  437. var m = MediaExtension();
  438. final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
  439. if (result == false) {
  440. showShortToast(context, "Something went wrong");
  441. }
  442. dialog.hide();
  443. } catch (e) {
  444. dialog.hide();
  445. _logger.severe("Failed to use as", e);
  446. showGenericErrorDialog(context);
  447. }
  448. }
  449. }