ソースを参照

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 {
   String get thumbnailUrl {
     final endpoint = Configuration.instance.getHttpEndpoint();
     final endpoint = Configuration.instance.getHttpEndpoint();
     if (endpoint != kDefaultProductionEndpoint ||
     if (endpoint != kDefaultProductionEndpoint ||

+ 4 - 1
lib/models/magic_metadata.dart

@@ -14,6 +14,7 @@ const subTypeKey = 'subType';
 
 
 const pubMagicKeyEditedTime = 'editedTime';
 const pubMagicKeyEditedTime = 'editedTime';
 const pubMagicKeyEditedName = 'editedName';
 const pubMagicKeyEditedName = 'editedName';
+const pubMagicKeyCaption = "caption";
 
 
 class MagicMetadata {
 class MagicMetadata {
   // 0 -> visible
   // 0 -> visible
@@ -39,8 +40,9 @@ class MagicMetadata {
 class PubMagicMetadata {
 class PubMagicMetadata {
   int? editedTime;
   int? editedTime;
   String? editedName;
   String? editedName;
+  String? caption;
 
 
-  PubMagicMetadata({this.editedTime, this.editedName});
+  PubMagicMetadata({this.editedTime, this.editedName, this.caption});
 
 
   factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
   factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
       PubMagicMetadata.fromJson(jsonDecode(encodedJson));
       PubMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -53,6 +55,7 @@ class PubMagicMetadata {
     return PubMagicMetadata(
     return PubMagicMetadata(
       editedTime: map[pubMagicKeyEditedTime],
       editedTime: map[pubMagicKeyEditedTime],
       editedName: map[pubMagicKeyEditedName],
       editedName: map[pubMagicKeyEditedName],
+      caption: map[pubMagicKeyCaption],
     );
     );
   }
   }
 }
 }

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

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

+ 24 - 0
lib/services/search_service.dart

@@ -209,6 +209,30 @@ class SearchService {
     return searchResults;
     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(
   Future<List<GenericSearchResult>> getFileExtensionResults(
     String query,
     String query,
   ) async {
   ) async {

+ 10 - 4
lib/theme/colors.dart

@@ -11,6 +11,7 @@ class EnteColorScheme {
   // Backdrop Colors
   // Backdrop Colors
   final Color backdropBase;
   final Color backdropBase;
   final Color backdropBaseMute;
   final Color backdropBaseMute;
+  final Color backdropFaint;
 
 
   // Text Colors
   // Text Colors
   final Color textBase;
   final Color textBase;
@@ -53,6 +54,7 @@ class EnteColorScheme {
     this.backgroundElevated2,
     this.backgroundElevated2,
     this.backdropBase,
     this.backdropBase,
     this.backdropBaseMute,
     this.backdropBaseMute,
+    this.backdropFaint,
     this.textBase,
     this.textBase,
     this.textMuted,
     this.textMuted,
     this.textFaint,
     this.textFaint,
@@ -84,7 +86,8 @@ const EnteColorScheme lightScheme = EnteColorScheme(
   backgroundElevatedLight,
   backgroundElevatedLight,
   backgroundElevated2Light,
   backgroundElevated2Light,
   backdropBaseLight,
   backdropBaseLight,
-  backdropBaseMuteLight,
+  backdropMutedLight,
+  backdropFaintLight,
   textBaseLight,
   textBaseLight,
   textMutedLight,
   textMutedLight,
   textFaintLight,
   textFaintLight,
@@ -107,7 +110,8 @@ const EnteColorScheme darkScheme = EnteColorScheme(
   backgroundElevatedDark,
   backgroundElevatedDark,
   backgroundElevated2Dark,
   backgroundElevated2Dark,
   backdropBaseDark,
   backdropBaseDark,
-  backdropBaseMuteDark,
+  backdropMutedDark,
+  backdropFaintDark,
   textBaseDark,
   textBaseDark,
   textMutedDark,
   textMutedDark,
   textFaintDark,
   textFaintDark,
@@ -136,10 +140,12 @@ const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1);
 
 
 // Backdrop Colors
 // Backdrop Colors
 const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.75);
 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 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
 // Text Colors
 const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
 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: [
             actionIcons: [
               IconButtonWidget(
               IconButtonWidget(
                 icon: Icons.close_outlined,
                 icon: Icons.close_outlined,
-                isSecondary: true,
+                iconButtonType: IconButtonType.secondary,
                 onTap: () {
                 onTap: () {
                   Navigator.pop(context);
                   Navigator.pop(context);
                   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,
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       children: [
       children: [
         IconButtonWidget(
         IconButtonWidget(
-          isPrimary: true,
+          iconButtonType: IconButtonType.primary,
           icon: Icons.menu_outlined,
           icon: Icons.menu_outlined,
           onTap: () {
           onTap: () {
             Scaffold.of(context).openDrawer();
             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/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 
 
+enum IconButtonType {
+  primary,
+  secondary,
+  rounded,
+}
+
 class IconButtonWidget extends StatefulWidget {
 class IconButtonWidget extends StatefulWidget {
-  final bool isPrimary;
-  final bool isSecondary;
-  final bool isRounded;
+  final IconButtonType iconButtonType;
   final IconData icon;
   final IconData icon;
   final bool disableGestureDetector;
   final bool disableGestureDetector;
   final VoidCallback? onTap;
   final VoidCallback? onTap;
@@ -14,9 +18,7 @@ class IconButtonWidget extends StatefulWidget {
   final Color? iconColor;
   final Color? iconColor;
   const IconButtonWidget({
   const IconButtonWidget({
     required this.icon,
     required this.icon,
-    this.isPrimary = false,
-    this.isSecondary = false,
-    this.isRounded = false,
+    required this.iconButtonType,
     this.disableGestureDetector = false,
     this.disableGestureDetector = false,
     this.onTap,
     this.onTap,
     this.defaultColor,
     this.defaultColor,
@@ -41,13 +43,12 @@ class _IconButtonWidgetState extends State<IconButtonWidget> {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    if (!widget.isPrimary && !widget.isRounded && !widget.isSecondary) {
-      return const SizedBox.shrink();
-    }
     final colorTheme = getEnteColorScheme(context);
     final colorTheme = getEnteColorScheme(context);
     iconStateColor ??
     iconStateColor ??
         (iconStateColor = widget.defaultColor ??
         (iconStateColor = widget.defaultColor ??
-            (widget.isRounded ? colorTheme.fillFaint : null));
+            (widget.iconButtonType == IconButtonType.rounded
+                ? colorTheme.fillFaint
+                : null));
     return widget.disableGestureDetector
     return widget.disableGestureDetector
         ? _iconButton(colorTheme)
         ? _iconButton(colorTheme)
         : GestureDetector(
         : GestureDetector(
@@ -72,7 +73,7 @@ class _IconButtonWidgetState extends State<IconButtonWidget> {
         child: Icon(
         child: Icon(
           widget.icon,
           widget.icon,
           color: widget.iconColor ??
           color: widget.iconColor ??
-              (widget.isSecondary
+              (widget.iconButtonType == IconButtonType.secondary
                   ? colorTheme.strokeMuted
                   ? colorTheme.strokeMuted
                   : colorTheme.strokeBase),
                   : colorTheme.strokeBase),
           size: 24,
           size: 24,
@@ -85,7 +86,9 @@ class _IconButtonWidgetState extends State<IconButtonWidget> {
     final colorTheme = getEnteColorScheme(context);
     final colorTheme = getEnteColorScheme(context);
     setState(() {
     setState(() {
       iconStateColor = widget.pressedColor ??
       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),
                   const SizedBox(width: 12),
                   IconButtonWidget(
                   IconButtonWidget(
                     icon: actionIcon,
                     icon: actionIcon,
-                    isRounded: true,
+                    iconButtonType: IconButtonType.rounded,
                     iconColor: strokeBaseDark,
                     iconColor: strokeBaseDark,
                     defaultColor: fillFaintDark,
                     defaultColor: fillFaintDark,
                     pressedColor: fillMutedDark,
                     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';
 import 'package:photos/ui/components/icon_button_widget.dart';
 
 
 class TitleBarWidget extends StatelessWidget {
 class TitleBarWidget extends StatelessWidget {
+  final IconButtonWidget? leading;
   final String? title;
   final String? title;
   final String? caption;
   final String? caption;
   final Widget? flexibleSpaceTitle;
   final Widget? flexibleSpaceTitle;
@@ -10,7 +11,9 @@ class TitleBarWidget extends StatelessWidget {
   final List<Widget>? actionIcons;
   final List<Widget>? actionIcons;
   final bool isTitleH2WithoutLeading;
   final bool isTitleH2WithoutLeading;
   final bool isFlexibleSpaceDisabled;
   final bool isFlexibleSpaceDisabled;
+  final bool isOnTopOfScreen;
   const TitleBarWidget({
   const TitleBarWidget({
+    this.leading,
     this.title,
     this.title,
     this.caption,
     this.caption,
     this.flexibleSpaceTitle,
     this.flexibleSpaceTitle,
@@ -18,6 +21,7 @@ class TitleBarWidget extends StatelessWidget {
     this.actionIcons,
     this.actionIcons,
     this.isTitleH2WithoutLeading = false,
     this.isTitleH2WithoutLeading = false,
     this.isFlexibleSpaceDisabled = false,
     this.isFlexibleSpaceDisabled = false,
+    this.isOnTopOfScreen = true,
     super.key,
     super.key,
   });
   });
 
 
@@ -27,13 +31,14 @@ class TitleBarWidget extends StatelessWidget {
     final textTheme = getEnteTextTheme(context);
     final textTheme = getEnteTextTheme(context);
     final colorTheme = getEnteColorScheme(context);
     final colorTheme = getEnteColorScheme(context);
     return SliverAppBar(
     return SliverAppBar(
+      primary: isOnTopOfScreen ? true : false,
       toolbarHeight: toolbarHeight,
       toolbarHeight: toolbarHeight,
       leadingWidth: 48,
       leadingWidth: 48,
       automaticallyImplyLeading: false,
       automaticallyImplyLeading: false,
       pinned: true,
       pinned: true,
-      expandedHeight: 102,
+      expandedHeight: isFlexibleSpaceDisabled ? toolbarHeight : 102,
       centerTitle: false,
       centerTitle: false,
-      titleSpacing: 0,
+      titleSpacing: 4,
       title: Padding(
       title: Padding(
         padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0),
         padding: EdgeInsets.only(left: isTitleH2WithoutLeading ? 16 : 0),
         child: Column(
         child: Column(
@@ -67,13 +72,14 @@ class TitleBarWidget extends StatelessWidget {
       ],
       ],
       leading: isTitleH2WithoutLeading
       leading: isTitleH2WithoutLeading
           ? null
           ? 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
       flexibleSpace: isFlexibleSpaceDisabled
           ? null
           ? null
           : FlexibleSpaceBar(
           : 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/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:page_transition/page_transition.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/models/file.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/selected_files.dart';
 import 'package:photos/models/trash_file.dart';
 import 'package:photos/models/trash_file.dart';
 import 'package:photos/services/collections_service.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/create_collection_page.dart';
 import 'package:photos/ui/viewer/file/file_info_widget.dart';
 import 'package:photos/ui/viewer/file/file_info_widget.dart';
 import 'package:photos/utils/delete_file_util.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,
               Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
               color: Colors.white,
               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(
             child: Padding(
               padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
               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 {
   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,
       context: context,
-      isScrollControlled: true,
       builder: (BuildContext context) {
       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/ente_theme_data.dart";
 import "package:photos/models/file.dart";
 import "package:photos/models/file.dart";
 import "package:photos/models/file_type.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/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/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/date_time_util.dart";
 import "package:photos/utils/exif_util.dart";
 import "package:photos/utils/exif_util.dart";
 import "package:photos/utils/file_util.dart";
 import "package:photos/utils/file_util.dart";
@@ -90,9 +93,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
     final bool showDimension =
     final bool showDimension =
         _exifData["resolution"] != null && _exifData["megaPixels"] != null;
         _exifData["resolution"] != null && _exifData["megaPixels"] != null;
     final listTiles = <Widget>[
     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(
       ListTile(
+        horizontalTitleGap: 2,
         leading: const Padding(
         leading: const Padding(
-          padding: EdgeInsets.only(top: 8, left: 6),
+          padding: EdgeInsets.only(top: 8),
           child: Icon(Icons.calendar_today_rounded),
           child: Icon(Icons.calendar_today_rounded),
         ),
         ),
         title: Text(
         title: Text(
@@ -121,17 +132,17 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
               )
               )
             : const SizedBox.shrink(),
             : const SizedBox.shrink(),
       ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       ListTile(
       ListTile(
+        horizontalTitleGap: 2,
         leading: _isImage
         leading: _isImage
             ? const Padding(
             ? const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(
                 child: Icon(
                   Icons.image,
                   Icons.image,
                 ),
                 ),
               )
               )
             : const Padding(
             : const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(
                 child: Icon(
                   Icons.video_camera_back,
                   Icons.video_camera_back,
                   size: 27,
                   size: 27,
@@ -169,13 +180,10 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
                 icon: const Icon(Icons.edit),
                 icon: const Icon(Icons.edit),
               ),
               ),
       ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       showExifListTile
       showExifListTile
           ? ListTile(
           ? 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"] ?? "--"),
               title: Text(_exifData["takenOnDevice"] ?? "--"),
               subtitle: Row(
               subtitle: Row(
                 children: [
                 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(
       SizedBox(
         height: 62,
         height: 62,
         child: ListTile(
         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
           title: fileIsBackedup
               ? CollectionsListOfFileWidget(allCollectionIDsOfFile)
               ? CollectionsListOfFileWidget(allCollectionIDsOfFile)
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
               : DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
         ),
         ),
       ),
       ),
-      const DividerWithPadding(left: 70, right: 20),
       (file.uploadedFileID != null && file.updationTime != null)
       (file.uploadedFileID != null && file.updationTime != null)
           ? ListTile(
           ? ListTile(
+              horizontalTitleGap: 2,
               leading: const Padding(
               leading: const Padding(
-                padding: EdgeInsets.only(top: 8, left: 6),
+                padding: EdgeInsets.only(top: 8),
                 child: Icon(Icons.cloud_upload_outlined),
                 child: Icon(Icons.cloud_upload_outlined),
               ),
               ),
               title: Text(
               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(
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
           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 Types of files (e.g. "Videos", ".gif")
 \u2022 Years and months (e.g. "2022", "January")
 \u2022 Years and months (e.g. "2022", "January")
 \u2022 Holidays (e.g. "Christmas")
 \u2022 Holidays (e.g. "Christmas")
+\u2022 Photo descriptions (e.g. “#fun”)
 ''',
 ''',
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: 14,
                   fontSize: 14,

+ 2 - 0
lib/ui/viewer/search/result/search_result_widget.dart

@@ -125,6 +125,8 @@ class SearchResultWidget extends StatelessWidget {
         return "Type";
         return "Type";
       case ResultType.fileExtension:
       case ResultType.fileExtension:
         return "File Extension";
         return "File Extension";
+      case ResultType.fileCaption:
+        return "Description";
       default:
       default:
         return type.name.toUpperCase();
         return type.name.toUpperCase();
     }
     }

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

@@ -34,7 +34,7 @@ class _SearchIconWidgetState extends State<SearchIconWidget> {
     return Hero(
     return Hero(
       tag: "search_icon",
       tag: "search_icon",
       child: IconButtonWidget(
       child: IconButtonWidget(
-        isPrimary: true,
+        iconButtonType: IconButtonType.primary,
         icon: Icons.search,
         icon: Icons.search,
         onTap: () {
         onTap: () {
           Navigator.push(
           Navigator.push(
@@ -196,6 +196,9 @@ class _SearchWidgetState extends State<SearchWidget> {
           await _searchService.getFileTypeResults(query);
           await _searchService.getFileTypeResults(query);
       allResults.addAll(fileTypeSearchResults);
       allResults.addAll(fileTypeSearchResults);
 
 
+      final fileCaptionResults = await _searchService.getCaptionResults(query);
+      allResults.addAll(fileCaptionResults);
+
       final fileExtnResult =
       final fileExtnResult =
           await _searchService.getFileExtensionResults(query);
           await _searchService.getFileExtensionResults(query);
       allResults.addAll(fileExtnResult);
       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/models/magic_metadata.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/file_magic_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/ui/common/rename_dialog.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:photos/utils/toast_util.dart';
@@ -123,7 +124,23 @@ Future<bool> editFilename(
     );
     );
     return true;
     return true;
   } catch (e) {
   } 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;
     return false;
   }
   }
 }
 }
@@ -137,19 +154,27 @@ Future<void> _updatePublicMetadata(
   if (files.isEmpty) {
   if (files.isEmpty) {
     return;
     return;
   }
   }
-  final dialog = createProgressDialog(context, 'please wait...');
-  await dialog.show();
+  ProgressDialog dialog;
+  if (context != null) {
+    dialog = createProgressDialog(context, 'Please wait...');
+    await dialog.show();
+  }
   try {
   try {
     final Map<String, dynamic> update = {key: value};
     final Map<String, dynamic> update = {key: value};
     await FileMagicService.instance.updatePublicMagicMetadata(files, update);
     await FileMagicService.instance.updatePublicMagicMetadata(files, update);
-    showShortToast(context, 'done');
-    await dialog.hide();
+    if (context != null) {
+      showShortToast(context, 'Done');
+      await dialog.hide();
+    }
+
     if (_shouldReloadGallery(key)) {
     if (_shouldReloadGallery(key)) {
       Bus.instance.fire(ForceReloadHomeGalleryEvent());
       Bus.instance.fire(ForceReloadHomeGalleryEvent());
     }
     }
   } catch (e, s) {
   } catch (e, s) {
     _logger.severe("failed to update $key = $value", e, s);
     _logger.severe("failed to update $key = $value", e, s);
-    await dialog.hide();
+    if (context != null) {
+      await dialog.hide();
+    }
     rethrow;
     rethrow;
   }
   }
 }
 }

+ 7 - 0
pubspec.lock

@@ -744,6 +744,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "1.0.2"
     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:
   motionphoto:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:

+ 2 - 1
pubspec.yaml

@@ -79,6 +79,7 @@ dependencies:
   lottie: ^1.2.2
   lottie: ^1.2.2
   media_extension:
   media_extension:
     git: "https://github.com/ente-io/media_extension.git"
     git: "https://github.com/ente-io/media_extension.git"
+  modal_bottom_sheet: ^2.1.2
   motionphoto:
   motionphoto:
     git: "https://github.com/ente-io/motionphoto.git"
     git: "https://github.com/ente-io/motionphoto.git"
   move_to_background: ^1.0.2
   move_to_background: ^1.0.2
@@ -91,7 +92,7 @@ dependencies:
   path: #dart
   path: #dart
   path_provider: ^2.0.1
   path_provider: ^2.0.1
   pedantic: ^1.9.2
   pedantic: ^1.9.2
-  photo_manager: ^2.4.1
+  photo_manager: ^2.5.0
   photo_view: ^0.14.0
   photo_view: ^0.14.0
   pinput: ^1.2.2
   pinput: ^1.2.2
   provider: ^6.0.0
   provider: ^6.0.0