Support for adding descriptions

File description
This commit is contained in:
Neeraj Gupta 2022-11-06 11:16:35 +05:30 committed by GitHub
commit 65bf985933
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 417 additions and 208 deletions

View file

@ -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 ||

View file

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

View file

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

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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();

View file

@ -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 IconButtonType iconButtonType;
final bool isSecondary;
final bool isRounded;
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, required this.iconButtonType,
this.isSecondary = false,
this.isRounded = false,
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);
}); });
} }

View file

@ -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,

View file

@ -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,9 +72,10 @@ class TitleBarWidget extends StatelessWidget {
], ],
leading: isTitleH2WithoutLeading leading: isTitleH2WithoutLeading
? null ? null
: IconButtonWidget( : leading ??
IconButtonWidget(
icon: Icons.arrow_back_outlined, icon: Icons.arrow_back_outlined,
isPrimary: true, iconButtonType: IconButtonType.primary,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
}, },

View file

@ -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: () { onPressed: () async {
_displayInfo(widget.file); 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,10 +191,32 @@ class FadingBottomBarState extends State<FadingBottomBar> {
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: safeAreaBottomPadding), padding: EdgeInsets.only(bottom: safeAreaBottomPadding),
child: Row( 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, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children, 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),
);
}, },
); );
} }

View file

@ -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;
});
},
);
}
}

View file

@ -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( horizontalTitleGap: 2,
padding: EdgeInsets.only(left: 6), leading: const Icon(Icons.camera_rounded),
child: 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(), : null,
showExifListTile
? const DividerWithPadding(left: 70, right: 20)
: const SizedBox.shrink(),
SizedBox( SizedBox(
height: 62, height: 62,
child: ListTile( child: ListTile(
leading: const Padding( horizontalTitleGap: 0,
padding: EdgeInsets.only(left: 6), leading: const Icon(Icons.folder_outlined),
child: 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(), : null,
_isImage _isImage ? RawExifListTileWidget(_exif, widget.file) : null,
? Padding(
padding: const EdgeInsets.fromLTRB(0, 24, 0, 16),
child: SafeArea(
child: RawExifButton(_exif, widget.file),
),
)
: const SizedBox(
height: 12,
)
]; ];
return Column( listTiles.removeWhere(
mainAxisSize: MainAxisSize.min, (element) => element == null,
children: [ );
Padding(
padding: const EdgeInsets.all(10), return SafeArea(
child: Row( top: false,
crossAxisAlignment: CrossAxisAlignment.center, child: Scrollbar(
children: [ thickness: 4,
IconButton( radius: const Radius.circular(2),
onPressed: () { thumbVisibility: true,
Navigator.pop(context); 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),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index.isOdd) {
return index == 1
? const SizedBox.shrink()
: const DividerWidget(dividerType: DividerType.menu);
} else {
return listTiles[index ~/ 2];
}
}, },
icon: const Icon( childCount: (listTiles.length * 2) - 1,
Icons.close,
),
),
const SizedBox(width: 6),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
"Details",
style: Theme.of(context).textTheme.bodyText1,
),
), ),
)
], ],
), ),
), ),
...listTiles ),
],
); );
} }

View file

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

View file

@ -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),
),
),
),
);
}
}

View file

@ -29,17 +29,16 @@ class NoResultWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Center( Container(
child: Container( margin: const EdgeInsets.only(top: 8),
margin: const EdgeInsets.all(8),
child: const Text( child: const Text(
"No results found", "No results found",
textAlign: TextAlign.left,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
), ),
), ),
), ),
),
Container( Container(
margin: const EdgeInsets.only(top: 16), margin: const EdgeInsets.only(top: 16),
child: Text( child: Text(
@ -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,

View file

@ -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();
} }

View file

@ -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);

View file

@ -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...'); ProgressDialog dialog;
if (context != null) {
dialog = createProgressDialog(context, 'Please wait...');
await dialog.show(); 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'); if (context != null) {
showShortToast(context, 'Done');
await dialog.hide(); 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);
if (context != null) {
await dialog.hide(); await dialog.hide();
}
rethrow; rethrow;
} }
} }

View file

@ -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:

View file

@ -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