mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:06:26 -04:00
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:
parent
7bd2455175
commit
0048662182
@ -5,6 +5,7 @@ const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
|||||||
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
|
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
|
||||||
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
|
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
|
||||||
const String assetEtagKey = 'immichAssetEtagKey'; // Key 5
|
const String assetEtagKey = 'immichAssetEtagKey'; // Key 5
|
||||||
|
const String userIdKey = 'immichUserIdKey'; // Key 6
|
||||||
|
|
||||||
// Login Info
|
// Login Info
|
||||||
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
|
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
|
||||||
|
@ -85,9 +85,11 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
(deviceId != asset.deviceId)
|
asset.isRemote
|
||||||
? Icons.cloud_done_outlined
|
? (deviceId == asset.deviceId
|
||||||
: Icons.photo_library_rounded,
|
? Icons.cloud_done_outlined
|
||||||
|
: Icons.cloud_outlined)
|
||||||
|
: Icons.cloud_off_outlined,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
@ -121,7 +121,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
asset.duration.substring(0, 7),
|
asset.duration.toString().substring(0, 7),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
@ -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/services/share.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService;
|
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);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||||
|
|
||||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||||
|
@ -2,10 +2,9 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.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:photo_manager/photo_manager.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -18,14 +17,12 @@ class ImageViewerService {
|
|||||||
|
|
||||||
ImageViewerService(this._apiService);
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
|
Future<bool> downloadAssetToDevice(Asset asset) async {
|
||||||
try {
|
try {
|
||||||
String fileName = p.basename(asset.originalPath);
|
|
||||||
|
|
||||||
// Download LivePhotos image and motion part
|
// 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(
|
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
asset.id,
|
asset.remoteId!,
|
||||||
);
|
);
|
||||||
|
|
||||||
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
@ -43,28 +40,28 @@ class ImageViewerService {
|
|||||||
entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
||||||
imageFile: imageFile,
|
imageFile: imageFile,
|
||||||
videoFile: videoFile,
|
videoFile: videoFile,
|
||||||
title: p.basename(asset.originalPath),
|
title: asset.fileName,
|
||||||
);
|
);
|
||||||
|
|
||||||
return entity != null;
|
return entity != null;
|
||||||
} else {
|
} else {
|
||||||
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
var res = await _apiService.assetApi
|
||||||
asset.id,
|
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||||
);
|
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
if (asset.type == AssetTypeEnum.IMAGE) {
|
if (asset.isImage) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
entity = await PhotoManager.editor.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: p.basename(asset.originalPath),
|
title: asset.fileName,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final tempDir = await getTemporaryDirectory();
|
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);
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
entity =
|
entity = await PhotoManager.editor
|
||||||
await PhotoManager.editor.saveVideo(tempFile, title: fileName);
|
.saveVideo(tempFile, title: asset.fileName);
|
||||||
}
|
}
|
||||||
return entity != null;
|
return entity != null;
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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: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:latlong2/latlong.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|
||||||
@ -68,7 +67,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final textColor = Theme.of(context).primaryColor;
|
final textColor = Theme.of(context).primaryColor;
|
||||||
|
|
||||||
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
|
ExifInfo? exifInfo = assetDetail.exifInfo;
|
||||||
|
|
||||||
buildLocationText() {
|
buildLocationText() {
|
||||||
return Text(
|
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(
|
return SingleChildScrollView(
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
@ -101,19 +111,18 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
child: CustomDraggingHandle(),
|
child: CustomDraggingHandle(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (exifInfo?.dateTimeOriginal != null)
|
Text(
|
||||||
Text(
|
DateFormat('date_format'.tr()).format(
|
||||||
DateFormat('date_format'.tr()).format(
|
assetDetail.createdAt.toLocal(),
|
||||||
exifInfo!.dateTimeOriginal!.toLocal(),
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
if (assetDetail.latitude != null)
|
if (assetDetail.latitude != null && assetDetail.longitude != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -126,74 +135,67 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
"exif_bottom_sheet_location",
|
"exif_bottom_sheet_location",
|
||||||
style: TextStyle(fontSize: 11, color: textColor),
|
style: TextStyle(fontSize: 11, color: textColor),
|
||||||
).tr(),
|
).tr(),
|
||||||
if (assetDetail.latitude != null &&
|
buildMap(),
|
||||||
assetDetail.longitude != null)
|
|
||||||
buildMap(),
|
|
||||||
if (exifInfo != null &&
|
if (exifInfo != null &&
|
||||||
exifInfo.city != null &&
|
exifInfo.city != null &&
|
||||||
exifInfo.state != null)
|
exifInfo.state != null)
|
||||||
buildLocationText(),
|
buildLocationText(),
|
||||||
Text(
|
Text(
|
||||||
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
|
"${assetDetail.latitude!.toStringAsFixed(4)}, ${assetDetail.longitude!.toStringAsFixed(4)}",
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Detail
|
// Detail
|
||||||
if (exifInfo != null)
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Divider(
|
||||||
Divider(
|
thickness: 1,
|
||||||
thickness: 1,
|
color: Colors.grey[600],
|
||||||
color: Colors.grey[600],
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
child: Text(
|
||||||
child: Text(
|
"exif_bottom_sheet_details",
|
||||||
"exif_bottom_sheet_details",
|
style: TextStyle(fontSize: 11, color: textColor),
|
||||||
style: TextStyle(fontSize: 11, color: textColor),
|
).tr(),
|
||||||
).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(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.image),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
|
"${exifInfo!.make} ${exifInfo.model}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: textColor,
|
color: textColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: exifInfo.exifImageHeight != null
|
subtitle: Text(
|
||||||
? Text(
|
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
|
||||||
"${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${formatBytes(exifInfo.fileSizeInByte ?? 0)} ",
|
),
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
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(
|
const SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
),
|
),
|
||||||
|
@ -43,7 +43,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (asset.remote?.livePhotoVideoId != null)
|
if (asset.livePhotoVideoId != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
@ -104,18 +104,17 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.isRemote)
|
IconButton(
|
||||||
IconButton(
|
iconSize: iconSize,
|
||||||
iconSize: iconSize,
|
splashRadius: iconSize,
|
||||||
splashRadius: iconSize,
|
onPressed: () {
|
||||||
onPressed: () {
|
onMoreInfoPressed();
|
||||||
onMoreInfoPressed();
|
},
|
||||||
},
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.more_horiz_rounded,
|
||||||
Icons.more_horiz_rounded,
|
color: Colors.grey[200],
|
||||||
color: Colors.grey[200],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.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/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/home/ui/delete_diaglog.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.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
|
/// Thumbnail image of a remote asset. Required asset.isRemote
|
||||||
ImageProvider remoteThumbnailImageProvider(Asset asset, api.ThumbnailFormat type) {
|
ImageProvider remoteThumbnailImageProvider(
|
||||||
|
Asset asset,
|
||||||
|
api.ThumbnailFormat type,
|
||||||
|
) {
|
||||||
return CachedNetworkImageProvider(
|
return CachedNetworkImageProvider(
|
||||||
getThumbnailUrl(
|
getThumbnailUrl(
|
||||||
asset.remote!,
|
asset,
|
||||||
type: type,
|
type: type,
|
||||||
),
|
),
|
||||||
cacheKey: getThumbnailCacheKey(
|
cacheKey: getThumbnailCacheKey(
|
||||||
asset.remote!,
|
asset,
|
||||||
type: type,
|
type: type,
|
||||||
),
|
),
|
||||||
headers: {"Authorization": authToken},
|
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) {
|
ImageProvider originalImageProvider(Asset asset) {
|
||||||
return CachedNetworkImageProvider(
|
return CachedNetworkImageProvider(
|
||||||
getImageUrl(asset.remote!),
|
getImageUrl(asset),
|
||||||
cacheKey: getImageCacheKey(asset.remote!),
|
cacheKey: getImageCacheKey(asset),
|
||||||
headers: {"Authorization": authToken},
|
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) {
|
ImageProvider localThumbnailImageProvider(Asset asset) {
|
||||||
return AssetEntityImageProvider(
|
return AssetEntityImageProvider(
|
||||||
asset.local!,
|
asset.local!,
|
||||||
@ -114,10 +117,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
MediaQuery.of(context).size.height.floor(),
|
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) {
|
ImageProvider localImageProvider(Asset asset) {
|
||||||
return AssetEntityImageProvider(asset.local!);
|
return AssetEntityImageProvider(asset.local!);
|
||||||
}
|
}
|
||||||
@ -132,7 +134,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
// Probably load WEBP either way
|
// Probably load WEBP either way
|
||||||
precacheImage(
|
precacheImage(
|
||||||
remoteThumbnailImageProvider(
|
remoteThumbnailImageProvider(
|
||||||
asset,
|
asset,
|
||||||
api.ThumbnailFormat.WEBP,
|
api.ThumbnailFormat.WEBP,
|
||||||
),
|
),
|
||||||
context,
|
context,
|
||||||
@ -154,26 +156,23 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfo() {
|
void showInfo() {
|
||||||
if (assetList[indexOfAsset.value].isRemote) {
|
showModalBottomSheet(
|
||||||
showModalBottomSheet(
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
),
|
||||||
),
|
barrierColor: Colors.transparent,
|
||||||
barrierColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
backgroundColor: Colors.transparent,
|
isScrollControlled: true,
|
||||||
isScrollControlled: true,
|
context: context,
|
||||||
context: context,
|
builder: (context) {
|
||||||
builder: (context) {
|
return ExifBottomSheet(assetDetail: assetDetail!);
|
||||||
return ExifBottomSheet(assetDetail: assetDetail!);
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDelete(Asset deleteAsset) {
|
void handleDelete(Asset deleteAsset) {
|
||||||
@ -244,7 +243,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
assetList[indexOfAsset.value].remote!,
|
assetList[indexOfAsset.value],
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -256,8 +255,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
onToggleMotionVideo: (() {
|
onToggleMotionVideo: (() {
|
||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
|
onDeletePressed: () =>
|
||||||
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
|
handleDelete((assetList[indexOfAsset.value])),
|
||||||
|
onAddToAlbumPressed: () =>
|
||||||
|
addToAlbum(assetList[indexOfAsset.value]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -268,117 +269,132 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
PhotoViewGallery.builder(
|
PhotoViewGallery.builder(
|
||||||
scaleStateChangedCallback: (state) {
|
scaleStateChangedCallback: (state) {
|
||||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||||
showAppBar.value = !isZoomed.value;
|
showAppBar.value = !isZoomed.value;
|
||||||
},
|
},
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
scrollPhysics: isZoomed.value
|
scrollPhysics: isZoomed.value
|
||||||
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
||||||
: (Platform.isIOS
|
: (Platform.isIOS
|
||||||
? const BouncingScrollPhysics() // Use bouncing physics for iOS
|
? const BouncingScrollPhysics() // Use bouncing physics for iOS
|
||||||
: const ClampingScrollPhysics() // Use heavy physics for Android
|
: 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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
itemCount: assetList.length,
|
||||||
}
|
scrollDirection: Axis.horizontal,
|
||||||
},
|
onPageChanged: (value) {
|
||||||
),
|
// Precache image
|
||||||
Positioned(
|
if (indexOfAsset.value < value) {
|
||||||
top: 0,
|
// Moving forwards, so precache the next asset
|
||||||
left: 0,
|
precacheNextImage(value + 1);
|
||||||
right: 0,
|
} else {
|
||||||
child: buildAppBar(),
|
// 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +53,8 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
final box = Hive.box(userInfoBox);
|
final box = Hive.box(userInfoBox);
|
||||||
final String jwtToken = box.get(accessTokenKey);
|
final String jwtToken = box.get(accessTokenKey);
|
||||||
final String videoUrl = isMotionVideo
|
final String videoUrl = isMotionVideo
|
||||||
? '${box.get(serverEndpointKey)}/asset/file/${asset.remote?.livePhotoVideoId!}'
|
? '${box.get(serverEndpointKey)}/asset/file/${asset.livePhotoVideoId}'
|
||||||
: '${box.get(serverEndpointKey)}/asset/file/${asset.id}';
|
: '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}';
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -75,6 +75,9 @@ class BackupService {
|
|||||||
final filter = FilterOptionGroup(
|
final filter = FilterOptionGroup(
|
||||||
containsPathModified: true,
|
containsPathModified: true,
|
||||||
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
||||||
|
// title is needed to create Assets
|
||||||
|
imageOption: const FilterOption(needTitle: true),
|
||||||
|
videoOption: const FilterOption(needTitle: true),
|
||||||
);
|
);
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final List<AssetPathEntity?> selectedAlbums =
|
final List<AssetPathEntity?> selectedAlbums =
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class ImmichAssetGroupByDate {
|
|
||||||
final String date;
|
|
||||||
List<AssetResponseDto> assets;
|
|
||||||
ImmichAssetGroupByDate({
|
|
||||||
required this.date,
|
|
||||||
required this.assets,
|
|
||||||
});
|
|
||||||
|
|
||||||
ImmichAssetGroupByDate copyWith({
|
|
||||||
String? date,
|
|
||||||
List<AssetResponseDto>? assets,
|
|
||||||
}) {
|
|
||||||
return ImmichAssetGroupByDate(
|
|
||||||
date: date ?? this.date,
|
|
||||||
assets: assets ?? this.assets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ImmichAssetGroupByDate &&
|
|
||||||
other.date == date &&
|
|
||||||
listEquals(other.assets, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => date.hashCode ^ assets.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GetAllAssetResponse {
|
|
||||||
final int count;
|
|
||||||
final List<ImmichAssetGroupByDate> data;
|
|
||||||
final String nextPageKey;
|
|
||||||
GetAllAssetResponse({
|
|
||||||
required this.count,
|
|
||||||
required this.data,
|
|
||||||
required this.nextPageKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
GetAllAssetResponse copyWith({
|
|
||||||
int? count,
|
|
||||||
List<ImmichAssetGroupByDate>? data,
|
|
||||||
String? nextPageKey,
|
|
||||||
}) {
|
|
||||||
return GetAllAssetResponse(
|
|
||||||
count: count ?? this.count,
|
|
||||||
data: data ?? this.data,
|
|
||||||
nextPageKey: nextPageKey ?? this.nextPageKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is GetAllAssetResponse &&
|
|
||||||
other.count == count &&
|
|
||||||
listEquals(other.data, data) &&
|
|
||||||
other.nextPageKey == nextPageKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => count.hashCode ^ data.hashCode ^ nextPageKey.hashCode;
|
|
||||||
}
|
|
@ -24,7 +24,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<String> _selectedAssets = HashSet();
|
final Set<String> _selectedAssets = HashSet();
|
||||||
|
|
||||||
|
|
||||||
Set<Asset> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return _selectedAssets
|
return _selectedAssets
|
||||||
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
||||||
@ -103,7 +102,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
return Row(
|
return Row(
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
children: row.assets.map((Asset asset) {
|
children: row.assets.map((Asset asset) {
|
||||||
bool last = asset == row.assets.last;
|
bool last = asset.id == row.assets.last.id;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
key: Key("asset-${asset.id}"),
|
key: Key("asset-${asset.id}"),
|
||||||
@ -224,7 +223,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<bool> onWillPop() async {
|
Future<bool> onWillPop() async {
|
||||||
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
||||||
_deselectAll();
|
_deselectAll();
|
||||||
@ -234,8 +232,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
|
@ -4,7 +4,7 @@ import 'package:hive/hive.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
@ -166,6 +166,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
|
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
|
||||||
userInfoHiveBox.put(accessTokenKey, accessToken);
|
userInfoHiveBox.put(accessTokenKey, accessToken);
|
||||||
|
userInfoHiveBox.put(userIdKey, userResponseDto.id);
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
|
@ -45,9 +45,11 @@ class SearchResultPageState {
|
|||||||
isLoading: map['isLoading'] ?? false,
|
isLoading: map['isLoading'] ?? false,
|
||||||
isSuccess: map['isSuccess'] ?? false,
|
isSuccess: map['isSuccess'] ?? false,
|
||||||
isError: map['isError'] ?? false,
|
isError: map['isError'] ?? false,
|
||||||
searchResult: List<Asset>.from(
|
searchResult: List.from(
|
||||||
map['searchResult']
|
map['searchResult']
|
||||||
?.map((x) => Asset.remote(AssetResponseDto.fromJson(x))),
|
.map(AssetResponseDto.fromJson)
|
||||||
|
.where((e) => e != null)
|
||||||
|
.map(Asset.remote),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
|||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Asset>? assets = (await _searchService.searchAsset(searchTerm))
|
List<Asset>? assets = await _searchService.searchAsset(searchTerm);
|
||||||
?.map((e) => Asset.remote(e))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@ -24,10 +25,14 @@ class SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AssetResponseDto>?> searchAsset(String searchTerm) async {
|
Future<List<Asset>?> searchAsset(String searchTerm) async {
|
||||||
try {
|
try {
|
||||||
return await _apiService.assetApi
|
final List<AssetResponseDto>? results = await _apiService.assetApi
|
||||||
.searchAsset(SearchAssetDto(searchTerm: searchTerm));
|
.searchAsset(SearchAssetDto(searchTerm: searchTerm));
|
||||||
|
if (results == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return results.map((e) => Asset.remote(e)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
@ -50,7 +55,7 @@ class SearchService {
|
|||||||
return await _apiService.assetApi.getCuratedObjects();
|
return await _apiService.assetApi.getCuratedObjects();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getCuratedObjects] ${e.toString()}");
|
debugPrint("Error [getCuratedObjects] ${e.toString()}");
|
||||||
throw [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,128 @@
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
/// Asset (online or local)
|
/// Asset (online or local)
|
||||||
class Asset {
|
class Asset {
|
||||||
Asset.remote(this.remote) {
|
Asset.remote(AssetResponseDto remote)
|
||||||
local = null;
|
: remoteId = remote.id,
|
||||||
}
|
createdAt = DateTime.parse(remote.createdAt),
|
||||||
|
modifiedAt = DateTime.parse(remote.modifiedAt),
|
||||||
|
durationInSeconds = remote.duration.toDuration().inSeconds,
|
||||||
|
fileName = p.basename(remote.originalPath),
|
||||||
|
height = remote.exifInfo?.exifImageHeight?.toInt(),
|
||||||
|
width = remote.exifInfo?.exifImageWidth?.toInt(),
|
||||||
|
livePhotoVideoId = remote.livePhotoVideoId,
|
||||||
|
deviceAssetId = remote.deviceAssetId,
|
||||||
|
deviceId = remote.deviceId,
|
||||||
|
ownerId = remote.ownerId,
|
||||||
|
latitude = remote.exifInfo?.latitude?.toDouble(),
|
||||||
|
longitude = remote.exifInfo?.longitude?.toDouble(),
|
||||||
|
exifInfo =
|
||||||
|
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null;
|
||||||
|
|
||||||
Asset.local(this.local) {
|
Asset.local(AssetEntity local, String owner)
|
||||||
remote = null;
|
: localId = local.id,
|
||||||
}
|
latitude = local.latitude,
|
||||||
|
longitude = local.longitude,
|
||||||
late final AssetResponseDto? remote;
|
durationInSeconds = local.duration,
|
||||||
late final AssetEntity? local;
|
height = local.height,
|
||||||
|
width = local.width,
|
||||||
bool get isRemote => remote != null;
|
fileName = local.title!,
|
||||||
bool get isLocal => local != null;
|
deviceAssetId = local.id,
|
||||||
|
deviceId = Hive.box(userInfoBox).get(deviceIdKey),
|
||||||
String get deviceId =>
|
ownerId = owner,
|
||||||
isRemote ? remote!.deviceId : Hive.box(userInfoBox).get(deviceIdKey);
|
modifiedAt = local.modifiedDateTime.toUtc(),
|
||||||
|
createdAt = local.createDateTime.toUtc() {
|
||||||
String get deviceAssetId => isRemote ? remote!.deviceAssetId : local!.id;
|
if (createdAt.year == 1970) {
|
||||||
|
createdAt = modifiedAt;
|
||||||
String get id => isLocal ? local!.id : remote!.id;
|
|
||||||
|
|
||||||
double? get latitude =>
|
|
||||||
isLocal ? local!.latitude : remote!.exifInfo?.latitude?.toDouble();
|
|
||||||
|
|
||||||
double? get longitude =>
|
|
||||||
isLocal ? local!.longitude : remote!.exifInfo?.longitude?.toDouble();
|
|
||||||
|
|
||||||
DateTime get createdAt {
|
|
||||||
if (isLocal) {
|
|
||||||
if (local!.createDateTime.year == 1970) {
|
|
||||||
return local!.modifiedDateTime;
|
|
||||||
}
|
|
||||||
return local!.createDateTime;
|
|
||||||
} else {
|
|
||||||
return DateTime.parse(remote!.createdAt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isImage => isLocal
|
Asset({
|
||||||
? local!.type == AssetType.image
|
this.localId,
|
||||||
: remote!.type == AssetTypeEnum.IMAGE;
|
this.remoteId,
|
||||||
|
required this.deviceAssetId,
|
||||||
|
required this.deviceId,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.modifiedAt,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
required this.durationInSeconds,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
required this.fileName,
|
||||||
|
this.livePhotoVideoId,
|
||||||
|
this.exifInfo,
|
||||||
|
});
|
||||||
|
|
||||||
String get duration => isRemote
|
AssetEntity? _local;
|
||||||
? remote!.duration
|
|
||||||
: Duration(seconds: local!.duration).toString();
|
|
||||||
|
|
||||||
/// use only for tests
|
AssetEntity? get local {
|
||||||
set createdAt(DateTime val) {
|
if (isLocal && _local == null) {
|
||||||
if (isRemote) {
|
_local = AssetEntity(
|
||||||
remote!.createdAt = val.toIso8601String();
|
id: localId!.toString(),
|
||||||
|
typeInt: isImage ? 1 : 2,
|
||||||
|
width: width!,
|
||||||
|
height: height!,
|
||||||
|
duration: durationInSeconds,
|
||||||
|
createDateSecond: createdAt.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
modifiedDateSecond: modifiedAt.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
title: fileName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return _local;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? localId;
|
||||||
|
|
||||||
|
String? remoteId;
|
||||||
|
|
||||||
|
String deviceAssetId;
|
||||||
|
|
||||||
|
String deviceId;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
DateTime modifiedAt;
|
||||||
|
|
||||||
|
double? latitude;
|
||||||
|
|
||||||
|
double? longitude;
|
||||||
|
|
||||||
|
int durationInSeconds;
|
||||||
|
|
||||||
|
int? width;
|
||||||
|
|
||||||
|
int? height;
|
||||||
|
|
||||||
|
String fileName;
|
||||||
|
|
||||||
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
|
ExifInfo? exifInfo;
|
||||||
|
|
||||||
|
String get id => isLocal ? localId.toString() : remoteId!;
|
||||||
|
|
||||||
|
String get name => p.withoutExtension(fileName);
|
||||||
|
|
||||||
|
bool get isRemote => remoteId != null;
|
||||||
|
|
||||||
|
bool get isLocal => localId != null;
|
||||||
|
|
||||||
|
bool get isImage => durationInSeconds == 0;
|
||||||
|
|
||||||
|
Duration get duration => Duration(seconds: durationInSeconds);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! Asset) return false;
|
if (other is! Asset) return false;
|
||||||
@ -67,12 +132,26 @@ class Asset {
|
|||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
int get hashCode => id.hashCode;
|
||||||
|
|
||||||
|
// methods below are only required for caching as JSON
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
if (isLocal) {
|
json["localId"] = localId;
|
||||||
json["local"] = _assetEntityToJson(local!);
|
json["remoteId"] = remoteId;
|
||||||
} else {
|
json["deviceAssetId"] = deviceAssetId;
|
||||||
json["remote"] = remote!.toJson();
|
json["deviceId"] = deviceId;
|
||||||
|
json["ownerId"] = ownerId;
|
||||||
|
json["createdAt"] = createdAt.millisecondsSinceEpoch;
|
||||||
|
json["modifiedAt"] = modifiedAt.millisecondsSinceEpoch;
|
||||||
|
json["latitude"] = latitude;
|
||||||
|
json["longitude"] = longitude;
|
||||||
|
json["durationInSeconds"] = durationInSeconds;
|
||||||
|
json["width"] = width;
|
||||||
|
json["height"] = height;
|
||||||
|
json["fileName"] = fileName;
|
||||||
|
json["livePhotoVideoId"] = livePhotoVideoId;
|
||||||
|
if (exifInfo != null) {
|
||||||
|
json["exifInfo"] = exifInfo!.toJson();
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -80,55 +159,28 @@ class Asset {
|
|||||||
static Asset? fromJson(dynamic value) {
|
static Asset? fromJson(dynamic value) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
final l = json["local"];
|
return Asset(
|
||||||
if (l != null) {
|
localId: json["localId"],
|
||||||
return Asset.local(_assetEntityFromJson(l));
|
remoteId: json["remoteId"],
|
||||||
} else {
|
deviceAssetId: json["deviceAssetId"],
|
||||||
return Asset.remote(AssetResponseDto.fromJson(json["remote"]));
|
deviceId: json["deviceId"],
|
||||||
}
|
ownerId: json["ownerId"],
|
||||||
|
createdAt:
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(json["createdAt"], isUtc: true),
|
||||||
|
modifiedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
json["modifiedAt"],
|
||||||
|
isUtc: true,
|
||||||
|
),
|
||||||
|
latitude: json["latitude"],
|
||||||
|
longitude: json["longitude"],
|
||||||
|
durationInSeconds: json["durationInSeconds"],
|
||||||
|
width: json["width"],
|
||||||
|
height: json["height"],
|
||||||
|
fileName: json["fileName"],
|
||||||
|
livePhotoVideoId: json["livePhotoVideoId"],
|
||||||
|
exifInfo: ExifInfo.fromJson(json["exifInfo"]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _assetEntityToJson(AssetEntity a) {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json["id"] = a.id;
|
|
||||||
json["typeInt"] = a.typeInt;
|
|
||||||
json["width"] = a.width;
|
|
||||||
json["height"] = a.height;
|
|
||||||
json["duration"] = a.duration;
|
|
||||||
json["orientation"] = a.orientation;
|
|
||||||
json["isFavorite"] = a.isFavorite;
|
|
||||||
json["title"] = a.title;
|
|
||||||
json["createDateSecond"] = a.createDateSecond;
|
|
||||||
json["modifiedDateSecond"] = a.modifiedDateSecond;
|
|
||||||
json["latitude"] = a.latitude;
|
|
||||||
json["longitude"] = a.longitude;
|
|
||||||
json["mimeType"] = a.mimeType;
|
|
||||||
json["subtype"] = a.subtype;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetEntity? _assetEntityFromJson(dynamic value) {
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
return AssetEntity(
|
|
||||||
id: json["id"],
|
|
||||||
typeInt: json["typeInt"],
|
|
||||||
width: json["width"],
|
|
||||||
height: json["height"],
|
|
||||||
duration: json["duration"],
|
|
||||||
orientation: json["orientation"],
|
|
||||||
isFavorite: json["isFavorite"],
|
|
||||||
title: json["title"],
|
|
||||||
createDateSecond: json["createDateSecond"],
|
|
||||||
modifiedDateSecond: json["modifiedDateSecond"],
|
|
||||||
latitude: json["latitude"],
|
|
||||||
longitude: json["longitude"],
|
|
||||||
mimeType: json["mimeType"],
|
|
||||||
subtype: json["subtype"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
86
mobile/lib/shared/models/exif_info.dart
Normal file
86
mobile/lib/shared/models/exif_info.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||||
|
|
||||||
|
class ExifInfo {
|
||||||
|
int? fileSize;
|
||||||
|
String? make;
|
||||||
|
String? model;
|
||||||
|
String? orientation;
|
||||||
|
String? lensModel;
|
||||||
|
double? fNumber;
|
||||||
|
double? focalLength;
|
||||||
|
int? iso;
|
||||||
|
double? exposureTime;
|
||||||
|
String? city;
|
||||||
|
String? state;
|
||||||
|
String? country;
|
||||||
|
|
||||||
|
ExifInfo.fromDto(ExifResponseDto dto)
|
||||||
|
: fileSize = dto.fileSizeInByte,
|
||||||
|
make = dto.make,
|
||||||
|
model = dto.model,
|
||||||
|
orientation = dto.orientation,
|
||||||
|
lensModel = dto.lensModel,
|
||||||
|
fNumber = dto.fNumber?.toDouble(),
|
||||||
|
focalLength = dto.focalLength?.toDouble(),
|
||||||
|
iso = dto.iso?.toInt(),
|
||||||
|
exposureTime = dto.exposureTime?.toDouble(),
|
||||||
|
city = dto.city,
|
||||||
|
state = dto.state,
|
||||||
|
country = dto.country;
|
||||||
|
|
||||||
|
// stuff below is only required for caching as JSON
|
||||||
|
|
||||||
|
ExifInfo(
|
||||||
|
this.fileSize,
|
||||||
|
this.make,
|
||||||
|
this.model,
|
||||||
|
this.orientation,
|
||||||
|
this.lensModel,
|
||||||
|
this.fNumber,
|
||||||
|
this.focalLength,
|
||||||
|
this.iso,
|
||||||
|
this.exposureTime,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.country,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json["fileSize"] = fileSize;
|
||||||
|
json["make"] = make;
|
||||||
|
json["model"] = model;
|
||||||
|
json["orientation"] = orientation;
|
||||||
|
json["lensModel"] = lensModel;
|
||||||
|
json["fNumber"] = fNumber;
|
||||||
|
json["focalLength"] = focalLength;
|
||||||
|
json["iso"] = iso;
|
||||||
|
json["exposureTime"] = exposureTime;
|
||||||
|
json["city"] = city;
|
||||||
|
json["state"] = state;
|
||||||
|
json["country"] = country;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ExifInfo? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return ExifInfo(
|
||||||
|
json["fileSize"],
|
||||||
|
json["make"],
|
||||||
|
json["model"],
|
||||||
|
json["orientation"],
|
||||||
|
json["lensModel"],
|
||||||
|
json["fNumber"],
|
||||||
|
json["focalLength"],
|
||||||
|
json["iso"],
|
||||||
|
json["exposureTime"],
|
||||||
|
json["city"],
|
||||||
|
json["state"],
|
||||||
|
json["country"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.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/services/asset_cache.service.dart';
|
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
@ -36,7 +36,7 @@ class AssetsState {
|
|||||||
return AssetsState([...allAssets, ...toAdd]);
|
return AssetsState([...allAssets, ...toAdd]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupByDate() async {
|
Future<Map<String, List<Asset>>> _groupByDate() async {
|
||||||
sortCompare(List<Asset> assets) {
|
sortCompare(List<Asset> assets) {
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => e.createdAt,
|
(e) => e.createdAt,
|
||||||
@ -50,11 +50,11 @@ class AssetsState {
|
|||||||
return await compute(sortCompare, allAssets.toList());
|
return await compute(sortCompare, allAssets.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromAssetList(List<Asset> assets) {
|
static AssetsState fromAssetList(List<Asset> assets) {
|
||||||
return AssetsState(assets);
|
return AssetsState(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static empty() {
|
static AssetsState empty() {
|
||||||
return AssetsState([]);
|
return AssetsState([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +82,10 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
this._settingsService,
|
this._settingsService,
|
||||||
) : super(AssetsState.fromAssetList([]));
|
) : super(AssetsState.fromAssetList([]));
|
||||||
|
|
||||||
_updateAssetsState(List<Asset> newAssetList, {bool cache = true}) async {
|
Future<void> _updateAssetsState(
|
||||||
|
List<Asset> newAssetList, {
|
||||||
|
bool cache = true,
|
||||||
|
}) async {
|
||||||
if (cache) {
|
if (cache) {
|
||||||
_assetCacheService.put(newAssetList);
|
_assetCacheService.put(newAssetList);
|
||||||
}
|
}
|
||||||
@ -101,20 +104,26 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
final stopwatch = Stopwatch();
|
final stopwatch = Stopwatch();
|
||||||
try {
|
try {
|
||||||
_getAllAssetInProgress = true;
|
_getAllAssetInProgress = true;
|
||||||
final bool isCacheValid = await _assetCacheService.isValid();
|
bool isCacheValid = await _assetCacheService.isValid();
|
||||||
stopwatch.start();
|
stopwatch.start();
|
||||||
final Box box = Hive.box(userInfoBox);
|
final Box box = Hive.box(userInfoBox);
|
||||||
|
if (isCacheValid && state.allAssets.isEmpty) {
|
||||||
|
final List<Asset>? cachedData = await _assetCacheService.get();
|
||||||
|
if (cachedData == null) {
|
||||||
|
isCacheValid = false;
|
||||||
|
log.warning("Cached asset data is invalid, fetching new data");
|
||||||
|
} else {
|
||||||
|
await _updateAssetsState(cachedData, cache: false);
|
||||||
|
log.info(
|
||||||
|
"Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stopwatch.reset();
|
||||||
|
}
|
||||||
final localTask = _assetService.getLocalAssets(urgent: !isCacheValid);
|
final localTask = _assetService.getLocalAssets(urgent: !isCacheValid);
|
||||||
final remoteTask = _assetService.getRemoteAssets(
|
final remoteTask = _assetService.getRemoteAssets(
|
||||||
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
||||||
);
|
);
|
||||||
if (isCacheValid && state.allAssets.isEmpty) {
|
|
||||||
await _updateAssetsState(await _assetCacheService.get(), cache: false);
|
|
||||||
log.info(
|
|
||||||
"Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms",
|
|
||||||
);
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
||||||
remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin;
|
remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin;
|
||||||
@ -184,7 +193,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
_updateAssetsState([]);
|
_updateAssetsState([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
void onNewAssetUploaded(Asset newAsset) {
|
||||||
final int i = state.allAssets.indexWhere(
|
final int i = state.allAssets.indexWhere(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.isRemote ||
|
a.isRemote ||
|
||||||
@ -192,13 +201,13 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) {
|
if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||||
_updateAssetsState([...state.allAssets, Asset.remote(newAsset)]);
|
_updateAssetsState([...state.allAssets, newAsset]);
|
||||||
} else {
|
} else {
|
||||||
// order is important to keep all local-only assets at the beginning!
|
// order is important to keep all local-only assets at the beginning!
|
||||||
_updateAssetsState([
|
_updateAssetsState([
|
||||||
...state.allAssets.slice(0, i),
|
...state.allAssets.slice(0, i),
|
||||||
...state.allAssets.slice(i + 1),
|
...state.allAssets.slice(i + 1),
|
||||||
Asset.remote(newAsset),
|
newAsset,
|
||||||
]);
|
]);
|
||||||
// TODO here is a place to unify local/remote assets by replacing the
|
// TODO here is a place to unify local/remote assets by replacing the
|
||||||
// local-only asset in the state with a local&remote asset
|
// local-only asset in the state with a local&remote asset
|
||||||
@ -230,7 +239,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
// Delete asset from device
|
// Delete asset from device
|
||||||
for (final Asset asset in assetsToDelete) {
|
for (final Asset asset in assetsToDelete) {
|
||||||
if (asset.isLocal) {
|
if (asset.isLocal) {
|
||||||
local.add(asset.id);
|
local.add(asset.localId!);
|
||||||
} else if (asset.deviceId == deviceId) {
|
} else if (asset.deviceId == deviceId) {
|
||||||
// Delete asset on device if it is still present
|
// Delete asset on device if it is still present
|
||||||
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||||
@ -252,8 +261,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
Future<Iterable<String>> _deleteRemoteAssets(
|
Future<Iterable<String>> _deleteRemoteAssets(
|
||||||
Set<Asset> assetsToDelete,
|
Set<Asset> assetsToDelete,
|
||||||
) async {
|
) async {
|
||||||
final Iterable<AssetResponseDto> remote =
|
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
||||||
assetsToDelete.where((e) => e.isRemote).map((e) => e.remote!);
|
|
||||||
final List<DeleteAssetResponseDto> deleteAssetResult =
|
final List<DeleteAssetResponseDto> deleteAssetResult =
|
||||||
await _assetService.deleteAssets(remote) ?? [];
|
await _assetService.deleteAssets(remote) ?? [];
|
||||||
return deleteAssetResult
|
return deleteAssetResult
|
||||||
|
@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@ -91,14 +92,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
state = WebsocketState(isConnected: false, socket: null);
|
state = WebsocketState(isConnected: false, socket: null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('on_upload_success', (data) {
|
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||||
var jsonString = jsonDecode(data.toString());
|
|
||||||
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
|
||||||
|
|
||||||
if (newAsset != null) {
|
|
||||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||||
}
|
}
|
||||||
@ -122,14 +116,16 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
|
|
||||||
listenUploadEvent() {
|
listenUploadEvent() {
|
||||||
debugPrint("Start listening to event on_upload_success");
|
debugPrint("Start listening to event on_upload_success");
|
||||||
state.socket?.on('on_upload_success', (data) {
|
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
|
||||||
var jsonString = jsonDecode(data.toString());
|
}
|
||||||
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
|
||||||
|
|
||||||
if (newAsset != null) {
|
_handleOnUploadSuccess(dynamic data) {
|
||||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
final jsonString = jsonDecode(data.toString());
|
||||||
}
|
final dto = AssetResponseDto.fromJson(jsonString);
|
||||||
});
|
if (dto != null) {
|
||||||
|
final newAsset = Asset.remote(dto);
|
||||||
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,10 +62,11 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||||
|
final String userId = Hive.box(userInfoBox).get(userIdKey);
|
||||||
if (backupAlbumInfo != null) {
|
if (backupAlbumInfo != null) {
|
||||||
return (await _backupService
|
return (await _backupService
|
||||||
.buildUploadCandidates(backupAlbumInfo.deepCopy()))
|
.buildUploadCandidates(backupAlbumInfo.deepCopy()))
|
||||||
.map(Asset.local)
|
.map((e) => Asset.local(e, userId))
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -76,21 +77,24 @@ class AssetService {
|
|||||||
|
|
||||||
Future<Asset?> getAssetById(String assetId) async {
|
Future<Asset?> getAssetById(String assetId) async {
|
||||||
try {
|
try {
|
||||||
return Asset.remote(await _apiService.assetApi.getAssetById(assetId));
|
final dto = await _apiService.assetApi.getAssetById(assetId);
|
||||||
|
if (dto != null) {
|
||||||
|
return Asset.remote(dto);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAssetById] ${e.toString()}");
|
debugPrint("Error [getAssetById] ${e.toString()}");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||||
Iterable<AssetResponseDto> deleteAssets,
|
Iterable<Asset> deleteAssets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final List<String> payload = [];
|
final List<String> payload = [];
|
||||||
|
|
||||||
for (final asset in deleteAssets) {
|
for (final asset in deleteAssets) {
|
||||||
payload.add(asset.id);
|
payload.add(asset.remoteId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _apiService.assetApi
|
return await _apiService.assetApi
|
@ -23,17 +23,15 @@ class AssetCacheService extends JsonCache<List<Asset>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Asset>> get() async {
|
Future<List<Asset>?> get() async {
|
||||||
try {
|
try {
|
||||||
final mapList = await readRawData() as List<dynamic>;
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
final responseData = await compute(_computeEncode, mapList);
|
final responseData = await compute(_computeEncode, mapList);
|
||||||
|
|
||||||
return responseData;
|
return responseData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(e.toString());
|
debugPrint(e.toString());
|
||||||
|
await invalidate();
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -60,5 +60,5 @@ abstract class JsonCache<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void put(T data);
|
void put(T data);
|
||||||
Future<T> get();
|
Future<T?> get();
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'api.service.dart';
|
import 'api.service.dart';
|
||||||
@ -25,11 +24,10 @@ class ShareService {
|
|||||||
final downloadedXFiles = assets.map<Future<XFile>>((asset) async {
|
final downloadedXFiles = assets.map<Future<XFile>>((asset) async {
|
||||||
if (asset.isRemote) {
|
if (asset.isRemote) {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final fileName = basename(asset.remote!.originalPath);
|
final fileName = asset.fileName;
|
||||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||||
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
final res = await _apiService.assetApi
|
||||||
asset.remote!.id,
|
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||||
);
|
|
||||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
return XFile(tempFile.path);
|
return XFile(tempFile.path);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@ -15,13 +16,28 @@ class ImmichImage extends StatelessWidget {
|
|||||||
this.useGrayBoxPlaceholder = false,
|
this.useGrayBoxPlaceholder = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final Asset asset;
|
final Asset? asset;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (this.asset == null) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.no_photography),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final Asset asset = this.asset!;
|
||||||
if (asset.isLocal) {
|
if (asset.isLocal) {
|
||||||
return Image(
|
return Image(
|
||||||
image: AssetEntityImageProvider(
|
image: AssetEntityImageProvider(
|
||||||
@ -49,7 +65,16 @@ class ImmichImage extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
debugPrint("Error getting thumb for assetId=${asset.id}: $error");
|
if (error is PlatformException &&
|
||||||
|
error.code == "The asset not found!") {
|
||||||
|
debugPrint(
|
||||||
|
"Asset ${asset.localId} does not exist anymore on device!",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
"Error getting thumb for assetId=${asset.localId}: $error",
|
||||||
|
);
|
||||||
|
}
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.image_not_supported_outlined,
|
Icons.image_not_supported_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@ -57,12 +82,12 @@ class ImmichImage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final String token = Hive.box(userInfoBox).get(accessTokenKey);
|
final String? token = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
final String thumbnailRequestUrl = getThumbnailUrl(asset.remote!);
|
final String thumbnailRequestUrl = getThumbnailUrl(asset);
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer $token"},
|
httpHeaders: {"Authorization": "Bearer $token"},
|
||||||
cacheKey: getThumbnailCacheKey(asset.remote!),
|
cacheKey: getThumbnailCacheKey(asset),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
||||||
|
11
mobile/lib/utils/builtin_extensions.dart
Normal file
11
mobile/lib/utils/builtin_extensions.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
extension DurationExtension on String {
|
||||||
|
Duration toDuration() {
|
||||||
|
final parts =
|
||||||
|
split(':').map((e) => double.parse(e).toInt()).toList(growable: false);
|
||||||
|
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
double? toDouble() {
|
||||||
|
return double.tryParse(this);
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,18 @@
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
import '../constants/hive_box.dart';
|
import '../constants/hive_box.dart';
|
||||||
|
|
||||||
String getThumbnailUrl(
|
String getThumbnailUrl(
|
||||||
final AssetResponseDto asset, {
|
final Asset asset, {
|
||||||
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||||
}) {
|
}) {
|
||||||
return _getThumbnailUrl(asset.id, type: type);
|
return _getThumbnailUrl(asset.remoteId!, type: type);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getThumbnailCacheKey(
|
String getThumbnailCacheKey(
|
||||||
final AssetResponseDto asset, {
|
final Asset asset, {
|
||||||
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||||
}) {
|
}) {
|
||||||
return _getThumbnailCacheKey(asset.id, type);
|
return _getThumbnailCacheKey(asset.id, type);
|
||||||
@ -45,12 +46,12 @@ String getAlbumThumbNailCacheKey(
|
|||||||
return _getThumbnailCacheKey(album.albumThumbnailAssetId!, type);
|
return _getThumbnailCacheKey(album.albumThumbnailAssetId!, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getImageUrl(final AssetResponseDto asset) {
|
String getImageUrl(final Asset asset) {
|
||||||
final box = Hive.box(userInfoBox);
|
final box = Hive.box(userInfoBox);
|
||||||
return '${box.get(serverEndpointKey)}/asset/file/${asset.id}?isThumb=false';
|
return '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}?isThumb=false';
|
||||||
}
|
}
|
||||||
|
|
||||||
String getImageCacheKey(final AssetResponseDto asset) {
|
String getImageCacheKey(final Asset asset) {
|
||||||
return '${asset.id}_fullStage';
|
return '${asset.id}_fullStage';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final List<Asset> testAssets = [];
|
final List<Asset> testAssets = [];
|
||||||
@ -13,24 +12,14 @@ void main() {
|
|||||||
DateTime date = DateTime(2022, month, day);
|
DateTime date = DateTime(2022, month, day);
|
||||||
|
|
||||||
testAssets.add(
|
testAssets.add(
|
||||||
Asset.remote(
|
Asset(
|
||||||
AssetResponseDto(
|
deviceAssetId: '$i',
|
||||||
type: AssetTypeEnum.IMAGE,
|
deviceId: '',
|
||||||
id: '$i',
|
ownerId: '',
|
||||||
deviceAssetId: '',
|
createdAt: date,
|
||||||
ownerId: '',
|
modifiedAt: date,
|
||||||
deviceId: '',
|
durationInSeconds: 0,
|
||||||
originalPath: '',
|
fileName: '',
|
||||||
resizePath: '',
|
|
||||||
createdAt: date.toIso8601String(),
|
|
||||||
modifiedAt: date.toIso8601String(),
|
|
||||||
isFavorite: false,
|
|
||||||
mimeType: 'image/jpeg',
|
|
||||||
duration: '',
|
|
||||||
webpPath: '',
|
|
||||||
encodedVideoPath: '',
|
|
||||||
livePhotoVideoId: '',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -70,11 +59,20 @@ void main() {
|
|||||||
// Day 1
|
// Day 1
|
||||||
// 15 Assets => 5 Rows
|
// 15 Assets => 5 Rows
|
||||||
expect(renderList.elements.length, 18);
|
expect(renderList.elements.length, 18);
|
||||||
expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle);
|
expect(
|
||||||
|
renderList.elements[0].type,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
);
|
||||||
expect(renderList.elements[0].date.month, 1);
|
expect(renderList.elements[0].date.month, 1);
|
||||||
expect(renderList.elements[7].type, RenderAssetGridElementType.monthTitle);
|
expect(
|
||||||
|
renderList.elements[7].type,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
);
|
||||||
expect(renderList.elements[7].date.month, 2);
|
expect(renderList.elements[7].date.month, 2);
|
||||||
expect(renderList.elements[11].type, RenderAssetGridElementType.monthTitle);
|
expect(
|
||||||
|
renderList.elements[11].type,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
);
|
||||||
expect(renderList.elements[11].date.month, 10);
|
expect(renderList.elements[11].date.month, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user