mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): show local assets (#905)
* introduce Asset as composition of AssetResponseDTO and AssetEntity * filter out duplicate assets (that are both local and remote, take only remote for now) * only allow remote images to be added to albums * introduce ImmichImage to render Asset using local or remote data * optimized deletion of local assets * local video file playback * allow multiple methods to wait on background service finished * skip local assets when adding to album from home screen * fix and optimize delete * show gray box placeholder for local assets * add comments * fix bug: duplicate assets in state after onNewAssetUploaded
This commit is contained in:
committed by
GitHub
parent
99da181cfc
commit
1633af7af6
@@ -2,12 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class ExifBottomSheet extends ConsumerWidget {
|
||||
final AssetResponseDto assetDetail;
|
||||
final Asset assetDetail;
|
||||
|
||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||
: super(key: key);
|
||||
@@ -26,8 +27,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
center: LatLng(
|
||||
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||
assetDetail.latitude ?? 0,
|
||||
assetDetail.longitude ?? 0,
|
||||
),
|
||||
zoom: 16.0,
|
||||
),
|
||||
@@ -48,8 +49,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
Marker(
|
||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||
point: LatLng(
|
||||
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||
assetDetail.latitude ?? 0,
|
||||
assetDetail.longitude ?? 0,
|
||||
),
|
||||
builder: (ctx) => const Image(
|
||||
image: AssetImage('assets/location-pin.png'),
|
||||
@@ -63,9 +64,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
|
||||
|
||||
_buildLocationText() {
|
||||
return Text(
|
||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||
"${exifInfo?.city}, ${exifInfo?.state}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[200],
|
||||
@@ -78,10 +81,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
||||
child: ListView(
|
||||
children: [
|
||||
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
||||
if (exifInfo?.dateTimeOriginal != null)
|
||||
Text(
|
||||
DateFormat('date_format'.tr()).format(
|
||||
assetDetail.exifInfo!.dateTimeOriginal!.toLocal(),
|
||||
exifInfo!.dateTimeOriginal!.toLocal(),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
@@ -101,7 +104,7 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
),
|
||||
|
||||
// Location
|
||||
if (assetDetail.exifInfo?.latitude != null)
|
||||
if (assetDetail.latitude != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 32.0),
|
||||
child: Column(
|
||||
@@ -115,21 +118,22 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
"exif_bottom_sheet_location",
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||
).tr(),
|
||||
if (assetDetail.exifInfo?.latitude != null &&
|
||||
assetDetail.exifInfo?.longitude != null)
|
||||
if (assetDetail.latitude != null &&
|
||||
assetDetail.longitude != null)
|
||||
_buildMap(),
|
||||
if (assetDetail.exifInfo?.city != null &&
|
||||
assetDetail.exifInfo?.state != null)
|
||||
if (exifInfo != null &&
|
||||
exifInfo.city != null &&
|
||||
exifInfo.state != null)
|
||||
_buildLocationText(),
|
||||
Text(
|
||||
"${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
|
||||
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
// Detail
|
||||
if (assetDetail.exifInfo != null)
|
||||
if (exifInfo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 32.0),
|
||||
child: Column(
|
||||
@@ -153,16 +157,16 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
iconColor: Colors.grey[300],
|
||||
leading: const Icon(Icons.image),
|
||||
title: Text(
|
||||
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
||||
"${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
||||
subtitle: exifInfo.exifImageHeight != null
|
||||
? Text(
|
||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
|
||||
"${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${exifInfo.fileSizeInByte!}B ",
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (assetDetail.exifInfo?.make != null)
|
||||
if (exifInfo.make != null)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
@@ -170,11 +174,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||
iconColor: Colors.grey[300],
|
||||
leading: const Icon(Icons.camera),
|
||||
title: Text(
|
||||
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
||||
"${exifInfo.make} ${exifInfo.model}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
|
||||
"ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength}mm ISO${exifInfo.iso} ",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart'
|
||||
show AssetEntityImageProvider, ThumbnailSize;
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
||||
|
||||
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||
late CachedNetworkImageProvider _imageProvider;
|
||||
late ImageProvider _imageProvider;
|
||||
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
||||
bool _zoomedIn = false;
|
||||
|
||||
late CachedNetworkImageProvider fullProvider;
|
||||
late CachedNetworkImageProvider previewProvider;
|
||||
late CachedNetworkImageProvider thumbnailProvider;
|
||||
late ImageProvider _fullProvider;
|
||||
late ImageProvider _previewProvider;
|
||||
late ImageProvider _thumbnailProvider;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -68,7 +73,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||
|
||||
void _performStateTransition(
|
||||
_RemoteImageStatus newStatus,
|
||||
CachedNetworkImageProvider provider,
|
||||
ImageProvider provider,
|
||||
) {
|
||||
if (_status == newStatus) return;
|
||||
|
||||
@@ -90,40 +95,58 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||
}
|
||||
|
||||
void _loadImages() {
|
||||
thumbnailProvider = _authorizedImageProvider(
|
||||
widget.thumbnailUrl,
|
||||
widget.cacheKey,
|
||||
);
|
||||
_imageProvider = thumbnailProvider;
|
||||
if (widget.asset.isLocal) {
|
||||
_imageProvider = AssetEntityImageProvider(
|
||||
widget.asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(250),
|
||||
);
|
||||
_fullProvider = AssetEntityImageProvider(widget.asset.local!);
|
||||
_fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener((ImageInfo image, _) {
|
||||
_performStateTransition(
|
||||
_RemoteImageStatus.full,
|
||||
_fullProvider,
|
||||
);
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||
_thumbnailProvider = _authorizedImageProvider(
|
||||
getThumbnailUrl(widget.asset.remote!),
|
||||
widget.asset.id,
|
||||
);
|
||||
_imageProvider = _thumbnailProvider;
|
||||
|
||||
_thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||
_performStateTransition(
|
||||
_RemoteImageStatus.thumbnail,
|
||||
thumbnailProvider,
|
||||
_thumbnailProvider,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
if (widget.previewUrl != null) {
|
||||
previewProvider = _authorizedImageProvider(
|
||||
widget.previewUrl!,
|
||||
"${widget.cacheKey}_previewStage",
|
||||
if (widget.threeStageLoading) {
|
||||
_previewProvider = _authorizedImageProvider(
|
||||
getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
|
||||
"${widget.asset.id}_previewStage",
|
||||
);
|
||||
previewProvider.resolve(const ImageConfiguration()).addListener(
|
||||
_previewProvider.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
|
||||
_performStateTransition(_RemoteImageStatus.preview, _previewProvider);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fullProvider = _authorizedImageProvider(
|
||||
widget.imageUrl,
|
||||
"${widget.cacheKey}_fullStage",
|
||||
_fullProvider = _authorizedImageProvider(
|
||||
getImageUrl(widget.asset.remote!),
|
||||
"${widget.asset.id}_fullStage",
|
||||
);
|
||||
fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||
_fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||
_performStateTransition(_RemoteImageStatus.full, _fullProvider);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -139,11 +162,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||
super.dispose();
|
||||
|
||||
if (_status == _RemoteImageStatus.full) {
|
||||
await fullProvider.evict();
|
||||
await _fullProvider.evict();
|
||||
} else if (_status == _RemoteImageStatus.preview) {
|
||||
await previewProvider.evict();
|
||||
await _previewProvider.evict();
|
||||
} else if (_status == _RemoteImageStatus.thumbnail) {
|
||||
await thumbnailProvider.evict();
|
||||
await _thumbnailProvider.evict();
|
||||
}
|
||||
|
||||
await _imageProvider.evict();
|
||||
@@ -153,23 +176,18 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||
class RemotePhotoView extends StatefulWidget {
|
||||
const RemotePhotoView({
|
||||
Key? key,
|
||||
required this.thumbnailUrl,
|
||||
required this.imageUrl,
|
||||
required this.asset,
|
||||
required this.authToken,
|
||||
required this.threeStageLoading,
|
||||
required this.isZoomedFunction,
|
||||
required this.isZoomedListener,
|
||||
required this.onSwipeDown,
|
||||
required this.onSwipeUp,
|
||||
this.previewUrl,
|
||||
required this.cacheKey,
|
||||
}) : super(key: key);
|
||||
|
||||
final String thumbnailUrl;
|
||||
final String imageUrl;
|
||||
final Asset asset;
|
||||
final String authToken;
|
||||
final String? previewUrl;
|
||||
final String cacheKey;
|
||||
|
||||
final bool threeStageLoading;
|
||||
final void Function() onSwipeDown;
|
||||
final void Function() onSwipeUp;
|
||||
final void Function() isZoomedFunction;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||
const TopControlAppBar({
|
||||
@@ -13,9 +13,9 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||
this.loading = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final AssetResponseDto asset;
|
||||
final Asset asset;
|
||||
final Function onMoreInfoPressed;
|
||||
final Function onDownloadPressed;
|
||||
final VoidCallback? onDownloadPressed;
|
||||
final Function onSharePressed;
|
||||
final bool loading;
|
||||
|
||||
@@ -47,17 +47,16 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||
child: const CircularProgressIndicator(strokeWidth: 2.0),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
iconSize: iconSize,
|
||||
splashRadius: iconSize,
|
||||
onPressed: () {
|
||||
onDownloadPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.cloud_download_rounded,
|
||||
color: Colors.grey[200],
|
||||
if (!asset.isLocal)
|
||||
IconButton(
|
||||
iconSize: iconSize,
|
||||
splashRadius: iconSize,
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
Icons.cloud_download_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
iconSize: iconSize,
|
||||
splashRadius: iconSize,
|
||||
@@ -69,17 +68,18 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
iconSize: iconSize,
|
||||
splashRadius: iconSize,
|
||||
onPressed: () {
|
||||
onMoreInfoPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
)
|
||||
if (asset.isRemote)
|
||||
IconButton(
|
||||
iconSize: iconSize,
|
||||
splashRadius: iconSize,
|
||||
onPressed: () {
|
||||
onMoreInfoPressed();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user