mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							7bd2455175
						
					
				
				
					commit
					0048662182
				
			@@ -85,9 +85,11 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
			
		||||
        right: 10,
 | 
			
		||||
        bottom: 5,
 | 
			
		||||
        child: Icon(
 | 
			
		||||
          (deviceId != asset.deviceId)
 | 
			
		||||
              ? Icons.cloud_done_outlined
 | 
			
		||||
              : Icons.photo_library_rounded,
 | 
			
		||||
          asset.isRemote
 | 
			
		||||
              ? (deviceId == asset.deviceId
 | 
			
		||||
                  ? Icons.cloud_done_outlined
 | 
			
		||||
                  : Icons.cloud_outlined)
 | 
			
		||||
              : Icons.cloud_off_outlined,
 | 
			
		||||
          color: Colors.white,
 | 
			
		||||
          size: 18,
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
 | 
			
		||||
              child: Row(
 | 
			
		||||
                children: [
 | 
			
		||||
                  Text(
 | 
			
		||||
                    asset.duration.substring(0, 7),
 | 
			
		||||
                    asset.duration.toString().substring(0, 7),
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                      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/ui/immich_toast.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
    bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,9 @@ import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/foundation.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/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:path_provider/path_provider.dart';
 | 
			
		||||
@@ -18,14 +17,12 @@ class ImageViewerService {
 | 
			
		||||
 | 
			
		||||
  ImageViewerService(this._apiService);
 | 
			
		||||
 | 
			
		||||
  Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
 | 
			
		||||
  Future<bool> downloadAssetToDevice(Asset asset) async {
 | 
			
		||||
    try {
 | 
			
		||||
      String fileName = p.basename(asset.originalPath);
 | 
			
		||||
 | 
			
		||||
      // 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(
 | 
			
		||||
          asset.id,
 | 
			
		||||
          asset.remoteId!,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
 | 
			
		||||
@@ -43,28 +40,28 @@ class ImageViewerService {
 | 
			
		||||
        entity = await PhotoManager.editor.darwin.saveLivePhoto(
 | 
			
		||||
          imageFile: imageFile,
 | 
			
		||||
          videoFile: videoFile,
 | 
			
		||||
          title: p.basename(asset.originalPath),
 | 
			
		||||
          title: asset.fileName,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return entity != null;
 | 
			
		||||
      } else {
 | 
			
		||||
        var res = await _apiService.assetApi.downloadFileWithHttpInfo(
 | 
			
		||||
          asset.id,
 | 
			
		||||
        );
 | 
			
		||||
        var res = await _apiService.assetApi
 | 
			
		||||
            .downloadFileWithHttpInfo(asset.remoteId!);
 | 
			
		||||
 | 
			
		||||
        final AssetEntity? entity;
 | 
			
		||||
 | 
			
		||||
        if (asset.type == AssetTypeEnum.IMAGE) {
 | 
			
		||||
        if (asset.isImage) {
 | 
			
		||||
          entity = await PhotoManager.editor.saveImage(
 | 
			
		||||
            res.bodyBytes,
 | 
			
		||||
            title: p.basename(asset.originalPath),
 | 
			
		||||
            title: asset.fileName,
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          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);
 | 
			
		||||
          entity =
 | 
			
		||||
              await PhotoManager.editor.saveVideo(tempFile, title: fileName);
 | 
			
		||||
          entity = await PhotoManager.editor
 | 
			
		||||
              .saveVideo(tempFile, title: asset.fileName);
 | 
			
		||||
        }
 | 
			
		||||
        return entity != null;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_map/flutter_map.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.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:openapi/api.dart';
 | 
			
		||||
import 'package:path/path.dart' as p;
 | 
			
		||||
import 'package:latlong2/latlong.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/bytes_units.dart';
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +67,7 @@ class ExifBottomSheet extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    final textColor = Theme.of(context).primaryColor;
 | 
			
		||||
 | 
			
		||||
    ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
 | 
			
		||||
    ExifInfo? exifInfo = assetDetail.exifInfo;
 | 
			
		||||
 | 
			
		||||
    buildLocationText() {
 | 
			
		||||
      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(
 | 
			
		||||
      child: Card(
 | 
			
		||||
        shape: const RoundedRectangleBorder(
 | 
			
		||||
@@ -101,19 +111,18 @@ class ExifBottomSheet extends HookConsumerWidget {
 | 
			
		||||
                child: CustomDraggingHandle(),
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 12),
 | 
			
		||||
              if (exifInfo?.dateTimeOriginal != null)
 | 
			
		||||
                Text(
 | 
			
		||||
                  DateFormat('date_format'.tr()).format(
 | 
			
		||||
                    exifInfo!.dateTimeOriginal!.toLocal(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  style: const TextStyle(
 | 
			
		||||
                    fontWeight: FontWeight.bold,
 | 
			
		||||
                    fontSize: 14,
 | 
			
		||||
                  ),
 | 
			
		||||
              Text(
 | 
			
		||||
                DateFormat('date_format'.tr()).format(
 | 
			
		||||
                  assetDetail.createdAt.toLocal(),
 | 
			
		||||
                ),
 | 
			
		||||
                style: const TextStyle(
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                  fontSize: 14,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
 | 
			
		||||
              // Location
 | 
			
		||||
              if (assetDetail.latitude != null)
 | 
			
		||||
              if (assetDetail.latitude != null && assetDetail.longitude != null)
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(top: 32.0),
 | 
			
		||||
                  child: Column(
 | 
			
		||||
@@ -126,74 +135,67 @@ class ExifBottomSheet extends HookConsumerWidget {
 | 
			
		||||
                        "exif_bottom_sheet_location",
 | 
			
		||||
                        style: TextStyle(fontSize: 11, color: textColor),
 | 
			
		||||
                      ).tr(),
 | 
			
		||||
                      if (assetDetail.latitude != null &&
 | 
			
		||||
                          assetDetail.longitude != null)
 | 
			
		||||
                        buildMap(),
 | 
			
		||||
                      buildMap(),
 | 
			
		||||
                      if (exifInfo != null &&
 | 
			
		||||
                          exifInfo.city != null &&
 | 
			
		||||
                          exifInfo.state != null)
 | 
			
		||||
                        buildLocationText(),
 | 
			
		||||
                      Text(
 | 
			
		||||
                        "${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
 | 
			
		||||
                        "${assetDetail.latitude!.toStringAsFixed(4)}, ${assetDetail.longitude!.toStringAsFixed(4)}",
 | 
			
		||||
                        style: const TextStyle(fontSize: 12),
 | 
			
		||||
                      )
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              // Detail
 | 
			
		||||
              if (exifInfo != null)
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(top: 32.0),
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Divider(
 | 
			
		||||
                        thickness: 1,
 | 
			
		||||
                        color: Colors.grey[600],
 | 
			
		||||
                      ),
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.only(bottom: 8.0),
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          "exif_bottom_sheet_details",
 | 
			
		||||
                          style: TextStyle(fontSize: 11, color: textColor),
 | 
			
		||||
                        ).tr(),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(top: 32.0),
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Divider(
 | 
			
		||||
                      thickness: 1,
 | 
			
		||||
                      color: Colors.grey[600],
 | 
			
		||||
                    ),
 | 
			
		||||
                    Padding(
 | 
			
		||||
                      padding: const EdgeInsets.only(bottom: 8.0),
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        "exif_bottom_sheet_details",
 | 
			
		||||
                        style: TextStyle(fontSize: 11, color: textColor),
 | 
			
		||||
                      ).tr(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ListTile(
 | 
			
		||||
                      contentPadding: const EdgeInsets.all(0),
 | 
			
		||||
                      dense: true,
 | 
			
		||||
                      leading: const Icon(Icons.image),
 | 
			
		||||
                      title: Text(
 | 
			
		||||
                        assetDetail.fileName,
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                          fontWeight: FontWeight.bold,
 | 
			
		||||
                          color: textColor,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      subtitle: buildSizeText(assetDetail),
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (exifInfo?.make != null)
 | 
			
		||||
                      ListTile(
 | 
			
		||||
                        contentPadding: const EdgeInsets.all(0),
 | 
			
		||||
                        dense: true,
 | 
			
		||||
                        leading: const Icon(Icons.image),
 | 
			
		||||
                        leading: const Icon(Icons.camera),
 | 
			
		||||
                        title: Text(
 | 
			
		||||
                          "${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
 | 
			
		||||
                          "${exifInfo!.make} ${exifInfo.model}",
 | 
			
		||||
                          style: TextStyle(
 | 
			
		||||
                            fontWeight: FontWeight.bold,
 | 
			
		||||
                            color: textColor,
 | 
			
		||||
                            fontWeight: FontWeight.bold,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        subtitle: exifInfo.exifImageHeight != null
 | 
			
		||||
                            ? Text(
 | 
			
		||||
                                "${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth}  ${formatBytes(exifInfo.fileSizeInByte ?? 0)} ",
 | 
			
		||||
                              )
 | 
			
		||||
                            : null,
 | 
			
		||||
                        subtitle: Text(
 | 
			
		||||
                          "ƒ/${exifInfo.fNumber}   ${exifInfo.exposureTime}   ${exifInfo.focalLength} mm   ISO${exifInfo.iso} ",
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      if (exifInfo.make != null)
 | 
			
		||||
                        ListTile(
 | 
			
		||||
                          contentPadding: const EdgeInsets.all(0),
 | 
			
		||||
                          dense: true,
 | 
			
		||||
                          leading: const Icon(Icons.camera),
 | 
			
		||||
                          title: Text(
 | 
			
		||||
                            "${exifInfo.make} ${exifInfo.model}",
 | 
			
		||||
                            style: TextStyle(
 | 
			
		||||
                              color: textColor,
 | 
			
		||||
                              fontWeight: FontWeight.bold,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          subtitle: Text(
 | 
			
		||||
                            "ƒ/${exifInfo.fNumber}   ${exifInfo.exposureTime}   ${exifInfo.focalLength} mm   ISO${exifInfo.iso} ",
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(
 | 
			
		||||
                height: 50,
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        if (asset.remote?.livePhotoVideoId != null)
 | 
			
		||||
        if (asset.livePhotoVideoId != null)
 | 
			
		||||
          IconButton(
 | 
			
		||||
            iconSize: iconSize,
 | 
			
		||||
            splashRadius: iconSize,
 | 
			
		||||
@@ -104,18 +104,17 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
			
		||||
            color: Colors.grey[200],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        if (asset.isRemote)
 | 
			
		||||
          IconButton(
 | 
			
		||||
            iconSize: iconSize,
 | 
			
		||||
            splashRadius: iconSize,
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              onMoreInfoPressed();
 | 
			
		||||
            },
 | 
			
		||||
            icon: Icon(
 | 
			
		||||
              Icons.more_horiz_rounded,
 | 
			
		||||
              color: Colors.grey[200],
 | 
			
		||||
            ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
          iconSize: iconSize,
 | 
			
		||||
          splashRadius: iconSize,
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            onMoreInfoPressed();
 | 
			
		||||
          },
 | 
			
		||||
          icon: Icon(
 | 
			
		||||
            Icons.more_horiz_rounded,
 | 
			
		||||
            color: Colors.grey[200],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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/top_control_app_bar.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/settings/providers/app_settings.provider.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
 | 
			
		||||
    ImageProvider remoteThumbnailImageProvider(Asset asset, api.ThumbnailFormat type) {
 | 
			
		||||
    /// Thumbnail image of a remote asset. Required asset.isRemote
 | 
			
		||||
    ImageProvider remoteThumbnailImageProvider(
 | 
			
		||||
      Asset asset,
 | 
			
		||||
      api.ThumbnailFormat type,
 | 
			
		||||
    ) {
 | 
			
		||||
      return CachedNetworkImageProvider(
 | 
			
		||||
        getThumbnailUrl(
 | 
			
		||||
          asset.remote!,
 | 
			
		||||
          asset,
 | 
			
		||||
          type: type,
 | 
			
		||||
        ),
 | 
			
		||||
        cacheKey: getThumbnailCacheKey(
 | 
			
		||||
          asset.remote!,
 | 
			
		||||
          asset,
 | 
			
		||||
          type: type,
 | 
			
		||||
        ),
 | 
			
		||||
        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) {
 | 
			
		||||
      return CachedNetworkImageProvider(
 | 
			
		||||
        getImageUrl(asset.remote!),
 | 
			
		||||
        cacheKey: getImageCacheKey(asset.remote!),
 | 
			
		||||
        getImageUrl(asset),
 | 
			
		||||
        cacheKey: getImageCacheKey(asset),
 | 
			
		||||
        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) {
 | 
			
		||||
      return AssetEntityImageProvider(
 | 
			
		||||
        asset.local!,
 | 
			
		||||
@@ -114,10 +117,9 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
          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) {
 | 
			
		||||
      return AssetEntityImageProvider(asset.local!);
 | 
			
		||||
    }
 | 
			
		||||
@@ -132,7 +134,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
          // Probably load WEBP either way
 | 
			
		||||
          precacheImage(
 | 
			
		||||
            remoteThumbnailImageProvider(
 | 
			
		||||
              asset, 
 | 
			
		||||
              asset,
 | 
			
		||||
              api.ThumbnailFormat.WEBP,
 | 
			
		||||
            ),
 | 
			
		||||
            context,
 | 
			
		||||
@@ -154,26 +156,23 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
              context,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void showInfo() {
 | 
			
		||||
      if (assetList[indexOfAsset.value].isRemote) {
 | 
			
		||||
        showModalBottomSheet(
 | 
			
		||||
          shape: RoundedRectangleBorder(
 | 
			
		||||
            borderRadius: BorderRadius.circular(15.0),
 | 
			
		||||
          ),
 | 
			
		||||
          barrierColor: Colors.transparent,
 | 
			
		||||
          backgroundColor: Colors.transparent,
 | 
			
		||||
          isScrollControlled: true,
 | 
			
		||||
          context: context,
 | 
			
		||||
          builder: (context) {
 | 
			
		||||
            return ExifBottomSheet(assetDetail: assetDetail!);
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      showModalBottomSheet(
 | 
			
		||||
        shape: RoundedRectangleBorder(
 | 
			
		||||
          borderRadius: BorderRadius.circular(15.0),
 | 
			
		||||
        ),
 | 
			
		||||
        barrierColor: Colors.transparent,
 | 
			
		||||
        backgroundColor: Colors.transparent,
 | 
			
		||||
        isScrollControlled: true,
 | 
			
		||||
        context: context,
 | 
			
		||||
        builder: (context) {
 | 
			
		||||
          return ExifBottomSheet(assetDetail: assetDetail!);
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void handleDelete(Asset deleteAsset) {
 | 
			
		||||
@@ -244,7 +243,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
                ? null
 | 
			
		||||
                : () {
 | 
			
		||||
                    ref.watch(imageViewerStateProvider.notifier).downloadAsset(
 | 
			
		||||
                          assetList[indexOfAsset.value].remote!,
 | 
			
		||||
                          assetList[indexOfAsset.value],
 | 
			
		||||
                          context,
 | 
			
		||||
                        );
 | 
			
		||||
                  },
 | 
			
		||||
@@ -256,8 +255,10 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
            onToggleMotionVideo: (() {
 | 
			
		||||
              isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
 | 
			
		||||
            }),
 | 
			
		||||
            onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
 | 
			
		||||
            onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
 | 
			
		||||
            onDeletePressed: () =>
 | 
			
		||||
                handleDelete((assetList[indexOfAsset.value])),
 | 
			
		||||
            onAddToAlbumPressed: () =>
 | 
			
		||||
                addToAlbum(assetList[indexOfAsset.value]),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
@@ -268,117 +269,132 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
			
		||||
      body: Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          PhotoViewGallery.builder(
 | 
			
		||||
          scaleStateChangedCallback: (state) {
 | 
			
		||||
            isZoomed.value = state != PhotoViewScaleState.initial;
 | 
			
		||||
            showAppBar.value = !isZoomed.value;
 | 
			
		||||
          },
 | 
			
		||||
          pageController: controller,
 | 
			
		||||
          scrollPhysics: isZoomed.value
 | 
			
		||||
              ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
 | 
			
		||||
              : (Platform.isIOS 
 | 
			
		||||
                ? const BouncingScrollPhysics()  // Use bouncing physics for iOS
 | 
			
		||||
                : const ClampingScrollPhysics() // Use heavy physics for Android
 | 
			
		||||
              ),
 | 
			
		||||
          itemCount: assetList.length,
 | 
			
		||||
          scrollDirection: Axis.horizontal,
 | 
			
		||||
          onPageChanged: (value) {
 | 
			
		||||
            // Precache image
 | 
			
		||||
            if (indexOfAsset.value < value) {
 | 
			
		||||
              // Moving forwards, so precache the next asset
 | 
			
		||||
              precacheNextImage(value + 1);
 | 
			
		||||
            } else {
 | 
			
		||||
              // Moving backwards, so precache previous asset
 | 
			
		||||
              precacheNextImage(value - 1);
 | 
			
		||||
            }
 | 
			
		||||
            indexOfAsset.value = value;
 | 
			
		||||
            HapticFeedback.selectionClick();
 | 
			
		||||
          },
 | 
			
		||||
          loadingBuilder: isLoadPreview.value ? (context, event) {
 | 
			
		||||
            final asset = assetList[indexOfAsset.value];
 | 
			
		||||
            if (!asset.isLocal) {
 | 
			
		||||
              // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
 | 
			
		||||
              // Three-Stage Loading (WEBP -> JPEG -> Original)
 | 
			
		||||
              final webPThumbnail = CachedNetworkImage(
 | 
			
		||||
                imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.WEBP),
 | 
			
		||||
                cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.WEBP),
 | 
			
		||||
                httpHeaders: { 'Authorization': authToken },
 | 
			
		||||
                progressIndicatorBuilder: (_, __, ___) => const Center(child: ImmichLoadingIndicator(),),
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 0),
 | 
			
		||||
                fit: BoxFit.contain,
 | 
			
		||||
              );
 | 
			
		||||
 | 
			
		||||
              return CachedNetworkImage(
 | 
			
		||||
                imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.JPEG),
 | 
			
		||||
                cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.JPEG),
 | 
			
		||||
                httpHeaders: { 'Authorization': authToken },
 | 
			
		||||
                fit: BoxFit.contain,
 | 
			
		||||
                fadeInDuration: const Duration(milliseconds: 0),
 | 
			
		||||
                placeholder: (_, __) => webPThumbnail,
 | 
			
		||||
              );
 | 
			
		||||
            } else {
 | 
			
		||||
              return Image(
 | 
			
		||||
                image: localThumbnailImageProvider(asset),
 | 
			
		||||
                fit: BoxFit.contain,
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          } : null,
 | 
			
		||||
          builder: (context, index) {
 | 
			
		||||
            getAssetExif();
 | 
			
		||||
            if (assetList[index].isImage && !isPlayingMotionVideo.value) {
 | 
			
		||||
              // Show photo
 | 
			
		||||
              final ImageProvider provider;
 | 
			
		||||
              if (assetList[index].isLocal) {
 | 
			
		||||
                provider = localImageProvider(assetList[index]);
 | 
			
		||||
              } else {
 | 
			
		||||
                if (isLoadOriginal.value) {
 | 
			
		||||
                  provider = originalImageProvider(assetList[index]);
 | 
			
		||||
                } else {
 | 
			
		||||
                  provider = remoteThumbnailImageProvider(
 | 
			
		||||
                    assetList[index], 
 | 
			
		||||
                    api.ThumbnailFormat.JPEG,
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              return PhotoViewGalleryPageOptions(
 | 
			
		||||
                onDragStart: (_, details, __) => localPosition = details.localPosition,
 | 
			
		||||
                onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
			
		||||
                onTapDown: (_, __, ___) => showAppBar.value = !showAppBar.value,
 | 
			
		||||
                imageProvider: provider,
 | 
			
		||||
                heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
 | 
			
		||||
                minScale: PhotoViewComputedScale.contained,
 | 
			
		||||
              );
 | 
			
		||||
            } else {
 | 
			
		||||
              return PhotoViewGalleryPageOptions.customChild(
 | 
			
		||||
                onDragStart: (_, details, __) => localPosition = details.localPosition,
 | 
			
		||||
                onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
			
		||||
                onTapDown: (_, __, ___) => showAppBar.value = !showAppBar.value,
 | 
			
		||||
                heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
 | 
			
		||||
                maxScale: 1.0,
 | 
			
		||||
                minScale: 1.0,
 | 
			
		||||
                child: SafeArea(
 | 
			
		||||
                  child: VideoViewerPage(
 | 
			
		||||
                    asset: assetList[index],
 | 
			
		||||
                    isMotionVideo: isPlayingMotionVideo.value,
 | 
			
		||||
                    onVideoEnded: () {
 | 
			
		||||
                      if (isPlayingMotionVideo.value) {
 | 
			
		||||
                        isPlayingMotionVideo.value = false;
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
            scaleStateChangedCallback: (state) {
 | 
			
		||||
              isZoomed.value = state != PhotoViewScaleState.initial;
 | 
			
		||||
              showAppBar.value = !isZoomed.value;
 | 
			
		||||
            },
 | 
			
		||||
            pageController: controller,
 | 
			
		||||
            scrollPhysics: isZoomed.value
 | 
			
		||||
                ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
 | 
			
		||||
                : (Platform.isIOS
 | 
			
		||||
                    ? const BouncingScrollPhysics() // Use bouncing physics for iOS
 | 
			
		||||
                    : const ClampingScrollPhysics() // Use heavy physics for Android
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        Positioned(
 | 
			
		||||
          top: 0,
 | 
			
		||||
          left: 0,
 | 
			
		||||
          right: 0,
 | 
			
		||||
          child: buildAppBar(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
            itemCount: assetList.length,
 | 
			
		||||
            scrollDirection: Axis.horizontal,
 | 
			
		||||
            onPageChanged: (value) {
 | 
			
		||||
              // Precache image
 | 
			
		||||
              if (indexOfAsset.value < value) {
 | 
			
		||||
                // Moving forwards, so precache the next asset
 | 
			
		||||
                precacheNextImage(value + 1);
 | 
			
		||||
              } else {
 | 
			
		||||
                // Moving backwards, so precache previous asset
 | 
			
		||||
                precacheNextImage(value - 1);
 | 
			
		||||
              }
 | 
			
		||||
              indexOfAsset.value = value;
 | 
			
		||||
              HapticFeedback.selectionClick();
 | 
			
		||||
            },
 | 
			
		||||
            loadingBuilder: isLoadPreview.value
 | 
			
		||||
                ? (context, event) {
 | 
			
		||||
                    final asset = assetList[indexOfAsset.value];
 | 
			
		||||
                    if (!asset.isLocal) {
 | 
			
		||||
                      // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
 | 
			
		||||
                      // Three-Stage Loading (WEBP -> JPEG -> Original)
 | 
			
		||||
                      final webPThumbnail = CachedNetworkImage(
 | 
			
		||||
                        imageUrl: getThumbnailUrl(asset),
 | 
			
		||||
                        cacheKey: getThumbnailCacheKey(asset),
 | 
			
		||||
                        httpHeaders: {'Authorization': authToken},
 | 
			
		||||
                        progressIndicatorBuilder: (_, __, ___) => const Center(
 | 
			
		||||
                          child: ImmichLoadingIndicator(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        fadeInDuration: const Duration(milliseconds: 0),
 | 
			
		||||
                        fit: BoxFit.contain,
 | 
			
		||||
                      );
 | 
			
		||||
 | 
			
		||||
                      return CachedNetworkImage(
 | 
			
		||||
                        imageUrl: getThumbnailUrl(
 | 
			
		||||
                          asset,
 | 
			
		||||
                          type: api.ThumbnailFormat.JPEG,
 | 
			
		||||
                        ),
 | 
			
		||||
                        cacheKey: getThumbnailCacheKey(
 | 
			
		||||
                          asset,
 | 
			
		||||
                          type: api.ThumbnailFormat.JPEG,
 | 
			
		||||
                        ),
 | 
			
		||||
                        httpHeaders: {'Authorization': authToken},
 | 
			
		||||
                        fit: BoxFit.contain,
 | 
			
		||||
                        fadeInDuration: const Duration(milliseconds: 0),
 | 
			
		||||
                        placeholder: (_, __) => webPThumbnail,
 | 
			
		||||
                      );
 | 
			
		||||
                    } else {
 | 
			
		||||
                      return Image(
 | 
			
		||||
                        image: localThumbnailImageProvider(asset),
 | 
			
		||||
                        fit: BoxFit.contain,
 | 
			
		||||
                      );
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                : null,
 | 
			
		||||
            builder: (context, index) {
 | 
			
		||||
              getAssetExif();
 | 
			
		||||
              if (assetList[index].isImage && !isPlayingMotionVideo.value) {
 | 
			
		||||
                // Show photo
 | 
			
		||||
                final ImageProvider provider;
 | 
			
		||||
                if (assetList[index].isLocal) {
 | 
			
		||||
                  provider = localImageProvider(assetList[index]);
 | 
			
		||||
                } else {
 | 
			
		||||
                  if (isLoadOriginal.value) {
 | 
			
		||||
                    provider = originalImageProvider(assetList[index]);
 | 
			
		||||
                  } else {
 | 
			
		||||
                    provider = remoteThumbnailImageProvider(
 | 
			
		||||
                      assetList[index],
 | 
			
		||||
                      api.ThumbnailFormat.JPEG,
 | 
			
		||||
                    );
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
                return PhotoViewGalleryPageOptions(
 | 
			
		||||
                  onDragStart: (_, details, __) =>
 | 
			
		||||
                      localPosition = details.localPosition,
 | 
			
		||||
                  onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
			
		||||
                  onTapDown: (_, __, ___) =>
 | 
			
		||||
                      showAppBar.value = !showAppBar.value,
 | 
			
		||||
                  imageProvider: provider,
 | 
			
		||||
                  heroAttributes:
 | 
			
		||||
                      PhotoViewHeroAttributes(tag: assetList[index].id),
 | 
			
		||||
                  minScale: PhotoViewComputedScale.contained,
 | 
			
		||||
                );
 | 
			
		||||
              } else {
 | 
			
		||||
                return PhotoViewGalleryPageOptions.customChild(
 | 
			
		||||
                  onDragStart: (_, details, __) =>
 | 
			
		||||
                      localPosition = details.localPosition,
 | 
			
		||||
                  onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
			
		||||
                  onTapDown: (_, __, ___) =>
 | 
			
		||||
                      showAppBar.value = !showAppBar.value,
 | 
			
		||||
                  heroAttributes:
 | 
			
		||||
                      PhotoViewHeroAttributes(tag: assetList[index].id),
 | 
			
		||||
                  maxScale: 1.0,
 | 
			
		||||
                  minScale: 1.0,
 | 
			
		||||
                  child: SafeArea(
 | 
			
		||||
                    child: VideoViewerPage(
 | 
			
		||||
                      asset: assetList[index],
 | 
			
		||||
                      isMotionVideo: isPlayingMotionVideo.value,
 | 
			
		||||
                      onVideoEnded: () {
 | 
			
		||||
                        if (isPlayingMotionVideo.value) {
 | 
			
		||||
                          isPlayingMotionVideo.value = false;
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          Positioned(
 | 
			
		||||
            top: 0,
 | 
			
		||||
            left: 0,
 | 
			
		||||
            right: 0,
 | 
			
		||||
            child: buildAppBar(),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,8 +53,8 @@ class VideoViewerPage extends HookConsumerWidget {
 | 
			
		||||
    final box = Hive.box(userInfoBox);
 | 
			
		||||
    final String jwtToken = box.get(accessTokenKey);
 | 
			
		||||
    final String videoUrl = isMotionVideo
 | 
			
		||||
        ? '${box.get(serverEndpointKey)}/asset/file/${asset.remote?.livePhotoVideoId!}'
 | 
			
		||||
        : '${box.get(serverEndpointKey)}/asset/file/${asset.id}';
 | 
			
		||||
        ? '${box.get(serverEndpointKey)}/asset/file/${asset.livePhotoVideoId}'
 | 
			
		||||
        : '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}';
 | 
			
		||||
 | 
			
		||||
    return Stack(
 | 
			
		||||
      children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,9 @@ class BackupService {
 | 
			
		||||
    final filter = FilterOptionGroup(
 | 
			
		||||
      containsPathModified: true,
 | 
			
		||||
      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 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;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,103 +0,0 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/api.service.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/openapi_extensions.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/tuple.dart';
 | 
			
		||||
import 'package:logging/logging.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
final assetServiceProvider = Provider(
 | 
			
		||||
  (ref) => AssetService(
 | 
			
		||||
    ref.watch(apiServiceProvider),
 | 
			
		||||
    ref.watch(backupServiceProvider),
 | 
			
		||||
    ref.watch(backgroundServiceProvider),
 | 
			
		||||
  ),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
class AssetService {
 | 
			
		||||
  final ApiService _apiService;
 | 
			
		||||
  final BackupService _backupService;
 | 
			
		||||
  final BackgroundService _backgroundService;
 | 
			
		||||
  final log = Logger('AssetService');
 | 
			
		||||
 | 
			
		||||
  AssetService(this._apiService, this._backupService, this._backgroundService);
 | 
			
		||||
 | 
			
		||||
  /// Returns `null` if the server state did not change, else list of assets
 | 
			
		||||
  Future<Pair<List<Asset>?, String?>> getRemoteAssets({String? etag}) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final Pair<List<AssetResponseDto>, String?>? remote =
 | 
			
		||||
          await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
 | 
			
		||||
      if (remote == null) {
 | 
			
		||||
        return const Pair(null, null);
 | 
			
		||||
      }
 | 
			
		||||
      return Pair(
 | 
			
		||||
        remote.first.map(Asset.remote).toList(growable: false),
 | 
			
		||||
        remote.second,
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e, stack) {
 | 
			
		||||
      log.severe('Error while getting remote assets', e, stack);
 | 
			
		||||
      return const Pair(null, null);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// if [urgent] is `true`, do not block by waiting on the background service
 | 
			
		||||
  /// to finish running. Returns `null` instead after a timeout.
 | 
			
		||||
  Future<List<Asset>?> getLocalAssets({bool urgent = false}) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final Future<bool> hasAccess = urgent
 | 
			
		||||
          ? _backgroundService.hasAccess
 | 
			
		||||
              .timeout(const Duration(milliseconds: 250))
 | 
			
		||||
          : _backgroundService.hasAccess;
 | 
			
		||||
      if (!await hasAccess) {
 | 
			
		||||
        throw Exception("Error [getAllAsset] failed to gain access");
 | 
			
		||||
      }
 | 
			
		||||
      final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
			
		||||
      final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
 | 
			
		||||
      if (backupAlbumInfo != null) {
 | 
			
		||||
        return (await _backupService
 | 
			
		||||
                .buildUploadCandidates(backupAlbumInfo.deepCopy()))
 | 
			
		||||
            .map(Asset.local)
 | 
			
		||||
            .toList(growable: false);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error [_getLocalAssets] ${e.toString()}");
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Asset?> getAssetById(String assetId) async {
 | 
			
		||||
    try {
 | 
			
		||||
      return Asset.remote(await _apiService.assetApi.getAssetById(assetId));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error [getAssetById]  ${e.toString()}");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<DeleteAssetResponseDto>?> deleteAssets(
 | 
			
		||||
    Iterable<AssetResponseDto> deleteAssets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final List<String> payload = [];
 | 
			
		||||
 | 
			
		||||
      for (final asset in deleteAssets) {
 | 
			
		||||
        payload.add(asset.id);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return await _apiService.assetApi
 | 
			
		||||
          .deleteAsset(DeleteAssetDto(ids: payload));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error getAllAsset  ${e.toString()}");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/json_cache.dart';
 | 
			
		||||
 | 
			
		||||
class AssetCacheService extends JsonCache<List<Asset>> {
 | 
			
		||||
  AssetCacheService() : super("asset_cache");
 | 
			
		||||
 | 
			
		||||
  static Future<List<Map<String, dynamic>>> _computeSerialize(
 | 
			
		||||
    List<Asset> assets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    return assets.map((e) => e.toJson()).toList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void put(List<Asset> data) async {
 | 
			
		||||
    putRawData(await compute(_computeSerialize, data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future<List<Asset>> _computeEncode(List<dynamic> data) async {
 | 
			
		||||
    return data.map((e) => Asset.fromJson(e)).whereNotNull().toList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<List<Asset>> get() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final mapList = await readRawData() as List<dynamic>;
 | 
			
		||||
 | 
			
		||||
      final responseData = await compute(_computeEncode, mapList);
 | 
			
		||||
 | 
			
		||||
      return responseData;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint(e.toString());
 | 
			
		||||
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final assetCacheServiceProvider = Provider(
 | 
			
		||||
  (ref) => AssetCacheService(),
 | 
			
		||||
);
 | 
			
		||||
@@ -24,7 +24,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
			
		||||
  bool _scrolling = false;
 | 
			
		||||
  final Set<String> _selectedAssets = HashSet();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  Set<Asset> _getSelectedAssets() {
 | 
			
		||||
    return _selectedAssets
 | 
			
		||||
        .map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
 | 
			
		||||
@@ -103,7 +102,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
			
		||||
    return Row(
 | 
			
		||||
      key: Key("asset-row-${row.assets.first.id}"),
 | 
			
		||||
      children: row.assets.map((Asset asset) {
 | 
			
		||||
        bool last = asset == row.assets.last;
 | 
			
		||||
        bool last = asset.id == row.assets.last.id;
 | 
			
		||||
 | 
			
		||||
        return Container(
 | 
			
		||||
          key: Key("asset-${asset.id}"),
 | 
			
		||||
@@ -224,7 +223,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  Future<bool> onWillPop() async {
 | 
			
		||||
    if (widget.selectionActive && _selectedAssets.isNotEmpty) {
 | 
			
		||||
      _deselectAll();
 | 
			
		||||
@@ -234,8 +232,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return WillPopScope(
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/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/hive_saved_login_info.model.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();
 | 
			
		||||
      userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
 | 
			
		||||
      userInfoHiveBox.put(accessTokenKey, accessToken);
 | 
			
		||||
      userInfoHiveBox.put(userIdKey, userResponseDto.id);
 | 
			
		||||
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
        isAuthenticated: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -45,9 +45,11 @@ class SearchResultPageState {
 | 
			
		||||
      isLoading: map['isLoading'] ?? false,
 | 
			
		||||
      isSuccess: map['isSuccess'] ?? false,
 | 
			
		||||
      isError: map['isError'] ?? false,
 | 
			
		||||
      searchResult: List<Asset>.from(
 | 
			
		||||
      searchResult: List.from(
 | 
			
		||||
        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,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    List<Asset>? assets = (await _searchService.searchAsset(searchTerm))
 | 
			
		||||
        ?.map((e) => Asset.remote(e))
 | 
			
		||||
        .toList();
 | 
			
		||||
    List<Asset>? assets = await _searchService.searchAsset(searchTerm);
 | 
			
		||||
 | 
			
		||||
    if (assets != null) {
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.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/services/api.service.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 {
 | 
			
		||||
      return await _apiService.assetApi
 | 
			
		||||
      final List<AssetResponseDto>? results = await _apiService.assetApi
 | 
			
		||||
          .searchAsset(SearchAssetDto(searchTerm: searchTerm));
 | 
			
		||||
      if (results == null) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      return results.map((e) => Asset.remote(e)).toList();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("[ERROR] [searchAsset] ${e.toString()}");
 | 
			
		||||
      return null;
 | 
			
		||||
@@ -50,7 +55,7 @@ class SearchService {
 | 
			
		||||
      return await _apiService.assetApi.getCuratedObjects();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error [getCuratedObjects] ${e.toString()}");
 | 
			
		||||
      throw [];
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user