feat: Add PhotoExif model and dialog for displaying EXIF data in gallery photos

+2
lib/models/gallery_photo.dart
··· 2 2 3 3 import 'aspect_ratio.dart'; 4 4 import 'gallery_state.dart'; 5 + import 'photo_exif.dart'; 5 6 6 7 part 'gallery_photo.freezed.dart'; 7 8 part 'gallery_photo.g.dart'; ··· 16 17 String? alt, 17 18 AspectRatio? aspectRatio, 18 19 GalleryState? gallery, 20 + PhotoExif? exif, 19 21 }) = _GalleryPhoto; 20 22 21 23 factory GalleryPhoto.fromJson(Map<String, dynamic> json) => _$GalleryPhotoFromJson(json);
+40 -2
lib/models/gallery_photo.freezed.dart
··· 28 28 String? get alt => throw _privateConstructorUsedError; 29 29 AspectRatio? get aspectRatio => throw _privateConstructorUsedError; 30 30 GalleryState? get gallery => throw _privateConstructorUsedError; 31 + PhotoExif? get exif => throw _privateConstructorUsedError; 31 32 32 33 /// Serializes this GalleryPhoto to a JSON map. 33 34 Map<String, dynamic> toJson() => throw _privateConstructorUsedError; ··· 54 55 String? alt, 55 56 AspectRatio? aspectRatio, 56 57 GalleryState? gallery, 58 + PhotoExif? exif, 57 59 }); 58 60 59 61 $AspectRatioCopyWith<$Res>? get aspectRatio; 60 62 $GalleryStateCopyWith<$Res>? get gallery; 63 + $PhotoExifCopyWith<$Res>? get exif; 61 64 } 62 65 63 66 /// @nodoc ··· 82 85 Object? alt = freezed, 83 86 Object? aspectRatio = freezed, 84 87 Object? gallery = freezed, 88 + Object? exif = freezed, 85 89 }) { 86 90 return _then( 87 91 _value.copyWith( ··· 113 117 ? _value.gallery 114 118 : gallery // ignore: cast_nullable_to_non_nullable 115 119 as GalleryState?, 120 + exif: freezed == exif 121 + ? _value.exif 122 + : exif // ignore: cast_nullable_to_non_nullable 123 + as PhotoExif?, 116 124 ) 117 125 as $Val, 118 126 ); ··· 145 153 return _then(_value.copyWith(gallery: value) as $Val); 146 154 }); 147 155 } 156 + 157 + /// Create a copy of GalleryPhoto 158 + /// with the given fields replaced by the non-null parameter values. 159 + @override 160 + @pragma('vm:prefer-inline') 161 + $PhotoExifCopyWith<$Res>? get exif { 162 + if (_value.exif == null) { 163 + return null; 164 + } 165 + 166 + return $PhotoExifCopyWith<$Res>(_value.exif!, (value) { 167 + return _then(_value.copyWith(exif: value) as $Val); 168 + }); 169 + } 148 170 } 149 171 150 172 /// @nodoc ··· 164 186 String? alt, 165 187 AspectRatio? aspectRatio, 166 188 GalleryState? gallery, 189 + PhotoExif? exif, 167 190 }); 168 191 169 192 @override 170 193 $AspectRatioCopyWith<$Res>? get aspectRatio; 171 194 @override 172 195 $GalleryStateCopyWith<$Res>? get gallery; 196 + @override 197 + $PhotoExifCopyWith<$Res>? get exif; 173 198 } 174 199 175 200 /// @nodoc ··· 193 218 Object? alt = freezed, 194 219 Object? aspectRatio = freezed, 195 220 Object? gallery = freezed, 221 + Object? exif = freezed, 196 222 }) { 197 223 return _then( 198 224 _$GalleryPhotoImpl( ··· 224 250 ? _value.gallery 225 251 : gallery // ignore: cast_nullable_to_non_nullable 226 252 as GalleryState?, 253 + exif: freezed == exif 254 + ? _value.exif 255 + : exif // ignore: cast_nullable_to_non_nullable 256 + as PhotoExif?, 227 257 ), 228 258 ); 229 259 } ··· 240 270 this.alt, 241 271 this.aspectRatio, 242 272 this.gallery, 273 + this.exif, 243 274 }); 244 275 245 276 factory _$GalleryPhotoImpl.fromJson(Map<String, dynamic> json) => ··· 259 290 final AspectRatio? aspectRatio; 260 291 @override 261 292 final GalleryState? gallery; 293 + @override 294 + final PhotoExif? exif; 262 295 263 296 @override 264 297 String toString() { 265 - return 'GalleryPhoto(uri: $uri, cid: $cid, thumb: $thumb, fullsize: $fullsize, alt: $alt, aspectRatio: $aspectRatio, gallery: $gallery)'; 298 + return 'GalleryPhoto(uri: $uri, cid: $cid, thumb: $thumb, fullsize: $fullsize, alt: $alt, aspectRatio: $aspectRatio, gallery: $gallery, exif: $exif)'; 266 299 } 267 300 268 301 @override ··· 278 311 (identical(other.alt, alt) || other.alt == alt) && 279 312 (identical(other.aspectRatio, aspectRatio) || 280 313 other.aspectRatio == aspectRatio) && 281 - (identical(other.gallery, gallery) || other.gallery == gallery)); 314 + (identical(other.gallery, gallery) || other.gallery == gallery) && 315 + (identical(other.exif, exif) || other.exif == exif)); 282 316 } 283 317 284 318 @JsonKey(includeFromJson: false, includeToJson: false) ··· 292 326 alt, 293 327 aspectRatio, 294 328 gallery, 329 + exif, 295 330 ); 296 331 297 332 /// Create a copy of GalleryPhoto ··· 317 352 final String? alt, 318 353 final AspectRatio? aspectRatio, 319 354 final GalleryState? gallery, 355 + final PhotoExif? exif, 320 356 }) = _$GalleryPhotoImpl; 321 357 322 358 factory _GalleryPhoto.fromJson(Map<String, dynamic> json) = ··· 336 372 AspectRatio? get aspectRatio; 337 373 @override 338 374 GalleryState? get gallery; 375 + @override 376 + PhotoExif? get exif; 339 377 340 378 /// Create a copy of GalleryPhoto 341 379 /// with the given fields replaced by the non-null parameter values.
+4
lib/models/gallery_photo.g.dart
··· 19 19 gallery: json['gallery'] == null 20 20 ? null 21 21 : GalleryState.fromJson(json['gallery'] as Map<String, dynamic>), 22 + exif: json['exif'] == null 23 + ? null 24 + : PhotoExif.fromJson(json['exif'] as Map<String, dynamic>), 22 25 ); 23 26 24 27 Map<String, dynamic> _$$GalleryPhotoImplToJson(_$GalleryPhotoImpl instance) => ··· 30 33 'alt': instance.alt, 31 34 'aspectRatio': instance.aspectRatio, 32 35 'gallery': instance.gallery, 36 + 'exif': instance.exif, 33 37 };
+5 -3
lib/models/photo_exif.dart
··· 8 8 const factory PhotoExif({ 9 9 required String photo, // at-uri 10 10 required String createdAt, // datetime 11 + String? uri, // at-uri 12 + String? cid, // cid 11 13 String? dateTimeOriginal, // datetime 12 - int? exposureTime, 13 - int? fNumber, 14 + String? exposureTime, 15 + String? fNumber, 14 16 String? flash, 15 - int? focalLengthIn35mmFormat, 17 + String? focalLengthIn35mmFormat, 16 18 int? iSO, 17 19 String? lensMake, 18 20 String? lensModel,
+69 -25
lib/models/photo_exif.freezed.dart
··· 23 23 mixin _$PhotoExif { 24 24 String get photo => throw _privateConstructorUsedError; // at-uri 25 25 String get createdAt => throw _privateConstructorUsedError; // datetime 26 + String? get uri => throw _privateConstructorUsedError; // at-uri 27 + String? get cid => throw _privateConstructorUsedError; // cid 26 28 String? get dateTimeOriginal => 27 29 throw _privateConstructorUsedError; // datetime 28 - int? get exposureTime => throw _privateConstructorUsedError; 29 - int? get fNumber => throw _privateConstructorUsedError; 30 + String? get exposureTime => throw _privateConstructorUsedError; 31 + String? get fNumber => throw _privateConstructorUsedError; 30 32 String? get flash => throw _privateConstructorUsedError; 31 - int? get focalLengthIn35mmFormat => throw _privateConstructorUsedError; 33 + String? get focalLengthIn35mmFormat => throw _privateConstructorUsedError; 32 34 int? get iSO => throw _privateConstructorUsedError; 33 35 String? get lensMake => throw _privateConstructorUsedError; 34 36 String? get lensModel => throw _privateConstructorUsedError; ··· 53 55 $Res call({ 54 56 String photo, 55 57 String createdAt, 58 + String? uri, 59 + String? cid, 56 60 String? dateTimeOriginal, 57 - int? exposureTime, 58 - int? fNumber, 61 + String? exposureTime, 62 + String? fNumber, 59 63 String? flash, 60 - int? focalLengthIn35mmFormat, 64 + String? focalLengthIn35mmFormat, 61 65 int? iSO, 62 66 String? lensMake, 63 67 String? lensModel, ··· 83 87 $Res call({ 84 88 Object? photo = null, 85 89 Object? createdAt = null, 90 + Object? uri = freezed, 91 + Object? cid = freezed, 86 92 Object? dateTimeOriginal = freezed, 87 93 Object? exposureTime = freezed, 88 94 Object? fNumber = freezed, ··· 104 110 ? _value.createdAt 105 111 : createdAt // ignore: cast_nullable_to_non_nullable 106 112 as String, 113 + uri: freezed == uri 114 + ? _value.uri 115 + : uri // ignore: cast_nullable_to_non_nullable 116 + as String?, 117 + cid: freezed == cid 118 + ? _value.cid 119 + : cid // ignore: cast_nullable_to_non_nullable 120 + as String?, 107 121 dateTimeOriginal: freezed == dateTimeOriginal 108 122 ? _value.dateTimeOriginal 109 123 : dateTimeOriginal // ignore: cast_nullable_to_non_nullable ··· 111 125 exposureTime: freezed == exposureTime 112 126 ? _value.exposureTime 113 127 : exposureTime // ignore: cast_nullable_to_non_nullable 114 - as int?, 128 + as String?, 115 129 fNumber: freezed == fNumber 116 130 ? _value.fNumber 117 131 : fNumber // ignore: cast_nullable_to_non_nullable 118 - as int?, 132 + as String?, 119 133 flash: freezed == flash 120 134 ? _value.flash 121 135 : flash // ignore: cast_nullable_to_non_nullable ··· 123 137 focalLengthIn35mmFormat: freezed == focalLengthIn35mmFormat 124 138 ? _value.focalLengthIn35mmFormat 125 139 : focalLengthIn35mmFormat // ignore: cast_nullable_to_non_nullable 126 - as int?, 140 + as String?, 127 141 iSO: freezed == iSO 128 142 ? _value.iSO 129 143 : iSO // ignore: cast_nullable_to_non_nullable ··· 162 176 $Res call({ 163 177 String photo, 164 178 String createdAt, 179 + String? uri, 180 + String? cid, 165 181 String? dateTimeOriginal, 166 - int? exposureTime, 167 - int? fNumber, 182 + String? exposureTime, 183 + String? fNumber, 168 184 String? flash, 169 - int? focalLengthIn35mmFormat, 185 + String? focalLengthIn35mmFormat, 170 186 int? iSO, 171 187 String? lensMake, 172 188 String? lensModel, ··· 191 207 $Res call({ 192 208 Object? photo = null, 193 209 Object? createdAt = null, 210 + Object? uri = freezed, 211 + Object? cid = freezed, 194 212 Object? dateTimeOriginal = freezed, 195 213 Object? exposureTime = freezed, 196 214 Object? fNumber = freezed, ··· 212 230 ? _value.createdAt 213 231 : createdAt // ignore: cast_nullable_to_non_nullable 214 232 as String, 233 + uri: freezed == uri 234 + ? _value.uri 235 + : uri // ignore: cast_nullable_to_non_nullable 236 + as String?, 237 + cid: freezed == cid 238 + ? _value.cid 239 + : cid // ignore: cast_nullable_to_non_nullable 240 + as String?, 215 241 dateTimeOriginal: freezed == dateTimeOriginal 216 242 ? _value.dateTimeOriginal 217 243 : dateTimeOriginal // ignore: cast_nullable_to_non_nullable ··· 219 245 exposureTime: freezed == exposureTime 220 246 ? _value.exposureTime 221 247 : exposureTime // ignore: cast_nullable_to_non_nullable 222 - as int?, 248 + as String?, 223 249 fNumber: freezed == fNumber 224 250 ? _value.fNumber 225 251 : fNumber // ignore: cast_nullable_to_non_nullable 226 - as int?, 252 + as String?, 227 253 flash: freezed == flash 228 254 ? _value.flash 229 255 : flash // ignore: cast_nullable_to_non_nullable ··· 231 257 focalLengthIn35mmFormat: freezed == focalLengthIn35mmFormat 232 258 ? _value.focalLengthIn35mmFormat 233 259 : focalLengthIn35mmFormat // ignore: cast_nullable_to_non_nullable 234 - as int?, 260 + as String?, 235 261 iSO: freezed == iSO 236 262 ? _value.iSO 237 263 : iSO // ignore: cast_nullable_to_non_nullable ··· 263 289 const _$PhotoExifImpl({ 264 290 required this.photo, 265 291 required this.createdAt, 292 + this.uri, 293 + this.cid, 266 294 this.dateTimeOriginal, 267 295 this.exposureTime, 268 296 this.fNumber, ··· 285 313 final String createdAt; 286 314 // datetime 287 315 @override 316 + final String? uri; 317 + // at-uri 318 + @override 319 + final String? cid; 320 + // cid 321 + @override 288 322 final String? dateTimeOriginal; 289 323 // datetime 290 324 @override 291 - final int? exposureTime; 325 + final String? exposureTime; 292 326 @override 293 - final int? fNumber; 327 + final String? fNumber; 294 328 @override 295 329 final String? flash; 296 330 @override 297 - final int? focalLengthIn35mmFormat; 331 + final String? focalLengthIn35mmFormat; 298 332 @override 299 333 final int? iSO; 300 334 @override ··· 308 342 309 343 @override 310 344 String toString() { 311 - return 'PhotoExif(photo: $photo, createdAt: $createdAt, dateTimeOriginal: $dateTimeOriginal, exposureTime: $exposureTime, fNumber: $fNumber, flash: $flash, focalLengthIn35mmFormat: $focalLengthIn35mmFormat, iSO: $iSO, lensMake: $lensMake, lensModel: $lensModel, make: $make, model: $model)'; 345 + return 'PhotoExif(photo: $photo, createdAt: $createdAt, uri: $uri, cid: $cid, dateTimeOriginal: $dateTimeOriginal, exposureTime: $exposureTime, fNumber: $fNumber, flash: $flash, focalLengthIn35mmFormat: $focalLengthIn35mmFormat, iSO: $iSO, lensMake: $lensMake, lensModel: $lensModel, make: $make, model: $model)'; 312 346 } 313 347 314 348 @override ··· 319 353 (identical(other.photo, photo) || other.photo == photo) && 320 354 (identical(other.createdAt, createdAt) || 321 355 other.createdAt == createdAt) && 356 + (identical(other.uri, uri) || other.uri == uri) && 357 + (identical(other.cid, cid) || other.cid == cid) && 322 358 (identical(other.dateTimeOriginal, dateTimeOriginal) || 323 359 other.dateTimeOriginal == dateTimeOriginal) && 324 360 (identical(other.exposureTime, exposureTime) || ··· 345 381 runtimeType, 346 382 photo, 347 383 createdAt, 384 + uri, 385 + cid, 348 386 dateTimeOriginal, 349 387 exposureTime, 350 388 fNumber, ··· 375 413 const factory _PhotoExif({ 376 414 required final String photo, 377 415 required final String createdAt, 416 + final String? uri, 417 + final String? cid, 378 418 final String? dateTimeOriginal, 379 - final int? exposureTime, 380 - final int? fNumber, 419 + final String? exposureTime, 420 + final String? fNumber, 381 421 final String? flash, 382 - final int? focalLengthIn35mmFormat, 422 + final String? focalLengthIn35mmFormat, 383 423 final int? iSO, 384 424 final String? lensMake, 385 425 final String? lensModel, ··· 395 435 @override 396 436 String get createdAt; // datetime 397 437 @override 438 + String? get uri; // at-uri 439 + @override 440 + String? get cid; // cid 441 + @override 398 442 String? get dateTimeOriginal; // datetime 399 443 @override 400 - int? get exposureTime; 444 + String? get exposureTime; 401 445 @override 402 - int? get fNumber; 446 + String? get fNumber; 403 447 @override 404 448 String? get flash; 405 449 @override 406 - int? get focalLengthIn35mmFormat; 450 + String? get focalLengthIn35mmFormat; 407 451 @override 408 452 int? get iSO; 409 453 @override
+7 -4
lib/models/photo_exif.g.dart
··· 10 10 _$PhotoExifImpl( 11 11 photo: json['photo'] as String, 12 12 createdAt: json['createdAt'] as String, 13 + uri: json['uri'] as String?, 14 + cid: json['cid'] as String?, 13 15 dateTimeOriginal: json['dateTimeOriginal'] as String?, 14 - exposureTime: (json['exposureTime'] as num?)?.toInt(), 15 - fNumber: (json['fNumber'] as num?)?.toInt(), 16 + exposureTime: json['exposureTime'] as String?, 17 + fNumber: json['fNumber'] as String?, 16 18 flash: json['flash'] as String?, 17 - focalLengthIn35mmFormat: (json['focalLengthIn35mmFormat'] as num?) 18 - ?.toInt(), 19 + focalLengthIn35mmFormat: json['focalLengthIn35mmFormat'] as String?, 19 20 iSO: (json['iSO'] as num?)?.toInt(), 20 21 lensMake: json['lensMake'] as String?, 21 22 lensModel: json['lensModel'] as String?, ··· 27 28 <String, dynamic>{ 28 29 'photo': instance.photo, 29 30 'createdAt': instance.createdAt, 31 + 'uri': instance.uri, 32 + 'cid': instance.cid, 30 33 'dateTimeOriginal': instance.dateTimeOriginal, 31 34 'exposureTime': instance.exposureTime, 32 35 'fNumber': instance.fNumber,
+1 -1
lib/providers/gallery_cache_provider.g.dart
··· 6 6 // RiverpodGenerator 7 7 // ************************************************************************** 8 8 9 - String _$galleryCacheHash() => r'00313970bd3f1f3b181afc2bc69dd77f0d9fdf8b'; 9 + String _$galleryCacheHash() => r'cd0665a1f246bd700195cf5ce50893ba73b878f9'; 10 10 11 11 /// Holds a cache of galleries by URI. 12 12 ///
+67 -44
lib/widgets/gallery_photo_view.dart
··· 6 6 import 'package:grain/widgets/add_comment_button.dart'; 7 7 import 'package:grain/widgets/add_comment_sheet.dart'; 8 8 import 'package:grain/widgets/app_image.dart'; 9 + import 'package:grain/widgets/photo_exif_dialog.dart'; 9 10 10 11 class GalleryPhotoView extends ConsumerStatefulWidget { 11 12 final List<GalleryPhoto> photos; ··· 98 99 top: false, 99 100 child: Padding( 100 101 padding: const EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 8), 101 - child: AddCommentButton( 102 - onPressed: () async { 103 - final photo = widget.photos[_currentIndex]; 104 - final creator = widget.gallery?.creator; 105 - final replyTo = { 106 - 'author': creator != null 107 - ? { 108 - 'avatar': creator.avatar, 109 - 'displayName': creator.displayName, 110 - 'handle': creator.handle, 111 - } 112 - : {'avatar': null, 'displayName': '', 'handle': ''}, 113 - 'focus': photo, 114 - 'text': '', 115 - }; 116 - bool commentPosted = false; 117 - await showAddCommentSheet( 118 - context, 119 - gallery: null, 120 - replyTo: replyTo, 121 - initialText: '', 122 - onSubmit: (text) async { 123 - final photo = widget.photos[_currentIndex]; 124 - final gallery = widget.gallery; 125 - final subject = gallery?.uri; 126 - final focus = photo.uri; 127 - if (subject == null || focus == null) { 128 - return; 129 - } 130 - // Use the provider's createComment method 131 - final notifier = ref.read(galleryThreadProvider(subject).notifier); 132 - final success = await notifier.createComment(text: text, focus: focus); 133 - if (success) commentPosted = true; 134 - // Sheet will pop itself 135 - }, 136 - ); 137 - // After sheet closes, notify parent if a comment was posted 138 - if (commentPosted && widget.gallery?.uri != null) { 139 - widget.onClose?.call(); // Remove GalleryPhotoView overlay 140 - widget.onCommentPosted?.call(widget.gallery!.uri); 141 - } 142 - }, 143 - backgroundColor: Colors.grey[900], 144 - foregroundColor: Colors.white, 102 + child: Row( 103 + children: [ 104 + Expanded( 105 + child: AddCommentButton( 106 + onPressed: () async { 107 + final photo = widget.photos[_currentIndex]; 108 + final creator = widget.gallery?.creator; 109 + final replyTo = { 110 + 'author': creator != null 111 + ? { 112 + 'avatar': creator.avatar, 113 + 'displayName': creator.displayName, 114 + 'handle': creator.handle, 115 + } 116 + : {'avatar': null, 'displayName': '', 'handle': ''}, 117 + 'focus': photo, 118 + 'text': '', 119 + }; 120 + bool commentPosted = false; 121 + await showAddCommentSheet( 122 + context, 123 + gallery: null, 124 + replyTo: replyTo, 125 + initialText: '', 126 + onSubmit: (text) async { 127 + final photo = widget.photos[_currentIndex]; 128 + final gallery = widget.gallery; 129 + final subject = gallery?.uri; 130 + final focus = photo.uri; 131 + if (subject == null || focus == null) { 132 + return; 133 + } 134 + // Use the provider's createComment method 135 + final notifier = ref.read( 136 + galleryThreadProvider(subject).notifier, 137 + ); 138 + final success = await notifier.createComment( 139 + text: text, 140 + focus: focus, 141 + ); 142 + if (success) commentPosted = true; 143 + // Sheet will pop itself 144 + }, 145 + ); 146 + // After sheet closes, notify parent if a comment was posted 147 + if (commentPosted && widget.gallery?.uri != null) { 148 + widget.onClose?.call(); // Remove GalleryPhotoView overlay 149 + widget.onCommentPosted?.call(widget.gallery!.uri); 150 + } 151 + }, 152 + backgroundColor: Colors.grey[900], 153 + foregroundColor: Colors.white, 154 + ), 155 + ), 156 + const SizedBox(width: 12), 157 + if (photo.exif != null) 158 + IconButton( 159 + icon: Icon(Icons.camera_alt, color: Colors.white), 160 + onPressed: () { 161 + showDialog( 162 + context: context, 163 + builder: (context) => PhotoExifDialog(exif: photo.exif!), 164 + ); 165 + }, 166 + ), 167 + ], 145 168 ), 146 169 ), 147 170 ),
+187
lib/widgets/photo_exif_dialog.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:grain/models/photo_exif.dart'; 3 + 4 + class PhotoExifDialog extends StatelessWidget { 5 + final PhotoExif exif; 6 + const PhotoExifDialog({super.key, required this.exif}); 7 + 8 + @override 9 + Widget build(BuildContext context) { 10 + return Dialog( 11 + backgroundColor: Colors.black45, 12 + child: Padding( 13 + padding: const EdgeInsets.all(20), 14 + child: SingleChildScrollView( 15 + child: Column( 16 + crossAxisAlignment: CrossAxisAlignment.start, 17 + children: [ 18 + Text( 19 + 'Camera Settings', 20 + style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), 21 + ), 22 + const SizedBox(height: 16), 23 + if (exif.make != null) 24 + RichText( 25 + text: TextSpan( 26 + children: [ 27 + TextSpan( 28 + text: 'Make: ', 29 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 30 + ), 31 + TextSpan( 32 + text: exif.make!, 33 + style: TextStyle(color: Colors.white), 34 + ), 35 + ], 36 + ), 37 + ), 38 + if (exif.model != null) 39 + RichText( 40 + text: TextSpan( 41 + children: [ 42 + TextSpan( 43 + text: 'Model: ', 44 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 45 + ), 46 + TextSpan( 47 + text: exif.model!, 48 + style: TextStyle(color: Colors.white), 49 + ), 50 + ], 51 + ), 52 + ), 53 + if (exif.lensMake != null) 54 + RichText( 55 + text: TextSpan( 56 + children: [ 57 + TextSpan( 58 + text: 'Lens Make: ', 59 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 60 + ), 61 + TextSpan( 62 + text: exif.lensMake!, 63 + style: TextStyle(color: Colors.white), 64 + ), 65 + ], 66 + ), 67 + ), 68 + if (exif.lensModel != null) 69 + RichText( 70 + text: TextSpan( 71 + children: [ 72 + TextSpan( 73 + text: 'Lens Model: ', 74 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 75 + ), 76 + TextSpan( 77 + text: exif.lensModel!, 78 + style: TextStyle(color: Colors.white), 79 + ), 80 + ], 81 + ), 82 + ), 83 + if (exif.fNumber != null) 84 + RichText( 85 + text: TextSpan( 86 + children: [ 87 + TextSpan( 88 + text: 'F Number: ', 89 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 90 + ), 91 + TextSpan( 92 + text: exif.fNumber!, 93 + style: TextStyle(color: Colors.white), 94 + ), 95 + ], 96 + ), 97 + ), 98 + if (exif.focalLengthIn35mmFormat != null) 99 + RichText( 100 + text: TextSpan( 101 + children: [ 102 + TextSpan( 103 + text: 'Focal Length (35mm): ', 104 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 105 + ), 106 + TextSpan( 107 + text: exif.focalLengthIn35mmFormat!, 108 + style: TextStyle(color: Colors.white), 109 + ), 110 + ], 111 + ), 112 + ), 113 + if (exif.exposureTime != null) 114 + RichText( 115 + text: TextSpan( 116 + children: [ 117 + TextSpan( 118 + text: 'Exposure Time: ', 119 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 120 + ), 121 + TextSpan( 122 + text: exif.exposureTime!, 123 + style: TextStyle(color: Colors.white), 124 + ), 125 + ], 126 + ), 127 + ), 128 + if (exif.iSO != null) 129 + RichText( 130 + text: TextSpan( 131 + children: [ 132 + TextSpan( 133 + text: 'ISO: ', 134 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 135 + ), 136 + TextSpan( 137 + text: exif.iSO.toString(), 138 + style: TextStyle(color: Colors.white), 139 + ), 140 + ], 141 + ), 142 + ), 143 + if (exif.flash != null) 144 + RichText( 145 + text: TextSpan( 146 + children: [ 147 + TextSpan( 148 + text: 'Flash: ', 149 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 150 + ), 151 + TextSpan( 152 + text: exif.flash!, 153 + style: TextStyle(color: Colors.white), 154 + ), 155 + ], 156 + ), 157 + ), 158 + if (exif.dateTimeOriginal != null) 159 + RichText( 160 + text: TextSpan( 161 + children: [ 162 + TextSpan( 163 + text: 'DateTime Original: ', 164 + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), 165 + ), 166 + TextSpan( 167 + text: exif.dateTimeOriginal!, 168 + style: TextStyle(color: Colors.white), 169 + ), 170 + ], 171 + ), 172 + ), 173 + const SizedBox(height: 20), 174 + Align( 175 + alignment: Alignment.centerRight, 176 + child: TextButton( 177 + onPressed: () => Navigator.of(context).pop(), 178 + child: Text('Close', style: TextStyle(color: Colors.white)), 179 + ), 180 + ), 181 + ], 182 + ), 183 + ), 184 + ), 185 + ); 186 + } 187 + }