ソースを参照

Display EXIF in image viewer, refactor

Tran, Alex 3 年 前
コミット
5e59bda33b

+ 0 - 0
mobile/lib/modules/asset_viewer/ui/bottom_control_app_bar.dart


+ 118 - 0
mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart

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

+ 3 - 3
mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart

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

+ 46 - 122
mobile/lib/modules/asset_viewer/views/image_viewer_page.dart

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

+ 9 - 1
mobile/lib/shared/models/immich_asset_with_exif.model.dart

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

+ 7 - 0
mobile/pubspec.lock

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

+ 1 - 0
mobile/pubspec.yaml

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