fading_app_bar.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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:logging/logging.dart';
  6. import 'package:media_extension/media_extension.dart';
  7. import 'package:path/path.dart' as file_path;
  8. import 'package:photo_manager/photo_manager.dart';
  9. import 'package:photos/core/event_bus.dart';
  10. import 'package:photos/db/files_db.dart';
  11. import 'package:photos/events/local_photos_updated_event.dart';
  12. import "package:photos/generated/l10n.dart";
  13. import 'package:photos/models/file.dart';
  14. import 'package:photos/models/file_type.dart';
  15. import 'package:photos/models/ignored_file.dart';
  16. import "package:photos/models/metadata/common_keys.dart";
  17. import 'package:photos/models/selected_files.dart';
  18. import 'package:photos/models/trash_file.dart';
  19. import 'package:photos/services/collections_service.dart';
  20. import 'package:photos/services/hidden_service.dart';
  21. import 'package:photos/services/ignored_files_service.dart';
  22. import 'package:photos/services/local_sync_service.dart';
  23. import 'package:photos/ui/collections/collection_action_sheet.dart';
  24. import 'package:photos/ui/viewer/file/custom_app_bar.dart';
  25. import "package:photos/ui/viewer/file_details/favorite_widget.dart";
  26. import "package:photos/ui/viewer/file_details/upload_icon_widget.dart";
  27. import 'package:photos/utils/dialog_util.dart';
  28. import 'package:photos/utils/file_util.dart';
  29. import "package:photos/utils/magic_util.dart";
  30. import 'package:photos/utils/toast_util.dart';
  31. class FadingAppBar extends StatefulWidget implements PreferredSizeWidget {
  32. final File file;
  33. final Function(File) onFileRemoved;
  34. final double height;
  35. final bool shouldShowActions;
  36. final int? userID;
  37. final ValueNotifier<bool> enableFullScreenNotifier;
  38. const FadingAppBar(
  39. this.file,
  40. this.onFileRemoved,
  41. this.userID,
  42. this.height,
  43. this.shouldShowActions, {
  44. required this.enableFullScreenNotifier,
  45. Key? key,
  46. }) : super(key: key);
  47. @override
  48. Size get preferredSize => Size.fromHeight(height);
  49. @override
  50. FadingAppBarState createState() => FadingAppBarState();
  51. }
  52. class FadingAppBarState extends State<FadingAppBar> {
  53. final _logger = Logger("FadingAppBar");
  54. @override
  55. Widget build(BuildContext context) {
  56. return CustomAppBar(
  57. ValueListenableBuilder(
  58. valueListenable: widget.enableFullScreenNotifier,
  59. builder: (context, bool isFullScreen, _) {
  60. return IgnorePointer(
  61. ignoring: isFullScreen,
  62. child: AnimatedOpacity(
  63. opacity: isFullScreen ? 0 : 1,
  64. duration: const Duration(milliseconds: 150),
  65. child: Container(
  66. decoration: BoxDecoration(
  67. gradient: LinearGradient(
  68. begin: Alignment.topCenter,
  69. end: Alignment.bottomCenter,
  70. colors: [
  71. Colors.black.withOpacity(0.72),
  72. Colors.black.withOpacity(0.6),
  73. Colors.transparent,
  74. ],
  75. stops: const [0, 0.2, 1],
  76. ),
  77. ),
  78. child: _buildAppBar(),
  79. ),
  80. ),
  81. );
  82. },
  83. ),
  84. Size.fromHeight(Platform.isAndroid ? 80 : 96),
  85. );
  86. }
  87. AppBar _buildAppBar() {
  88. debugPrint("building app bar");
  89. final List<Widget> actions = [];
  90. final isTrashedFile = widget.file is TrashFile;
  91. final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
  92. final bool isOwnedByUser =
  93. widget.file.ownerID == null || widget.file.ownerID == widget.userID;
  94. final bool isFileUploaded = widget.file.isUploaded;
  95. bool isFileHidden = false;
  96. if (isOwnedByUser && isFileUploaded) {
  97. isFileHidden = CollectionsService.instance
  98. .getCollectionByID(widget.file.collectionID!)
  99. ?.isHidden() ??
  100. false;
  101. }
  102. // only show fav option for files owned by the user
  103. if (isOwnedByUser && !isFileHidden && isFileUploaded) {
  104. actions.add(FavoriteWidget(widget.file));
  105. }
  106. if (!isFileUploaded) {
  107. actions.add(
  108. UploadIconWidget(
  109. file: widget.file,
  110. key: ValueKey(widget.file.tag),
  111. ),
  112. );
  113. }
  114. actions.add(
  115. PopupMenuButton(
  116. itemBuilder: (context) {
  117. final List<PopupMenuItem> items = [];
  118. if (widget.file.isRemoteFile) {
  119. items.add(
  120. PopupMenuItem(
  121. value: 1,
  122. child: Row(
  123. children: [
  124. Icon(
  125. Platform.isAndroid
  126. ? Icons.download
  127. : CupertinoIcons.cloud_download,
  128. color: Theme.of(context).iconTheme.color,
  129. ),
  130. const Padding(
  131. padding: EdgeInsets.all(8),
  132. ),
  133. Text(S.of(context).download),
  134. ],
  135. ),
  136. ),
  137. );
  138. }
  139. // options for files owned by the user
  140. if (isOwnedByUser && !isFileHidden) {
  141. final bool isArchived =
  142. widget.file.magicMetadata.visibility == archiveVisibility;
  143. items.add(
  144. PopupMenuItem(
  145. value: 2,
  146. child: Row(
  147. children: [
  148. Icon(
  149. isArchived ? Icons.unarchive : Icons.archive_outlined,
  150. color: Theme.of(context).iconTheme.color,
  151. ),
  152. const Padding(
  153. padding: EdgeInsets.all(8),
  154. ),
  155. Text(
  156. isArchived
  157. ? S.of(context).unarchive
  158. : S.of(context).archive,
  159. ),
  160. ],
  161. ),
  162. ),
  163. );
  164. }
  165. if ((widget.file.fileType == FileType.image ||
  166. widget.file.fileType == FileType.livePhoto) &&
  167. Platform.isAndroid) {
  168. items.add(
  169. PopupMenuItem(
  170. value: 3,
  171. child: Row(
  172. children: [
  173. Icon(
  174. Icons.wallpaper_outlined,
  175. color: Theme.of(context).iconTheme.color,
  176. ),
  177. const Padding(
  178. padding: EdgeInsets.all(8),
  179. ),
  180. Text(S.of(context).setAs),
  181. ],
  182. ),
  183. ),
  184. );
  185. }
  186. if (isOwnedByUser && widget.file.isUploaded) {
  187. if (!isFileHidden) {
  188. items.add(
  189. PopupMenuItem(
  190. value: 4,
  191. child: Row(
  192. children: [
  193. Icon(
  194. Icons.visibility_off,
  195. color: Theme.of(context).iconTheme.color,
  196. ),
  197. const Padding(
  198. padding: EdgeInsets.all(8),
  199. ),
  200. Text(S.of(context).hide),
  201. ],
  202. ),
  203. ),
  204. );
  205. } else {
  206. items.add(
  207. PopupMenuItem(
  208. value: 5,
  209. child: Row(
  210. children: [
  211. Icon(
  212. Icons.visibility,
  213. color: Theme.of(context).iconTheme.color,
  214. ),
  215. const Padding(
  216. padding: EdgeInsets.all(8),
  217. ),
  218. Text(S.of(context).unhide),
  219. ],
  220. ),
  221. ),
  222. );
  223. }
  224. }
  225. return items;
  226. },
  227. onSelected: (dynamic value) async {
  228. if (value == 1) {
  229. _download(widget.file);
  230. } else if (value == 2) {
  231. await _toggleFileArchiveStatus(widget.file);
  232. } else if (value == 3) {
  233. _setAs(widget.file);
  234. } else if (value == 4) {
  235. _handleHideRequest(context);
  236. } else if (value == 5) {
  237. _handleUnHideRequest(context);
  238. }
  239. },
  240. ),
  241. );
  242. return AppBar(
  243. iconTheme:
  244. const IconThemeData(color: Colors.white), //same for both themes
  245. actions: shouldShowActions ? actions : [],
  246. elevation: 0,
  247. backgroundColor: const Color(0x00000000),
  248. );
  249. }
  250. Future<void> _handleHideRequest(BuildContext context) async {
  251. try {
  252. final hideResult =
  253. await CollectionsService.instance.hideFiles(context, [widget.file]);
  254. if (hideResult) {
  255. widget.onFileRemoved(widget.file);
  256. }
  257. } catch (e, s) {
  258. _logger.severe("failed to update file visibility", e, s);
  259. await showGenericErrorDialog(context: context);
  260. }
  261. }
  262. Future<void> _handleUnHideRequest(BuildContext context) async {
  263. final selectedFiles = SelectedFiles();
  264. selectedFiles.files.add(widget.file);
  265. showCollectionActionSheet(
  266. context,
  267. selectedFiles: selectedFiles,
  268. actionType: CollectionActionType.unHide,
  269. );
  270. }
  271. Future<void> _toggleFileArchiveStatus(File file) async {
  272. final bool isArchived =
  273. widget.file.magicMetadata.visibility == archiveVisibility;
  274. await changeVisibility(
  275. context,
  276. [widget.file],
  277. isArchived ? visibleVisibility : archiveVisibility,
  278. );
  279. if (mounted) {
  280. setState(() {});
  281. }
  282. }
  283. Future<void> _download(File file) async {
  284. final dialog = createProgressDialog(context, "Downloading...");
  285. await dialog.show();
  286. try {
  287. final FileType type = file.fileType;
  288. final bool downloadLivePhotoOnDroid =
  289. type == FileType.livePhoto && Platform.isAndroid;
  290. AssetEntity? savedAsset;
  291. final io.File? fileToSave = await getFile(file);
  292. //Disabling notifications for assets changing to insert the file into
  293. //files db before triggering a sync.
  294. PhotoManager.stopChangeNotify();
  295. if (type == FileType.image) {
  296. savedAsset = await PhotoManager.editor
  297. .saveImageWithPath(fileToSave!.path, title: file.title!);
  298. } else if (type == FileType.video) {
  299. savedAsset = await PhotoManager.editor
  300. .saveVideo(fileToSave!, title: file.title!);
  301. } else if (type == FileType.livePhoto) {
  302. final io.File? liveVideoFile =
  303. await getFileFromServer(file, liveVideo: true);
  304. if (liveVideoFile == null) {
  305. throw AssertionError("Live video can not be null");
  306. }
  307. if (downloadLivePhotoOnDroid) {
  308. await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file);
  309. } else {
  310. savedAsset = await PhotoManager.editor.darwin.saveLivePhoto(
  311. imageFile: fileToSave!,
  312. videoFile: liveVideoFile,
  313. title: file.title!,
  314. );
  315. }
  316. }
  317. if (savedAsset != null) {
  318. file.localID = savedAsset.id;
  319. await FilesDB.instance.insert(file);
  320. Bus.instance.fire(
  321. LocalPhotosUpdatedEvent(
  322. [file],
  323. source: "download",
  324. ),
  325. );
  326. } else if (!downloadLivePhotoOnDroid && savedAsset == null) {
  327. _logger.severe('Failed to save assert of type $type');
  328. }
  329. showToast(context, S.of(context).fileSavedToGallery);
  330. await dialog.hide();
  331. } catch (e) {
  332. _logger.warning("Failed to save file", e);
  333. await dialog.hide();
  334. showGenericErrorDialog(context: context);
  335. } finally {
  336. PhotoManager.startChangeNotify();
  337. LocalSyncService.instance.checkAndSync().ignore();
  338. }
  339. }
  340. Future<void> _saveLivePhotoOnDroid(
  341. io.File image,
  342. io.File video,
  343. File enteFile,
  344. ) async {
  345. debugPrint("Downloading LivePhoto on Droid");
  346. AssetEntity? savedAsset = await (PhotoManager.editor
  347. .saveImageWithPath(image.path, title: enteFile.title!));
  348. if (savedAsset == null) {
  349. throw Exception("Failed to save image of live photo");
  350. }
  351. IgnoredFile ignoreVideoFile = IgnoredFile(
  352. savedAsset.id,
  353. savedAsset.title ?? '',
  354. savedAsset.relativePath ?? 'remoteDownload',
  355. "remoteDownload",
  356. );
  357. await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
  358. final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) +
  359. file_path.extension(video.path);
  360. savedAsset = (await (PhotoManager.editor.saveVideo(
  361. video,
  362. title: videoTitle,
  363. )));
  364. if (savedAsset == null) {
  365. throw Exception("Failed to save video of live photo");
  366. }
  367. ignoreVideoFile = IgnoredFile(
  368. savedAsset.id,
  369. savedAsset.title ?? videoTitle,
  370. savedAsset.relativePath ?? 'remoteDownload',
  371. "remoteDownload",
  372. );
  373. await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]);
  374. }
  375. Future<void> _setAs(File file) async {
  376. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  377. await dialog.show();
  378. try {
  379. final io.File? fileToSave = await (getFile(file));
  380. if (fileToSave == null) {
  381. throw Exception("Fail to get file for setAs operation");
  382. }
  383. final m = MediaExtension();
  384. final bool result = await m.setAs("file://${fileToSave.path}", "image/*");
  385. if (result == false) {
  386. showShortToast(context, S.of(context).somethingWentWrong);
  387. }
  388. dialog.hide();
  389. } catch (e) {
  390. dialog.hide();
  391. _logger.severe("Failed to use as", e);
  392. showGenericErrorDialog(context: context);
  393. }
  394. }
  395. }