refactor(mobile): reworked Asset, store all required fields from local & remote (#1539)

replace usage of AssetResponseDto with Asset

Add new class ExifInfo to store data from ExifResponseDto
This commit is contained in:
Fynn Petersen-Frey
2023-02-04 21:42:42 +01:00
committed by GitHub
parent 7bd2455175
commit 0048662182
28 changed files with 626 additions and 504 deletions

View File

@@ -7,7 +7,6 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:openapi/api.dart';
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final ImageViewerService _imageViewerService;
@@ -20,7 +19,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
),
);
void downloadAsset(AssetResponseDto asset, BuildContext context) async {
void downloadAsset(Asset asset, BuildContext context) async {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);

View File

@@ -2,10 +2,9 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:photo_manager/photo_manager.dart';
import 'package:path_provider/path_provider.dart';
@@ -18,14 +17,12 @@ class ImageViewerService {
ImageViewerService(this._apiService);
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
Future<bool> downloadAssetToDevice(Asset asset) async {
try {
String fileName = p.basename(asset.originalPath);
// Download LivePhotos image and motion part
if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) {
if (asset.isImage && asset.livePhotoVideoId != null) {
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id,
asset.remoteId!,
);
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
@@ -43,28 +40,28 @@ class ImageViewerService {
entity = await PhotoManager.editor.darwin.saveLivePhoto(
imageFile: imageFile,
videoFile: videoFile,
title: p.basename(asset.originalPath),
title: asset.fileName,
);
return entity != null;
} else {
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id,
);
var res = await _apiService.assetApi
.downloadFileWithHttpInfo(asset.remoteId!);
final AssetEntity? entity;
if (asset.type == AssetTypeEnum.IMAGE) {
if (asset.isImage) {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
title: p.basename(asset.originalPath),
title: asset.fileName,
);
} else {
final tempDir = await getTemporaryDirectory();
File tempFile = await File('${tempDir.path}/$fileName').create();
File tempFile =
await File('${tempDir.path}/${asset.fileName}').create();
tempFile.writeAsBytesSync(res.bodyBytes);
entity =
await PhotoManager.editor.saveVideo(tempFile, title: fileName);
entity = await PhotoManager.editor
.saveVideo(tempFile, title: asset.fileName);
}
return entity != null;
}

View File

@@ -3,9 +3,8 @@ 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:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:latlong2/latlong.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
@@ -68,7 +67,7 @@ class ExifBottomSheet extends HookConsumerWidget {
final textColor = Theme.of(context).primaryColor;
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
ExifInfo? exifInfo = assetDetail.exifInfo;
buildLocationText() {
return Text(
@@ -81,6 +80,17 @@ class ExifBottomSheet extends HookConsumerWidget {
);
}
buildSizeText(Asset a) {
String resolution = a.width != null && a.height != null
? "${a.height} x ${a.width} "
: "";
String fileSize = a.exifInfo?.fileSize != null
? formatBytes(a.exifInfo!.fileSize!)
: "";
String text = resolution + fileSize;
return text.isEmpty ? null : Text(text);
}
return SingleChildScrollView(
child: Card(
shape: const RoundedRectangleBorder(
@@ -101,19 +111,18 @@ class ExifBottomSheet extends HookConsumerWidget {
child: CustomDraggingHandle(),
),
const SizedBox(height: 12),
if (exifInfo?.dateTimeOriginal != null)
Text(
DateFormat('date_format'.tr()).format(
exifInfo!.dateTimeOriginal!.toLocal(),
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
Text(
DateFormat('date_format'.tr()).format(
assetDetail.createdAt.toLocal(),
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
// Location
if (assetDetail.latitude != null)
if (assetDetail.latitude != null && assetDetail.longitude != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
@@ -126,74 +135,67 @@ class ExifBottomSheet extends HookConsumerWidget {
"exif_bottom_sheet_location",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
if (assetDetail.latitude != null &&
assetDetail.longitude != null)
buildMap(),
buildMap(),
if (exifInfo != null &&
exifInfo.city != null &&
exifInfo.state != null)
buildLocationText(),
Text(
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
"${assetDetail.latitude!.toStringAsFixed(4)}, ${assetDetail.longitude!.toStringAsFixed(4)}",
style: const TextStyle(fontSize: 12),
)
],
),
),
// Detail
if (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(
"exif_bottom_sheet_details",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
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(
"exif_bottom_sheet_details",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.image),
title: Text(
assetDetail.fileName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
subtitle: buildSizeText(assetDetail),
),
if (exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.image),
leading: const Icon(Icons.camera),
title: Text(
"${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
"${exifInfo!.make} ${exifInfo.model}",
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
fontWeight: FontWeight.bold,
),
),
subtitle: exifInfo.exifImageHeight != null
? Text(
"${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${formatBytes(exifInfo.fileSizeInByte ?? 0)} ",
)
: null,
subtitle: Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
),
),
if (exifInfo.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.camera),
title: Text(
"${exifInfo.make} ${exifInfo.model}",
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
),
),
],
),
],
),
),
const SizedBox(
height: 50,
),

View File

@@ -43,7 +43,7 @@ class TopControlAppBar extends HookConsumerWidget {
),
),
actions: [
if (asset.remote?.livePhotoVideoId != null)
if (asset.livePhotoVideoId != null)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
@@ -104,18 +104,17 @@ class TopControlAppBar extends HookConsumerWidget {
color: Colors.grey[200],
),
),
if (asset.isRemote)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onMoreInfoPressed();
},
icon: Icon(
Icons.more_horiz_rounded,
color: Colors.grey[200],
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onMoreInfoPressed();
},
icon: Icon(
Icons.more_horiz_rounded,
color: Colors.grey[200],
),
),
],
);
}

