feat: support iOS LivePhoto backup (#950)

This commit is contained in:
Alex
2022-11-18 23:12:54 -06:00
committed by GitHub
parent 83e2cabbcc
commit 8bc64be77b
30 changed files with 678 additions and 243 deletions

View File

@@ -22,27 +22,58 @@ class ImageViewerService {
try {
String fileName = p.basename(asset.originalPath);
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id,
isThumb: false,
isWeb: false,
);
// Download LivePhotos image and motion part
if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) {
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id,
isThumb: false,
isWeb: false,
);
final AssetEntity? entity;
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.livePhotoVideoId!,
isThumb: false,
isWeb: false,
);
if (asset.type == AssetTypeEnum.IMAGE) {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
final AssetEntity? entity;
final tempDir = await getTemporaryDirectory();
File videoFile = await File('${tempDir.path}/livephoto.mov').create();
File imageFile = await File('${tempDir.path}/livephoto.heic').create();
videoFile.writeAsBytesSync(motionReponse.bodyBytes);
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
entity = await PhotoManager.editor.darwin.saveLivePhoto(
imageFile: imageFile,
videoFile: videoFile,
title: p.basename(asset.originalPath),
);
} else {
final tempDir = await getTemporaryDirectory();
File tempFile = await File('${tempDir.path}/$fileName').create();
tempFile.writeAsBytesSync(res.bodyBytes);
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
}
return entity != null;
return entity != null;
} else {
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id,
isThumb: false,
isWeb: false,
);
final AssetEntity? entity;
if (asset.type == AssetTypeEnum.IMAGE) {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
title: p.basename(asset.originalPath),
);
} else {
final tempDir = await getTemporaryDirectory();
File tempFile = await File('${tempDir.path}/$fileName').create();
tempFile.writeAsBytesSync(res.bodyBytes);
entity =
await PhotoManager.editor.saveVideo(tempFile, title: fileName);
}
return entity != null;
}
} catch (e) {
debugPrint("Error saving file $e");
return false;

View File

@@ -37,7 +37,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
void handleSwipUpDown(PointerMoveEvent details) {
int sensitivity = 10;
int sensitivity = 15;
if (_zoomedIn) {
return;

View File

@@ -3,21 +3,23 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
const TopControlAppBar({
Key? key,
required this.asset,
required this.onMoreInfoPressed,
required this.onDownloadPressed,
required this.onSharePressed,
this.loading = false,
required this.onToggleMotionVideo,
required this.isPlayingMotionVideo,
}) : super(key: key);
final Asset asset;
final Function onMoreInfoPressed;
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final Function onSharePressed;
final bool loading;
final bool isPlayingMotionVideo;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -38,14 +40,16 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
),
),
actions: [
if (loading)
Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
),
if (asset.remote?.livePhotoVideoId != null)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onToggleMotionVideo();
},
icon: isPlayingMotionVideo
? const Icon(Icons.motion_photos_pause_outlined)
: const Icon(Icons.play_circle_outline_rounded),
),
if (!asset.isLocal)
IconButton(
@@ -79,7 +83,7 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
Icons.more_horiz_rounded,
color: Colors.grey[200],
),
)
),
],
);
}

View File

