Display EXIF in image viewer, refactor

This commit is contained in:
Tran, Alex 2022-02-10 20:37:55 -06:00
parent c7daba2cef
commit 5e59bda33b
7 changed files with 185 additions and 127 deletions

View file

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
class ExifBottomSheet extends ConsumerWidget {
final ImmichAssetWithExif assetDetail;
const ExifBottomSheet({Key? key, required this.assetDetail}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
child: ListView(
children: [
assetDetail.exifInfo?.dateTimeOriginal != null
? Text(
DateFormat('E, LLL d, y • h:mm a').format(
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
),
style: TextStyle(
color: Colors.grey[400],
fontWeight: FontWeight.bold,
fontSize: 14,
),
)
: Container(),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
"Add Description...",
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
),
),
),
// Location
assetDetail.exifInfo?.latitude != null
? Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Text(
"LOCATION",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
Text(
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
)
],
),
)
: Container(),
// Detail
assetDetail.exifInfo != null
? Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"DETAILS",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.image),
title: Text(
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"${assetDetail.exifInfo?.exifImageHeight!} x ${assetDetail.exifInfo?.exifImageWidth!} ${assetDetail.exifInfo?.fileSizeInByte!}B "),
),
assetDetail.exifInfo?.make != null
? ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
)
: Container()
],
),
)
: Container()
],
),
);
}
}

View file

@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
const TopControlAppBar({Key? key, required this.asset}) : super(key: key);
const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key);
final ImmichAsset asset;
final Function onMoreInfoPressed;
@override
Widget build(BuildContext context) {
double iconSize = 18.0;
@ -45,7 +45,7 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
print("show modal");
onMoreInfoPressed();
},
icon: const Icon(Icons.more_horiz_rounded))
],

View file

@ -1,5 +1,3 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@ -7,154 +5,80 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:photo_view/photo_view.dart';
// ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget {
final String imageUrl;
final String heroTag;
final String thumbnailUrl;
final ImmichAsset asset;
final AssetService _assetService = AssetService();
ImmichAssetWithExif? assetDetail;
ImageViewerPage(
{Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl, required this.asset})
: super(key: key);
final AssetService _assetService = AssetService();
@override
Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox);
getDetail() async {
var detail = await _assetService.getAssetById(asset.id);
getAssetExif() async {
assetDetail = await _assetService.getAssetById(asset.id);
}
useEffect(() {
getDetail();
return null;
getAssetExif();
}, []);
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: asset,
),
body: Builder(
builder: (context) {
return ListView(
children: [
Padding(
padding: const EdgeInsets.only(top: 60),
child: Dismissible(
direction: DismissDirection.down,
onDismissed: (_) {
AutoRouter.of(context).pop();
},
movementDuration: const Duration(milliseconds: 5),
resizeDuration: const Duration(milliseconds: 5),
key: Key(heroTag),
child: GestureDetector(
child: Center(
child: Hero(
tag: heroTag,
child: CachedNetworkImage(
fit: BoxFit.fill,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
errorWidget: (context, url, error) => const Icon(Icons.error),
placeholder: (context, url) {
return CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
placeholderFadeInDuration: const Duration(milliseconds: 0),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
);
},
),
),
),
),
),
),
Container(
decoration: const BoxDecoration(color: Colors.black),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height / 2,
child: Column(
children: [
Icon(
Icons.horizontal_rule_rounded,
color: Colors.grey[50],
size: 40,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ControlBoxButton(
iconData: Icons.delete_forever_rounded,
label: "Delete",
onPressed: () {},
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Divider(
endIndent: 10,
indent: 10,
thickness: 1,
color: Colors.grey[800],
),
)
],
),
),
],
);
},
));
}
}
class ControlBoxButton extends StatelessWidget {
const ControlBoxButton({Key? key, required this.label, required this.iconData, required this.onPressed})
: super(key: key);
final String label;
final IconData iconData;
final Function onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 60,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: () {
onPressed();
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: asset,
onMoreInfoPressed: () {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
});
},
),
body: Center(
child: Hero(
tag: heroTag,
child: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
errorWidget: (context, url, error) => const Icon(Icons.error),
imageBuilder: (context, imageProvider) {
return PhotoView(imageProvider: imageProvider);
},
placeholder: (context, url) {
return CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
placeholderFadeInDuration: const Duration(milliseconds: 0),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
);
},
icon: Icon(
iconData,
size: 25,
color: Colors.grey[50],
),
),
Text(
label,
style: TextStyle(fontSize: 10, color: Colors.grey[50]),
)
],
),
),
);
}

View file

@ -10,6 +10,7 @@ class ImmichAssetWithExif {
final String type;
final String createdAt;
final String modifiedAt;
final String originalPath;
final bool isFavorite;
final String? duration;
final ImmichExif? exifInfo;
@ -22,6 +23,7 @@ class ImmichAssetWithExif {
required this.type,
required this.createdAt,
required this.modifiedAt,
required this.originalPath,
required this.isFavorite,
this.duration,
this.exifInfo,
@ -35,6 +37,7 @@ class ImmichAssetWithExif {
String? type,
String? createdAt,
String? modifiedAt,
String? originalPath,
bool? isFavorite,
String? duration,
ImmichExif? exifInfo,
@ -47,6 +50,7 @@ class ImmichAssetWithExif {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
modifiedAt: modifiedAt ?? this.modifiedAt,
originalPath: originalPath ?? this.originalPath,
isFavorite: isFavorite ?? this.isFavorite,
duration: duration ?? this.duration,
exifInfo: exifInfo ?? this.exifInfo,
@ -62,6 +66,7 @@ class ImmichAssetWithExif {
'type': type,
'createdAt': createdAt,
'modifiedAt': modifiedAt,
'originalPath': originalPath,
'isFavorite': isFavorite,
'duration': duration,
'exifInfo': exifInfo?.toMap(),
@ -77,6 +82,7 @@ class ImmichAssetWithExif {
type: map['type'] ?? '',
createdAt: map['createdAt'] ?? '',
modifiedAt: map['modifiedAt'] ?? '',
originalPath: map['originalPath'] ?? '',
isFavorite: map['isFavorite'] ?? false,
duration: map['duration'],
exifInfo: map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
@ -89,7 +95,7 @@ class ImmichAssetWithExif {
@override
String toString() {
return 'ImmichAssetWithExif(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration, exifInfo: $exifInfo)';
return 'ImmichAssetWithExif(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, originalPath: $originalPath, isFavorite: $isFavorite, duration: $duration, exifInfo: $exifInfo)';
}
@override
@ -104,6 +110,7 @@ class ImmichAssetWithExif {
other.type == type &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.originalPath == originalPath &&
other.isFavorite == isFavorite &&
other.duration == duration &&
other.exifInfo == exifInfo;
@ -118,6 +125,7 @@ class ImmichAssetWithExif {
type.hashCode ^
createdAt.hashCode ^
modifiedAt.hashCode ^
originalPath.hashCode ^
isFavorite.hashCode ^
duration.hashCode ^
exifInfo.hashCode;

View file

@ -639,6 +639,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.10"
photo_view:
dependency: "direct main"
description:
name: photo_view
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.0"
platform:
dependency: transitive
description:

View file

@ -32,6 +32,7 @@ dependencies:
chewie: ^1.2.2
sliver_tools: ^0.2.5
badges: ^2.0.2
photo_view: ^0.13.0
dev_dependencies:
flutter_test: