ente/lib/ui/viewer/file/file_details_widget.dart

355 lines
12 KiB
Dart
Raw Normal View History

import "package:exif/exif.dart";
import "package:flutter/material.dart";
2023-04-18 07:46:55 +00:00
import "package:logging/logging.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:photos/core/configuration.dart";
2023-05-05 08:43:47 +00:00
import "package:photos/generated/l10n.dart";
2023-08-25 04:39:30 +00:00
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/services/file_magic_service.dart";
import "package:photos/services/update_service.dart";
import "package:photos/theme/colors.dart";
2022-11-08 09:27:39 +00:00
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import "package:photos/ui/components/divider_widget.dart";
import "package:photos/ui/components/info_item_widget.dart";
import 'package:photos/ui/components/title_bar_widget.dart';
import 'package:photos/ui/viewer/file/file_caption_widget.dart';
import "package:photos/ui/viewer/file_details/added_by_widget.dart";
import "package:photos/ui/viewer/file_details/albums_item_widget.dart";
2023-03-13 15:09:51 +00:00
import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart';
import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
2023-03-14 12:53:54 +00:00
import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
import "package:photos/ui/viewer/file_details/objects_item_widget.dart";
import 'package:photos/ui/viewer/location/add_location_data_widget.dart';
import "package:photos/utils/exif_util.dart";
2021-07-06 11:50:35 +00:00
class FileDetailsWidget extends StatefulWidget {
2023-08-24 16:56:24 +00:00
final EnteFile file;
2023-05-08 17:11:09 +00:00
const FileDetailsWidget(
2021-07-07 01:01:14 +00:00
this.file, {
Key? key,
2021-07-06 11:51:51 +00:00
}) : super(key: key);
2021-07-06 11:50:35 +00:00
2021-07-10 05:48:00 +00:00
@override
State<FileDetailsWidget> createState() => _FileDetailsWidgetState();
2021-07-10 05:48:00 +00:00
}
class _FileDetailsWidgetState extends State<FileDetailsWidget> {
final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
final Map<String, dynamic> _exifData = {
"focalLength": null,
"fNumber": null,
"resolution": null,
"takenOnDevice": null,
"exposureTime": null,
"ISO": null,
"megaPixels": null,
"lat": null,
"long": null,
"latRef": null,
"longRef": null,
};
2021-07-11 07:54:38 +00:00
bool _isImage = false;
late int _currentUserID;
bool showExifListTile = false;
2023-04-18 08:07:34 +00:00
final ValueNotifier<bool> hasLocationData = ValueNotifier(false);
2023-04-18 07:46:55 +00:00
final Logger _logger = Logger("_FileDetailsWidgetState");
2021-07-10 05:48:00 +00:00
@override
void initState() {
debugPrint('file_details_sheet initState');
_currentUserID = Configuration.instance.getUserID()!;
2023-04-18 08:07:34 +00:00
hasLocationData.value = widget.file.hasLocation;
2021-08-09 06:33:47 +00:00
_isImage = widget.file.fileType == FileType.image ||
widget.file.fileType == FileType.livePhoto;
2023-04-18 08:07:34 +00:00
2023-04-18 06:27:55 +00:00
_exifNotifier.addListener(() {
2023-04-18 08:36:41 +00:00
if (_exifNotifier.value != null && !widget.file.hasLocation) {
2023-04-18 07:46:55 +00:00
_updateLocationFromExif(_exifNotifier.value!).ignore();
}
});
2023-04-18 08:07:34 +00:00
2021-07-11 07:54:38 +00:00
if (_isImage) {
_exifNotifier.addListener(() {
if (_exifNotifier.value != null) {
_generateExifForDetails(_exifNotifier.value!);
}
showExifListTile = _exifData["focalLength"] != null ||
_exifData["fNumber"] != null ||
_exifData["takenOnDevice"] != null ||
_exifData["exposureTime"] != null ||
_exifData["ISO"] != null;
});
2021-07-11 07:54:38 +00:00
}
getExif(widget.file).then((exif) {
_exifNotifier.value = exif;
});
2021-07-10 05:48:00 +00:00
super.initState();
}
@override
void dispose() {
_exifNotifier.dispose();
super.dispose();
}
2021-07-06 11:50:35 +00:00
@override
Widget build(BuildContext context) {
2021-11-12 11:36:18 +00:00
final file = widget.file;
2022-12-17 11:52:44 +00:00
final bool isFileOwner =
file.ownerID == null || file.ownerID == _currentUserID;
//Make sure the bottom most tile is always the same one, that is it should
//not be rendered only if a condition is met.
final fileDetailsTiles = <Widget>[];
fileDetailsTiles.add(
2022-12-17 11:52:44 +00:00
!widget.file.isUploaded ||
(!isFileOwner && (widget.file.caption?.isEmpty ?? true))
? const SizedBox(height: 16)
: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 24),
2022-12-17 11:52:44 +00:00
child: isFileOwner
? FileCaptionWidget(file: widget.file)
: FileCaptionReadyOnly(caption: widget.file.caption!),
),
);
fileDetailsTiles.addAll([
CreationTimeItem(file, _currentUserID),
const FileDetailsDivider(),
ValueListenableBuilder(
valueListenable: _exifNotifier,
builder: (context, _, __) => FilePropertiesItemWidget(
file,
_isImage,
_exifData,
_currentUserID,
),
),
const FileDetailsDivider(),
]);
fileDetailsTiles.add(
ValueListenableBuilder(
valueListenable: _exifNotifier,
builder: (context, value, _) {
return showExifListTile
? Column(
children: [
BasicExifItemWidget(_exifData),
const FileDetailsDivider(),
],
)
: const SizedBox.shrink();
},
),
);
2023-04-18 08:22:11 +00:00
fileDetailsTiles.addAll([
ValueListenableBuilder(
valueListenable: hasLocationData,
builder: (context, bool value, __) {
return value
? Column(
children: [
LocationTagsWidget(
widget.file.location!,
),
const FileDetailsDivider(),
],
)
: Column(
children: [
InfoItemWidget(
leadingIcon: Icons.pin_drop_outlined,
title: "No location data",
subtitleSection: Future.value(
[
Text(
"Add location data",
style: getEnteTextTheme(context).miniBoldMuted,
),
],
),
hasChipButtons: false,
onTap: () {
showBarModalBottomSheet(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(5),
),
),
backgroundColor:
getEnteColorScheme(context).backgroundElevated,
barrierColor: backdropFaintDark,
context: context,
builder: (context) {
return const AddLocationDataWidget();
},
);
},
),
const FileDetailsDivider(),
],
);
2023-04-18 08:22:11 +00:00
},
2023-08-19 11:39:56 +00:00
),
2023-04-18 08:22:11 +00:00
]);
if (_isImage) {
fileDetailsTiles.addAll([
ValueListenableBuilder(
valueListenable: _exifNotifier,
builder: (context, value, _) {
return Column(
children: [
AllExifItemWidget(file, _exifNotifier.value),
const FileDetailsDivider(),
],
);
},
2023-08-19 11:39:56 +00:00
),
]);
}
2023-05-08 17:11:09 +00:00
if (!UpdateService.instance.isFdroidFlavor()) {
fileDetailsTiles.addAll([
ObjectsItemWidget(file),
const FileDetailsDivider(),
]);
}
2023-05-08 17:11:09 +00:00
if (file.uploadedFileID != null && file.updationTime != null) {
fileDetailsTiles.addAll(
[
BackedUpTimeItemWidget(file),
const FileDetailsDivider(),
],
);
}
fileDetailsTiles.add(AlbumsItemWidget(file, _currentUserID));
2022-11-03 05:45:41 +00:00
return SafeArea(
top: false,
2022-11-05 06:28:17 +00:00
child: Scrollbar(
thickness: 4,
radius: const Radius.circular(2),
thumbVisibility: true,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CustomScrollView(
2022-11-07 08:13:41 +00:00
physics: const ClampingScrollPhysics(),
2022-11-05 06:28:17 +00:00
shrinkWrap: true,
slivers: <Widget>[
TitleBarWidget(
isFlexibleSpaceDisabled: true,
2023-05-05 08:43:47 +00:00
title: S.of(context).details,
2022-11-05 06:28:17 +00:00
isOnTopOfScreen: false,
2022-11-08 09:27:39 +00:00
backgroundColor: getEnteColorScheme(context).backgroundElevated,
2022-11-05 06:28:17 +00:00
leading: IconButtonWidget(
icon: Icons.expand_more_outlined,
2022-11-05 06:28:17 +00:00
iconButtonType: IconButtonType.primary,
onTap: () => Navigator.pop(context),
),
2022-11-03 05:45:41 +00:00
),
2023-08-25 07:21:21 +00:00
SliverToBoxAdapter(child: AddedByWidget(widget.file)),
2022-11-05 06:28:17 +00:00
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return fileDetailsTiles[index];
2022-11-05 06:28:17 +00:00
},
childCount: fileDetailsTiles.length,
2022-11-05 06:28:17 +00:00
),
2023-08-19 11:39:56 +00:00
),
2022-11-05 06:28:17 +00:00
],
),
2022-11-03 05:45:41 +00:00
),
),
2021-07-10 05:48:00 +00:00
);
}
2023-04-18 07:46:55 +00:00
//This code is for updating the location of files in which location data is
//missing and the EXIF has location data. This is only happens for a
//certain specific minority of devices.
Future<void> _updateLocationFromExif(Map<String, IfdTag> exif) async {
// If the file is not uploaded or the file is not owned by the current user
// then we don't need to update the location.
if (!widget.file.isUploaded || widget.file.ownerID! != _currentUserID) {
return;
}
2023-04-18 07:46:55 +00:00
try {
final locationDataFromExif = locationFromExif(exif);
if (locationDataFromExif?.latitude != null &&
locationDataFromExif?.longitude != null) {
widget.file.location = locationDataFromExif;
await FileMagicService.instance.updatePublicMagicMetadata([
2023-08-19 11:39:56 +00:00
widget.file,
2023-04-18 07:46:55 +00:00
], {
latKey: locationDataFromExif!.latitude,
2023-08-19 11:39:56 +00:00
longKey: locationDataFromExif.longitude,
2023-04-18 07:46:55 +00:00
});
2023-04-18 08:07:34 +00:00
hasLocationData.value = true;
2023-04-18 07:46:55 +00:00
}
} catch (e, s) {
_logger.severe("Error while updating location from EXIF", e, s);
}
}
_generateExifForDetails(Map<String, IfdTag> exif) {
if (exif["EXIF FocalLength"] != null) {
_exifData["focalLength"] =
(exif["EXIF FocalLength"]!.values.toList()[0] as Ratio).numerator /
(exif["EXIF FocalLength"]!.values.toList()[0] as Ratio)
.denominator;
}
if (exif["EXIF FNumber"] != null) {
_exifData["fNumber"] =
(exif["EXIF FNumber"]!.values.toList()[0] as Ratio).numerator /
(exif["EXIF FNumber"]!.values.toList()[0] as Ratio).denominator;
}
final imageWidth = exif["EXIF ExifImageWidth"] ?? exif["Image ImageWidth"];
final imageLength = exif["EXIF ExifImageLength"] ??
exif["Image "
"ImageLength"];
if (imageWidth != null && imageLength != null) {
2022-07-16 08:31:36 +00:00
_exifData["resolution"] = '$imageWidth x $imageLength';
final double megaPixels =
(imageWidth.values.firstAsInt() * imageLength.values.firstAsInt()) /
1000000;
final double roundedMegaPixels = (megaPixels * 10).round() / 10.0;
_exifData['megaPixels'] = roundedMegaPixels..toStringAsFixed(1);
} else {
debugPrint("No image width/height");
}
if (exif["Image Make"] != null && exif["Image Model"] != null) {
_exifData["takenOnDevice"] =
exif["Image Make"].toString() + " " + exif["Image Model"].toString();
}
if (exif["EXIF ExposureTime"] != null) {
_exifData["exposureTime"] = exif["EXIF ExposureTime"].toString();
}
if (exif["EXIF ISOSpeedRatings"] != null) {
_exifData['ISO'] = exif["EXIF ISOSpeedRatings"].toString();
}
}
2021-07-06 11:50:35 +00:00
}
class FileDetailsDivider extends StatelessWidget {
const FileDetailsDivider({super.key});
@override
Widget build(BuildContext context) {
2023-06-13 14:43:52 +00:00
const dividerPadding = EdgeInsets.symmetric(vertical: 9.5);
return const DividerWidget(
dividerType: DividerType.menu,
divColorHasBlur: false,
padding: dividerPadding,
);
}
}