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: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<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: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,10 +23,8 @@ 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(
 | 
			
		||||
    bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
 | 
			
		||||
    return SliverAppBar(
 | 
			
		||||
      centerTitle: true,
 | 
			
		||||
      floating: true,
 | 
			
		||||
      pinned: false,
 | 
			
		||||
@ -70,7 +71,20 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
                  )
 | 
			
		||||
                : Container(),
 | 
			
		||||
            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',
 | 
			
		||||
              onPressed: () async {
 | 
			
		||||
                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',
 | 
			
		||||
                heroTag: asset.id,
 | 
			
		||||
                thumbnailUrl: thumbnailRequestUrl,
 | 
			
		||||
                asset: asset,
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
@ -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<VideoViewerRouteArgs>();
 | 
			
		||||
@ -96,14 +97,16 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
 | 
			
		||||
      {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}';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
#################################
 | 
			
		||||
FROM node:16-alpine3.14 AS production
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
 | 
			
		||||
COPY package.json package-lock.json ./
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# 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"]
 | 
			
		||||
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-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",
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 {}
 | 
			
		||||
 | 
			
		||||
@ -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')
 | 
			
		||||
@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 {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 { 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: [],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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(
 | 
			
		||||
      'get-video-thumbnail',
 | 
			
		||||
      {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user