import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:media_extension/media_extension.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/file/trash_file.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_service.dart'; import 'package:photos/ui/collections/collection_action_sheet.dart'; import 'package:photos/ui/viewer/file/custom_app_bar.dart'; import "package:photos/ui/viewer/file_details/favorite_widget.dart"; import "package:photos/ui/viewer/file_details/upload_icon_widget.dart"; import 'package:photos/utils/dialog_util.dart'; import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/file_util.dart'; import "package:photos/utils/magic_util.dart"; import 'package:photos/utils/toast_util.dart'; class FileAppBar extends StatefulWidget { final EnteFile file; final Function(EnteFile) onFileRemoved; final double height; final bool shouldShowActions; final ValueNotifier enableFullScreenNotifier; const FileAppBar( this.file, this.onFileRemoved, this.height, this.shouldShowActions, { required this.enableFullScreenNotifier, Key? key, }) : super(key: key); @override FileAppBarState createState() => FileAppBarState(); } class FileAppBarState extends State { final _logger = Logger("FadingAppBar"); final List _actions = []; @override void didUpdateWidget(FileAppBar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.file.generatedID != widget.file.generatedID) { _getActions(); } } @override Widget build(BuildContext context) { _logger.fine("building app bar ${widget.file.generatedID?.toString()}"); //When the widget is initialized, the actions are not available. //Cannot call _getActions() in initState. if (_actions.isEmpty) { _getActions(); } final isTrashedFile = widget.file is TrashFile; final shouldShowActions = widget.shouldShowActions && !isTrashedFile; return CustomAppBar( ValueListenableBuilder( valueListenable: widget.enableFullScreenNotifier, builder: (context, bool isFullScreen, child) { return IgnorePointer( ignoring: isFullScreen, child: AnimatedOpacity( opacity: isFullScreen ? 0 : 1, duration: const Duration(milliseconds: 150), child: child, ), ); }, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.72), Colors.black.withOpacity(0.6), Colors.transparent, ], stops: const [0, 0.2, 1], ), ), child: AppBar( iconTheme: const IconThemeData( color: Colors.white, ), //same for both themes actions: shouldShowActions ? _actions : [], elevation: 0, backgroundColor: const Color(0x00000000), ), ), ), Size.fromHeight(Platform.isAndroid ? 84 : 96), ); } List _getActions() { _actions.clear(); final bool isOwnedByUser = widget.file.isOwner; final bool isFileUploaded = widget.file.isUploaded; bool isFileHidden = false; if (isOwnedByUser && isFileUploaded) { isFileHidden = CollectionsService.instance .getCollectionByID(widget.file.collectionID!) ?.isHidden() ?? false; } if (widget.file.isLiveOrMotionPhoto) { _actions.add( IconButton( icon: const Icon(Icons.album_outlined), onPressed: () { showShortToast( context, S.of(context).pressAndHoldToPlayVideoDetailed, ); }, ), ); } // only show fav option for files owned by the user if ((isOwnedByUser || flagService.internalUser) && !isFileHidden && isFileUploaded) { _actions.add(FavoriteWidget(widget.file)); } if (!isFileUploaded) { _actions.add( UploadIconWidget( file: widget.file, key: ValueKey(widget.file.tag), ), ); } final List items = []; if (widget.file.isRemoteFile) { items.add( PopupMenuItem( value: 1, child: Row( children: [ Icon( Platform.isAndroid ? Icons.download : CupertinoIcons.cloud_download, color: Theme.of(context).iconTheme.color, ), const Padding( padding: EdgeInsets.all(8), ), Text(S.of(context).download), ], ), ), ); } // options for files owned by the user if (isOwnedByUser && !isFileHidden && isFileUploaded) { final bool isArchived = widget.file.magicMetadata.visibility == archiveVisibility; items.add( PopupMenuItem( value: 2, child: Row( children: [ Icon( isArchived ? Icons.unarchive : Icons.archive_outlined, color: Theme.of(context).iconTheme.color, ), const Padding( padding: EdgeInsets.all(8), ), Text( isArchived ? S.of(context).unarchive : S.of(context).archive, ), ], ), ), ); } if ((widget.file.fileType == FileType.image || widget.file.fileType == FileType.livePhoto) && Platform.isAndroid) { items.add( PopupMenuItem( value: 3, child: Row( children: [ Icon( Icons.wallpaper_outlined, color: Theme.of(context).iconTheme.color, ), const Padding( padding: EdgeInsets.all(8), ), Text(S.of(context).setAs), ], ), ), ); } if (isOwnedByUser && widget.file.isUploaded) { if (!isFileHidden) { items.add( PopupMenuItem( value: 4, child: Row( children: [ Icon( Icons.visibility_off, color: Theme.of(context).iconTheme.color, ), const Padding( padding: EdgeInsets.all(8), ), Text(S.of(context).hide), ], ), ), ); } else { items.add( PopupMenuItem( value: 5, child: Row( children: [ Icon( Icons.visibility, color: Theme.of(context).iconTheme.color, ), const Padding( padding: EdgeInsets.all(8), ), Text(S.of(context).unhide), ], ), ), ); } } if (items.isNotEmpty) { _actions.add( PopupMenuButton( itemBuilder: (context) { return items; }, onSelected: (dynamic value) async { if (value == 1) { await _download(widget.file); } else if (value == 2) { await _toggleFileArchiveStatus(widget.file); } else if (value == 3) { await _setAs(widget.file); } else if (value == 4) { await _handleHideRequest(context); } else if (value == 5) { await _handleUnHideRequest(context); } }, ), ); } return _actions; } Future _handleHideRequest(BuildContext context) async { try { final hideResult = await CollectionsService.instance.hideFiles(context, [widget.file]); if (hideResult) { widget.onFileRemoved(widget.file); } } catch (e, s) { _logger.severe("failed to update file visibility", e, s); await showGenericErrorDialog(context: context, error: e); } } Future _handleUnHideRequest(BuildContext context) async { final selectedFiles = SelectedFiles(); selectedFiles.files.add(widget.file); showCollectionActionSheet( context, selectedFiles: selectedFiles, actionType: CollectionActionType.unHide, ); } Future _toggleFileArchiveStatus(EnteFile file) async { final bool isArchived = widget.file.magicMetadata.visibility == archiveVisibility; await changeVisibility( context, [widget.file], isArchived ? visibleVisibility : archiveVisibility, ); if (mounted) { setState(() {}); } } Future _download(EnteFile file) async { final dialog = createProgressDialog( context, context.l10n.downloading, isDismissible: true, ); await dialog.show(); try { await downloadToGallery(file); showToast(context, S.of(context).fileSavedToGallery); await dialog.hide(); } catch (e) { _logger.warning("Failed to save file", e); await dialog.hide(); await showGenericErrorDialog(context: context, error: e); } } Future _setAs(EnteFile file) async { final dialog = createProgressDialog(context, S.of(context).pleaseWait); await dialog.show(); try { final File? fileToSave = await (getFile(file)); if (fileToSave == null) { throw Exception("Fail to get file for setAs operation"); } final m = MediaExtension(); final bool result = await m.setAs("file://${fileToSave.path}", "image/*"); if (result == false) { showShortToast(context, S.of(context).somethingWentWrong); } await dialog.hide(); } catch (e) { await dialog.hide(); _logger.severe("Failed to use as", e); await showGenericErrorDialog(context: context, error: e); } } }