-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented EXIF store and display (#19)
* Added EXIF extracting in the backend * Added EXIF displaying on `image_viewer_page.dart` * Added Icon for backup option not enable
- Loading branch information
1 parent
d149850
commit de1dbce
Showing
35 changed files
with
1,089 additions
and
844 deletions.
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import 'dart:convert'; | ||
|
||
class ImageViewerPageState { | ||
final bool isBottomSheetEnable; | ||
ImageViewerPageState({ | ||
required this.isBottomSheetEnable, | ||
}); | ||
|
||
ImageViewerPageState copyWith({ | ||
bool? isBottomSheetEnable, | ||
}) { | ||
return ImageViewerPageState( | ||
isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable, | ||
); | ||
} | ||
|
||
Map<String, dynamic> toMap() { | ||
return { | ||
'isBottomSheetEnable': isBottomSheetEnable, | ||
}; | ||
} | ||
|
||
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) { | ||
return ImageViewerPageState( | ||
isBottomSheetEnable: map['isBottomSheetEnable'] ?? false, | ||
); | ||
} | ||
|
||
String toJson() => json.encode(toMap()); | ||
|
||
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source)); | ||
|
||
@override | ||
String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)'; | ||
|
||
@override | ||
bool operator ==(Object other) { | ||
if (identical(this, other)) return true; | ||
|
||
return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable; | ||
} | ||
|
||
@override | ||
int get hashCode => isBottomSheetEnable.hashCode; | ||
} |
Empty file.
21 changes: 21 additions & 0 deletions
21
mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; | ||
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; | ||
import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||
|
||
class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> { | ||
ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false)); | ||
|
||
void toggleBottomSheet() { | ||
bool isBottomSheetEnable = state.isBottomSheetEnable; | ||
|
||
if (isBottomSheetEnable) { | ||
state.copyWith(isBottomSheetEnable: false); | ||
} else { | ||
state.copyWith(isBottomSheetEnable: true); | ||
} | ||
} | ||
} | ||
|
||
final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>( | ||
((ref) => ImageViewerPageStateNotifier())); |
Empty file.
118 changes: 118 additions & 0 deletions
118
mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
], | ||
), | ||
); | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import 'package:auto_route/auto_route.dart'; | ||
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, required this.onMoreInfoPressed}) : super(key: key); | ||
|
||
final ImmichAsset asset; | ||
final Function onMoreInfoPressed; | ||
@override | ||
Widget build(BuildContext context) { | ||
double iconSize = 18.0; | ||
|
||
return AppBar( | ||
foregroundColor: Colors.grey[100], | ||
toolbarHeight: 60, | ||
backgroundColor: Colors.black, | ||
leading: IconButton( | ||
onPressed: () { | ||
AutoRouter.of(context).pop(); | ||
}, | ||
icon: const Icon( | ||
Icons.arrow_back_ios_new_rounded, | ||
size: 20.0, | ||
), | ||
), | ||
actions: [ | ||
IconButton( | ||
iconSize: iconSize, | ||
splashRadius: iconSize, | ||
onPressed: () { | ||
print("backup"); | ||
}, | ||
icon: const Icon(Icons.backup_outlined), | ||
), | ||
IconButton( | ||
iconSize: iconSize, | ||
splashRadius: iconSize, | ||
onPressed: () { | ||
print("favorite"); | ||
}, | ||
icon: asset.isFavorite ? const Icon(Icons.favorite_rounded) : const Icon(Icons.favorite_border_rounded), | ||
), | ||
IconButton( | ||
iconSize: iconSize, | ||
splashRadius: iconSize, | ||
onPressed: () { | ||
onMoreInfoPressed(); | ||
}, | ||
icon: const Icon(Icons.more_horiz_rounded)) | ||
], | ||
); | ||
} | ||
|
||
@override | ||
Size get preferredSize => const Size.fromHeight(kToolbarHeight); | ||
} |
85 changes: 85 additions & 0 deletions
85
mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import 'package:auto_route/auto_route.dart'; | ||
import 'package:cached_network_image/cached_network_image.dart'; | ||
import 'package:flutter/material.dart'; | ||
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); | ||
|
||
@override | ||
Widget build(BuildContext context, WidgetRef ref) { | ||
var box = Hive.box(userInfoBox); | ||
|
||
getAssetExif() async { | ||
assetDetail = await _assetService.getAssetById(asset.id); | ||
} | ||
|
||
useEffect(() { | ||
getAssetExif(); | ||
}, []); | ||
|
||
return Scaffold( | ||
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), | ||
); | ||
}, | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.