@@ -33,10 +33,10 @@ class GalleryViewerPage extends HookConsumerWidget {
final Box<dynamic> box = Hive.box(userInfoBox);
final appSettingService = ref.watch(appSettingsServiceProvider);
final threeStageLoading = useState(false);
final loading = useState(false);
final isZoomed = useState<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
final indexOfAsset = useState(assetList.indexOf(asset));
final isPlayingMotionVideo = useState(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
PageController controller =
PageController(initialPage: assetList.indexOf(asset));
@@ -45,6 +45,7 @@ class GalleryViewerPage extends HookConsumerWidget {
() {
threeStageLoading.value = appSettingService
.getSetting<bool>(AppSettingsEnum.threeStageLoading);
isPlayingMotionVideo.value = false;
return null;
},
[],
@@ -85,7 +86,7 @@ class GalleryViewerPage extends HookConsumerWidget {
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
loading: loading.value,
isPlayingMotionVideo: isPlayingMotionVideo.value,
asset: assetList[indexOfAsset.value],
onMoreInfoPressed: () {
showInfo();
@@ -94,13 +95,18 @@ class GalleryViewerPage extends HookConsumerWidget {
? null
: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
assetList[indexOfAsset.value].remote!, context);
assetList[indexOfAsset.value].remote!,
context,
);
},
onSharePressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.shareAsset(assetList[indexOfAsset.value], context);
},
onToggleMotionVideo: (() {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
),
body: SafeArea(
child: PageView.builder(
@@ -119,18 +125,28 @@ class GalleryViewerPage extends HookConsumerWidget {
getAssetExif();
if (assetList[index].isImage) {
return ImageViewerPage(
authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
asset: assetList[index],
heroTag: assetList[index].id,
threeStageLoading: threeStageLoading.value,
);
if (isPlayingMotionVideo.value) {
return VideoViewerPage(
asset: assetList[index],
isMotionVideo: true,
onVideoEnded: () {
isPlayingMotionVideo.value = false;
},
);
} else {
return ImageViewerPage(
authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
asset: assetList[index],
heroTag: assetList[index].id,
threeStageLoading: threeStageLoading.value,
);
}
} else {
return GestureDetector(
onVerticalDragUpdate: (details) {
const int sensitivity = 10;
const int sensitivity = 15;
if (details.delta.dy > sensitivity) {
// swipe down
AutoRouter.of(context).pop();
@@ -141,7 +157,11 @@ class GalleryViewerPage extends HookConsumerWidget {
},
child: Hero(
tag: assetList[index].id,
child: VideoViewerPage(asset: assetList[index]),
child: VideoViewerPage(
asset: assetList[index],
isMotionVideo: false,
onVideoEnded: () {},
),
),
);
}

View File

@@ -15,15 +15,26 @@ import 'package:video_player/video_player.dart';
// ignore: must_be_immutable
class VideoViewerPage extends HookConsumerWidget {
final Asset asset;
final bool isMotionVideo;
final VoidCallback onVideoEnded;
const VideoViewerPage({Key? key, required this.asset}) : super(key: key);
const VideoViewerPage({
Key? key,
required this.asset,
required this.isMotionVideo,
required this.onVideoEnded,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
if (asset.isLocal) {
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
return videoFile.when(
data: (data) => VideoThumbnailPlayer(file: data),
data: (data) => VideoThumbnailPlayer(
file: data,
isMotionVideo: false,
onVideoEnded: () {},
),
error: (error, stackTrace) => Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
@@ -41,14 +52,17 @@ class VideoViewerPage extends HookConsumerWidget {
ref.watch(imageViewerStateProvider).downloadAssetStatus;
final box = Hive.box(userInfoBox);
final String jwtToken = box.get(accessTokenKey);
final String videoUrl =
'${box.get(serverEndpointKey)}/asset/file/${asset.id}';
final String videoUrl = isMotionVideo
? '${box.get(serverEndpointKey)}/asset/file/${asset.remote?.livePhotoVideoId!}'
: '${box.get(serverEndpointKey)}/asset/file/${asset.id}';
return Stack(
children: [
VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
isMotionVideo: isMotionVideo,
onVideoEnded: onVideoEnded,
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
@@ -72,9 +86,17 @@ class VideoThumbnailPlayer extends StatefulWidget {
final String? url;
final String? jwtToken;
final File? file;
final bool isMotionVideo;
final VoidCallback onVideoEnded;
const VideoThumbnailPlayer({Key? key, this.url, this.jwtToken, this.file})
: super(key: key);
const VideoThumbnailPlayer({
Key? key,
this.url,
this.jwtToken,
this.file,
required this.onVideoEnded,
required this.isMotionVideo,
}) : super(key: key);
@override
State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
@@ -88,6 +110,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
void initState() {
super.initState();
initializePlayer();
videoPlayerController.addListener(() {
if (videoPlayerController.value.position ==
videoPlayerController.value.duration) {
widget.onVideoEnded();
}
});
}
Future<void> initializePlayer() async {
@@ -115,7 +144,7 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
autoPlay: true,
autoInitialize: true,
allowFullScreen: true,
showControls: true,
showControls: !widget.isMotionVideo,
hideControlsTimer: const Duration(seconds: 5),
);
}

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
@@ -263,6 +264,13 @@ class BackupService {
req.files.add(assetRawUploadData);
if (entity.isLivePhoto) {
var livePhotoRawUploadData = await _getLivePhotoFile(entity);
if (livePhotoRawUploadData != null) {
req.files.add(livePhotoRawUploadData);
}
}
setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
@@ -322,6 +330,33 @@ class BackupService {
return !anyErrors;
}
Future<MultipartFile?> _getLivePhotoFile(AssetEntity entity) async {
var motionFilePath = await entity.getMediaUrl();
// var motionFilePath = '/var/mobile/Media/DCIM/103APPLE/IMG_3371.MOV'
if (motionFilePath != null) {
var validPath = motionFilePath.replaceAll('file://', '');
var motionFile = File(validPath);
var fileStream = motionFile.openRead();
String originalFileName = await entity.titleAsync;
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
var mimeType = FileHelper.getMimeType(validPath);
return http.MultipartFile(
"livePhotoData",
fileStream,
motionFile.lengthSync(),
filename: fileNameWithoutPath,
contentType: MediaType(
mimeType["type"],
mimeType["subType"],
),
);
}
return null;
}
String _getAssetType(AssetType assetType) {
switch (assetType) {
case AssetType.audio:

View File

@@ -65,7 +65,11 @@ class _$AppRouter extends RootStackRouter {
final args = routeData.argsAs<VideoViewerRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: VideoViewerPage(key: args.key, asset: args.asset));
child: VideoViewerPage(
key: args.key,
asset: args.asset,
isMotionVideo: args.isMotionVideo,
onVideoEnded: args.onVideoEnded));
},
BackupControllerRoute.name: (routeData) {
return MaterialPageX<dynamic>(
@@ -340,24 +344,40 @@ class ImageViewerRouteArgs {
/// generated route for
/// [VideoViewerPage]
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
VideoViewerRoute({Key? key, required Asset asset})
VideoViewerRoute(
{Key? key,
required Asset asset,
required bool isMotionVideo,
required void Function() onVideoEnded})
: super(VideoViewerRoute.name,
path: '/video-viewer-page',
args: VideoViewerRouteArgs(key: key, asset: asset));
args: VideoViewerRouteArgs(
key: key,
asset: asset,
isMotionVideo: isMotionVideo,
onVideoEnded: onVideoEnded));
static const String name = 'VideoViewerRoute';
}
class VideoViewerRouteArgs {
const VideoViewerRouteArgs({this.key, required this.asset});
const VideoViewerRouteArgs(
{this.key,
required this.asset,
required this.isMotionVideo,
required this.onVideoEnded});
final Key? key;
final Asset asset;
final bool isMotionVideo;
final void Function() onVideoEnded;
@override
String toString() {
return 'VideoViewerRouteArgs{key: $key, asset: $asset}';
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded}';
}
}

View File

@@ -24,6 +24,7 @@ Name | Type | Description | Notes
**encodedVideoPath** | **String** | |
**exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) | | [optional]
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**livePhotoVideoId** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -43,48 +43,51 @@ class AlbumResponseDto {
List<AssetResponseDto> assets;
@override
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
other.assetCount == assetCount &&
other.id == id &&
other.ownerId == ownerId &&
other.albumName == albumName &&
other.createdAt == createdAt &&
other.albumThumbnailAssetId == albumThumbnailAssetId &&
other.shared == shared &&
other.sharedUsers == sharedUsers &&
other.assets == assets;
bool operator ==(Object other) =>
identical(this, other) ||
other is AlbumResponseDto &&
other.assetCount == assetCount &&
other.id == id &&
other.ownerId == ownerId &&
other.albumName == albumName &&
other.createdAt == createdAt &&
other.albumThumbnailAssetId == albumThumbnailAssetId &&
other.shared == shared &&
other.sharedUsers == sharedUsers &&
other.assets == assets;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetCount.hashCode) +
(id.hashCode) +
(ownerId.hashCode) +
(albumName.hashCode) +
(createdAt.hashCode) +
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
(shared.hashCode) +
(sharedUsers.hashCode) +
(assets.hashCode);
// ignore: unnecessary_parenthesis
(assetCount.hashCode) +
(id.hashCode) +
(ownerId.hashCode) +
(albumName.hashCode) +
(createdAt.hashCode) +
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
(shared.hashCode) +
(sharedUsers.hashCode) +
(assets.hashCode);
@override
String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
String toString() =>
'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'assetCount'] = assetCount;
_json[r'id'] = id;
_json[r'ownerId'] = ownerId;
_json[r'albumName'] = albumName;
_json[r'createdAt'] = createdAt;
_json[r'assetCount'] = assetCount;
_json[r'id'] = id;
_json[r'ownerId'] = ownerId;
_json[r'albumName'] = albumName;
_json[r'createdAt'] = createdAt;
if (albumThumbnailAssetId != null) {
_json[r'albumThumbnailAssetId'] = albumThumbnailAssetId;
} else {
_json[r'albumThumbnailAssetId'] = null;
}
_json[r'shared'] = shared;
_json[r'sharedUsers'] = sharedUsers;
_json[r'assets'] = assets;
_json[r'shared'] = shared;
_json[r'sharedUsers'] = sharedUsers;
_json[r'assets'] = assets;
return _json;
}
@@ -98,13 +101,13 @@ class AlbumResponseDto {
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
// assert(() {
// requiredKeys.forEach((key) {
// assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
// });
// return true;
// }());
return AlbumResponseDto(
assetCount: mapValueOfType<int>(json, r'assetCount')!,
@@ -112,7 +115,8 @@ class AlbumResponseDto {
ownerId: mapValueOfType<String>(json, r'ownerId')!,
albumName: mapValueOfType<String>(json, r'albumName')!,
createdAt: mapValueOfType<String>(json, r'createdAt')!,
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
albumThumbnailAssetId:
mapValueOfType<String>(json, r'albumThumbnailAssetId'),
shared: mapValueOfType<bool>(json, r'shared')!,
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
assets: AssetResponseDto.listFromJson(json[r'assets'])!,
@@ -121,7 +125,10 @@ class AlbumResponseDto {
return null;
}
static List<AlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
static List<AlbumResponseDto>? listFromJson(
dynamic json, {
bool growable = false,
}) {
final result = <AlbumResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
@@ -149,12 +156,18 @@ class AlbumResponseDto {
}
// maps a json object with a list of AlbumResponseDto-objects as value to a dart map
static Map<String, List<AlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
static Map<String, List<AlbumResponseDto>> mapListFromJson(
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AlbumResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AlbumResponseDto.listFromJson(entry.value, growable: growable,);
final value = AlbumResponseDto.listFromJson(
entry.value,
growable: growable,
);
if (value != null) {
map[entry.key] = value;
}
@@ -176,4 +189,3 @@ class AlbumResponseDto {
'assets',
};
}

View File

@@ -29,6 +29,7 @@ class AssetResponseDto {
required this.encodedVideoPath,
this.exifInfo,
this.smartInfo,
required this.livePhotoVideoId,
});
AssetTypeEnum type;
@@ -75,70 +76,77 @@ class AssetResponseDto {
///
SmartInfoResponseDto? smartInfo;
String? livePhotoVideoId;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
other.type == type &&
other.id == id &&
other.deviceAssetId == deviceAssetId &&
other.ownerId == ownerId &&
other.deviceId == deviceId &&
other.originalPath == originalPath &&
other.resizePath == resizePath &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.mimeType == mimeType &&
other.duration == duration &&
other.webpPath == webpPath &&
other.encodedVideoPath == encodedVideoPath &&
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo;
bool operator ==(Object other) =>
identical(this, other) ||
other is AssetResponseDto &&
other.type == type &&
other.id == id &&
other.deviceAssetId == deviceAssetId &&
other.ownerId == ownerId &&
other.deviceId == deviceId &&
other.originalPath == originalPath &&
other.resizePath == resizePath &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.mimeType == mimeType &&
other.duration == duration &&
other.webpPath == webpPath &&
other.encodedVideoPath == encodedVideoPath &&
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo &&
other.livePhotoVideoId == livePhotoVideoId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(type.hashCode) +
(id.hashCode) +
(deviceAssetId.hashCode) +
(ownerId.hashCode) +
(deviceId.hashCode) +
(originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) +
(modifiedAt.hashCode) +
(isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode);
// ignore: unnecessary_parenthesis
(type.hashCode) +
(id.hashCode) +
(deviceAssetId.hashCode) +
(ownerId.hashCode) +
(deviceId.hashCode) +
(originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) +
(modifiedAt.hashCode) +
(isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode);
@override
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
String toString() =>
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'type'] = type;
_json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath;
_json[r'type'] = type;
_json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath;
if (resizePath != null) {
_json[r'resizePath'] = resizePath;
} else {
_json[r'resizePath'] = null;
}
_json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite;
_json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite;
if (mimeType != null) {
_json[r'mimeType'] = mimeType;
} else {
_json[r'mimeType'] = null;
}
_json[r'duration'] = duration;
_json[r'duration'] = duration;
if (webpPath != null) {
_json[r'webpPath'] = webpPath;
} else {
@@ -159,6 +167,11 @@ class AssetResponseDto {
} else {
_json[r'smartInfo'] = null;
}
if (livePhotoVideoId != null) {
_json[r'livePhotoVideoId'] = livePhotoVideoId;
} else {
_json[r'livePhotoVideoId'] = null;
}
return _json;
}
@@ -172,13 +185,13 @@ class AssetResponseDto {
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
// assert(() {
// requiredKeys.forEach((key) {
// assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
// });
// return true;
// }());
return AssetResponseDto(
type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -197,12 +210,16 @@ class AssetResponseDto {
encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
);
}
return null;
}
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
static List<AssetResponseDto>? listFromJson(
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
@@ -230,12 +247,18 @@ class AssetResponseDto {
}
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
static Map<String, List<AssetResponseDto>> mapListFromJson(
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
final value = AssetResponseDto.listFromJson(
entry.value,
growable: growable,
);
if (value != null) {
map[entry.key] = value;
}
@@ -260,6 +283,6 @@ class AssetResponseDto {
'duration',
'webpPath',
'encodedVideoPath',
'livePhotoVideoId',
};
}

View File

@@ -734,7 +734,7 @@ packages:
name: photo_manager
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.5.0"
photo_view:
dependency: "direct main"
description:

View File

@@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
photo_manager: ^2.2.1
photo_manager: ^2.5.0
flutter_hooks: ^0.18.0
hooks_riverpod: ^2.0.0-dev.0
hive: ^2.2.1
@@ -47,7 +47,6 @@ dependencies:
# easy to remove packages:
image_picker: ^0.8.5+3 # only used to select user profile image from system gallery -> we can simply select an image from within immich?
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -29,6 +29,7 @@ void main() {
duration: '',
webpPath: '',
encodedVideoPath: '',
livePhotoVideoId: '',
),
),
);