diff --git a/lib/models/file.dart b/lib/models/file.dart index ee2a9d321..b48082d20 100644 --- a/lib/models/file.dart +++ b/lib/models/file.dart @@ -211,6 +211,10 @@ class File extends EnteFile { } } + String? get caption { + return pubMagicMetadata?.caption; + } + String get thumbnailUrl { final endpoint = Configuration.instance.getHttpEndpoint(); if (endpoint != kDefaultProductionEndpoint || diff --git a/lib/models/magic_metadata.dart b/lib/models/magic_metadata.dart index 137223d76..c55dff52f 100644 --- a/lib/models/magic_metadata.dart +++ b/lib/models/magic_metadata.dart @@ -14,6 +14,7 @@ const subTypeKey = 'subType'; const pubMagicKeyEditedTime = 'editedTime'; const pubMagicKeyEditedName = 'editedName'; +const pubMagicKeyCaption = "caption"; class MagicMetadata { // 0 -> visible @@ -39,8 +40,9 @@ class MagicMetadata { class PubMagicMetadata { int? editedTime; String? editedName; + String? caption; - PubMagicMetadata({this.editedTime, this.editedName}); + PubMagicMetadata({this.editedTime, this.editedName, this.caption}); factory PubMagicMetadata.fromEncodedJson(String encodedJson) => PubMagicMetadata.fromJson(jsonDecode(encodedJson)); @@ -53,6 +55,7 @@ class PubMagicMetadata { return PubMagicMetadata( editedTime: map[pubMagicKeyEditedTime], editedName: map[pubMagicKeyEditedName], + caption: map[pubMagicKeyCaption], ); } } diff --git a/lib/models/search/search_result.dart b/lib/models/search/search_result.dart index c03473957..6226b63a2 100644 --- a/lib/models/search/search_result.dart +++ b/lib/models/search/search_result.dart @@ -22,5 +22,6 @@ enum ResultType { year, fileType, fileExtension, + fileCaption, event } diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 7ca727aa5..e5b737de4 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -209,6 +209,30 @@ class SearchService { return searchResults; } + Future> getCaptionResults( + String query, + ) async { + final List searchResults = []; + if (query.isEmpty) { + return searchResults; + } + final RegExp pattern = RegExp(query, caseSensitive: false); + final List allFiles = await _getAllFiles(); + final matchedFiles = allFiles + .where((e) => e.caption != null && pattern.hasMatch(e.caption)) + .toList(); + if (matchedFiles.isNotEmpty) { + searchResults.add( + GenericSearchResult( + ResultType.fileCaption, + query, + matchedFiles, + ), + ); + } + return searchResults; + } + Future> getFileExtensionResults( String query, ) async { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index b163d39b9..976d34d88 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -11,6 +11,7 @@ class EnteColorScheme { // Backdrop Colors final Color backdropBase; final Color backdropBaseMute; + final Color backdropFaint; // Text Colors final Color textBase; @@ -53,6 +54,7 @@ class EnteColorScheme { this.backgroundElevated2, this.backdropBase, this.backdropBaseMute, + this.backdropFaint, this.textBase, this.textMuted, this.textFaint, @@ -84,7 +86,8 @@ const EnteColorScheme lightScheme = EnteColorScheme( backgroundElevatedLight, backgroundElevated2Light, backdropBaseLight, - backdropBaseMuteLight, + backdropMutedLight, + backdropFaintLight, textBaseLight, textMutedLight, textFaintLight, @@ -107,7 +110,8 @@ const EnteColorScheme darkScheme = EnteColorScheme( backgroundElevatedDark, backgroundElevated2Dark, backdropBaseDark, - backdropBaseMuteDark, + backdropMutedDark, + backdropFaintDark, textBaseDark, textMutedDark, textFaintDark, @@ -136,10 +140,12 @@ const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1); // Backdrop Colors const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.75); -const Color backdropBaseMuteLight = Color.fromRGBO(255, 255, 255, 0.30); +const Color backdropMutedLight = Color.fromRGBO(255, 255, 255, 0.30); +const Color backdropFaintLight = Color.fromRGBO(255, 255, 255, 0.15); const Color backdropBaseDark = Color.fromRGBO(0, 0, 0, 0.65); -const Color backdropBaseMuteDark = Color.fromRGBO(0, 0, 0, 0.20); +const Color backdropMutedDark = Color.fromRGBO(0, 0, 0, 0.20); +const Color backdropFaintDark = Color.fromRGBO(0, 0, 0, 0.08); // Text Colors const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1); diff --git a/lib/ui/backup_settings_screen.dart b/lib/ui/backup_settings_screen.dart index 9f87464b9..520a58455 100644 --- a/lib/ui/backup_settings_screen.dart +++ b/lib/ui/backup_settings_screen.dart @@ -29,7 +29,7 @@ class BackupSettingsScreen extends StatelessWidget { actionIcons: [ IconButtonWidget( icon: Icons.close_outlined, - isSecondary: true, + iconButtonType: IconButtonType.secondary, onTap: () { Navigator.pop(context); Navigator.pop(context); diff --git a/lib/ui/components/home_header_widget.dart b/lib/ui/components/home_header_widget.dart index 6f10cc2d7..b43c3c758 100644 --- a/lib/ui/components/home_header_widget.dart +++ b/lib/ui/components/home_header_widget.dart @@ -20,7 +20,7 @@ class _HomeHeaderWidgetState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButtonWidget( - isPrimary: true, + iconButtonType: IconButtonType.primary, icon: Icons.menu_outlined, onTap: () { Scaffold.of(context).openDrawer(); diff --git a/lib/ui/components/icon_button_widget.dart b/lib/ui/components/icon_button_widget.dart index ae3b682aa..2bee00ca1 100644 --- a/lib/ui/components/icon_button_widget.dart +++ b/lib/ui/components/icon_button_widget.dart @@ -2,10 +2,14 @@ import 'package:flutter/material.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; +enum IconButtonType { + primary, + secondary, + rounded, +} + class IconButtonWidget extends StatefulWidget { - final bool isPrimary; - final bool isSecondary; - final bool isRounded; + final IconButtonType iconButtonType; final IconData icon; final bool disableGestureDetector; final VoidCallback? onTap; @@ -14,9 +18,7 @@ class IconButtonWidget extends StatefulWidget { final Color? iconColor; const IconButtonWidget({ required this.icon, - this.isPrimary = false, - this.isSecondary = false, - this.isRounded = false, + required this.iconButtonType, this.disableGestureDetector = false, this.onTap, this.defaultColor, @@ -41,13 +43,12 @@ class _IconButtonWidgetState extends State { @override Widget build(BuildContext context) { - if (!widget.isPrimary && !widget.isRounded && !widget.isSecondary) { - return const SizedBox.shrink(); - } final colorTheme = getEnteColorScheme(context); iconStateColor ?? (iconStateColor = widget.defaultColor ?? - (widget.isRounded ? colorTheme.fillFaint : null)); + (widget.iconButtonType == IconButtonType.rounded + ? colorTheme.fillFaint + : null)); return widget.disableGestureDetector ? _iconButton(colorTheme) : GestureDetector( @@ -72,7 +73,7 @@ class _IconButtonWidgetState extends State { child: Icon( widget.icon, color: widget.iconColor ?? - (widget.isSecondary + (widget.iconButtonType == IconButtonType.secondary ? colorTheme.strokeMuted : colorTheme.strokeBase), size: 24, @@ -85,7 +86,9 @@ class _IconButtonWidgetState extends State { final colorTheme = getEnteColorScheme(context); setState(() { iconStateColor = widget.pressedColor ?? - (widget.isRounded ? colorTheme.fillMuted : colorTheme.fillFaint); + (widget.iconButtonType == IconButtonType.rounded + ? colorTheme.fillMuted + : colorTheme.fillFaint); }); } diff --git a/lib/ui/components/notification_warning_widget.dart b/lib/ui/components/notification_warning_widget.dart index 4572fa4e2..7f39468bb 100644 --- a/lib/ui/components/notification_warning_widget.dart +++ b/lib/ui/components/notification_warning_widget.dart @@ -54,7 +54,7 @@ class NotificationWarningWidget extends StatelessWidget { const SizedBox(width: 12), IconButtonWidget( icon: actionIcon, - isRounded: true, + iconButtonType: IconButtonType.rounded, iconColor: strokeBaseDark, defaultColor: fillFaintDark, pressedColor: fillMutedDark, diff --git a/lib/ui/components/title_bar_widget.dart b/lib/ui/components/title_bar_widget.dart index 7136c5a93..8cc009ec1 100644 --- a/lib/ui/components/title_bar_widget.dart +++ b/lib/ui/components/title_bar_widget.dart @@ -3,6 +3,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/icon_button_widget.dart'; class TitleBarWidget extends StatelessWidget { + final IconButtonWidget? leading; final String? title; final String? caption; final Widget? flexibleSpaceTitle; @@ -10,7 +11,9 @@ class TitleBarWidget extends StatelessWidget { final List? actionIcons; final bool isTitleH2WithoutLeading; final bool isFlexibleSpaceDisabled; + final bool isOnTopOfScreen; const TitleBarWidget({ + this.leading, this.title, this.caption, this.flexibleSpaceTitle, @@ -18,6 +21,7 @@ class TitleBarWidget extends StatelessWidget { this.actionIcons, this.isTitleH2WithoutLeading = false, this.isFlexibleSpaceDisabled = false, + this.isOnTopOfScreen = true, super.key, }); @@ -27,13 +31,14 @@ class TitleBarWidget extends StatelessWidget { final textTheme = getEnteTextTheme(context); final colorTheme = getEnteColorScheme(context); return SliverAppBar( + primary: isOnTopOfScreen ? true : false, toolbarHeight: toolbarHeight, leadingWidth: 48, automaticallyImplyLeading: false, pinned: true, - expandedHeight: 102, + expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102, centerTitle: false, - titleSpacing: 0, + titleSpacing: 4, title: Padding( padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0), child: Column( @@ -67,13 +72,14 @@ class TitleBarWidget extends StatelessWidget { ], leading: isTitleH2WithoutLeading ? null - : IconButtonWidget( - icon: Icons.arrow_back_outlined, - isPrimary: true, - onTap: () { - Navigator.pop(context); - }, - ), + : leading ?? + IconButtonWidget( + icon: Icons.arrow_back_outlined, + iconButtonType: IconButtonType.primary, + onTap: () { + Navigator.pop(context); + }, + ), flexibleSpace: isFlexibleSpaceDisabled ? null : FlexibleSpaceBar( diff --git a/lib/ui/viewer/file/fading_bottom_bar.dart b/lib/ui/viewer/file/fading_bottom_bar.dart index f0e75985c..9ecf94326 100644 --- a/lib/ui/viewer/file/fading_bottom_bar.dart +++ b/lib/ui/viewer/file/fading_bottom_bar.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:page_transition/page_transition.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/models/file.dart'; @@ -12,6 +13,8 @@ import 'package:photos/models/magic_metadata.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/models/trash_file.dart'; import 'package:photos/services/collections_service.dart'; +import 'package:photos/theme/colors.dart'; +import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/create_collection_page.dart'; import 'package:photos/ui/viewer/file/file_info_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; @@ -73,8 +76,13 @@ class FadingBottomBarState extends State { Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info, color: Colors.white, ), - onPressed: () { - _displayInfo(widget.file); + onPressed: () async { + await _displayInfo(widget.file); + safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed + await Future.delayed( + const Duration(milliseconds: 500), + ); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done' + safeRefresh(); }, ), ), @@ -183,9 +191,31 @@ class FadingBottomBarState extends State { ), child: Padding( padding: EdgeInsets.only(bottom: safeAreaBottomPadding), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + widget.file.caption?.isNotEmpty ?? false + ? Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 28, + 16, + 12, + ), + child: Text( + widget.file.caption, + style: getEnteTextTheme(context) + .small + .copyWith(color: textBaseDark), + textAlign: TextAlign.center, + ), + ) + : const SizedBox.shrink(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ), + ], ), ), ), @@ -249,11 +279,19 @@ class FadingBottomBarState extends State { } Future _displayInfo(File file) async { - return showModalBottomSheet( + final colorScheme = getEnteColorScheme(context); + return showBarModalBottomSheet( + topControl: const SizedBox.shrink(), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)), + backgroundColor: colorScheme.backgroundBase, + barrierColor: backdropFaintDark, context: context, - isScrollControlled: true, builder: (BuildContext context) { - return FileInfoWidget(file); + return Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: FileInfoWidget(file), + ); }, ); } diff --git a/lib/ui/viewer/file/file_caption_widget.dart b/lib/ui/viewer/file/file_caption_widget.dart new file mode 100644 index 000000000..026281748 --- /dev/null +++ b/lib/ui/viewer/file/file_caption_widget.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:photos/models/file.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/utils/magic_util.dart'; + +class FileCaptionWidget extends StatefulWidget { + final File file; + const FileCaptionWidget({required this.file, super.key}); + + @override + State createState() => _FileCaptionWidgetState(); +} + +class _FileCaptionWidgetState extends State { + int maxLength = 280; + int currentLength = 0; + final _textController = TextEditingController(); + final _focusNode = FocusNode(); + String? editedCaption; + String? hintText = "Add a description..."; + + @override + void initState() { + _focusNode.addListener(() { + final caption = widget.file.caption; + if (_focusNode.hasFocus && caption != null) { + _textController.text = caption; + editedCaption = caption; + } + }); + editedCaption = widget.file.caption; + if (editedCaption != null && editedCaption!.isNotEmpty) { + hintText = editedCaption; + } + super.initState(); + } + + @override + void dispose() { + if (editedCaption != null) { + editFileCaption(null, widget.file, editedCaption); + } + _textController.dispose(); + _focusNode.removeListener(() {}); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return TextField( + onEditingComplete: () async { + if (editedCaption != null) { + await editFileCaption(context, widget.file, editedCaption); + if (mounted) { + setState(() {}); + } + } + _focusNode.unfocus(); + }, + controller: _textController, + focusNode: _focusNode, + decoration: InputDecoration( + counterStyle: textTheme.mini.copyWith(color: colorScheme.textMuted), + counterText: currentLength > 99 + ? currentLength.toString() + " / " + maxLength.toString() + : "", + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(2), + borderSide: const BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(2), + borderSide: const BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + filled: true, + fillColor: colorScheme.fillFaint, + hintText: hintText, + hintStyle: getEnteTextTheme(context) + .small + .copyWith(color: colorScheme.textMuted), + ), + style: getEnteTextTheme(context).small, + cursorWidth: 1.5, + maxLength: maxLength, + minLines: 1, + maxLines: 6, + textCapitalization: TextCapitalization.sentences, + keyboardType: TextInputType.text, + onChanged: (value) { + setState(() { + hintText = "Add a description..."; + currentLength = value.length; + editedCaption = value; + }); + }, + ); + } +} diff --git a/lib/ui/viewer/file/file_info_widget.dart b/lib/ui/viewer/file/file_info_widget.dart index 0513d20b1..dded6d08e 100644 --- a/lib/ui/viewer/file/file_info_widget.dart +++ b/lib/ui/viewer/file/file_info_widget.dart @@ -9,10 +9,13 @@ import 'package:photos/db/files_db.dart'; import "package:photos/ente_theme_data.dart"; import "package:photos/models/file.dart"; import "package:photos/models/file_type.dart"; -import 'package:photos/ui/common/DividerWithPadding.dart'; +import 'package:photos/ui/components/divider_widget.dart'; +import 'package:photos/ui/components/icon_button_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; import 'package:photos/ui/viewer/file/collections_list_of_file_widget.dart'; import 'package:photos/ui/viewer/file/device_folders_list_of_file_widget.dart'; -import 'package:photos/ui/viewer/file/raw_exif_button.dart'; +import 'package:photos/ui/viewer/file/file_caption_widget.dart'; +import 'package:photos/ui/viewer/file/raw_exif_list_tile_widget.dart'; import "package:photos/utils/date_time_util.dart"; import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_util.dart"; @@ -90,9 +93,17 @@ class _FileInfoWidgetState extends State { final bool showDimension = _exifData["resolution"] != null && _exifData["megaPixels"] != null; final listTiles = [ + widget.file.uploadedFileID == null || + Configuration.instance.getUserID() != file.ownerID + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(top: 8, bottom: 4), + child: FileCaptionWidget(file: widget.file), + ), ListTile( + horizontalTitleGap: 2, leading: const Padding( - padding: EdgeInsets.only(top: 8, left: 6), + padding: EdgeInsets.only(top: 8), child: Icon(Icons.calendar_today_rounded), ), title: Text( @@ -121,17 +132,17 @@ class _FileInfoWidgetState extends State { ) : const SizedBox.shrink(), ), - const DividerWithPadding(left: 70, right: 20), ListTile( + horizontalTitleGap: 2, leading: _isImage ? const Padding( - padding: EdgeInsets.only(top: 8, left: 6), + padding: EdgeInsets.only(top: 8), child: Icon( Icons.image, ), ) : const Padding( - padding: EdgeInsets.only(top: 8, left: 6), + padding: EdgeInsets.only(top: 8), child: Icon( Icons.video_camera_back, size: 27, @@ -169,13 +180,10 @@ class _FileInfoWidgetState extends State { icon: const Icon(Icons.edit), ), ), - const DividerWithPadding(left: 70, right: 20), showExifListTile ? ListTile( - leading: const Padding( - padding: EdgeInsets.only(left: 6), - child: Icon(Icons.camera_rounded), - ), + horizontalTitleGap: 2, + leading: const Icon(Icons.camera_rounded), title: Text(_exifData["takenOnDevice"] ?? "--"), subtitle: Row( children: [ @@ -207,27 +215,22 @@ class _FileInfoWidgetState extends State { ], ), ) - : const SizedBox.shrink(), - showExifListTile - ? const DividerWithPadding(left: 70, right: 20) - : const SizedBox.shrink(), + : null, SizedBox( height: 62, child: ListTile( - leading: const Padding( - padding: EdgeInsets.only(left: 6), - child: Icon(Icons.folder_outlined), - ), + horizontalTitleGap: 0, + leading: const Icon(Icons.folder_outlined), title: fileIsBackedup ? CollectionsListOfFileWidget(allCollectionIDsOfFile) : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile), ), ), - const DividerWithPadding(left: 70, right: 20), (file.uploadedFileID != null && file.updationTime != null) ? ListTile( + horizontalTitleGap: 2, leading: const Padding( - padding: EdgeInsets.only(top: 8, left: 6), + padding: EdgeInsets.only(top: 8), child: Icon(Icons.cloud_upload_outlined), ), title: Text( @@ -247,48 +250,53 @@ class _FileInfoWidgetState extends State { ), ), ) - : const SizedBox.shrink(), - _isImage - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 24, 0, 16), - child: SafeArea( - child: RawExifButton(_exif, widget.file), - ), - ) - : const SizedBox( - height: 12, - ) + : null, + _isImage ? RawExifListTileWidget(_exif, widget.file) : null, ]; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: const Icon( - Icons.close, + listTiles.removeWhere( + (element) => element == null, + ); + + return SafeArea( + top: false, + child: Scrollbar( + thickness: 4, + radius: const Radius.circular(2), + thumbVisibility: true, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CustomScrollView( + shrinkWrap: true, + slivers: [ + TitleBarWidget( + isFlexibleSpaceDisabled: true, + title: "Details", + isOnTopOfScreen: false, + leading: IconButtonWidget( + icon: Icons.close_outlined, + iconButtonType: IconButtonType.primary, + onTap: () => Navigator.pop(context), ), ), - const SizedBox(width: 6), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - "Details", - style: Theme.of(context).textTheme.bodyText1, + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index.isOdd) { + return index == 1 + ? const SizedBox.shrink() + : const DividerWidget(dividerType: DividerType.menu); + } else { + return listTiles[index ~/ 2]; + } + }, + childCount: (listTiles.length * 2) - 1, ), - ), + ) ], ), ), - ...listTiles - ], + ), ); } diff --git a/lib/ui/viewer/file/raw_exif_button.dart b/lib/ui/viewer/file/raw_exif_button.dart deleted file mode 100644 index a34414c46..000000000 --- a/lib/ui/viewer/file/raw_exif_button.dart +++ /dev/null @@ -1,100 +0,0 @@ -// @dart=2.9 - -import 'package:exif/exif.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; -import "package:photos/models/file.dart"; -import 'package:photos/ui/viewer/file/exif_info_dialog.dart'; -import 'package:photos/utils/toast_util.dart'; - -enum Status { - loading, - exifIsAvailable, - noExif, -} - -class RawExifButton extends StatelessWidget { - final File file; - final Map exif; - const RawExifButton(this.exif, this.file, {Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - Status exifStatus = Status.loading; - if (exif == null) { - exifStatus = Status.loading; - } else if (exif.isNotEmpty) { - exifStatus = Status.exifIsAvailable; - } else { - exifStatus = Status.noExif; - } - return GestureDetector( - onTap: - exifStatus == Status.loading || exifStatus == Status.exifIsAvailable - ? () { - showDialog( - context: context, - builder: (BuildContext context) { - return ExifInfoDialog(file); - }, - barrierColor: Colors.black87, - ); - } - : exifStatus == Status.noExif - ? () { - showShortToast(context, "This image has no exif data"); - } - : null, - child: Container( - height: 40, - width: 140, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .inverseBackgroundColor - .withOpacity(0.12), - borderRadius: const BorderRadius.all( - Radius.circular(20), - ), - ), - child: Center( - child: exifStatus == Status.loading - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - CupertinoActivityIndicator( - radius: 8, - ), - SizedBox( - width: 8, - ), - Text('EXIF') - ], - ) - : exifStatus == Status.exifIsAvailable - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.feed_outlined), - SizedBox( - width: 8, - ), - Text('Raw EXIF'), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.feed_outlined), - SizedBox( - width: 8, - ), - Text('No EXIF'), - ], - ), - ), - ), - ); - } -} diff --git a/lib/ui/viewer/file/raw_exif_list_tile_widget.dart b/lib/ui/viewer/file/raw_exif_list_tile_widget.dart new file mode 100644 index 000000000..40afb57ec --- /dev/null +++ b/lib/ui/viewer/file/raw_exif_list_tile_widget.dart @@ -0,0 +1,71 @@ +// @dart=2.9 + +import 'package:exif/exif.dart'; +import 'package:flutter/material.dart'; +import 'package:photos/ente_theme_data.dart'; +import "package:photos/models/file.dart"; +import 'package:photos/ui/viewer/file/exif_info_dialog.dart'; +import 'package:photos/utils/toast_util.dart'; + +enum Status { + loading, + exifIsAvailable, + noExif, +} + +class RawExifListTileWidget extends StatelessWidget { + final File file; + final Map exif; + const RawExifListTileWidget(this.exif, this.file, {Key key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + Status exifStatus = Status.loading; + if (exif == null) { + exifStatus = Status.loading; + } else if (exif.isNotEmpty) { + exifStatus = Status.exifIsAvailable; + } else { + exifStatus = Status.noExif; + } + return GestureDetector( + onTap: exifStatus == Status.exifIsAvailable + ? () { + showDialog( + context: context, + builder: (BuildContext context) { + return ExifInfoDialog(file); + }, + barrierColor: Colors.black87, + ); + } + : exifStatus == Status.noExif + ? () { + showShortToast(context, "This image has no exif data"); + } + : null, + child: ListTile( + horizontalTitleGap: 2, + leading: const Padding( + padding: EdgeInsets.only(top: 8), + child: Icon(Icons.feed_outlined), + ), + title: const Text("EXIF"), + subtitle: Text( + exifStatus == Status.loading + ? "Loading EXIF data.." + : exifStatus == Status.exifIsAvailable + ? "View all EXIF data" + : "No EXIF data", + style: Theme.of(context).textTheme.bodyText2.copyWith( + color: Theme.of(context) + .colorScheme + .defaultTextColor + .withOpacity(0.5), + ), + ), + ), + ); + } +} diff --git a/lib/ui/viewer/search/result/no_result_widget.dart b/lib/ui/viewer/search/result/no_result_widget.dart index 25a0a379d..ecb6ee2b0 100644 --- a/lib/ui/viewer/search/result/no_result_widget.dart +++ b/lib/ui/viewer/search/result/no_result_widget.dart @@ -29,14 +29,13 @@ class NoResultWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Container( - margin: const EdgeInsets.all(8), - child: const Text( - "No results found", - style: TextStyle( - fontSize: 16, - ), + Container( + margin: const EdgeInsets.only(top: 8), + child: const Text( + "No results found", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, ), ), ), @@ -61,6 +60,7 @@ class NoResultWidget extends StatelessWidget { \u2022 Types of files (e.g. "Videos", ".gif") \u2022 Years and months (e.g. "2022", "January") \u2022 Holidays (e.g. "Christmas") +\u2022 Photo descriptions (e.g. “#fun”) ''', style: TextStyle( fontSize: 14, diff --git a/lib/ui/viewer/search/result/search_result_widget.dart b/lib/ui/viewer/search/result/search_result_widget.dart index 7588975f0..5825094d2 100644 --- a/lib/ui/viewer/search/result/search_result_widget.dart +++ b/lib/ui/viewer/search/result/search_result_widget.dart @@ -125,6 +125,8 @@ class SearchResultWidget extends StatelessWidget { return "Type"; case ResultType.fileExtension: return "File Extension"; + case ResultType.fileCaption: + return "Description"; default: return type.name.toUpperCase(); } diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index f7a7cd063..dbe1376ec 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -34,7 +34,7 @@ class _SearchIconWidgetState extends State { return Hero( tag: "search_icon", child: IconButtonWidget( - isPrimary: true, + iconButtonType: IconButtonType.primary, icon: Icons.search, onTap: () { Navigator.push( @@ -196,6 +196,9 @@ class _SearchWidgetState extends State { await _searchService.getFileTypeResults(query); allResults.addAll(fileTypeSearchResults); + final fileCaptionResults = await _searchService.getCaptionResults(query); + allResults.addAll(fileCaptionResults); + final fileExtnResult = await _searchService.getFileExtensionResults(query); allResults.addAll(fileExtnResult); diff --git a/lib/utils/magic_util.dart b/lib/utils/magic_util.dart index 3f93deef1..d41b26ce4 100644 --- a/lib/utils/magic_util.dart +++ b/lib/utils/magic_util.dart @@ -10,6 +10,7 @@ import 'package:photos/models/file.dart'; import 'package:photos/models/magic_metadata.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/file_magic_service.dart'; +import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/common/rename_dialog.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; @@ -123,7 +124,23 @@ Future editFilename( ); return true; } catch (e) { - showToast(context, 'something went wrong'); + showToast(context, 'Something went wrong'); + return false; + } +} + +Future editFileCaption( + BuildContext context, + File file, + String caption, +) async { + try { + await _updatePublicMetadata(context, [file], pubMagicKeyCaption, caption); + return true; + } catch (e) { + if (context != null) { + showToast(context, "Something went wrong"); + } return false; } } @@ -137,19 +154,27 @@ Future _updatePublicMetadata( if (files.isEmpty) { return; } - final dialog = createProgressDialog(context, 'please wait...'); - await dialog.show(); + ProgressDialog dialog; + if (context != null) { + dialog = createProgressDialog(context, 'Please wait...'); + await dialog.show(); + } try { final Map update = {key: value}; await FileMagicService.instance.updatePublicMagicMetadata(files, update); - showShortToast(context, 'done'); - await dialog.hide(); + if (context != null) { + showShortToast(context, 'Done'); + await dialog.hide(); + } + if (_shouldReloadGallery(key)) { Bus.instance.fire(ForceReloadHomeGalleryEvent()); } } catch (e, s) { _logger.severe("failed to update $key = $value", e, s); - await dialog.hide(); + if (context != null) { + await dialog.hide(); + } rethrow; } } diff --git a/pubspec.lock b/pubspec.lock index 9962c7b56..7b1524c65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -744,6 +744,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + modal_bottom_sheet: + dependency: "direct main" + description: + name: modal_bottom_sheet + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" motionphoto: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e200cdf9f..b4ee205fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: lottie: ^1.2.2 media_extension: git: "https://github.com/ente-io/media_extension.git" + modal_bottom_sheet: ^2.1.2 motionphoto: git: "https://github.com/ente-io/motionphoto.git" move_to_background: ^1.0.2 @@ -91,7 +92,7 @@ dependencies: path: #dart path_provider: ^2.0.1 pedantic: ^1.9.2 - photo_manager: ^2.4.1 + photo_manager: ^2.5.0 photo_view: ^0.14.0 pinput: ^1.2.2 provider: ^6.0.0