浏览代码

Support for adding descriptions

File description
Neeraj Gupta 2 年之前
父节点
当前提交
65bf985933

+ 4 - 0
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 ||

+ 4 - 1
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],
     );
   }
 }

+ 1 - 0
lib/models/search/search_result.dart

@@ -22,5 +22,6 @@ enum ResultType {
   year,
   fileType,
   fileExtension,
+  fileCaption,
   event
 }

+ 24 - 0
lib/services/search_service.dart

@@ -209,6 +209,30 @@ class SearchService {
     return searchResults;
   }
 
+  Future<List<GenericSearchResult>> getCaptionResults(
+    String query,
+  ) async {
+    final List<GenericSearchResult> searchResults = [];
+    if (query.isEmpty) {
+      return searchResults;
+    }
+    final RegExp pattern = RegExp(query, caseSensitive: false);
+    final List<File> 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<List<GenericSearchResult>> getFileExtensionResults(
     String query,
   ) async {

+ 10 - 4
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);

+ 1 - 1
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);

+ 1 - 1
lib/ui/components/home_header_widget.dart

@@ -20,7 +20,7 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       children: [
         IconButtonWidget(
-          isPrimary: true,
+          iconButtonType: IconButtonType.primary,
           icon: Icons.menu_outlined,
           onTap: () {
             Scaffold.of(context).openDrawer();

+ 15 - 12
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<IconButtonWidget> {
 
   @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<IconButtonWidget> {
         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<IconButtonWidget> {
     final colorTheme = getEnteColorScheme(context);
     setState(() {
       iconStateColor = widget.pressedColor ??
-          (widget.isRounded ? colorTheme.fillMuted : colorTheme.fillFaint);
+          (widget.iconButtonType == IconButtonType.rounded
+              ? colorTheme.fillMuted
+              : colorTheme.fillFaint);
     });
   }
 

+ 1 - 1
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,

+ 15 - 9
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<Widget>? 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(

+ 46 - 8
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<FadingBottomBar> {
               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<FadingBottomBar> {
             ),
             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<FadingBottomBar> {
   }
 
   Future<void> _displayInfo(File file) async {
-    return showModalBottomSheet<void>(
+    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),
+        );
       },
     );
   }

+ 107 - 0
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<FileCaptionWidget> createState() => _FileCaptionWidgetState();
+}
+
+class _FileCaptionWidgetState extends State<FileCaptionWidget> {
+  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;
+        });
+      },
+    );
+  }
+}

+ 63 - 55
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<FileInfoWidget> {
     final bool showDimension =
         _exifData["resolution"] != null && _exifData["megaPixels"] != null;
     final listTiles = <Widget>[
+      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<FileInfoWidget> {
               )
             : 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<FileInfoWidget> {
                 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<FileInfoWidget> {
                 ],
               ),
             )
-          : 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<FileInfoWidget> {
                     ),
               ),
             )
-          : 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: <Widget>[
+              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
-      ],
+      ),
     );
   }
 

+ 0 - 100
lib/ui/viewer/file/raw_exif_button.dart

@@ -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<String, IfdTag> 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'),
-                      ],
-                    ),
-        ),
-      ),
-    );
-  }
-}

+ 71 - 0
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<String, IfdTag> 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),
+              ),
+        ),
+      ),
+    );
+  }
+}

+ 8 - 8
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,

+ 2 - 0
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();
     }

+ 4 - 1
lib/ui/viewer/search/search_widget.dart

@@ -34,7 +34,7 @@ class _SearchIconWidgetState extends State<SearchIconWidget> {
     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<SearchWidget> {
           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);

+ 31 - 6
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<bool> editFilename(
     );
     return true;
   } catch (e) {
-    showToast(context, 'something went wrong');
+    showToast(context, 'Something went wrong');
+    return false;
+  }
+}
+
+Future<bool> 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<void> _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<String, dynamic> 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;
   }
 }

+ 7 - 0
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:

+ 2 - 1
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