View File

@@ -13,7 +13,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_s
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/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
@@ -80,31 +80,34 @@ class GalleryViewerPage extends HookConsumerWidget {
}
}
/// Thumbnail image of a remote asset. Required asset.remote != null
ImageProvider remoteThumbnailImageProvider(Asset asset, api.ThumbnailFormat type) {
/// Thumbnail image of a remote asset. Required asset.isRemote
ImageProvider remoteThumbnailImageProvider(
Asset asset,
api.ThumbnailFormat type,
) {
return CachedNetworkImageProvider(
getThumbnailUrl(
asset.remote!,
asset,
type: type,
),
cacheKey: getThumbnailCacheKey(
asset.remote!,
asset,
type: type,
),
headers: {"Authorization": authToken},
);
}
/// Original (large) image of a remote asset. Required asset.remote != null
/// Original (large) image of a remote asset. Required asset.isRemote
ImageProvider originalImageProvider(Asset asset) {
return CachedNetworkImageProvider(
getImageUrl(asset.remote!),
cacheKey: getImageCacheKey(asset.remote!),
getImageUrl(asset),
cacheKey: getImageCacheKey(asset),
headers: {"Authorization": authToken},
);
}
/// Thumbnail image of a local asset. Required asset.local != null
/// Thumbnail image of a local asset. Required asset.isLocal
ImageProvider localThumbnailImageProvider(Asset asset) {
return AssetEntityImageProvider(
asset.local!,
@@ -114,10 +117,9 @@ class GalleryViewerPage extends HookConsumerWidget {
MediaQuery.of(context).size.height.floor(),
),
);
}
/// Original (large) image of a local asset. Required asset.local != null
/// Original (large) image of a local asset. Required asset.isLocal
ImageProvider localImageProvider(Asset asset) {
return AssetEntityImageProvider(asset.local!);
}
@@ -132,7 +134,7 @@ class GalleryViewerPage extends HookConsumerWidget {
// Probably load WEBP either way
precacheImage(
remoteThumbnailImageProvider(
asset,
asset,
api.ThumbnailFormat.WEBP,
),
context,
@@ -154,26 +156,23 @@ class GalleryViewerPage extends HookConsumerWidget {
context,
);
}
}
}
}
void showInfo() {
if (assetList[indexOfAsset.value].isRemote) {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
void handleDelete(Asset deleteAsset) {
@@ -244,7 +243,7 @@ class GalleryViewerPage extends HookConsumerWidget {
? null
: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
assetList[indexOfAsset.value].remote!,
assetList[indexOfAsset.value],
context,
);
},
@@ -256,8 +255,10 @@ class GalleryViewerPage extends HookConsumerWidget {
onToggleMotionVideo: (() {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
onDeletePressed: () =>
handleDelete((assetList[indexOfAsset.value])),
onAddToAlbumPressed: () =>
addToAlbum(assetList[indexOfAsset.value]),
),
),
);
@@ -268,117 +269,132 @@ class GalleryViewerPage extends HookConsumerWidget {
body: Stack(
children: [
PhotoViewGallery.builder(
scaleStateChangedCallback: (state) {
isZoomed.value = state != PhotoViewScaleState.initial;
showAppBar.value = !isZoomed.value;
},
pageController: controller,
scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
: (Platform.isIOS
? const BouncingScrollPhysics() // Use bouncing physics for iOS
: const ClampingScrollPhysics() // Use heavy physics for Android
),
itemCount: assetList.length,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
// Precache image
if (indexOfAsset.value < value) {
// Moving forwards, so precache the next asset
precacheNextImage(value + 1);
} else {
// Moving backwards, so precache previous asset
precacheNextImage(value - 1);
}
indexOfAsset.value = value;
HapticFeedback.selectionClick();
},
loadingBuilder: isLoadPreview.value ? (context, event) {
final asset = assetList[indexOfAsset.value];
if (!asset.isLocal) {
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
// Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.WEBP),
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.WEBP),
httpHeaders: { 'Authorization': authToken },
progressIndicatorBuilder: (_, __, ___) => const Center(child: ImmichLoadingIndicator(),),
fadeInDuration: const Duration(milliseconds: 0),
fit: BoxFit.contain,
);
return CachedNetworkImage(
imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.JPEG),
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.JPEG),
httpHeaders: { 'Authorization': authToken },
fit: BoxFit.contain,
fadeInDuration: const Duration(milliseconds: 0),
placeholder: (_, __) => webPThumbnail,
);
} else {
return Image(
image: localThumbnailImageProvider(asset),
fit: BoxFit.contain,
);
}
} : null,
builder: (context, index) {
getAssetExif();
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
// Show photo
final ImageProvider provider;
if (assetList[index].isLocal) {
provider = localImageProvider(assetList[index]);
} else {
if (isLoadOriginal.value) {
provider = originalImageProvider(assetList[index]);
} else {
provider = remoteThumbnailImageProvider(
assetList[index],
api.ThumbnailFormat.JPEG,
);
}
}
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) => localPosition = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
onTapDown: (_, __, ___) => showAppBar.value = !showAppBar.value,
imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
minScale: PhotoViewComputedScale.contained,
);
} else {
return PhotoViewGalleryPageOptions.customChild(
onDragStart: (_, details, __) => localPosition = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
onTapDown: (_, __, ___) => showAppBar.value = !showAppBar.value,
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
maxScale: 1.0,
minScale: 1.0,
child: SafeArea(
child: VideoViewerPage(
asset: assetList[index],
isMotionVideo: isPlayingMotionVideo.value,
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
),
scaleStateChangedCallback: (state) {
isZoomed.value = state != PhotoViewScaleState.initial;
showAppBar.value = !isZoomed.value;
},
pageController: controller,
scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
: (Platform.isIOS
? const BouncingScrollPhysics() // Use bouncing physics for iOS
: const ClampingScrollPhysics() // Use heavy physics for Android
),
);
}
},
),
Positioned(
top: 0,
left: 0,
right: 0,
child: buildAppBar(),
),
],
itemCount: assetList.length,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
// Precache image
if (indexOfAsset.value < value) {
// Moving forwards, so precache the next asset
precacheNextImage(value + 1);
} else {
// Moving backwards, so precache previous asset
precacheNextImage(value - 1);
}
indexOfAsset.value = value;
HapticFeedback.selectionClick();
},
loadingBuilder: isLoadPreview.value
? (context, event) {
final asset = assetList[indexOfAsset.value];
if (!asset.isLocal) {
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
// Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl(asset),
cacheKey: getThumbnailCacheKey(asset),
httpHeaders: {'Authorization': authToken},
progressIndicatorBuilder: (_, __, ___) => const Center(
child: ImmichLoadingIndicator(),
),
fadeInDuration: const Duration(milliseconds: 0),
fit: BoxFit.contain,
);
return CachedNetworkImage(
imageUrl: getThumbnailUrl(
asset,
type: api.ThumbnailFormat.JPEG,
),
cacheKey: getThumbnailCacheKey(
asset,
type: api.ThumbnailFormat.JPEG,
),
httpHeaders: {'Authorization': authToken},
fit: BoxFit.contain,
fadeInDuration: const Duration(milliseconds: 0),
placeholder: (_, __) => webPThumbnail,
);
} else {
return Image(
image: localThumbnailImageProvider(asset),
fit: BoxFit.contain,
);
}
}
: null,
builder: (context, index) {
getAssetExif();
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
// Show photo
final ImageProvider provider;
if (assetList[index].isLocal) {
provider = localImageProvider(assetList[index]);
} else {
if (isLoadOriginal.value) {
provider = originalImageProvider(assetList[index]);
} else {
provider = remoteThumbnailImageProvider(
assetList[index],
api.ThumbnailFormat.JPEG,
);
}
}
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) =>
localPosition = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
onTapDown: (_, __, ___) =>
showAppBar.value = !showAppBar.value,
imageProvider: provider,
heroAttributes:
PhotoViewHeroAttributes(tag: assetList[index].id),
minScale: PhotoViewComputedScale.contained,
);
} else {
return PhotoViewGalleryPageOptions.customChild(
onDragStart: (_, details, __) =>
localPosition = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
onTapDown: (_, __, ___) =>
showAppBar.value = !showAppBar.value,
heroAttributes:
PhotoViewHeroAttributes(tag: assetList[index].id),
maxScale: 1.0,
minScale: 1.0,
child: SafeArea(
child: VideoViewerPage(
asset: assetList[index],
isMotionVideo: isPlayingMotionVideo.value,
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
),
),
);
}
},
),
Positioned(
top: 0,
left: 0,
right: 0,
child: buildAppBar(),
),
],
),
);
}
}

View File

@@ -53,8 +53,8 @@ class VideoViewerPage extends HookConsumerWidget {
final box = Hive.box(userInfoBox);
final String jwtToken = box.get(accessTokenKey);
final String videoUrl = isMotionVideo
? '${box.get(serverEndpointKey)}/asset/file/${asset.remote?.livePhotoVideoId!}'
: '${box.get(serverEndpointKey)}/asset/file/${asset.id}';
? '${box.get(serverEndpointKey)}/asset/file/${asset.livePhotoVideoId}'
: '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}';
return Stack(
children: [