diff --git a/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart b/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart new file mode 100644 index 0000000000000..63280ff69d942 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart @@ -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 toMap() { + return { + 'isBottomSheetEnable': isBottomSheetEnable, + }; + } + + factory ImageViewerPageState.fromMap(Map 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; +} diff --git a/mobile/lib/modules/asset_viewer/models/store_model_here.txt b/mobile/lib/modules/asset_viewer/models/store_model_here.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart new file mode 100644 index 0000000000000..c7854a53162ab --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart @@ -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 { + 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( + ((ref) => ImageViewerPageStateNotifier())); diff --git a/mobile/lib/modules/asset_viewer/services/store_services_here.txt b/mobile/lib/modules/asset_viewer/services/store_services_here.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart new file mode 100644 index 0000000000000..5952eacc25f85 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -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() + ], + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart new file mode 100644 index 0000000000000..6d56bbf16a74f --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart @@ -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); +} diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart new file mode 100644 index 0000000000000..689bfe9da624e --- /dev/null +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -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), + ); + }, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 51d1528528438..c0c41eff12dee 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.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_with_exif.model.dart'; import 'package:immich_mobile/shared/services/network.service.dart'; class AssetService { @@ -58,4 +59,21 @@ class AssetService { return []; } } + + Future getAssetById(String assetId) async { + try { + var res = await _networkService.getRequest( + url: "asset/assetById/$assetId", + ); + + Map decodedData = jsonDecode(res.toString()); + + ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData); + print("result $result"); + return result; + } catch (e) { + debugPrint("Error getAllAsset ${e.toString()}"); + return null; + } + } } diff --git a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart index b1f0013cfb2d8..d1702b9756437 100644 --- a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart +++ b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart @@ -1,7 +1,10 @@ import 'package:auto_route/auto_route.dart'; +import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.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/shared/models/backup_state.model.dart'; @@ -20,79 +23,89 @@ class ImmichSliverAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final BackUpState _backupState = ref.watch(backupProvider); - - return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), - sliver: SliverAppBar( - centerTitle: true, - floating: true, - pinned: false, - snap: false, - backgroundColor: Colors.grey[200], - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - leading: Builder( - builder: (BuildContext context) { - return IconButton( - icon: const Icon(Icons.account_circle_rounded), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, - ); - }, - ), - title: Text( - 'IMMICH', - style: GoogleFonts.snowburstOne( - textStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Theme.of(context).primaryColor, - ), + bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup; + return SliverAppBar( + centerTitle: true, + floating: true, + pinned: false, + snap: false, + backgroundColor: Colors.grey[200], + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), + leading: Builder( + builder: (BuildContext context) { + return IconButton( + icon: const Icon(Icons.account_circle_rounded), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + ); + }, + ), + title: Text( + 'IMMICH', + style: GoogleFonts.snowburstOne( + textStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).primaryColor, ), ), - actions: [ - Stack( - alignment: AlignmentDirectional.center, - children: [ - _backupState.backupProgress == BackUpProgressEnum.inProgress - ? Positioned( - top: 10, - right: 12, - child: SizedBox( - height: 8, - width: 8, - child: CircularProgressIndicator( - strokeWidth: 1, - valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), - ), + ), + actions: [ + Stack( + alignment: AlignmentDirectional.center, + children: [ + _backupState.backupProgress == BackUpProgressEnum.inProgress + ? Positioned( + top: 10, + right: 12, + child: SizedBox( + height: 8, + width: 8, + child: CircularProgressIndicator( + strokeWidth: 1, + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), ), - ) - : Container(), - IconButton( - icon: const Icon(Icons.backup_rounded), - tooltip: 'Backup Controller', - onPressed: () async { - var onPop = await AutoRouter.of(context).push(const BackupControllerRoute()); - - if (onPop == true) { - onPopBack!(); - } - }, - ), - _backupState.backupProgress == BackUpProgressEnum.inProgress - ? Positioned( - bottom: 5, - child: Text( - _backupState.backingUpAssetCount.toString(), - style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold), + ), + ) + : Container(), + IconButton( + 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, ), - ) - : Container() - ], - ), - ], - ), + child: const Icon(Icons.backup_rounded)), + tooltip: 'Backup Controller', + onPressed: () async { + var onPop = await AutoRouter.of(context).push(const BackupControllerRoute()); + + if (onPop == true) { + onPopBack!(); + } + }, + ), + _backupState.backupProgress == BackUpProgressEnum.inProgress + ? Positioned( + bottom: 5, + child: Text( + _backupState.backingUpAssetCount.toString(), + style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold), + ), + ) + : Container() + ], + ), + ], ); } } diff --git a/mobile/lib/modules/home/ui/thumbnail_image.dart b/mobile/lib/modules/home/ui/thumbnail_image.dart index 6d357499332cc..758c912a21e7e 100644 --- a/mobile/lib/modules/home/ui/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/thumbnail_image.dart @@ -56,6 +56,7 @@ class ThumbnailImage extends HookConsumerWidget { '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false', heroTag: asset.id, thumbnailUrl: thumbnailRequestUrl, + asset: asset, ), ); } else { diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index cdb801c4eea15..769a383a3bd44 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final usernameController = useTextEditingController(text: 'testuser@email.com'); 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( child: ConstrainedBox( diff --git a/mobile/lib/modules/login/views/login_page.dart b/mobile/lib/modules/login/views/login_page.dart index c590b8d574d87..42c02a6eacbf3 100644 --- a/mobile/lib/modules/login/views/login_page.dart +++ b/mobile/lib/modules/login/views/login_page.dart @@ -1,7 +1,5 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.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'; class LoginPage extends HookConsumerWidget { diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 367352a27ab09..32e21727f3a2c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -3,8 +3,9 @@ import 'package:flutter/widgets.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/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/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'; part 'router.gr.dart'; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index ce8ceb5d61e85..197abc1b1f492 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -41,7 +41,8 @@ class _$AppRouter extends RootStackRouter { key: args.key, imageUrl: args.imageUrl, heroTag: args.heroTag, - thumbnailUrl: args.thumbnailUrl)); + thumbnailUrl: args.thumbnailUrl, + asset: args.asset)); }, VideoViewerRoute.name: (routeData) { final args = routeData.argsAs(); @@ -96,14 +97,16 @@ class ImageViewerRoute extends PageRouteInfo { {Key? key, required String imageUrl, required String heroTag, - required String thumbnailUrl}) + required String thumbnailUrl, + required ImmichAsset asset}) : super(ImageViewerRoute.name, path: '/image-viewer-page', args: ImageViewerRouteArgs( key: key, imageUrl: imageUrl, heroTag: heroTag, - thumbnailUrl: thumbnailUrl)); + thumbnailUrl: thumbnailUrl, + asset: asset)); static const String name = 'ImageViewerRoute'; } @@ -113,7 +116,8 @@ class ImageViewerRouteArgs { {this.key, required this.imageUrl, required this.heroTag, - required this.thumbnailUrl}); + required this.thumbnailUrl, + required this.asset}); final Key? key; @@ -123,9 +127,11 @@ class ImageViewerRouteArgs { final String thumbnailUrl; + final ImmichAsset asset; + @override String toString() { - return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl}'; + return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset}'; } } diff --git a/mobile/lib/shared/models/exif.model.dart b/mobile/lib/shared/models/exif.model.dart new file mode 100644 index 0000000000000..0c1f48a992a76 --- /dev/null +++ b/mobile/lib/shared/models/exif.model.dart @@ -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 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 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; + } +} diff --git a/mobile/lib/shared/models/immich_asset_with_exif.model.dart b/mobile/lib/shared/models/immich_asset_with_exif.model.dart new file mode 100644 index 0000000000000..538b6921aecad --- /dev/null +++ b/mobile/lib/shared/models/immich_asset_with_exif.model.dart @@ -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 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 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; + } +} diff --git a/mobile/lib/shared/views/image_viewer_page.dart b/mobile/lib/shared/views/image_viewer_page.dart deleted file mode 100644 index 63de2462e6e79..0000000000000 --- a/mobile/lib/shared/views/image_viewer_page.dart +++ /dev/null @@ -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), - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/mobile/makefile b/mobile/makefile index abd6772374ff9..2b2b7be44dc83 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -2,7 +2,7 @@ build: flutter packages pub run build_runner build watch: - flutter packages pub run build_runner watch + flutter packages pub run build_runner watch --delete-conflicting-outputs create_app_icon: flutter pub run flutter_launcher_icons:main \ No newline at end of file diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index b566ebb8e1af2..48c23fe506a3b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -50,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.1" + badges: + dependency: "direct main" + description: + name: badges + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" boolean_selector: dependency: transitive description: @@ -513,13 +520,6 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -639,6 +639,13 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -825,7 +832,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.3" timing: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index e92b81f4bda29..1cd5d85b26df1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -31,6 +31,8 @@ dependencies: video_player: ^2.2.18 chewie: ^1.2.2 sliver_tools: ^0.2.5 + badges: ^2.0.2 + photo_view: ^0.13.0 dev_dependencies: flutter_test: diff --git a/server/Dockerfile b/server/Dockerfile index 5e51376524f04..5e8914b815cb2 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -17,35 +17,25 @@ COPY . . RUN npm run build -################################## +################################# # PRODUCTION -################################## -# FROM node:16-bullseye-slim as production -# ARG DEBIAN_FRONTEND=noninteractive -# ARG NODE_ENV=production -# ENV NODE_ENV=${NODE_ENV} - -# WORKDIR /usr/src/app - -# COPY package.json yarn.lock ./ - -# RUN apt-get update -# RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y +################################# +FROM node:16-alpine3.14 AS production -# RUN npm i -g yarn --force +ARG DEBIAN_FRONTEND=noninteractive +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} -# RUN yarn install --only=production +WORKDIR /usr/src/app -# COPY . . +COPY package.json package-lock.json ./ -# COPY --from=development /usr/src/app/dist ./dist +RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg -# # Clean up commands -# RUN apt-get autoremove -y && apt-get clean && \ -# rm -rf /usr/local/src/* +RUN npm install --only=production -# RUN apt-get clean && \ -# rm -rf /var/lib/apt/lists/* +COPY . . +COPY --from=development /usr/src/app/dist ./dist -# CMD ["node", "dist/main"] \ No newline at end of file +CMD ["node", "dist/main"] \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 9a7b2037bf6a1..ff2a2ad54c5a4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,8 +24,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "dotenv": "^14.2.0", + "exifr": "^7.1.3", "fluent-ffmpeg": "^2.1.2", - "heic-convert": "^1.2.4", "joi": "^17.5.0", "lodash": "^4.17.21", "passport": "^0.5.2", @@ -1775,177 +1775,6 @@ "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz", "integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==" }, - "node_modules/@tensorflow-models/coco-ssd": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@tensorflow-models/coco-ssd/-/coco-ssd-2.2.2.tgz", - "integrity": "sha512-Jey2JscmKEValcFZH2ZLz14s8KPRmVtfJ0d0M3dPhvBp9dJiGNanVXr/pJAY5OS7emKj9uSciGhdkHWXY9Hovw==", - "peerDependencies": { - "@tensorflow/tfjs-converter": "^3.3.0", - "@tensorflow/tfjs-core": "^3.3.0" - } - }, - "node_modules/@tensorflow/tfjs": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-3.13.0.tgz", - "integrity": "sha512-B5HvNH+6hHhQQkn+AG+u4j5sxZBMYdsq4IWXlBZzioJcVygtZhBWXkxp01boSwngjqUBgi8S2DopBE7McAUKqQ==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.13.0", - "@tensorflow/tfjs-backend-webgl": "3.13.0", - "@tensorflow/tfjs-converter": "3.13.0", - "@tensorflow/tfjs-core": "3.13.0", - "@tensorflow/tfjs-data": "3.13.0", - "@tensorflow/tfjs-layers": "3.13.0", - "argparse": "^1.0.10", - "chalk": "^4.1.0", - "core-js": "3", - "regenerator-runtime": "^0.13.5", - "yargs": "^16.0.3" - }, - "bin": { - "tfjs-custom-module": "dist/tools/custom_module/cli.js" - } - }, - "node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.13.0.tgz", - "integrity": "sha512-POmzUoAP8HooYYTZ72O1ZYkpVZB0f+8PeAkbTxIG0oahcJccj6a0Vovp1A6xWKfljUoPlJb3jWVC++S603ZL8w==", - "dependencies": { - "@types/seedrandom": "2.4.27", - "seedrandom": "2.4.3" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.13.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.13.0.tgz", - "integrity": "sha512-ZuJS11tCoZx2F1Eq7wqiqu8euJpPW/JV0qOKBehlRpV2qQrR+wHMpBT1hhDl4qU4LdgFTtSggKIRg/L8b0ScUQ==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.13.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "2.4.27", - "@types/webgl-ext": "0.0.30", - "@types/webgl2": "0.0.6", - "seedrandom": "2.4.3" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.13.0" - } - }, - "node_modules/@tensorflow/tfjs-converter": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.13.0.tgz", - "integrity": "sha512-H2VpDTv9Ve0HBt7ttzz46DmnsPaiT0B+yJjVH3NebGZbgY9C8boBgJIsdyqfiqEWBS3WxF8h4rh58Hv5XXMgaQ==", - "peerDependencies": { - "@tensorflow/tfjs-core": "3.13.0" - } - }, - "node_modules/@tensorflow/tfjs-core": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.13.0.tgz", - "integrity": "sha512-18qBEVIB/4u2OUK9nA5P1XT3e3LyarElD1UKNSNDpnMLxhLTUVZaCR71eHJcpl9wP2Q0cciaTJCTpJdPv1tNDQ==", - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "2.4.27", - "@types/webgl-ext": "0.0.30", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "2.4.3" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs-data": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-3.13.0.tgz", - "integrity": "sha512-n50+lxPK0CU72nlFt4dzMCCNV44CQsQU3sSP9zdR2bYHeoFqjjy1ISp+UV5N5DNLj7bsEMs73kGS1EuJ7YcdqQ==", - "dependencies": { - "@types/node-fetch": "^2.1.2", - "node-fetch": "~2.6.1" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.13.0", - "seedrandom": "~2.4.3" - } - }, - "node_modules/@tensorflow/tfjs-layers": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-3.13.0.tgz", - "integrity": "sha512-kTWJ/+9fbNCMDA9iQjDMYHmWivsiWz8CKNSOZdeCW7tiBwF1EiREBVQXMk1JI11ngQa8f+rYSLs7rkhp3SYl5Q==", - "peerDependencies": { - "@tensorflow/tfjs-core": "3.13.0" - } - }, - "node_modules/@tensorflow/tfjs-node": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-3.13.0.tgz", - "integrity": "sha512-LYM3ck/TyipxMFD23moX9qC3F23UBC3zbiw85HTxZ9FPlE1QNLP1UNlfFGeUTnPvY6CUcvPyQsrG9fBTvtwB1A==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "1.0.4", - "@tensorflow/tfjs": "3.13.0", - "adm-zip": "^0.5.2", - "google-protobuf": "^3.9.2", - "https-proxy-agent": "^2.2.1", - "progress": "^2.0.0", - "rimraf": "^2.6.2", - "tar": "^4.4.6" - }, - "engines": { - "node": ">=8.11.0" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/@tensorflow/tfjs-node/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2210,11 +2039,6 @@ "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, - "node_modules/@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -2235,20 +2059,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz", "integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==" }, - "node_modules/@types/node-fetch": { - "version": "2.5.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", - "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", - "dependencies": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "node_modules/@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" - }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -2312,11 +2122,6 @@ "@types/node": "*" } }, - "node_modules/@types/seedrandom": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz", - "integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE=" - }, "node_modules/@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", @@ -2361,16 +2166,6 @@ "@types/superagent": "*" } }, - "node_modules/@types/webgl-ext": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", - "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" - }, - "node_modules/@types/webgl2": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" - }, "node_modules/@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", @@ -2822,14 +2617,6 @@ "node": ">=0.4.0" } }, - "node_modules/adm-zip": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", - "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==", - "engines": { - "node": ">=6.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3041,6 +2828,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -3073,7 +2861,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "node_modules/at-least-node": { "version": "1.0.0", @@ -3779,6 +3568,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3915,16 +3705,6 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, - "node_modules/core-js": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.0.tgz", - "integrity": "sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -4129,6 +3909,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -4388,19 +4169,6 @@ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", "dev": true }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "node_modules/es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dependencies": { - "es6-promise": "^4.0.3" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -4822,6 +4590,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exifr": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", + "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -5276,6 +5049,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5608,11 +5382,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-protobuf": { - "version": "3.19.4", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.19.4.tgz", - "integrity": "sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg==" - }, "node_modules/graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", @@ -5656,30 +5425,6 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, - "node_modules/heic-convert": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/heic-convert/-/heic-convert-1.2.4.tgz", - "integrity": "sha512-klJHyv+BqbgKiCQvCqI9IKIvweCcohDuDl0Jphearj8+16+v8eff2piVevHqq4dW9TK0r1onTR6PKHP1I4hdbA==", - "dependencies": { - "heic-decode": "^1.1.2", - "jpeg-js": "^0.4.1", - "pngjs": "^3.4.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/heic-decode": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-1.1.2.tgz", - "integrity": "sha512-UF8teegxvzQPdSTcx5frIUhitNDliz/9Pui0JFdIqVRE00spVE33DcCYtZqaLNyd4y5RP/QQWZFIc1YWVKKm2A==", - "dependencies": { - "libheif-js": "^1.10.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -6894,11 +6639,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/jpeg-js": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", - "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7111,14 +6851,6 @@ "node": ">= 0.8.0" } }, - "node_modules/libheif-js": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.12.0.tgz", - "integrity": "sha512-hDs6xQ7028VOwAFwEtM0Q+B2x2NW69Jb2MhQFUbk3rUrHzz4qo5mqS8VrqNgYnSc8TiUGnR691LnO4uIfEE23w==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/libphonenumber-js": { "version": "1.9.48", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.48.tgz", @@ -7260,11 +6992,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8244,14 +7971,6 @@ "node": ">=4" } }, - "node_modules/pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -8382,14 +8101,6 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8598,11 +8309,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -8848,11 +8554,6 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" }, - "node_modules/seedrandom": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" - }, "node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -9154,7 +8855,8 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "node_modules/stack-utils": { "version": "2.0.5", @@ -9427,23 +9129,6 @@ "node": ">=6" } }, - "node_modules/tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" - } - }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -9475,41 +9160,6 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/tar/node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -11930,137 +11580,6 @@ "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz", "integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==" }, - "@tensorflow-models/coco-ssd": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@tensorflow-models/coco-ssd/-/coco-ssd-2.2.2.tgz", - "integrity": "sha512-Jey2JscmKEValcFZH2ZLz14s8KPRmVtfJ0d0M3dPhvBp9dJiGNanVXr/pJAY5OS7emKj9uSciGhdkHWXY9Hovw==", - "requires": {} - }, - "@tensorflow/tfjs": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-3.13.0.tgz", - "integrity": "sha512-B5HvNH+6hHhQQkn+AG+u4j5sxZBMYdsq4IWXlBZzioJcVygtZhBWXkxp01boSwngjqUBgi8S2DopBE7McAUKqQ==", - "requires": { - "@tensorflow/tfjs-backend-cpu": "3.13.0", - "@tensorflow/tfjs-backend-webgl": "3.13.0", - "@tensorflow/tfjs-converter": "3.13.0", - "@tensorflow/tfjs-core": "3.13.0", - "@tensorflow/tfjs-data": "3.13.0", - "@tensorflow/tfjs-layers": "3.13.0", - "argparse": "^1.0.10", - "chalk": "^4.1.0", - "core-js": "3", - "regenerator-runtime": "^0.13.5", - "yargs": "^16.0.3" - } - }, - "@tensorflow/tfjs-backend-cpu": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.13.0.tgz", - "integrity": "sha512-POmzUoAP8HooYYTZ72O1ZYkpVZB0f+8PeAkbTxIG0oahcJccj6a0Vovp1A6xWKfljUoPlJb3jWVC++S603ZL8w==", - "requires": { - "@types/seedrandom": "2.4.27", - "seedrandom": "2.4.3" - } - }, - "@tensorflow/tfjs-backend-webgl": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.13.0.tgz", - "integrity": "sha512-ZuJS11tCoZx2F1Eq7wqiqu8euJpPW/JV0qOKBehlRpV2qQrR+wHMpBT1hhDl4qU4LdgFTtSggKIRg/L8b0ScUQ==", - "requires": { - "@tensorflow/tfjs-backend-cpu": "3.13.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "2.4.27", - "@types/webgl-ext": "0.0.30", - "@types/webgl2": "0.0.6", - "seedrandom": "2.4.3" - } - }, - "@tensorflow/tfjs-converter": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.13.0.tgz", - "integrity": "sha512-H2VpDTv9Ve0HBt7ttzz46DmnsPaiT0B+yJjVH3NebGZbgY9C8boBgJIsdyqfiqEWBS3WxF8h4rh58Hv5XXMgaQ==", - "requires": {} - }, - "@tensorflow/tfjs-core": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.13.0.tgz", - "integrity": "sha512-18qBEVIB/4u2OUK9nA5P1XT3e3LyarElD1UKNSNDpnMLxhLTUVZaCR71eHJcpl9wP2Q0cciaTJCTpJdPv1tNDQ==", - "requires": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "2.4.27", - "@types/webgl-ext": "0.0.30", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "2.4.3" - } - }, - "@tensorflow/tfjs-data": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-3.13.0.tgz", - "integrity": "sha512-n50+lxPK0CU72nlFt4dzMCCNV44CQsQU3sSP9zdR2bYHeoFqjjy1ISp+UV5N5DNLj7bsEMs73kGS1EuJ7YcdqQ==", - "requires": { - "@types/node-fetch": "^2.1.2", - "node-fetch": "~2.6.1" - } - }, - "@tensorflow/tfjs-layers": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-3.13.0.tgz", - "integrity": "sha512-kTWJ/+9fbNCMDA9iQjDMYHmWivsiWz8CKNSOZdeCW7tiBwF1EiREBVQXMk1JI11ngQa8f+rYSLs7rkhp3SYl5Q==", - "requires": {} - }, - "@tensorflow/tfjs-node": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-3.13.0.tgz", - "integrity": "sha512-LYM3ck/TyipxMFD23moX9qC3F23UBC3zbiw85HTxZ9FPlE1QNLP1UNlfFGeUTnPvY6CUcvPyQsrG9fBTvtwB1A==", - "requires": { - "@mapbox/node-pre-gyp": "1.0.4", - "@tensorflow/tfjs": "3.13.0", - "adm-zip": "^0.5.2", - "google-protobuf": "^3.9.2", - "https-proxy-agent": "^2.2.1", - "progress": "^2.0.0", - "rimraf": "^2.6.2", - "tar": "^4.4.6" - }, - "dependencies": { - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -12322,11 +11841,6 @@ "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -12347,20 +11861,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz", "integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==" }, - "@types/node-fetch": { - "version": "2.5.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", - "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" - }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -12424,11 +11924,6 @@ "@types/node": "*" } }, - "@types/seedrandom": { - "version": "2.4.27", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz", - "integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE=" - }, "@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", @@ -12473,16 +11968,6 @@ "@types/superagent": "*" } }, - "@types/webgl-ext": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", - "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" - }, - "@types/webgl2": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" - }, "@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", @@ -12825,11 +12310,6 @@ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true }, - "adm-zip": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", - "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==" - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -13002,6 +12482,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -13031,7 +12512,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "at-least-node": { "version": "1.0.0", @@ -13576,6 +13058,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -13698,11 +13181,6 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, - "core-js": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.0.tgz", - "integrity": "sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ==" - }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -13868,7 +13346,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "delegates": { "version": "1.0.0", @@ -14079,19 +13558,6 @@ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", "dev": true }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -14391,6 +13857,11 @@ "strip-final-newline": "^2.0.0" } }, + "exifr": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", + "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -14765,6 +14236,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -15009,11 +14481,6 @@ "slash": "^3.0.0" } }, - "google-protobuf": { - "version": "3.19.4", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.19.4.tgz", - "integrity": "sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg==" - }, "graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", @@ -15045,24 +14512,6 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, - "heic-convert": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/heic-convert/-/heic-convert-1.2.4.tgz", - "integrity": "sha512-klJHyv+BqbgKiCQvCqI9IKIvweCcohDuDl0Jphearj8+16+v8eff2piVevHqq4dW9TK0r1onTR6PKHP1I4hdbA==", - "requires": { - "heic-decode": "^1.1.2", - "jpeg-js": "^0.4.1", - "pngjs": "^3.4.0" - } - }, - "heic-decode": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-1.1.2.tgz", - "integrity": "sha512-UF8teegxvzQPdSTcx5frIUhitNDliz/9Pui0JFdIqVRE00spVE33DcCYtZqaLNyd4y5RP/QQWZFIc1YWVKKm2A==", - "requires": { - "libheif-js": "^1.10.0" - } - }, "hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -15984,11 +15433,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "jpeg-js": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", - "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16164,11 +15608,6 @@ "type-check": "~0.4.0" } }, - "libheif-js": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.12.0.tgz", - "integrity": "sha512-hDs6xQ7028VOwAFwEtM0Q+B2x2NW69Jb2MhQFUbk3rUrHzz4qo5mqS8VrqNgYnSc8TiUGnR691LnO4uIfEE23w==" - }, "libphonenumber-js": { "version": "1.9.48", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.48.tgz", @@ -16296,11 +15735,6 @@ "is-unicode-supported": "^0.1.0" } }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -17047,11 +16481,6 @@ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true }, - "pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" - }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -17145,11 +16574,6 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -17304,11 +16728,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -17469,11 +16888,6 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" }, - "seedrandom": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" - }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -17712,7 +17126,8 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "stack-utils": { "version": "2.0.5", @@ -17903,57 +17318,6 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, - "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "dependencies": { - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", diff --git a/server/package.json b/server/package.json index a34046b6fa588..4a34ad11c2fb8 100644 --- a/server/package.json +++ b/server/package.json @@ -36,6 +36,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "dotenv": "^14.2.0", + "exifr": "^7.1.3", "fluent-ffmpeg": "^2.1.2", "joi": "^17.5.0", "lodash": "^4.17.21", diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index 1db90a7802e61..81b499d299b93 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -30,6 +30,7 @@ import { promisify } from 'util'; import { stat } from 'fs'; import { pipeline } from 'stream'; import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; +import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; const fileInfo = promisify(stat); @@ -37,8 +38,9 @@ const fileInfo = promisify(stat); @Controller('asset') export class AssetController { constructor( - private readonly assetService: AssetService, - private readonly assetOptimizeService: AssetOptimizeService, + private assetService: AssetService, + private assetOptimizeService: AssetOptimizeService, + private backgroundTaskService: BackgroundTaskService, ) {} @Post('upload') @@ -53,6 +55,7 @@ export class AssetController { if (savedAsset && savedAsset.type == AssetType.IMAGE) { await this.assetOptimizeService.resizeImage(savedAsset); + await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size); } if (savedAsset && savedAsset.type == AssetType.VIDEO) { @@ -155,4 +158,9 @@ export class AssetController { async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) { return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId); } + + @Get('/assetById/:assetId') + async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId) { + return this.assetService.getAssetById(authUser, assetId); + } } diff --git a/server/src/api-v1/asset/asset.module.ts b/server/src/api-v1/asset/asset.module.ts index 161e00748a754..0d4466b9a5df5 100644 --- a/server/src/api-v1/asset/asset.module.ts +++ b/server/src/api-v1/asset/asset.module.ts @@ -6,6 +6,8 @@ import { AssetEntity } from './entities/asset.entity'; import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module'; import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service'; import { BullModule } from '@nestjs/bull'; +import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; +import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; @Module({ imports: [ @@ -17,11 +19,20 @@ import { BullModule } from '@nestjs/bull'; removeOnFail: false, }, }), + BullModule.registerQueue({ + name: 'background-task', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), TypeOrmModule.forFeature([AssetEntity]), ImageOptimizeModule, + BackgroundTaskModule, ], controllers: [AssetController], - providers: [AssetService, AssetOptimizeService], + providers: [AssetService, AssetOptimizeService, BackgroundTaskService], exports: [], }) export class AssetModule {} diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts index 4d5c672f4d74c..2ffebbff510d0 100644 --- a/server/src/api-v1/asset/asset.service.ts +++ b/server/src/api-v1/asset/asset.service.ts @@ -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'], + }); + } } diff --git a/server/src/api-v1/asset/dto/create-exif.dto.ts b/server/src/api-v1/asset/dto/create-exif.dto.ts new file mode 100644 index 0000000000000..0214c1ee57beb --- /dev/null +++ b/server/src/api-v1/asset/dto/create-exif.dto.ts @@ -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; +} diff --git a/server/src/api-v1/asset/dto/update-exif.dto.ts b/server/src/api-v1/asset/dto/update-exif.dto.ts new file mode 100644 index 0000000000000..1bf5066649bea --- /dev/null +++ b/server/src/api-v1/asset/dto/update-exif.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateExifDto } from './create-exif.dto'; + +export class UpdateExifDto extends PartialType(CreateExifDto) {} diff --git a/server/src/api-v1/asset/entities/asset.entity.ts b/server/src/api-v1/asset/entities/asset.entity.ts index 4d78096d25d35..8fd1be779d0e4 100644 --- a/server/src/api-v1/asset/entities/asset.entity.ts +++ b/server/src/api-v1/asset/entities/asset.entity.ts @@ -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') @Unique(['deviceAssetId', 'userId', 'deviceId']) @@ -38,6 +39,9 @@ export class AssetEntity { @Column({ nullable: true }) duration: string; + + @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset) + exifInfo: ExifEntity; } export enum AssetType { diff --git a/server/src/api-v1/asset/entities/exif.entity.ts b/server/src/api-v1/asset/entities/exif.entity.ts new file mode 100644 index 0000000000000..ffc42464103b7 --- /dev/null +++ b/server/src/api-v1/asset/entities/exif.entity.ts @@ -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; +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index a4c02409a36a0..339b436ae9488 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -13,6 +13,7 @@ import { immichAppConfig } from './config/app.config'; import { BullModule } from '@nestjs/bull'; import { ImageOptimizeModule } from './modules/image-optimize/image-optimize.module'; import { ServerInfoModule } from './api-v1/server-info/server-info.module'; +import { BackgroundTaskModule } from './modules/background-task/background-task.module'; @Module({ imports: [ @@ -29,7 +30,6 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module'; redis: { host: 'immich_redis', port: 6379, - // password: configService.get('REDIS_PASSWORD'), }, }), inject: [ConfigService], @@ -38,6 +38,8 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module'; ImageOptimizeModule, ServerInfoModule, + + BackgroundTaskModule, ], controllers: [], providers: [], diff --git a/server/src/modules/background-task/background-task.module.ts b/server/src/modules/background-task/background-task.module.ts new file mode 100644 index 0000000000000..3bd2e419a98a7 --- /dev/null +++ b/server/src/modules/background-task/background-task.module.ts @@ -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 {} diff --git a/server/src/modules/background-task/background-task.processor.ts b/server/src/modules/background-task/background-task.processor.ts new file mode 100644 index 0000000000000..442475665449e --- /dev/null +++ b/server/src/modules/background-task/background-task.processor.ts @@ -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, + + @InjectRepository(ExifEntity) + private exifRepository: Repository, + + 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'); + } + } +} diff --git a/server/src/modules/background-task/background-task.service.ts b/server/src/modules/background-task/background-task.service.ts new file mode 100644 index 0000000000000..f0db4d1a92176 --- /dev/null +++ b/server/src/modules/background-task/background-task.service.ts @@ -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() }, + ); + } +} diff --git a/server/src/modules/image-optimize/image-optimize.service.ts b/server/src/modules/image-optimize/image-optimize.service.ts index 37f7a488bedd6..31c0a466c049e 100644 --- a/server/src/modules/image-optimize/image-optimize.service.ts +++ b/server/src/modules/image-optimize/image-optimize.service.ts @@ -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( 'get-video-thumbnail', {