Support for adding descriptions
File description
This commit is contained in:
commit
65bf985933
21 changed files with 417 additions and 208 deletions
|
@ -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 ||
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@ enum ResultType {
|
|||
year,
|
||||
fileType,
|
||||
fileExtension,
|
||||
fileCaption,
|
||||
event
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
lib/ui/viewer/file/file_caption_widget.dart
Normal file
107
lib/ui/viewer/file/file_caption_widget.dart
Normal 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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
lib/ui/viewer/file/raw_exif_list_tile_widget.dart
Normal file
71
lib/ui/viewer/file/raw_exif_list_tile_widget.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue