mirror of
https://github.com/immich-app/immich.git
synced 2025-12-05 12:23:58 +09:00
feat: resurrect advanced info (#21633)
* feat: resurrect advanced info * display null values as well * add exif details --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
345
mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart
Normal file
345
mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart
Normal file
@@ -0,0 +1,345 @@
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user