mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	Implemented EXIF store and display (#19)
* Added EXIF extracting in the backend * Added EXIF displaying on `image_viewer_page.dart` * Added Icon for backup option not enable
This commit is contained in:
		
							parent
							
								
									d1498506a8
								
							
						
					
					
						commit
						de1dbcea9c
					
				@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImageViewerPageState {
 | 
				
			||||||
 | 
					  final bool isBottomSheetEnable;
 | 
				
			||||||
 | 
					  ImageViewerPageState({
 | 
				
			||||||
 | 
					    required this.isBottomSheetEnable,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImageViewerPageState copyWith({
 | 
				
			||||||
 | 
					    bool? isBottomSheetEnable,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return ImageViewerPageState(
 | 
				
			||||||
 | 
					      isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'isBottomSheetEnable': isBottomSheetEnable,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
 | 
					    return ImageViewerPageState(
 | 
				
			||||||
 | 
					      isBottomSheetEnable: map['isBottomSheetEnable'] ?? false,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode => isBottomSheetEnable.hashCode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> {
 | 
				
			||||||
 | 
					  ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void toggleBottomSheet() {
 | 
				
			||||||
 | 
					    bool isBottomSheetEnable = state.isBottomSheetEnable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isBottomSheetEnable) {
 | 
				
			||||||
 | 
					      state.copyWith(isBottomSheetEnable: false);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      state.copyWith(isBottomSheetEnable: true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>(
 | 
				
			||||||
 | 
					    ((ref) => ImageViewerPageStateNotifier()));
 | 
				
			||||||
							
								
								
									
										118
									
								
								mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
 | 
				
			||||||
 | 
					import 'package:intl/intl.dart';
 | 
				
			||||||
 | 
					import 'package:path/path.dart' as p;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ExifBottomSheet extends ConsumerWidget {
 | 
				
			||||||
 | 
					  final ImmichAssetWithExif assetDetail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ExifBottomSheet({Key? key, required this.assetDetail}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    return Padding(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
 | 
				
			||||||
 | 
					      child: ListView(
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          assetDetail.exifInfo?.dateTimeOriginal != null
 | 
				
			||||||
 | 
					              ? Text(
 | 
				
			||||||
 | 
					                  DateFormat('E, LLL d, y • h:mm a').format(
 | 
				
			||||||
 | 
					                    DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  style: TextStyle(
 | 
				
			||||||
 | 
					                    color: Colors.grey[400],
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                    fontSize: 14,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : Container(),
 | 
				
			||||||
 | 
					          Padding(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.only(top: 16.0),
 | 
				
			||||||
 | 
					            child: Text(
 | 
				
			||||||
 | 
					              "Add Description...",
 | 
				
			||||||
 | 
					              style: TextStyle(
 | 
				
			||||||
 | 
					                color: Colors.grey[500],
 | 
				
			||||||
 | 
					                fontSize: 11,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Location
 | 
				
			||||||
 | 
					          assetDetail.exifInfo?.latitude != null
 | 
				
			||||||
 | 
					              ? Padding(
 | 
				
			||||||
 | 
					                  padding: const EdgeInsets.only(top: 32.0),
 | 
				
			||||||
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Divider(
 | 
				
			||||||
 | 
					                        thickness: 1,
 | 
				
			||||||
 | 
					                        color: Colors.grey[600],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        "LOCATION",
 | 
				
			||||||
 | 
					                        style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        "${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
 | 
				
			||||||
 | 
					                        style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : Container(),
 | 
				
			||||||
 | 
					          // Detail
 | 
				
			||||||
 | 
					          assetDetail.exifInfo != null
 | 
				
			||||||
 | 
					              ? Padding(
 | 
				
			||||||
 | 
					                  padding: const EdgeInsets.only(top: 32.0),
 | 
				
			||||||
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Divider(
 | 
				
			||||||
 | 
					                        thickness: 1,
 | 
				
			||||||
 | 
					                        color: Colors.grey[600],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      Padding(
 | 
				
			||||||
 | 
					                        padding: const EdgeInsets.only(bottom: 8.0),
 | 
				
			||||||
 | 
					                        child: Text(
 | 
				
			||||||
 | 
					                          "DETAILS",
 | 
				
			||||||
 | 
					                          style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      ListTile(
 | 
				
			||||||
 | 
					                        contentPadding: const EdgeInsets.all(0),
 | 
				
			||||||
 | 
					                        dense: true,
 | 
				
			||||||
 | 
					                        textColor: Colors.grey[300],
 | 
				
			||||||
 | 
					                        iconColor: Colors.grey[300],
 | 
				
			||||||
 | 
					                        leading: const Icon(Icons.image),
 | 
				
			||||||
 | 
					                        title: Text(
 | 
				
			||||||
 | 
					                          "${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
 | 
				
			||||||
 | 
					                          style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        subtitle: Text(
 | 
				
			||||||
 | 
					                            "${assetDetail.exifInfo?.exifImageHeight!} x ${assetDetail.exifInfo?.exifImageWidth!}  ${assetDetail.exifInfo?.fileSizeInByte!}B "),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      assetDetail.exifInfo?.make != null
 | 
				
			||||||
 | 
					                          ? ListTile(
 | 
				
			||||||
 | 
					                              contentPadding: const EdgeInsets.all(0),
 | 
				
			||||||
 | 
					                              dense: true,
 | 
				
			||||||
 | 
					                              textColor: Colors.grey[300],
 | 
				
			||||||
 | 
					                              iconColor: Colors.grey[300],
 | 
				
			||||||
 | 
					                              leading: const Icon(Icons.camera),
 | 
				
			||||||
 | 
					                              title: Text(
 | 
				
			||||||
 | 
					                                "${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
 | 
				
			||||||
 | 
					                                style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              subtitle: Text(
 | 
				
			||||||
 | 
					                                  "ƒ/${assetDetail.exifInfo?.fNumber}   1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)}   ${assetDetail.exifInfo?.focalLength}mm   ISO${assetDetail.exifInfo?.iso} "),
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                          : Container()
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : Container()
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
 | 
				
			||||||
 | 
					  const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ImmichAsset asset;
 | 
				
			||||||
 | 
					  final Function onMoreInfoPressed;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    double iconSize = 18.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppBar(
 | 
				
			||||||
 | 
					      foregroundColor: Colors.grey[100],
 | 
				
			||||||
 | 
					      toolbarHeight: 60,
 | 
				
			||||||
 | 
					      backgroundColor: Colors.black,
 | 
				
			||||||
 | 
					      leading: IconButton(
 | 
				
			||||||
 | 
					        onPressed: () {
 | 
				
			||||||
 | 
					          AutoRouter.of(context).pop();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        icon: const Icon(
 | 
				
			||||||
 | 
					          Icons.arrow_back_ios_new_rounded,
 | 
				
			||||||
 | 
					          size: 20.0,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        IconButton(
 | 
				
			||||||
 | 
					          iconSize: iconSize,
 | 
				
			||||||
 | 
					          splashRadius: iconSize,
 | 
				
			||||||
 | 
					          onPressed: () {
 | 
				
			||||||
 | 
					            print("backup");
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          icon: const Icon(Icons.backup_outlined),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        IconButton(
 | 
				
			||||||
 | 
					          iconSize: iconSize,
 | 
				
			||||||
 | 
					          splashRadius: iconSize,
 | 
				
			||||||
 | 
					          onPressed: () {
 | 
				
			||||||
 | 
					            print("favorite");
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          icon: asset.isFavorite ? const Icon(Icons.favorite_rounded) : const Icon(Icons.favorite_border_rounded),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        IconButton(
 | 
				
			||||||
 | 
					            iconSize: iconSize,
 | 
				
			||||||
 | 
					            splashRadius: iconSize,
 | 
				
			||||||
 | 
					            onPressed: () {
 | 
				
			||||||
 | 
					              onMoreInfoPressed();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            icon: const Icon(Icons.more_horiz_rounded))
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:hive/hive.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/constants/hive_box.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/home/services/asset.service.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
 | 
				
			||||||
 | 
					import 'package:photo_view/photo_view.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore: must_be_immutable
 | 
				
			||||||
 | 
					class ImageViewerPage extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final String imageUrl;
 | 
				
			||||||
 | 
					  final String heroTag;
 | 
				
			||||||
 | 
					  final String thumbnailUrl;
 | 
				
			||||||
 | 
					  final ImmichAsset asset;
 | 
				
			||||||
 | 
					  final AssetService _assetService = AssetService();
 | 
				
			||||||
 | 
					  ImmichAssetWithExif? assetDetail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImageViewerPage(
 | 
				
			||||||
 | 
					      {Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl, required this.asset})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    var box = Hive.box(userInfoBox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getAssetExif() async {
 | 
				
			||||||
 | 
					      assetDetail = await _assetService.getAssetById(asset.id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      getAssetExif();
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      backgroundColor: Colors.black,
 | 
				
			||||||
 | 
					      appBar: TopControlAppBar(
 | 
				
			||||||
 | 
					        asset: asset,
 | 
				
			||||||
 | 
					        onMoreInfoPressed: () {
 | 
				
			||||||
 | 
					          showModalBottomSheet(
 | 
				
			||||||
 | 
					              backgroundColor: Colors.black,
 | 
				
			||||||
 | 
					              barrierColor: Colors.transparent,
 | 
				
			||||||
 | 
					              isScrollControlled: false,
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              builder: (context) {
 | 
				
			||||||
 | 
					                return ExifBottomSheet(assetDetail: assetDetail!);
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: Center(
 | 
				
			||||||
 | 
					        child: Hero(
 | 
				
			||||||
 | 
					          tag: heroTag,
 | 
				
			||||||
 | 
					          child: CachedNetworkImage(
 | 
				
			||||||
 | 
					            fit: BoxFit.cover,
 | 
				
			||||||
 | 
					            imageUrl: imageUrl,
 | 
				
			||||||
 | 
					            httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
				
			||||||
 | 
					            fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
 | 
					            errorWidget: (context, url, error) => const Icon(Icons.error),
 | 
				
			||||||
 | 
					            imageBuilder: (context, imageProvider) {
 | 
				
			||||||
 | 
					              return PhotoView(imageProvider: imageProvider);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            placeholder: (context, url) {
 | 
				
			||||||
 | 
					              return CachedNetworkImage(
 | 
				
			||||||
 | 
					                fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                imageUrl: thumbnailUrl,
 | 
				
			||||||
 | 
					                httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
				
			||||||
 | 
					                placeholderFadeInDuration: const Duration(milliseconds: 0),
 | 
				
			||||||
 | 
					                progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
 | 
				
			||||||
 | 
					                  scale: 0.2,
 | 
				
			||||||
 | 
					                  child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                errorWidget: (context, url, error) => const Icon(Icons.error),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,6 +3,7 @@ import 'dart:convert';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
 | 
					import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/services/network.service.dart';
 | 
					import 'package:immich_mobile/shared/services/network.service.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetService {
 | 
					class AssetService {
 | 
				
			||||||
@ -58,4 +59,21 @@ class AssetService {
 | 
				
			|||||||
      return [];
 | 
					      return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<ImmichAssetWithExif?> getAssetById(String assetId) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      var res = await _networkService.getRequest(
 | 
				
			||||||
 | 
					        url: "asset/assetById/$assetId",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Map<String, dynamic> decodedData = jsonDecode(res.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData);
 | 
				
			||||||
 | 
					      print("result $result");
 | 
				
			||||||
 | 
					      return result;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      debugPrint("Error getAllAsset  ${e.toString()}");
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,10 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:badges/badges.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:google_fonts/google_fonts.dart';
 | 
					import 'package:google_fonts/google_fonts.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
 | 
					import 'package:immich_mobile/shared/models/backup_state.model.dart';
 | 
				
			||||||
@ -20,10 +23,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final BackUpState _backupState = ref.watch(backupProvider);
 | 
					    final BackUpState _backupState = ref.watch(backupProvider);
 | 
				
			||||||
 | 
					    bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
 | 
				
			||||||
    return SliverPadding(
 | 
					    return SliverAppBar(
 | 
				
			||||||
      padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
 | 
					 | 
				
			||||||
      sliver: SliverAppBar(
 | 
					 | 
				
			||||||
      centerTitle: true,
 | 
					      centerTitle: true,
 | 
				
			||||||
      floating: true,
 | 
					      floating: true,
 | 
				
			||||||
      pinned: false,
 | 
					      pinned: false,
 | 
				
			||||||
@ -70,7 +71,20 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
				
			|||||||
                  )
 | 
					                  )
 | 
				
			||||||
                : Container(),
 | 
					                : Container(),
 | 
				
			||||||
            IconButton(
 | 
					            IconButton(
 | 
				
			||||||
                icon: const Icon(Icons.backup_rounded),
 | 
					              splashRadius: 25,
 | 
				
			||||||
 | 
					              iconSize: 30,
 | 
				
			||||||
 | 
					              icon: _isEnableAutoBackup
 | 
				
			||||||
 | 
					                  ? const Icon(Icons.backup_rounded)
 | 
				
			||||||
 | 
					                  : Badge(
 | 
				
			||||||
 | 
					                      padding: const EdgeInsets.all(4),
 | 
				
			||||||
 | 
					                      elevation: 1,
 | 
				
			||||||
 | 
					                      position: BadgePosition.bottomEnd(bottom: -4, end: -4),
 | 
				
			||||||
 | 
					                      badgeColor: Colors.white,
 | 
				
			||||||
 | 
					                      badgeContent: const Icon(
 | 
				
			||||||
 | 
					                        Icons.cloud_off_rounded,
 | 
				
			||||||
 | 
					                        size: 8,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      child: const Icon(Icons.backup_rounded)),
 | 
				
			||||||
              tooltip: 'Backup Controller',
 | 
					              tooltip: 'Backup Controller',
 | 
				
			||||||
              onPressed: () async {
 | 
					              onPressed: () async {
 | 
				
			||||||
                var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
 | 
					                var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
 | 
				
			||||||
@ -92,7 +106,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -56,6 +56,7 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
                    '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
 | 
					                    '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
 | 
				
			||||||
                heroTag: asset.id,
 | 
					                heroTag: asset.id,
 | 
				
			||||||
                thumbnailUrl: thumbnailRequestUrl,
 | 
					                thumbnailUrl: thumbnailRequestUrl,
 | 
				
			||||||
 | 
					                asset: asset,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final usernameController = useTextEditingController(text: 'testuser@email.com');
 | 
					    final usernameController = useTextEditingController(text: 'testuser@email.com');
 | 
				
			||||||
    final passwordController = useTextEditingController(text: 'password');
 | 
					    final passwordController = useTextEditingController(text: 'password');
 | 
				
			||||||
    final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
 | 
					    final serverEndpointController = useTextEditingController(text: 'http://192.168.1.204:2283');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Center(
 | 
					    return Center(
 | 
				
			||||||
      child: ConstrainedBox(
 | 
					      child: ConstrainedBox(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,5 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					 | 
				
			||||||
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/modules/login/providers/authentication.provider.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/login/ui/login_form.dart';
 | 
					import 'package:immich_mobile/modules/login/ui/login_form.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LoginPage extends HookConsumerWidget {
 | 
					class LoginPage extends HookConsumerWidget {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,9 @@ import 'package:flutter/widgets.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
 | 
					import 'package:immich_mobile/modules/login/views/login_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/views/home_page.dart';
 | 
					import 'package:immich_mobile/modules/home/views/home_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/auth_guard.dart';
 | 
					import 'package:immich_mobile/routing/auth_guard.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
 | 
					import 'package:immich_mobile/shared/views/backup_controller_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/image_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/video_viewer_page.dart';
 | 
					import 'package:immich_mobile/shared/views/video_viewer_page.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'router.gr.dart';
 | 
					part 'router.gr.dart';
 | 
				
			||||||
 | 
				
			|||||||
@ -41,7 +41,8 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
              key: args.key,
 | 
					              key: args.key,
 | 
				
			||||||
              imageUrl: args.imageUrl,
 | 
					              imageUrl: args.imageUrl,
 | 
				
			||||||
              heroTag: args.heroTag,
 | 
					              heroTag: args.heroTag,
 | 
				
			||||||
              thumbnailUrl: args.thumbnailUrl));
 | 
					              thumbnailUrl: args.thumbnailUrl,
 | 
				
			||||||
 | 
					              asset: args.asset));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    VideoViewerRoute.name: (routeData) {
 | 
					    VideoViewerRoute.name: (routeData) {
 | 
				
			||||||
      final args = routeData.argsAs<VideoViewerRouteArgs>();
 | 
					      final args = routeData.argsAs<VideoViewerRouteArgs>();
 | 
				
			||||||
@ -96,14 +97,16 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
 | 
				
			|||||||
      {Key? key,
 | 
					      {Key? key,
 | 
				
			||||||
      required String imageUrl,
 | 
					      required String imageUrl,
 | 
				
			||||||
      required String heroTag,
 | 
					      required String heroTag,
 | 
				
			||||||
      required String thumbnailUrl})
 | 
					      required String thumbnailUrl,
 | 
				
			||||||
 | 
					      required ImmichAsset asset})
 | 
				
			||||||
      : super(ImageViewerRoute.name,
 | 
					      : super(ImageViewerRoute.name,
 | 
				
			||||||
            path: '/image-viewer-page',
 | 
					            path: '/image-viewer-page',
 | 
				
			||||||
            args: ImageViewerRouteArgs(
 | 
					            args: ImageViewerRouteArgs(
 | 
				
			||||||
                key: key,
 | 
					                key: key,
 | 
				
			||||||
                imageUrl: imageUrl,
 | 
					                imageUrl: imageUrl,
 | 
				
			||||||
                heroTag: heroTag,
 | 
					                heroTag: heroTag,
 | 
				
			||||||
                thumbnailUrl: thumbnailUrl));
 | 
					                thumbnailUrl: thumbnailUrl,
 | 
				
			||||||
 | 
					                asset: asset));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const String name = 'ImageViewerRoute';
 | 
					  static const String name = 'ImageViewerRoute';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -113,7 +116,8 @@ class ImageViewerRouteArgs {
 | 
				
			|||||||
      {this.key,
 | 
					      {this.key,
 | 
				
			||||||
      required this.imageUrl,
 | 
					      required this.imageUrl,
 | 
				
			||||||
      required this.heroTag,
 | 
					      required this.heroTag,
 | 
				
			||||||
      required this.thumbnailUrl});
 | 
					      required this.thumbnailUrl,
 | 
				
			||||||
 | 
					      required this.asset});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Key? key;
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -123,9 +127,11 @@ class ImageViewerRouteArgs {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final String thumbnailUrl;
 | 
					  final String thumbnailUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ImmichAsset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl}';
 | 
					    return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset}';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										187
									
								
								mobile/lib/shared/models/exif.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								mobile/lib/shared/models/exif.model.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,187 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImmichExif {
 | 
				
			||||||
 | 
					  final int? id;
 | 
				
			||||||
 | 
					  final String? assetId;
 | 
				
			||||||
 | 
					  final String? make;
 | 
				
			||||||
 | 
					  final String? model;
 | 
				
			||||||
 | 
					  final String? imageName;
 | 
				
			||||||
 | 
					  final int? exifImageWidth;
 | 
				
			||||||
 | 
					  final int? exifImageHeight;
 | 
				
			||||||
 | 
					  final int? fileSizeInByte;
 | 
				
			||||||
 | 
					  final String? orientation;
 | 
				
			||||||
 | 
					  final String? dateTimeOriginal;
 | 
				
			||||||
 | 
					  final String? modifyDate;
 | 
				
			||||||
 | 
					  final String? lensModel;
 | 
				
			||||||
 | 
					  final double? fNumber;
 | 
				
			||||||
 | 
					  final double? focalLength;
 | 
				
			||||||
 | 
					  final int? iso;
 | 
				
			||||||
 | 
					  final double? exposureTime;
 | 
				
			||||||
 | 
					  final double? latitude;
 | 
				
			||||||
 | 
					  final double? longitude;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImmichExif({
 | 
				
			||||||
 | 
					    this.id,
 | 
				
			||||||
 | 
					    this.assetId,
 | 
				
			||||||
 | 
					    this.make,
 | 
				
			||||||
 | 
					    this.model,
 | 
				
			||||||
 | 
					    this.imageName,
 | 
				
			||||||
 | 
					    this.exifImageWidth,
 | 
				
			||||||
 | 
					    this.exifImageHeight,
 | 
				
			||||||
 | 
					    this.fileSizeInByte,
 | 
				
			||||||
 | 
					    this.orientation,
 | 
				
			||||||
 | 
					    this.dateTimeOriginal,
 | 
				
			||||||
 | 
					    this.modifyDate,
 | 
				
			||||||
 | 
					    this.lensModel,
 | 
				
			||||||
 | 
					    this.fNumber,
 | 
				
			||||||
 | 
					    this.focalLength,
 | 
				
			||||||
 | 
					    this.iso,
 | 
				
			||||||
 | 
					    this.exposureTime,
 | 
				
			||||||
 | 
					    this.latitude,
 | 
				
			||||||
 | 
					    this.longitude,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImmichExif copyWith({
 | 
				
			||||||
 | 
					    int? id,
 | 
				
			||||||
 | 
					    String? assetId,
 | 
				
			||||||
 | 
					    String? make,
 | 
				
			||||||
 | 
					    String? model,
 | 
				
			||||||
 | 
					    String? imageName,
 | 
				
			||||||
 | 
					    int? exifImageWidth,
 | 
				
			||||||
 | 
					    int? exifImageHeight,
 | 
				
			||||||
 | 
					    int? fileSizeInByte,
 | 
				
			||||||
 | 
					    String? orientation,
 | 
				
			||||||
 | 
					    String? dateTimeOriginal,
 | 
				
			||||||
 | 
					    String? modifyDate,
 | 
				
			||||||
 | 
					    String? lensModel,
 | 
				
			||||||
 | 
					    double? fNumber,
 | 
				
			||||||
 | 
					    double? focalLength,
 | 
				
			||||||
 | 
					    int? iso,
 | 
				
			||||||
 | 
					    double? exposureTime,
 | 
				
			||||||
 | 
					    double? latitude,
 | 
				
			||||||
 | 
					    double? longitude,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return ImmichExif(
 | 
				
			||||||
 | 
					      id: id ?? this.id,
 | 
				
			||||||
 | 
					      assetId: assetId ?? this.assetId,
 | 
				
			||||||
 | 
					      make: make ?? this.make,
 | 
				
			||||||
 | 
					      model: model ?? this.model,
 | 
				
			||||||
 | 
					      imageName: imageName ?? this.imageName,
 | 
				
			||||||
 | 
					      exifImageWidth: exifImageWidth ?? this.exifImageWidth,
 | 
				
			||||||
 | 
					      exifImageHeight: exifImageHeight ?? this.exifImageHeight,
 | 
				
			||||||
 | 
					      fileSizeInByte: fileSizeInByte ?? this.fileSizeInByte,
 | 
				
			||||||
 | 
					      orientation: orientation ?? this.orientation,
 | 
				
			||||||
 | 
					      dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
 | 
				
			||||||
 | 
					      modifyDate: modifyDate ?? this.modifyDate,
 | 
				
			||||||
 | 
					      lensModel: lensModel ?? this.lensModel,
 | 
				
			||||||
 | 
					      fNumber: fNumber ?? this.fNumber,
 | 
				
			||||||
 | 
					      focalLength: focalLength ?? this.focalLength,
 | 
				
			||||||
 | 
					      iso: iso ?? this.iso,
 | 
				
			||||||
 | 
					      exposureTime: exposureTime ?? this.exposureTime,
 | 
				
			||||||
 | 
					      latitude: latitude ?? this.latitude,
 | 
				
			||||||
 | 
					      longitude: longitude ?? this.longitude,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'id': id,
 | 
				
			||||||
 | 
					      'assetId': assetId,
 | 
				
			||||||
 | 
					      'make': make,
 | 
				
			||||||
 | 
					      'model': model,
 | 
				
			||||||
 | 
					      'imageName': imageName,
 | 
				
			||||||
 | 
					      'exifImageWidth': exifImageWidth,
 | 
				
			||||||
 | 
					      'exifImageHeight': exifImageHeight,
 | 
				
			||||||
 | 
					      'fileSizeInByte': fileSizeInByte,
 | 
				
			||||||
 | 
					      'orientation': orientation,
 | 
				
			||||||
 | 
					      'dateTimeOriginal': dateTimeOriginal,
 | 
				
			||||||
 | 
					      'modifyDate': modifyDate,
 | 
				
			||||||
 | 
					      'lensModel': lensModel,
 | 
				
			||||||
 | 
					      'fNumber': fNumber,
 | 
				
			||||||
 | 
					      'focalLength': focalLength,
 | 
				
			||||||
 | 
					      'iso': iso,
 | 
				
			||||||
 | 
					      'exposureTime': exposureTime,
 | 
				
			||||||
 | 
					      'latitude': latitude,
 | 
				
			||||||
 | 
					      'longitude': longitude,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ImmichExif.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
 | 
					    return ImmichExif(
 | 
				
			||||||
 | 
					      id: map['id']?.toInt(),
 | 
				
			||||||
 | 
					      assetId: map['assetId'],
 | 
				
			||||||
 | 
					      make: map['make'],
 | 
				
			||||||
 | 
					      model: map['model'],
 | 
				
			||||||
 | 
					      imageName: map['imageName'],
 | 
				
			||||||
 | 
					      exifImageWidth: map['exifImageWidth']?.toInt(),
 | 
				
			||||||
 | 
					      exifImageHeight: map['exifImageHeight']?.toInt(),
 | 
				
			||||||
 | 
					      fileSizeInByte: map['fileSizeInByte']?.toInt(),
 | 
				
			||||||
 | 
					      orientation: map['orientation'],
 | 
				
			||||||
 | 
					      dateTimeOriginal: map['dateTimeOriginal'],
 | 
				
			||||||
 | 
					      modifyDate: map['modifyDate'],
 | 
				
			||||||
 | 
					      lensModel: map['lensModel'],
 | 
				
			||||||
 | 
					      fNumber: map['fNumber']?.toDouble(),
 | 
				
			||||||
 | 
					      focalLength: map['focalLength']?.toDouble(),
 | 
				
			||||||
 | 
					      iso: map['iso']?.toInt(),
 | 
				
			||||||
 | 
					      exposureTime: map['exposureTime']?.toDouble(),
 | 
				
			||||||
 | 
					      latitude: map['latitude']?.toDouble(),
 | 
				
			||||||
 | 
					      longitude: map['longitude']?.toDouble(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ImmichExif.fromJson(String source) => ImmichExif.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'ImmichExif(id: $id, assetId: $assetId, make: $make, model: $model, imageName: $imageName, exifImageWidth: $exifImageWidth, exifImageHeight: $exifImageHeight, fileSizeInByte: $fileSizeInByte, orientation: $orientation, dateTimeOriginal: $dateTimeOriginal, modifyDate: $modifyDate, lensModel: $lensModel, fNumber: $fNumber, focalLength: $focalLength, iso: $iso, exposureTime: $exposureTime, latitude: $latitude, longitude: $longitude)';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is ImmichExif &&
 | 
				
			||||||
 | 
					        other.id == id &&
 | 
				
			||||||
 | 
					        other.assetId == assetId &&
 | 
				
			||||||
 | 
					        other.make == make &&
 | 
				
			||||||
 | 
					        other.model == model &&
 | 
				
			||||||
 | 
					        other.imageName == imageName &&
 | 
				
			||||||
 | 
					        other.exifImageWidth == exifImageWidth &&
 | 
				
			||||||
 | 
					        other.exifImageHeight == exifImageHeight &&
 | 
				
			||||||
 | 
					        other.fileSizeInByte == fileSizeInByte &&
 | 
				
			||||||
 | 
					        other.orientation == orientation &&
 | 
				
			||||||
 | 
					        other.dateTimeOriginal == dateTimeOriginal &&
 | 
				
			||||||
 | 
					        other.modifyDate == modifyDate &&
 | 
				
			||||||
 | 
					        other.lensModel == lensModel &&
 | 
				
			||||||
 | 
					        other.fNumber == fNumber &&
 | 
				
			||||||
 | 
					        other.focalLength == focalLength &&
 | 
				
			||||||
 | 
					        other.iso == iso &&
 | 
				
			||||||
 | 
					        other.exposureTime == exposureTime &&
 | 
				
			||||||
 | 
					        other.latitude == latitude &&
 | 
				
			||||||
 | 
					        other.longitude == longitude;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode {
 | 
				
			||||||
 | 
					    return id.hashCode ^
 | 
				
			||||||
 | 
					        assetId.hashCode ^
 | 
				
			||||||
 | 
					        make.hashCode ^
 | 
				
			||||||
 | 
					        model.hashCode ^
 | 
				
			||||||
 | 
					        imageName.hashCode ^
 | 
				
			||||||
 | 
					        exifImageWidth.hashCode ^
 | 
				
			||||||
 | 
					        exifImageHeight.hashCode ^
 | 
				
			||||||
 | 
					        fileSizeInByte.hashCode ^
 | 
				
			||||||
 | 
					        orientation.hashCode ^
 | 
				
			||||||
 | 
					        dateTimeOriginal.hashCode ^
 | 
				
			||||||
 | 
					        modifyDate.hashCode ^
 | 
				
			||||||
 | 
					        lensModel.hashCode ^
 | 
				
			||||||
 | 
					        fNumber.hashCode ^
 | 
				
			||||||
 | 
					        focalLength.hashCode ^
 | 
				
			||||||
 | 
					        iso.hashCode ^
 | 
				
			||||||
 | 
					        exposureTime.hashCode ^
 | 
				
			||||||
 | 
					        latitude.hashCode ^
 | 
				
			||||||
 | 
					        longitude.hashCode;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										133
									
								
								mobile/lib/shared/models/immich_asset_with_exif.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								mobile/lib/shared/models/immich_asset_with_exif.model.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/exif.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImmichAssetWithExif {
 | 
				
			||||||
 | 
					  final String id;
 | 
				
			||||||
 | 
					  final String deviceAssetId;
 | 
				
			||||||
 | 
					  final String userId;
 | 
				
			||||||
 | 
					  final String deviceId;
 | 
				
			||||||
 | 
					  final String type;
 | 
				
			||||||
 | 
					  final String createdAt;
 | 
				
			||||||
 | 
					  final String modifiedAt;
 | 
				
			||||||
 | 
					  final String originalPath;
 | 
				
			||||||
 | 
					  final bool isFavorite;
 | 
				
			||||||
 | 
					  final String? duration;
 | 
				
			||||||
 | 
					  final ImmichExif? exifInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImmichAssetWithExif({
 | 
				
			||||||
 | 
					    required this.id,
 | 
				
			||||||
 | 
					    required this.deviceAssetId,
 | 
				
			||||||
 | 
					    required this.userId,
 | 
				
			||||||
 | 
					    required this.deviceId,
 | 
				
			||||||
 | 
					    required this.type,
 | 
				
			||||||
 | 
					    required this.createdAt,
 | 
				
			||||||
 | 
					    required this.modifiedAt,
 | 
				
			||||||
 | 
					    required this.originalPath,
 | 
				
			||||||
 | 
					    required this.isFavorite,
 | 
				
			||||||
 | 
					    this.duration,
 | 
				
			||||||
 | 
					    this.exifInfo,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImmichAssetWithExif copyWith({
 | 
				
			||||||
 | 
					    String? id,
 | 
				
			||||||
 | 
					    String? deviceAssetId,
 | 
				
			||||||
 | 
					    String? userId,
 | 
				
			||||||
 | 
					    String? deviceId,
 | 
				
			||||||
 | 
					    String? type,
 | 
				
			||||||
 | 
					    String? createdAt,
 | 
				
			||||||
 | 
					    String? modifiedAt,
 | 
				
			||||||
 | 
					    String? originalPath,
 | 
				
			||||||
 | 
					    bool? isFavorite,
 | 
				
			||||||
 | 
					    String? duration,
 | 
				
			||||||
 | 
					    ImmichExif? exifInfo,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return ImmichAssetWithExif(
 | 
				
			||||||
 | 
					      id: id ?? this.id,
 | 
				
			||||||
 | 
					      deviceAssetId: deviceAssetId ?? this.deviceAssetId,
 | 
				
			||||||
 | 
					      userId: userId ?? this.userId,
 | 
				
			||||||
 | 
					      deviceId: deviceId ?? this.deviceId,
 | 
				
			||||||
 | 
					      type: type ?? this.type,
 | 
				
			||||||
 | 
					      createdAt: createdAt ?? this.createdAt,
 | 
				
			||||||
 | 
					      modifiedAt: modifiedAt ?? this.modifiedAt,
 | 
				
			||||||
 | 
					      originalPath: originalPath ?? this.originalPath,
 | 
				
			||||||
 | 
					      isFavorite: isFavorite ?? this.isFavorite,
 | 
				
			||||||
 | 
					      duration: duration ?? this.duration,
 | 
				
			||||||
 | 
					      exifInfo: exifInfo ?? this.exifInfo,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'id': id,
 | 
				
			||||||
 | 
					      'deviceAssetId': deviceAssetId,
 | 
				
			||||||
 | 
					      'userId': userId,
 | 
				
			||||||
 | 
					      'deviceId': deviceId,
 | 
				
			||||||
 | 
					      'type': type,
 | 
				
			||||||
 | 
					      'createdAt': createdAt,
 | 
				
			||||||
 | 
					      'modifiedAt': modifiedAt,
 | 
				
			||||||
 | 
					      'originalPath': originalPath,
 | 
				
			||||||
 | 
					      'isFavorite': isFavorite,
 | 
				
			||||||
 | 
					      'duration': duration,
 | 
				
			||||||
 | 
					      'exifInfo': exifInfo?.toMap(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ImmichAssetWithExif.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
 | 
					    return ImmichAssetWithExif(
 | 
				
			||||||
 | 
					      id: map['id'] ?? '',
 | 
				
			||||||
 | 
					      deviceAssetId: map['deviceAssetId'] ?? '',
 | 
				
			||||||
 | 
					      userId: map['userId'] ?? '',
 | 
				
			||||||
 | 
					      deviceId: map['deviceId'] ?? '',
 | 
				
			||||||
 | 
					      type: map['type'] ?? '',
 | 
				
			||||||
 | 
					      createdAt: map['createdAt'] ?? '',
 | 
				
			||||||
 | 
					      modifiedAt: map['modifiedAt'] ?? '',
 | 
				
			||||||
 | 
					      originalPath: map['originalPath'] ?? '',
 | 
				
			||||||
 | 
					      isFavorite: map['isFavorite'] ?? false,
 | 
				
			||||||
 | 
					      duration: map['duration'],
 | 
				
			||||||
 | 
					      exifInfo: map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ImmichAssetWithExif.fromJson(String source) => ImmichAssetWithExif.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'ImmichAssetWithExif(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, originalPath: $originalPath, isFavorite: $isFavorite, duration: $duration, exifInfo: $exifInfo)';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is ImmichAssetWithExif &&
 | 
				
			||||||
 | 
					        other.id == id &&
 | 
				
			||||||
 | 
					        other.deviceAssetId == deviceAssetId &&
 | 
				
			||||||
 | 
					        other.userId == userId &&
 | 
				
			||||||
 | 
					        other.deviceId == deviceId &&
 | 
				
			||||||
 | 
					        other.type == type &&
 | 
				
			||||||
 | 
					        other.createdAt == createdAt &&
 | 
				
			||||||
 | 
					        other.modifiedAt == modifiedAt &&
 | 
				
			||||||
 | 
					        other.originalPath == originalPath &&
 | 
				
			||||||
 | 
					        other.isFavorite == isFavorite &&
 | 
				
			||||||
 | 
					        other.duration == duration &&
 | 
				
			||||||
 | 
					        other.exifInfo == exifInfo;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode {
 | 
				
			||||||
 | 
					    return id.hashCode ^
 | 
				
			||||||
 | 
					        deviceAssetId.hashCode ^
 | 
				
			||||||
 | 
					        userId.hashCode ^
 | 
				
			||||||
 | 
					        deviceId.hashCode ^
 | 
				
			||||||
 | 
					        type.hashCode ^
 | 
				
			||||||
 | 
					        createdAt.hashCode ^
 | 
				
			||||||
 | 
					        modifiedAt.hashCode ^
 | 
				
			||||||
 | 
					        originalPath.hashCode ^
 | 
				
			||||||
 | 
					        isFavorite.hashCode ^
 | 
				
			||||||
 | 
					        duration.hashCode ^
 | 
				
			||||||
 | 
					        exifInfo.hashCode;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,64 +0,0 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:hive/hive.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ImageViewerPage extends StatelessWidget {
 | 
					 | 
				
			||||||
  final String imageUrl;
 | 
					 | 
				
			||||||
  final String heroTag;
 | 
					 | 
				
			||||||
  final String thumbnailUrl;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const ImageViewerPage({Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl})
 | 
					 | 
				
			||||||
      : super(key: key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Scaffold(
 | 
					 | 
				
			||||||
      backgroundColor: Colors.black,
 | 
					 | 
				
			||||||
      appBar: AppBar(
 | 
					 | 
				
			||||||
        toolbarHeight: 60,
 | 
					 | 
				
			||||||
        backgroundColor: Colors.black,
 | 
					 | 
				
			||||||
        leading: IconButton(
 | 
					 | 
				
			||||||
            onPressed: () {
 | 
					 | 
				
			||||||
              AutoRouter.of(context).pop();
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            icon: const Icon(Icons.arrow_back_ios)),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      body: Dismissible(
 | 
					 | 
				
			||||||
        direction: DismissDirection.vertical,
 | 
					 | 
				
			||||||
        onDismissed: (_) {
 | 
					 | 
				
			||||||
          AutoRouter.of(context).pop();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        key: Key(heroTag),
 | 
					 | 
				
			||||||
        child: Center(
 | 
					 | 
				
			||||||
          child: Hero(
 | 
					 | 
				
			||||||
            tag: heroTag,
 | 
					 | 
				
			||||||
            child: CachedNetworkImage(
 | 
					 | 
				
			||||||
              fit: BoxFit.cover,
 | 
					 | 
				
			||||||
              imageUrl: imageUrl,
 | 
					 | 
				
			||||||
              httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					 | 
				
			||||||
              fadeInDuration: const Duration(milliseconds: 250),
 | 
					 | 
				
			||||||
              errorWidget: (context, url, error) => const Icon(Icons.error),
 | 
					 | 
				
			||||||
              placeholder: (context, url) {
 | 
					 | 
				
			||||||
                return CachedNetworkImage(
 | 
					 | 
				
			||||||
                  fit: BoxFit.cover,
 | 
					 | 
				
			||||||
                  imageUrl: thumbnailUrl,
 | 
					 | 
				
			||||||
                  httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					 | 
				
			||||||
                  placeholderFadeInDuration: const Duration(milliseconds: 0),
 | 
					 | 
				
			||||||
                  progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
 | 
					 | 
				
			||||||
                    scale: 0.2,
 | 
					 | 
				
			||||||
                    child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  errorWidget: (context, url, error) => const Icon(Icons.error),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -2,7 +2,7 @@ build:
 | 
				
			|||||||
	flutter packages pub run build_runner build
 | 
						flutter packages pub run build_runner build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch:
 | 
					watch:
 | 
				
			||||||
	flutter packages pub run build_runner watch
 | 
						flutter packages pub run build_runner watch --delete-conflicting-outputs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create_app_icon:
 | 
					create_app_icon:
 | 
				
			||||||
	flutter pub run flutter_launcher_icons:main
 | 
						flutter pub run flutter_launcher_icons:main
 | 
				
			||||||
@ -50,6 +50,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.2.1"
 | 
					    version: "3.2.1"
 | 
				
			||||||
 | 
					  badges:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: badges
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.0.2"
 | 
				
			||||||
  boolean_selector:
 | 
					  boolean_selector:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@ -513,13 +520,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.12.11"
 | 
					    version: "0.12.11"
 | 
				
			||||||
  material_color_utilities:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: material_color_utilities
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "0.1.3"
 | 
					 | 
				
			||||||
  meta:
 | 
					  meta:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@ -639,6 +639,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.10"
 | 
					    version: "1.3.10"
 | 
				
			||||||
 | 
					  photo_view:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: photo_view
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.13.0"
 | 
				
			||||||
  platform:
 | 
					  platform:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@ -825,7 +832,7 @@ packages:
 | 
				
			|||||||
      name: test_api
 | 
					      name: test_api
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.4.8"
 | 
					    version: "0.4.3"
 | 
				
			||||||
  timing:
 | 
					  timing:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,8 @@ dependencies:
 | 
				
			|||||||
  video_player: ^2.2.18
 | 
					  video_player: ^2.2.18
 | 
				
			||||||
  chewie: ^1.2.2
 | 
					  chewie: ^1.2.2
 | 
				
			||||||
  sliver_tools: ^0.2.5
 | 
					  sliver_tools: ^0.2.5
 | 
				
			||||||
 | 
					  badges: ^2.0.2
 | 
				
			||||||
 | 
					  photo_view: ^0.13.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 | 
				
			|||||||
@ -17,35 +17,25 @@ COPY . .
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RUN npm run build
 | 
					RUN npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##################################
 | 
					#################################
 | 
				
			||||||
# PRODUCTION
 | 
					# PRODUCTION
 | 
				
			||||||
##################################
 | 
					#################################
 | 
				
			||||||
# FROM node:16-bullseye-slim as production
 | 
					FROM node:16-alpine3.14 AS production
 | 
				
			||||||
# ARG DEBIAN_FRONTEND=noninteractive
 | 
					 | 
				
			||||||
# ARG NODE_ENV=production
 | 
					 | 
				
			||||||
# ENV NODE_ENV=${NODE_ENV}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# WORKDIR /usr/src/app
 | 
					ARG DEBIAN_FRONTEND=noninteractive
 | 
				
			||||||
 | 
					ARG NODE_ENV=production
 | 
				
			||||||
 | 
					ENV NODE_ENV=${NODE_ENV}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# COPY package.json yarn.lock ./
 | 
					WORKDIR /usr/src/app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# RUN apt-get update
 | 
					COPY package.json package-lock.json ./
 | 
				
			||||||
# RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# RUN npm i -g yarn --force
 | 
					RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# RUN yarn install --only=production
 | 
					RUN npm install --only=production
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# COPY . .
 | 
					COPY . .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# COPY --from=development /usr/src/app/dist ./dist
 | 
					COPY --from=development /usr/src/app/dist ./dist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# # Clean up commands
 | 
					CMD ["node", "dist/main"]
 | 
				
			||||||
# RUN apt-get autoremove -y && apt-get clean && \
 | 
					 | 
				
			||||||
#   rm -rf /usr/local/src/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# RUN apt-get clean && \
 | 
					 | 
				
			||||||
#   rm -rf /var/lib/apt/lists/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# CMD ["node", "dist/main"]
 | 
					 | 
				
			||||||
							
								
								
									
										692
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										692
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -36,6 +36,7 @@
 | 
				
			|||||||
    "class-transformer": "^0.5.1",
 | 
					    "class-transformer": "^0.5.1",
 | 
				
			||||||
    "class-validator": "^0.13.2",
 | 
					    "class-validator": "^0.13.2",
 | 
				
			||||||
    "dotenv": "^14.2.0",
 | 
					    "dotenv": "^14.2.0",
 | 
				
			||||||
 | 
					    "exifr": "^7.1.3",
 | 
				
			||||||
    "fluent-ffmpeg": "^2.1.2",
 | 
					    "fluent-ffmpeg": "^2.1.2",
 | 
				
			||||||
    "joi": "^17.5.0",
 | 
					    "joi": "^17.5.0",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@ import { promisify } from 'util';
 | 
				
			|||||||
import { stat } from 'fs';
 | 
					import { stat } from 'fs';
 | 
				
			||||||
import { pipeline } from 'stream';
 | 
					import { pipeline } from 'stream';
 | 
				
			||||||
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
 | 
					import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
 | 
				
			||||||
 | 
					import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fileInfo = promisify(stat);
 | 
					const fileInfo = promisify(stat);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,8 +38,9 @@ const fileInfo = promisify(stat);
 | 
				
			|||||||
@Controller('asset')
 | 
					@Controller('asset')
 | 
				
			||||||
export class AssetController {
 | 
					export class AssetController {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private readonly assetService: AssetService,
 | 
					    private assetService: AssetService,
 | 
				
			||||||
    private readonly assetOptimizeService: AssetOptimizeService,
 | 
					    private assetOptimizeService: AssetOptimizeService,
 | 
				
			||||||
 | 
					    private backgroundTaskService: BackgroundTaskService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('upload')
 | 
					  @Post('upload')
 | 
				
			||||||
@ -53,6 +55,7 @@ export class AssetController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (savedAsset && savedAsset.type == AssetType.IMAGE) {
 | 
					      if (savedAsset && savedAsset.type == AssetType.IMAGE) {
 | 
				
			||||||
        await this.assetOptimizeService.resizeImage(savedAsset);
 | 
					        await this.assetOptimizeService.resizeImage(savedAsset);
 | 
				
			||||||
 | 
					        await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (savedAsset && savedAsset.type == AssetType.VIDEO) {
 | 
					      if (savedAsset && savedAsset.type == AssetType.VIDEO) {
 | 
				
			||||||
@ -155,4 +158,9 @@ export class AssetController {
 | 
				
			|||||||
  async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
 | 
					  async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
 | 
				
			||||||
    return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
 | 
					    return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('/assetById/:assetId')
 | 
				
			||||||
 | 
					  async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId) {
 | 
				
			||||||
 | 
					    return this.assetService.getAssetById(authUser, assetId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,8 @@ import { AssetEntity } from './entities/asset.entity';
 | 
				
			|||||||
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
 | 
					import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
 | 
				
			||||||
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
 | 
					import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
 | 
				
			||||||
import { BullModule } from '@nestjs/bull';
 | 
					import { BullModule } from '@nestjs/bull';
 | 
				
			||||||
 | 
					import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
 | 
				
			||||||
 | 
					import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
@ -17,11 +19,20 @@ import { BullModule } from '@nestjs/bull';
 | 
				
			|||||||
        removeOnFail: false,
 | 
					        removeOnFail: false,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					    BullModule.registerQueue({
 | 
				
			||||||
 | 
					      name: 'background-task',
 | 
				
			||||||
 | 
					      defaultJobOptions: {
 | 
				
			||||||
 | 
					        attempts: 3,
 | 
				
			||||||
 | 
					        removeOnComplete: true,
 | 
				
			||||||
 | 
					        removeOnFail: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity]),
 | 
					    TypeOrmModule.forFeature([AssetEntity]),
 | 
				
			||||||
    ImageOptimizeModule,
 | 
					    ImageOptimizeModule,
 | 
				
			||||||
 | 
					    BackgroundTaskModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [AssetController],
 | 
					  controllers: [AssetController],
 | 
				
			||||||
  providers: [AssetService, AssetOptimizeService],
 | 
					  providers: [AssetService, AssetOptimizeService, BackgroundTaskService],
 | 
				
			||||||
  exports: [],
 | 
					  exports: [],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AssetModule {}
 | 
					export class AssetModule {}
 | 
				
			||||||
 | 
				
			|||||||
@ -112,4 +112,14 @@ export class AssetService {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public async getAssetById(authUser: AuthUserDto, assetId: string) {
 | 
				
			||||||
 | 
					    return await this.assetRepository.findOne({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        userId: authUser.id,
 | 
				
			||||||
 | 
					        id: assetId,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      relations: ['exifInfo'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										48
									
								
								server/src/api-v1/asset/dto/create-exif.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server/src/api-v1/asset/dto/create-exif.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import { IsNotEmpty, IsOptional } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CreateExifDto {
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  assetId: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  make: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  imageName: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  exifImageWidth: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  exifImageHeight: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  fileSizeInByte: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  orientation: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  dateTimeOriginal: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  modifiedDate: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  lensModel: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  fNumber: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  focalLenght: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  iso: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  exposureTime: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								server/src/api-v1/asset/dto/update-exif.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/src/api-v1/asset/dto/update-exif.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					import { PartialType } from '@nestjs/mapped-types';
 | 
				
			||||||
 | 
					import { CreateExifDto } from './create-exif.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpdateExifDto extends PartialType(CreateExifDto) {}
 | 
				
			||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
					import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
				
			||||||
 | 
					import { ExifEntity } from './exif.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('assets')
 | 
					@Entity('assets')
 | 
				
			||||||
@Unique(['deviceAssetId', 'userId', 'deviceId'])
 | 
					@Unique(['deviceAssetId', 'userId', 'deviceId'])
 | 
				
			||||||
@ -38,6 +39,9 @@ export class AssetEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @Column({ nullable: true })
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
  duration: string;
 | 
					  duration: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
 | 
				
			||||||
 | 
					  exifInfo: ExifEntity;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum AssetType {
 | 
					export enum AssetType {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										67
									
								
								server/src/api-v1/asset/entities/exif.entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								server/src/api-v1/asset/entities/exif.entity.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					import { Index, JoinColumn, OneToOne } from 'typeorm';
 | 
				
			||||||
 | 
					import { Column } from 'typeorm/decorator/columns/Column';
 | 
				
			||||||
 | 
					import { PrimaryGeneratedColumn } from 'typeorm/decorator/columns/PrimaryGeneratedColumn';
 | 
				
			||||||
 | 
					import { Entity } from 'typeorm/decorator/entity/Entity';
 | 
				
			||||||
 | 
					import { AssetEntity } from './asset.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('exif')
 | 
				
			||||||
 | 
					export class ExifEntity {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn()
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Index({ unique: true })
 | 
				
			||||||
 | 
					  @Column({ type: 'uuid' })
 | 
				
			||||||
 | 
					  assetId: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  make: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  imageName: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  exifImageWidth: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  exifImageHeight: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  fileSizeInByte: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  orientation: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'timestamptz', nullable: true })
 | 
				
			||||||
 | 
					  dateTimeOriginal: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'timestamptz', nullable: true })
 | 
				
			||||||
 | 
					  modifyDate: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  lensModel: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'float8', nullable: true })
 | 
				
			||||||
 | 
					  fNumber: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'float8', nullable: true })
 | 
				
			||||||
 | 
					  focalLength: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true })
 | 
				
			||||||
 | 
					  iso: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'float', nullable: true })
 | 
				
			||||||
 | 
					  exposureTime: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'float', nullable: true })
 | 
				
			||||||
 | 
					  latitude: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'float', nullable: true })
 | 
				
			||||||
 | 
					  longitude: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
 | 
				
			||||||
 | 
					  @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
 | 
				
			||||||
 | 
					  asset: ExifEntity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,6 +13,7 @@ import { immichAppConfig } from './config/app.config';
 | 
				
			|||||||
import { BullModule } from '@nestjs/bull';
 | 
					import { BullModule } from '@nestjs/bull';
 | 
				
			||||||
import { ImageOptimizeModule } from './modules/image-optimize/image-optimize.module';
 | 
					import { ImageOptimizeModule } from './modules/image-optimize/image-optimize.module';
 | 
				
			||||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
 | 
					import { ServerInfoModule } from './api-v1/server-info/server-info.module';
 | 
				
			||||||
 | 
					import { BackgroundTaskModule } from './modules/background-task/background-task.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
@ -29,7 +30,6 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module';
 | 
				
			|||||||
        redis: {
 | 
					        redis: {
 | 
				
			||||||
          host: 'immich_redis',
 | 
					          host: 'immich_redis',
 | 
				
			||||||
          port: 6379,
 | 
					          port: 6379,
 | 
				
			||||||
          // password: configService.get('REDIS_PASSWORD'),
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
      inject: [ConfigService],
 | 
					      inject: [ConfigService],
 | 
				
			||||||
@ -38,6 +38,8 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module';
 | 
				
			|||||||
    ImageOptimizeModule,
 | 
					    ImageOptimizeModule,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ServerInfoModule,
 | 
					    ServerInfoModule,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    BackgroundTaskModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [],
 | 
					  controllers: [],
 | 
				
			||||||
  providers: [],
 | 
					  providers: [],
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								server/src/modules/background-task/background-task.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/src/modules/background-task/background-task.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					import { BullModule } from '@nestjs/bull';
 | 
				
			||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
 | 
				
			||||||
 | 
					import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
 | 
				
			||||||
 | 
					import { BackgroundTaskProcessor } from './background-task.processor';
 | 
				
			||||||
 | 
					import { BackgroundTaskService } from './background-task.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    BullModule.registerQueue({
 | 
				
			||||||
 | 
					      name: 'background-task',
 | 
				
			||||||
 | 
					      defaultJobOptions: {
 | 
				
			||||||
 | 
					        attempts: 3,
 | 
				
			||||||
 | 
					        removeOnComplete: true,
 | 
				
			||||||
 | 
					        removeOnFail: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  providers: [BackgroundTaskService, BackgroundTaskProcessor],
 | 
				
			||||||
 | 
					  exports: [BackgroundTaskService],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class BackgroundTaskModule {}
 | 
				
			||||||
@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					import { InjectQueue, Process, Processor } from '@nestjs/bull';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Job, Queue } from 'bull';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
 | 
				
			||||||
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import exifr from 'exifr';
 | 
				
			||||||
 | 
					import { readFile } from 'fs/promises';
 | 
				
			||||||
 | 
					import { Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Processor('background-task')
 | 
				
			||||||
 | 
					export class BackgroundTaskProcessor {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(AssetEntity)
 | 
				
			||||||
 | 
					    private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @InjectRepository(ExifEntity)
 | 
				
			||||||
 | 
					    private exifRepository: Repository<ExifEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private configService: ConfigService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process('extract-exif')
 | 
				
			||||||
 | 
					  async extractExif(job: Job) {
 | 
				
			||||||
 | 
					    const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
 | 
				
			||||||
 | 
					      job.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fileBuffer = await readFile(savedAsset.originalPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const exifData = await exifr.parse(fileBuffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newExif = new ExifEntity();
 | 
				
			||||||
 | 
					    newExif.assetId = savedAsset.id;
 | 
				
			||||||
 | 
					    newExif.make = exifData['Make'] || null;
 | 
				
			||||||
 | 
					    newExif.model = exifData['Model'] || null;
 | 
				
			||||||
 | 
					    newExif.imageName = fileName || null;
 | 
				
			||||||
 | 
					    newExif.exifImageHeight = exifData['ExifImageHeight'] || null;
 | 
				
			||||||
 | 
					    newExif.exifImageWidth = exifData['ExifImageWidth'] || null;
 | 
				
			||||||
 | 
					    newExif.fileSizeInByte = fileSize || null;
 | 
				
			||||||
 | 
					    newExif.orientation = exifData['Orientation'] || null;
 | 
				
			||||||
 | 
					    newExif.dateTimeOriginal = exifData['DateTimeOriginal'] || null;
 | 
				
			||||||
 | 
					    newExif.modifyDate = exifData['ModifyDate'] || null;
 | 
				
			||||||
 | 
					    newExif.lensModel = exifData['LensModel'] || null;
 | 
				
			||||||
 | 
					    newExif.fNumber = exifData['FNumber'] || null;
 | 
				
			||||||
 | 
					    newExif.focalLength = exifData['FocalLength'] || null;
 | 
				
			||||||
 | 
					    newExif.iso = exifData['ISO'] || null;
 | 
				
			||||||
 | 
					    newExif.exposureTime = exifData['ExposureTime'] || null;
 | 
				
			||||||
 | 
					    newExif.latitude = exifData['latitude'] || null;
 | 
				
			||||||
 | 
					    newExif.longitude = exifData['longitude'] || null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.exifRepository.save(newExif);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { InjectQueue } from '@nestjs/bull/dist/decorators';
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Queue } from 'bull';
 | 
				
			||||||
 | 
					import { randomUUID } from 'node:crypto';
 | 
				
			||||||
 | 
					import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class BackgroundTaskService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectQueue('background-task')
 | 
				
			||||||
 | 
					    private backgroundTaskQueue: Queue,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async extractExif(savedAsset: AssetEntity, fileName: string, fileSize: number) {
 | 
				
			||||||
 | 
					    const job = await this.backgroundTaskQueue.add(
 | 
				
			||||||
 | 
					      'extract-exif',
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        savedAsset,
 | 
				
			||||||
 | 
					        fileName,
 | 
				
			||||||
 | 
					        fileSize,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { jobId: randomUUID() },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -22,7 +22,7 @@ export class AssetOptimizeService {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async getVideoThumbnail(savedAsset: AssetEntity, filename: String) {
 | 
					  public async getVideoThumbnail(savedAsset: AssetEntity, filename: string) {
 | 
				
			||||||
    const job = await this.optimizeQueue.add(
 | 
					    const job = await this.optimizeQueue.add(
 | 
				
			||||||
      'get-video-thumbnail',
 | 
					      'get-video-thumbnail',
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user