fading_app_bar.dart 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import 'dart:io';
  2. import 'dart:io' as io;
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:like_button/like_button.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photo_manager/photo_manager.dart';
  8. import 'package:photos/core/event_bus.dart';
  9. import 'package:photos/db/files_db.dart';
  10. import 'package:photos/events/local_photos_updated_event.dart';
  11. import 'package:photos/models/file.dart';
  12. import 'package:photos/models/file_type.dart';
  13. import 'package:photos/services/favorites_service.dart';
  14. import 'package:photos/services/local_sync_service.dart';
  15. import 'package:photos/ui/custom_app_bar.dart';
  16. import 'package:photos/utils/date_time_util.dart';
  17. import 'package:photos/utils/delete_file_util.dart';
  18. import 'package:photos/utils/dialog_util.dart';
  19. import 'package:photos/utils/file_util.dart';
  20. import 'package:photos/utils/toast_util.dart';
  21. class FadingAppBar extends StatefulWidget implements PreferredSizeWidget {
  22. final File file;
  23. final Function(File) onFileDeleted;
  24. final double height;
  25. final bool shouldShowActions;
  26. final int userID;
  27. FadingAppBar(
  28. this.file,
  29. this.onFileDeleted,
  30. this.userID,
  31. this.height,
  32. this.shouldShowActions, {
  33. Key key,
  34. }) : super(key: key);
  35. @override
  36. Size get preferredSize => Size.fromHeight(height);
  37. @override
  38. FadingAppBarState createState() => FadingAppBarState();
  39. }
  40. class FadingAppBarState extends State<FadingAppBar> {
  41. final _logger = Logger("FadingAppBar");
  42. bool _shouldHide = false;
  43. @override
  44. Widget build(BuildContext context) {
  45. return CustomAppBar(
  46. AnimatedOpacity(
  47. child: Container(
  48. decoration: BoxDecoration(
  49. gradient: LinearGradient(
  50. begin: Alignment.topCenter,
  51. end: Alignment.bottomCenter,
  52. colors: [
  53. Colors.black.withOpacity(0.64),
  54. Colors.black.withOpacity(0.5),
  55. Colors.transparent,
  56. ],
  57. stops: [0, 0.2, 1],
  58. ),
  59. ),
  60. child: _buildAppBar(),
  61. ),
  62. opacity: _shouldHide ? 0 : 1,
  63. duration: Duration(milliseconds: 150),
  64. ),
  65. height: 100,
  66. );
  67. }
  68. void hide() {
  69. setState(() {
  70. _shouldHide = true;
  71. });
  72. }
  73. void show() {
  74. setState(() {
  75. _shouldHide = false;
  76. });
  77. }
  78. AppBar _buildAppBar() {
  79. final List<Widget> actions = [];
  80. // only show fav option for files owned by the user
  81. if (widget.file.ownerID == null || widget.file.ownerID == widget.userID) {
  82. actions.add(_getFavoriteButton());
  83. }
  84. actions.add(PopupMenuButton(
  85. itemBuilder: (context) {
  86. final List<PopupMenuItem> items = [];
  87. if (widget.file.isRemoteFile()) {
  88. items.add(
  89. PopupMenuItem(
  90. value: 1,
  91. child: Row(
  92. children: [
  93. Icon(Platform.isAndroid
  94. ? Icons.download
  95. : CupertinoIcons.cloud_download),
  96. Padding(
  97. padding: EdgeInsets.all(8),
  98. ),
  99. Text("download"),
  100. ],
  101. ),
  102. ),
  103. );
  104. }
  105. // only show delete option for files owned by the user
  106. if (widget.file.ownerID == null ||
  107. widget.file.ownerID == widget.userID) {
  108. items.add(
  109. PopupMenuItem(
  110. value: 2,
  111. child: Row(
  112. children: [
  113. Icon(Platform.isAndroid
  114. ? Icons.delete_outline
  115. : CupertinoIcons.delete),
  116. Padding(
  117. padding: EdgeInsets.all(8),
  118. ),
  119. Text("delete"),
  120. ],
  121. ),
  122. ),
  123. );
  124. }
  125. return items;
  126. },
  127. onSelected: (value) {
  128. if (value == 1) {
  129. _download(widget.file);
  130. } else if (value == 2) {
  131. _showDeleteSheet(widget.file);
  132. }
  133. },
  134. ));
  135. return AppBar(
  136. title: Text(
  137. getDayTitle(widget.file.creationTime),
  138. style: TextStyle(
  139. fontSize: 14,
  140. ),
  141. ),
  142. actions: widget.shouldShowActions ? actions : [],
  143. backgroundColor: Color(0x00000000),
  144. elevation: 0,
  145. );
  146. }
  147. Widget _getFavoriteButton() {
  148. return FutureBuilder(
  149. future: FavoritesService.instance.isFavorite(widget.file),
  150. builder: (context, snapshot) {
  151. if (snapshot.hasData) {
  152. return _getLikeButton(widget.file, snapshot.data);
  153. } else {
  154. return _getLikeButton(widget.file, false);
  155. }
  156. },
  157. );
  158. }
  159. Widget _getLikeButton(File file, bool isLiked) {
  160. return LikeButton(
  161. isLiked: isLiked,
  162. onTap: (oldValue) async {
  163. final isLiked = !oldValue;
  164. bool hasError = false;
  165. if (isLiked) {
  166. final shouldBlockUser = file.uploadedFileID == null;
  167. var dialog;
  168. if (shouldBlockUser) {
  169. dialog = createProgressDialog(context, "adding to favorites...");
  170. await dialog.show();
  171. }
  172. try {
  173. await FavoritesService.instance.addToFavorites(file);
  174. } catch (e, s) {
  175. _logger.severe(e, s);
  176. hasError = true;
  177. showToast("sorry, could not add this to favorites!");
  178. } finally {
  179. if (shouldBlockUser) {
  180. await dialog.hide();
  181. }
  182. }
  183. } else {
  184. try {
  185. await FavoritesService.instance.removeFromFavorites(file);
  186. } catch (e, s) {
  187. _logger.severe(e, s);
  188. hasError = true;
  189. showToast("sorry, could not remove this from favorites!");
  190. }
  191. }
  192. return hasError ? oldValue : isLiked;
  193. },
  194. likeBuilder: (isLiked) {
  195. return Icon(
  196. Icons.favorite_border,
  197. color: isLiked ? Colors.pinkAccent : Colors.white,
  198. size: 24,
  199. );
  200. },
  201. );
  202. }
  203. void _showDeleteSheet(File file) {
  204. final List<Widget> actions = [];
  205. if (file.uploadedFileID == null) {
  206. actions.add(CupertinoActionSheetAction(
  207. child: Text("everywhere"),
  208. isDestructiveAction: true,
  209. onPressed: () async {
  210. await deleteFilesFromEverywhere(context, [file]);
  211. widget.onFileDeleted(file);
  212. },
  213. ));
  214. } else {
  215. if (file.localID != null) {
  216. actions.add(CupertinoActionSheetAction(
  217. child: Text("on this device"),
  218. isDestructiveAction: true,
  219. onPressed: () async {
  220. await deleteFilesOnDeviceOnly(context, [file]);
  221. showToast("file deleted from device");
  222. Navigator.of(context, rootNavigator: true).pop();
  223. },
  224. ));
  225. }
  226. actions.add(CupertinoActionSheetAction(
  227. child: Text("everywhere"),
  228. isDestructiveAction: true,
  229. onPressed: () async {
  230. await deleteFilesFromEverywhere(context, [file]);
  231. widget.onFileDeleted(file);
  232. },
  233. ));
  234. }
  235. final action = CupertinoActionSheet(
  236. title: Text("delete file?"),
  237. actions: actions,
  238. cancelButton: CupertinoActionSheetAction(
  239. child: Text("cancel"),
  240. onPressed: () {
  241. Navigator.of(context, rootNavigator: true).pop();
  242. },
  243. ),
  244. );
  245. showCupertinoModalPopup(context: context, builder: (_) => action);
  246. }
  247. Future<void> _download(File file) async {
  248. final dialog = createProgressDialog(context, "downloading...");
  249. await dialog.show();
  250. FileType type = file.fileType;
  251. // save and track image for livePhoto/image and video for FileType.video
  252. io.File fileToSave = await getFile(file);
  253. final savedAsset = type == FileType.video
  254. ? (await PhotoManager.editor.saveVideo(fileToSave, title: file.title))
  255. : (await PhotoManager.editor
  256. .saveImageWithPath(fileToSave.path, title: file.title));
  257. // immediately track assetID to avoid duplicate upload
  258. await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
  259. file.localID = savedAsset.id;
  260. await FilesDB.instance.insert(file);
  261. if (type == FileType.livePhoto) {
  262. io.File liveVideo = await getFileFromServer(file, liveVideo: true);
  263. if (liveVideo == null) {
  264. _logger.warning("Failed to find live video" + file.tag());
  265. } else {
  266. final savedAsset = (await PhotoManager.editor.saveVideo(liveVideo));
  267. // in case of livePhoto, file.localID only points the image asset.
  268. // ignore the saved video asset for live photo from future downloads
  269. await LocalSyncService.instance.trackDownloadedFile(savedAsset.id);
  270. }
  271. }
  272. Bus.instance.fire(LocalPhotosUpdatedEvent([file]));
  273. await dialog.hide();
  274. if (file.fileType == FileType.livePhoto) {
  275. showToast("photo and video saved to gallery");
  276. } else {
  277. showToast("file saved to gallery");
  278. }
  279. }
  280. }