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