mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:27:08 -04:00 
			
		
		
		
	* feat: resurrect advanced info * display null values as well * add exif details --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
		
			
				
	
	
		
			346 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:auto_route/auto_route.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
 | |
| import 'package:immich_mobile/domain/models/exif.model.dart';
 | |
| import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
 | |
| 
 | |
| @RoutePage()
 | |
| class AssetTroubleshootPage extends ConsumerWidget {
 | |
|   final BaseAsset asset;
 | |
| 
 | |
|   const AssetTroubleshootPage({super.key, required this.asset});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     return Scaffold(
 | |
|       appBar: AppBar(title: const Text("Asset Troubleshoot")),
 | |
|       body: SingleChildScrollView(
 | |
|         child: Padding(
 | |
|           padding: const EdgeInsets.all(16.0),
 | |
|           child: _AssetDetailsView(asset: asset),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _AssetDetailsView extends ConsumerWidget {
 | |
|   final BaseAsset asset;
 | |
| 
 | |
|   const _AssetDetailsView({required this.asset});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     return Column(
 | |
|       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|       children: [
 | |
|         _AssetPropertiesSection(asset: asset),
 | |
|         const SizedBox(height: 16),
 | |
|         Text('Matching Assets', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
 | |
|         if (asset.checksum != null) ...[
 | |
|           _LocalAssetsSection(asset: asset),
 | |
|           const SizedBox(height: 16),
 | |
|           _RemoteAssetSection(asset: asset),
 | |
|         ] else ...[
 | |
|           const _PropertySectionCard(
 | |
|             title: 'Local Assets',
 | |
|             properties: [_PropertyItem(label: 'Status', value: 'No checksum available - cannot fetch local assets')],
 | |
|           ),
 | |
|           const SizedBox(height: 16),
 | |
|           const _PropertySectionCard(
 | |
|             title: 'Remote Assets',
 | |
|             properties: [_PropertyItem(label: 'Status', value: 'No checksum available - cannot fetch remote asset')],
 | |
|           ),
 | |
|         ],
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _AssetPropertiesSection extends ConsumerStatefulWidget {
 | |
|   final BaseAsset asset;
 | |
| 
 | |
|   const _AssetPropertiesSection({required this.asset});
 | |
| 
 | |
|   @override
 | |
|   ConsumerState createState() => _AssetPropertiesSectionState();
 | |
| }
 | |
| 
 | |
| class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection> {
 | |
|   List<_PropertyItem> properties = [];
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _buildAssetProperties(widget.asset).whenComplete(() {
 | |
|       if (mounted) {
 | |
|         setState(() {});
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final title = _getAssetTypeTitle(widget.asset);
 | |
| 
 | |
|     return _PropertySectionCard(title: title, properties: properties);
 | |
|   }
 | |
| 
 | |
|   Future<void> _buildAssetProperties(BaseAsset asset) async {
 | |
|     _addCommonProperties();
 | |
| 
 | |
|     if (asset is LocalAsset) {
 | |
|       await _addLocalAssetProperties(asset);
 | |
|     } else if (asset is RemoteAsset) {
 | |
|       await _addRemoteAssetProperties(asset);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _addCommonProperties() {
 | |
|     final asset = widget.asset;
 | |
|     properties.addAll([
 | |
|       _PropertyItem(label: 'Name', value: asset.name),
 | |
|       _PropertyItem(label: 'Checksum', value: asset.checksum),
 | |
|       _PropertyItem(label: 'Type', value: asset.type.toString()),
 | |
|       _PropertyItem(label: 'Created At', value: asset.createdAt.toString()),
 | |
|       _PropertyItem(label: 'Updated At', value: asset.updatedAt.toString()),
 | |
|       _PropertyItem(label: 'Width', value: asset.width?.toString()),
 | |
|       _PropertyItem(label: 'Height', value: asset.height?.toString()),
 | |
|       _PropertyItem(
 | |
|         label: 'Duration',
 | |
|         value: asset.durationInSeconds != null ? '${asset.durationInSeconds} seconds' : null,
 | |
|       ),
 | |
|       _PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
 | |
|       _PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Future<void> _addLocalAssetProperties(LocalAsset asset) async {
 | |
|     properties.insertAll(0, [
 | |
|       _PropertyItem(label: 'Local ID', value: asset.id),
 | |
|       _PropertyItem(label: 'Remote ID', value: asset.remoteId),
 | |
|     ]);
 | |
| 
 | |
|     properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
 | |
|     final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
 | |
|     properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
 | |
|   }
 | |
| 
 | |
|   Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {
 | |
|     properties.insertAll(0, [
 | |
|       _PropertyItem(label: 'Remote ID', value: asset.id),
 | |
|       _PropertyItem(label: 'Local ID', value: asset.localId),
 | |
|       _PropertyItem(label: 'Owner ID', value: asset.ownerId),
 | |
|     ]);
 | |
| 
 | |
|     final additionalProps = <_PropertyItem>[
 | |
|       _PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
 | |
|       _PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
 | |
|       _PropertyItem(label: 'Stack ID', value: asset.stackId),
 | |
|     ];
 | |
| 
 | |
|     properties.insertAll(4, additionalProps);
 | |
| 
 | |
|     final exif = await ref.read(assetServiceProvider).getExif(asset);
 | |
|     if (exif != null) {
 | |
|       _addExifProperties(exif);
 | |
|     } else {
 | |
|       properties.add(const _PropertyItem(label: 'EXIF', value: null));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _addExifProperties(ExifInfo exif) {
 | |
|     properties.addAll([
 | |
|       _PropertyItem(
 | |
|         label: 'File Size',
 | |
|         value: exif.fileSize != null ? '${(exif.fileSize! / 1024 / 1024).toStringAsFixed(2)} MB' : null,
 | |
|       ),
 | |
|       _PropertyItem(label: 'Description', value: exif.description),
 | |
|       _PropertyItem(label: 'EXIF Width', value: exif.width?.toString()),
 | |
|       _PropertyItem(label: 'EXIF Height', value: exif.height?.toString()),
 | |
|       _PropertyItem(label: 'Date Taken', value: exif.dateTimeOriginal?.toString()),
 | |
|       _PropertyItem(label: 'Time Zone', value: exif.timeZone),
 | |
|       _PropertyItem(label: 'Camera Make', value: exif.make),
 | |
|       _PropertyItem(label: 'Camera Model', value: exif.model),
 | |
|       _PropertyItem(label: 'Lens', value: exif.lens),
 | |
|       _PropertyItem(label: 'F-Number', value: exif.f != null ? 'f/${exif.fNumber}' : null),
 | |
|       _PropertyItem(label: 'Focal Length', value: exif.mm != null ? '${exif.focalLength}mm' : null),
 | |
|       _PropertyItem(label: 'ISO', value: exif.iso?.toString()),
 | |
|       _PropertyItem(label: 'Exposure Time', value: exif.exposureTime.isNotEmpty ? exif.exposureTime : null),
 | |
|       _PropertyItem(
 | |
|         label: 'GPS Coordinates',
 | |
|         value: exif.hasCoordinates ? '${exif.latitude}, ${exif.longitude}' : null,
 | |
|       ),
 | |
|       _PropertyItem(
 | |
|         label: 'Location',
 | |
|         value: [exif.city, exif.state, exif.country].where((e) => e != null && e.isNotEmpty).join(', '),
 | |
|       ),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   String _getAssetTypeTitle(BaseAsset asset) {
 | |
|     if (asset is LocalAsset) return 'Local Asset';
 | |
|     if (asset is RemoteAsset) return 'Remote Asset';
 | |
|     return 'Base Asset';
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _LocalAssetsSection extends ConsumerWidget {
 | |
|   final BaseAsset asset;
 | |
| 
 | |
|   const _LocalAssetsSection({required this.asset});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final assetService = ref.watch(assetServiceProvider);
 | |
| 
 | |
|     return FutureBuilder<List<LocalAsset?>>(
 | |
|       future: assetService.getLocalAssetsByChecksum(asset.checksum!),
 | |
|       builder: (context, snapshot) {
 | |
|         if (snapshot.connectionState == ConnectionState.waiting) {
 | |
|           return const _PropertySectionCard(
 | |
|             title: 'Local Assets',
 | |
|             properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         if (snapshot.hasError) {
 | |
|           return _PropertySectionCard(
 | |
|             title: 'Local Assets',
 | |
|             properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         final localAssets = snapshot.data?.cast<LocalAsset>() ?? [];
 | |
|         if (asset is LocalAsset) {
 | |
|           localAssets.removeWhere((a) => a.id == (asset as LocalAsset).id);
 | |
| 
 | |
|           if (localAssets.isEmpty) {
 | |
|             return const SizedBox.shrink();
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (localAssets.isEmpty) {
 | |
|           return const _PropertySectionCard(
 | |
|             title: 'Local Assets',
 | |
|             properties: [_PropertyItem(label: 'Status', value: 'No local assets found with this checksum')],
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         return Column(
 | |
|           children: [
 | |
|             if (localAssets.length > 1)
 | |
|               _PropertySectionCard(
 | |
|                 title: 'Local Assets Summary',
 | |
|                 properties: [_PropertyItem(label: 'Total Count', value: localAssets.length.toString())],
 | |
|               ),
 | |
|             ...localAssets.map((localAsset) {
 | |
|               return Padding(
 | |
|                 padding: const EdgeInsets.only(top: 16),
 | |
|                 child: _AssetPropertiesSection(asset: localAsset),
 | |
|               );
 | |
|             }),
 | |
|           ],
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _RemoteAssetSection extends ConsumerWidget {
 | |
|   final BaseAsset asset;
 | |
| 
 | |
|   const _RemoteAssetSection({required this.asset});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final assetService = ref.watch(assetServiceProvider);
 | |
| 
 | |
|     if (asset is RemoteAsset) {
 | |
|       return const SizedBox.shrink();
 | |
|     }
 | |
| 
 | |
|     return FutureBuilder<RemoteAsset?>(
 | |
|       future: assetService.getRemoteAssetByChecksum(asset.checksum!),
 | |
|       builder: (context, snapshot) {
 | |
|         if (snapshot.connectionState == ConnectionState.waiting) {
 | |
|           return const _PropertySectionCard(
 | |
|             title: 'Remote Assets',
 | |
|             properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         if (snapshot.hasError) {
 | |
|           return _PropertySectionCard(
 | |
|             title: 'Remote Assets',
 | |
|             properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         final remoteAsset = snapshot.data;
 | |
| 
 | |
|         if (remoteAsset == null) {
 | |
|           return const _PropertySectionCard(
 | |
|             title: 'Remote Assets',
 | |
|             properties: [_PropertyItem(label: 'Status', value: 'No remote asset found with this checksum')],
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         return _AssetPropertiesSection(asset: remoteAsset);
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _PropertySectionCard extends StatelessWidget {
 | |
|   final String title;
 | |
|   final List<_PropertyItem> properties;
 | |
| 
 | |
|   const _PropertySectionCard({required this.title, required this.properties});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Card(
 | |
|       margin: const EdgeInsets.symmetric(vertical: 8),
 | |
|       child: Padding(
 | |
|         padding: const EdgeInsets.all(12),
 | |
|         child: Column(
 | |
|           crossAxisAlignment: CrossAxisAlignment.start,
 | |
|           children: [
 | |
|             Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
 | |
|             const SizedBox(height: 8),
 | |
|             ...properties,
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _PropertyItem extends StatelessWidget {
 | |
|   final String label;
 | |
|   final String? value;
 | |
| 
 | |
|   const _PropertyItem({required this.label, this.value});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.symmetric(vertical: 4),
 | |
|       child: Row(
 | |
|         crossAxisAlignment: CrossAxisAlignment.start,
 | |
|         children: [
 | |
|           SizedBox(
 | |
|             width: 120,
 | |
|             child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500)),
 | |
|           ),
 | |
|           Expanded(
 | |
|             child: Text(value ?? 'N/A', style: TextStyle(color: Theme.of(context).colorScheme.secondary)),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |