mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(mobile): show local assets (#905)
* introduce Asset as composition of AssetResponseDTO and AssetEntity * filter out duplicate assets (that are both local and remote, take only remote for now) * only allow remote images to be added to albums * introduce ImmichImage to render Asset using local or remote data * optimized deletion of local assets * local video file playback * allow multiple methods to wait on background service finished * skip local assets when adding to album from home screen * fix and optimize delete * show gray box placeholder for local assets * add comments * fix bug: duplicate assets in state after onNewAssetUploaded
This commit is contained in:
		
							parent
							
								
									99da181cfc
								
							
						
					
					
						commit
						1633af7af6
					
				@ -134,13 +134,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun stopEngine(result: Result?) {
 | 
					    private fun stopEngine(result: Result?) {
 | 
				
			||||||
 | 
					        clearBackgroundNotification()
 | 
				
			||||||
 | 
					        engine?.destroy()
 | 
				
			||||||
 | 
					        engine = null
 | 
				
			||||||
        if (result != null) {
 | 
					        if (result != null) {
 | 
				
			||||||
            Log.d(TAG, "stopEngine result=${result}")
 | 
					            Log.d(TAG, "stopEngine result=${result}")
 | 
				
			||||||
            resolvableFuture.set(result)
 | 
					            resolvableFuture.set(result)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        engine?.destroy()
 | 
					 | 
				
			||||||
        engine = null
 | 
					 | 
				
			||||||
        clearBackgroundNotification()
 | 
					 | 
				
			||||||
        waitOnSetForegroundAsync()
 | 
					        waitOnSetForegroundAsync()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -35,10 +35,12 @@ void main() async {
 | 
				
			|||||||
  await Future.wait([
 | 
					  await Future.wait([
 | 
				
			||||||
    Hive.openBox(userInfoBox),
 | 
					    Hive.openBox(userInfoBox),
 | 
				
			||||||
    Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
 | 
					    Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
 | 
				
			||||||
    Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
 | 
					 | 
				
			||||||
    Hive.openBox(hiveGithubReleaseInfoBox),
 | 
					    Hive.openBox(hiveGithubReleaseInfoBox),
 | 
				
			||||||
    Hive.openBox(userSettingInfoBox),
 | 
					    Hive.openBox(userSettingInfoBox),
 | 
				
			||||||
    Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
 | 
					    if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
 | 
				
			||||||
 | 
					    if (!Platform.isAndroid)
 | 
				
			||||||
 | 
					      Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
 | 
				
			||||||
 | 
					    if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
 | 
				
			||||||
    EasyLocalization.ensureInitialized(),
 | 
					    EasyLocalization.ensureInitialized(),
 | 
				
			||||||
  ]);
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -86,8 +88,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
 | 
				
			|||||||
        var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
 | 
					        var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isAuthenticated) {
 | 
					        if (isAuthenticated) {
 | 
				
			||||||
 | 
					          ref.read(backupProvider.notifier).resumeBackup();
 | 
				
			||||||
          ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
 | 
					          ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
 | 
				
			||||||
          ref.watch(backupProvider.notifier).resumeBackup();
 | 
					 | 
				
			||||||
          ref.watch(assetProvider.notifier).getAllAsset();
 | 
					          ref.watch(assetProvider.notifier).getAllAsset();
 | 
				
			||||||
          ref.watch(serverInfoProvider.notifier).getServerVersion();
 | 
					          ref.watch(serverInfoProvider.notifier).getServerVersion();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,9 @@
 | 
				
			|||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetSelectionPageResult {
 | 
					class AssetSelectionPageResult {
 | 
				
			||||||
  final Set<AssetResponseDto> selectedNewAsset;
 | 
					  final Set<Asset> selectedNewAsset;
 | 
				
			||||||
  final Set<AssetResponseDto> selectedAdditionalAsset;
 | 
					  final Set<Asset> selectedAdditionalAsset;
 | 
				
			||||||
  final bool isAlbumExist;
 | 
					  final bool isAlbumExist;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AssetSelectionPageResult({
 | 
					  AssetSelectionPageResult({
 | 
				
			||||||
@ -14,8 +13,8 @@ class AssetSelectionPageResult {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AssetSelectionPageResult copyWith({
 | 
					  AssetSelectionPageResult copyWith({
 | 
				
			||||||
    Set<AssetResponseDto>? selectedNewAsset,
 | 
					    Set<Asset>? selectedNewAsset,
 | 
				
			||||||
    Set<AssetResponseDto>? selectedAdditionalAsset,
 | 
					    Set<Asset>? selectedAdditionalAsset,
 | 
				
			||||||
    bool? isAlbumExist,
 | 
					    bool? isAlbumExist,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return AssetSelectionPageResult(
 | 
					    return AssetSelectionPageResult(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,11 @@
 | 
				
			|||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetSelectionState {
 | 
					class AssetSelectionState {
 | 
				
			||||||
  final Set<String> selectedMonths;
 | 
					  final Set<String> selectedMonths;
 | 
				
			||||||
  final Set<AssetResponseDto> selectedNewAssetsForAlbum;
 | 
					  final Set<Asset> selectedNewAssetsForAlbum;
 | 
				
			||||||
  final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
 | 
					  final Set<Asset> selectedAdditionalAssetsForAlbum;
 | 
				
			||||||
  final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
 | 
					  final Set<Asset> selectedAssetsInAlbumViewer;
 | 
				
			||||||
  final bool isMultiselectEnable;
 | 
					  final bool isMultiselectEnable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Indicate the asset selection page is navigated from existing album
 | 
					  /// Indicate the asset selection page is navigated from existing album
 | 
				
			||||||
@ -22,9 +21,9 @@ class AssetSelectionState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  AssetSelectionState copyWith({
 | 
					  AssetSelectionState copyWith({
 | 
				
			||||||
    Set<String>? selectedMonths,
 | 
					    Set<String>? selectedMonths,
 | 
				
			||||||
    Set<AssetResponseDto>? selectedNewAssetsForAlbum,
 | 
					    Set<Asset>? selectedNewAssetsForAlbum,
 | 
				
			||||||
    Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
 | 
					    Set<Asset>? selectedAdditionalAssetsForAlbum,
 | 
				
			||||||
    Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
 | 
					    Set<Asset>? selectedAssetsInAlbumViewer,
 | 
				
			||||||
    bool? isMultiselectEnable,
 | 
					    bool? isMultiselectEnable,
 | 
				
			||||||
    bool? isAlbumExist,
 | 
					    bool? isAlbumExist,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
					import 'package:immich_mobile/modules/album/services/album.service.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/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
					class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
				
			||||||
@ -13,7 +14,6 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAllAlbums() async {
 | 
					  getAllAlbums() async {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (await _albumCacheService.isValid() && state.isEmpty) {
 | 
					    if (await _albumCacheService.isValid() && state.isEmpty) {
 | 
				
			||||||
      state = await _albumCacheService.get();
 | 
					      state = await _albumCacheService.get();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -34,7 +34,7 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Future<AlbumResponseDto?> createAlbum(
 | 
					  Future<AlbumResponseDto?> createAlbum(
 | 
				
			||||||
    String albumTitle,
 | 
					    String albumTitle,
 | 
				
			||||||
    Set<AssetResponseDto> assets,
 | 
					    Set<Asset> assets,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    AlbumResponseDto? album =
 | 
					    AlbumResponseDto? album =
 | 
				
			||||||
        await _albumService.createAlbum(albumTitle, assets, []);
 | 
					        await _albumService.createAlbum(albumTitle, assets, []);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
 | 
					import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
					class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			||||||
  AssetSelectionNotifier()
 | 
					  AssetSelectionNotifier()
 | 
				
			||||||
@ -22,15 +21,15 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void removeAssetsInMonth(
 | 
					  void removeAssetsInMonth(
 | 
				
			||||||
    String removedMonth,
 | 
					    String removedMonth,
 | 
				
			||||||
    List<AssetResponseDto> assetsInMonth,
 | 
					    List<Asset> assetsInMonth,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
 | 
					    Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
 | 
				
			||||||
    Set<String> currentMonthList = state.selectedMonths;
 | 
					    Set<String> currentMonthList = state.selectedMonths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    currentMonthList
 | 
					    currentMonthList
 | 
				
			||||||
        .removeWhere((selectedMonth) => selectedMonth == removedMonth);
 | 
					        .removeWhere((selectedMonth) => selectedMonth == removedMonth);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (AssetResponseDto asset in assetsInMonth) {
 | 
					    for (Asset asset in assetsInMonth) {
 | 
				
			||||||
      currentAssetList.removeWhere((e) => e.id == asset.id);
 | 
					      currentAssetList.removeWhere((e) => e.id == asset.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -40,7 +39,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addAdditionalAssets(List<AssetResponseDto> assets) {
 | 
					  void addAdditionalAssets(List<Asset> assets) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedAdditionalAssetsForAlbum: {
 | 
					      selectedAdditionalAssetsForAlbum: {
 | 
				
			||||||
        ...state.selectedAdditionalAssetsForAlbum,
 | 
					        ...state.selectedAdditionalAssetsForAlbum,
 | 
				
			||||||
@ -49,7 +48,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
 | 
					  void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedMonths: {...state.selectedMonths, month},
 | 
					      selectedMonths: {...state.selectedMonths, month},
 | 
				
			||||||
      selectedNewAssetsForAlbum: {
 | 
					      selectedNewAssetsForAlbum: {
 | 
				
			||||||
@ -59,7 +58,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addNewAssets(List<AssetResponseDto> assets) {
 | 
					  void addNewAssets(List<Asset> assets) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedNewAssetsForAlbum: {
 | 
					      selectedNewAssetsForAlbum: {
 | 
				
			||||||
        ...state.selectedNewAssetsForAlbum,
 | 
					        ...state.selectedNewAssetsForAlbum,
 | 
				
			||||||
@ -68,20 +67,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void removeSelectedNewAssets(List<AssetResponseDto> assets) {
 | 
					  void removeSelectedNewAssets(List<Asset> assets) {
 | 
				
			||||||
    Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
 | 
					    Set<Asset> currentList = state.selectedNewAssetsForAlbum;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (AssetResponseDto asset in assets) {
 | 
					    for (Asset asset in assets) {
 | 
				
			||||||
      currentList.removeWhere((e) => e.id == asset.id);
 | 
					      currentList.removeWhere((e) => e.id == asset.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    state = state.copyWith(selectedNewAssetsForAlbum: currentList);
 | 
					    state = state.copyWith(selectedNewAssetsForAlbum: currentList);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
 | 
					  void removeSelectedAdditionalAssets(List<Asset> assets) {
 | 
				
			||||||
    Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
 | 
					    Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (AssetResponseDto asset in assets) {
 | 
					    for (Asset asset in assets) {
 | 
				
			||||||
      currentList.removeWhere((e) => e.id == asset.id);
 | 
					      currentList.removeWhere((e) => e.id == asset.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,7 +108,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
 | 
					  void addAssetsInAlbumViewer(List<Asset> assets) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedAssetsInAlbumViewer: {
 | 
					      selectedAssetsInAlbumViewer: {
 | 
				
			||||||
        ...state.selectedAssetsInAlbumViewer,
 | 
					        ...state.selectedAssetsInAlbumViewer,
 | 
				
			||||||
@ -118,10 +117,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
 | 
					  void removeAssetsInAlbumViewer(List<Asset> assets) {
 | 
				
			||||||
    Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
 | 
					    Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (AssetResponseDto asset in assets) {
 | 
					    for (Asset asset in assets) {
 | 
				
			||||||
      currentList.removeWhere((e) => e.id == asset.id);
 | 
					      currentList.removeWhere((e) => e.id == asset.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
					import 'package:immich_mobile/modules/album/services/album.service.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/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
					class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
				
			||||||
  SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
 | 
					  SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService)
 | 
				
			||||||
 | 
					      : super([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final AlbumService _sharedAlbumService;
 | 
					  final AlbumService _sharedAlbumService;
 | 
				
			||||||
  final SharedAlbumCacheService _sharedAlbumCacheService;
 | 
					  final SharedAlbumCacheService _sharedAlbumCacheService;
 | 
				
			||||||
@ -16,7 +18,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Future<AlbumResponseDto?> createSharedAlbum(
 | 
					  Future<AlbumResponseDto?> createSharedAlbum(
 | 
				
			||||||
    String albumName,
 | 
					    String albumName,
 | 
				
			||||||
    Set<AssetResponseDto> assets,
 | 
					    Set<Asset> assets,
 | 
				
			||||||
    List<String> sharedUserIds,
 | 
					    List<String> sharedUserIds,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import 'dart:async';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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:openapi/api.dart';
 | 
				
			||||||
@ -29,7 +30,7 @@ class AlbumService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Future<AlbumResponseDto?> createAlbum(
 | 
					  Future<AlbumResponseDto?> createAlbum(
 | 
				
			||||||
    String albumName,
 | 
					    String albumName,
 | 
				
			||||||
    Set<AssetResponseDto> assets,
 | 
					    Iterable<Asset> assets,
 | 
				
			||||||
    List<String> sharedUserIds,
 | 
					    List<String> sharedUserIds,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@ -65,7 +66,7 @@ class AlbumService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<AlbumResponseDto?> createAlbumWithGeneratedName(
 | 
					  Future<AlbumResponseDto?> createAlbumWithGeneratedName(
 | 
				
			||||||
    Set<AssetResponseDto> assets,
 | 
					    Iterable<Asset> assets,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    return createAlbum(
 | 
					    return createAlbum(
 | 
				
			||||||
        _getNextAlbumName(await getAlbums(isShared: false)), assets, []);
 | 
					        _getNextAlbumName(await getAlbums(isShared: false)), assets, []);
 | 
				
			||||||
@ -81,7 +82,7 @@ class AlbumService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
 | 
					  Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
 | 
				
			||||||
    Set<AssetResponseDto> assets,
 | 
					    Iterable<Asset> assets,
 | 
				
			||||||
    String albumId,
 | 
					    String albumId,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,15 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hive_flutter/hive_flutter.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/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
					class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
  final List<AssetResponseDto> assetList;
 | 
					  final List<Asset> assetList;
 | 
				
			||||||
  final bool showStorageIndicator;
 | 
					  final bool showStorageIndicator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AlbumViewerThumbnail({
 | 
					  const AlbumViewerThumbnail({
 | 
				
			||||||
@ -24,8 +21,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					 | 
				
			||||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
					 | 
				
			||||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
					    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
				
			||||||
    final selectedAssetsInAlbumViewer =
 | 
					    final selectedAssetsInAlbumViewer =
 | 
				
			||||||
        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
					        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
				
			||||||
@ -120,27 +115,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
    _buildThumbnailImage() {
 | 
					    _buildThumbnailImage() {
 | 
				
			||||||
      return Container(
 | 
					      return Container(
 | 
				
			||||||
        decoration: BoxDecoration(border: drawBorderColor()),
 | 
					        decoration: BoxDecoration(border: drawBorderColor()),
 | 
				
			||||||
        child: CachedNetworkImage(
 | 
					        child: ImmichImage(asset, width: 300, height: 300),
 | 
				
			||||||
          cacheKey: asset.id,
 | 
					 | 
				
			||||||
          width: 300,
 | 
					 | 
				
			||||||
          height: 300,
 | 
					 | 
				
			||||||
          memCacheHeight: 200,
 | 
					 | 
				
			||||||
          fit: BoxFit.cover,
 | 
					 | 
				
			||||||
          imageUrl: thumbnailRequestUrl,
 | 
					 | 
				
			||||||
          httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					 | 
				
			||||||
          fadeInDuration: const Duration(milliseconds: 250),
 | 
					 | 
				
			||||||
          progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
					 | 
				
			||||||
              Transform.scale(
 | 
					 | 
				
			||||||
            scale: 0.2,
 | 
					 | 
				
			||||||
            child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          errorWidget: (context, url, error) {
 | 
					 | 
				
			||||||
            return Icon(
 | 
					 | 
				
			||||||
              Icons.image_not_supported_outlined,
 | 
					 | 
				
			||||||
              color: Theme.of(context).primaryColor,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -167,7 +142,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          _buildThumbnailImage(),
 | 
					          _buildThumbnailImage(),
 | 
				
			||||||
          if (showStorageIndicator) _buildAssetStoreLocationIcon(),
 | 
					          if (showStorageIndicator) _buildAssetStoreLocationIcon(),
 | 
				
			||||||
          if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
 | 
					          if (!asset.isImage) _buildVideoLabel(),
 | 
				
			||||||
          if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
 | 
					          if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
 | 
					import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetGridByMonth extends HookConsumerWidget {
 | 
					class AssetGridByMonth extends HookConsumerWidget {
 | 
				
			||||||
  final List<AssetResponseDto> assetGroup;
 | 
					  final List<Asset> assetGroup;
 | 
				
			||||||
  const AssetGridByMonth({Key? key, required this.assetGroup})
 | 
					  const AssetGridByMonth({Key? key, required this.assetGroup})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MonthGroupTitle extends HookConsumerWidget {
 | 
					class MonthGroupTitle extends HookConsumerWidget {
 | 
				
			||||||
  final String month;
 | 
					  final String month;
 | 
				
			||||||
  final List<AssetResponseDto> assetGroup;
 | 
					  final List<Asset> assetGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const MonthGroupTitle({
 | 
					  const MonthGroupTitle({
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,29 +1,24 @@
 | 
				
			|||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hive_flutter/hive_flutter.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/modules/album/providers/asset_selection.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SelectionThumbnailImage extends HookConsumerWidget {
 | 
					class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const SelectionThumbnailImage({Key? key, required this.asset})
 | 
					  const SelectionThumbnailImage({Key? key, required this.asset})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					 | 
				
			||||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
					 | 
				
			||||||
    var selectedAsset =
 | 
					    var selectedAsset =
 | 
				
			||||||
        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
					        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
				
			||||||
    var newAssetsForAlbum =
 | 
					    var newAssetsForAlbum =
 | 
				
			||||||
        ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
 | 
					        ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
 | 
				
			||||||
    var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
					    var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget _buildSelectionIcon(AssetResponseDto asset) {
 | 
					    Widget _buildSelectionIcon(Asset asset) {
 | 
				
			||||||
      var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
 | 
					      var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
 | 
				
			||||||
      var isNewlySelected =
 | 
					      var isNewlySelected =
 | 
				
			||||||
          newAssetsForAlbum.map((item) => item.id).contains(asset.id);
 | 
					          newAssetsForAlbum.map((item) => item.id).contains(asset.id);
 | 
				
			||||||
@ -110,30 +105,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Container(
 | 
					          Container(
 | 
				
			||||||
            decoration: BoxDecoration(border: drawBorderColor()),
 | 
					            decoration: BoxDecoration(border: drawBorderColor()),
 | 
				
			||||||
            child: CachedNetworkImage(
 | 
					            child: ImmichImage(asset, width: 150, height: 150),
 | 
				
			||||||
              cacheKey: asset.id,
 | 
					 | 
				
			||||||
              width: 150,
 | 
					 | 
				
			||||||
              height: 150,
 | 
					 | 
				
			||||||
              memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
 | 
					 | 
				
			||||||
              fit: BoxFit.cover,
 | 
					 | 
				
			||||||
              imageUrl: thumbnailRequestUrl,
 | 
					 | 
				
			||||||
              httpHeaders: {
 | 
					 | 
				
			||||||
                "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              fadeInDuration: const Duration(milliseconds: 250),
 | 
					 | 
				
			||||||
              progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
					 | 
				
			||||||
                  Transform.scale(
 | 
					 | 
				
			||||||
                scale: 0.2,
 | 
					 | 
				
			||||||
                child:
 | 
					 | 
				
			||||||
                    CircularProgressIndicator(value: downloadProgress.progress),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              errorWidget: (context, url, error) {
 | 
					 | 
				
			||||||
                return Icon(
 | 
					 | 
				
			||||||
                  Icons.image_not_supported_outlined,
 | 
					 | 
				
			||||||
                  color: Theme.of(context).primaryColor,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Padding(
 | 
					          Padding(
 | 
				
			||||||
            padding: const EdgeInsets.all(3.0),
 | 
					            padding: const EdgeInsets.all(3.0),
 | 
				
			||||||
@ -142,7 +114,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
              child: _buildSelectionIcon(asset),
 | 
					              child: _buildSelectionIcon(asset),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          if (asset.type != AssetTypeEnum.IMAGE)
 | 
					          if (!asset.isImage)
 | 
				
			||||||
            Positioned(
 | 
					            Positioned(
 | 
				
			||||||
              bottom: 5,
 | 
					              bottom: 5,
 | 
				
			||||||
              right: 5,
 | 
					              right: 5,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,49 +1,23 @@
 | 
				
			|||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hive_flutter/hive_flutter.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/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
 | 
					class SharedAlbumThumbnailImage extends HookConsumerWidget {
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const SharedAlbumThumbnailImage({Key? key, required this.asset})
 | 
					  const SharedAlbumThumbnailImage({Key? key, required this.asset})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return GestureDetector(
 | 
					    return GestureDetector(
 | 
				
			||||||
      onTap: () {
 | 
					      onTap: () {
 | 
				
			||||||
        // debugPrint("View ${asset.id}");
 | 
					        // debugPrint("View ${asset.id}");
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      child: Stack(
 | 
					      child: Stack(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          CachedNetworkImage(
 | 
					          ImmichImage(asset, width: 500, height: 500),
 | 
				
			||||||
            cacheKey: asset.id,
 | 
					 | 
				
			||||||
            width: 500,
 | 
					 | 
				
			||||||
            height: 500,
 | 
					 | 
				
			||||||
            memCacheHeight: 500,
 | 
					 | 
				
			||||||
            fit: BoxFit.cover,
 | 
					 | 
				
			||||||
            imageUrl: getThumbnailUrl(asset),
 | 
					 | 
				
			||||||
            httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					 | 
				
			||||||
            fadeInDuration: const Duration(milliseconds: 250),
 | 
					 | 
				
			||||||
            progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
					 | 
				
			||||||
                Transform.scale(
 | 
					 | 
				
			||||||
              scale: 0.2,
 | 
					 | 
				
			||||||
              child:
 | 
					 | 
				
			||||||
                  CircularProgressIndicator(value: downloadProgress.progress),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            errorWidget: (context, url, error) {
 | 
					 | 
				
			||||||
              return Icon(
 | 
					 | 
				
			||||||
                Icons.image_not_supported_outlined,
 | 
					 | 
				
			||||||
                color: Theme.of(context).primaryColor,
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.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';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 | 
					import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 | 
				
			||||||
@ -38,9 +39,9 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    /// If they exist, add to selected asset state to show they are already selected.
 | 
					    /// If they exist, add to selected asset state to show they are already selected.
 | 
				
			||||||
    void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
 | 
					    void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
 | 
				
			||||||
      if (albumInfo.assets.isNotEmpty == true) {
 | 
					      if (albumInfo.assets.isNotEmpty == true) {
 | 
				
			||||||
        ref
 | 
					        ref.watch(assetSelectionProvider.notifier).addNewAssets(
 | 
				
			||||||
            .watch(assetSelectionProvider.notifier)
 | 
					              albumInfo.assets.map((e) => Asset.remote(e)).toList(),
 | 
				
			||||||
            .addNewAssets(albumInfo.assets.toList());
 | 
					            );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
 | 
					      ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
 | 
				
			||||||
@ -205,8 +206,9 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
            delegate: SliverChildBuilderDelegate(
 | 
					            delegate: SliverChildBuilderDelegate(
 | 
				
			||||||
              (BuildContext context, int index) {
 | 
					              (BuildContext context, int index) {
 | 
				
			||||||
                return AlbumViewerThumbnail(
 | 
					                return AlbumViewerThumbnail(
 | 
				
			||||||
                  asset: albumInfo.assets[index],
 | 
					                  asset: Asset.remote(albumInfo.assets[index]),
 | 
				
			||||||
                  assetList: albumInfo.assets,
 | 
					                  assetList:
 | 
				
			||||||
 | 
					                      albumInfo.assets.map((e) => Asset.remote(e)).toList(),
 | 
				
			||||||
                  showStorageIndicator: showStorageIndicator,
 | 
					                  showStorageIndicator: showStorageIndicator,
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
 | 
				
			|||||||
@ -166,7 +166,7 @@ class CreateAlbumPage extends HookConsumerWidget {
 | 
				
			|||||||
                return GestureDetector(
 | 
					                return GestureDetector(
 | 
				
			||||||
                  onTap: _onBackgroundTapped,
 | 
					                  onTap: _onBackgroundTapped,
 | 
				
			||||||
                  child: SharedAlbumThumbnailImage(
 | 
					                  child: SharedAlbumThumbnailImage(
 | 
				
			||||||
                    asset: selectedAssets.toList()[index],
 | 
					                    asset: selectedAssets.elementAt(index),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,6 @@ class SharingPage extends HookConsumerWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					    var box = Hive.box(userInfoBox);
 | 
				
			||||||
    var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
 | 
					 | 
				
			||||||
    final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
 | 
					    final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(
 | 
					    useEffect(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
 | 
				
			||||||
 | 
					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';
 | 
				
			||||||
@ -47,7 +47,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
 | 
				
			|||||||
    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
 | 
					    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void shareAsset(AssetResponseDto asset, BuildContext context) async {
 | 
					  void shareAsset(Asset asset, BuildContext context) async {
 | 
				
			||||||
    showDialog(
 | 
					    showDialog(
 | 
				
			||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (BuildContext buildContext) {
 | 
					      builder: (BuildContext buildContext) {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					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:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
import 'package:path/path.dart' as p;
 | 
					import 'package:path/path.dart' as p;
 | 
				
			||||||
import 'package:latlong2/latlong.dart';
 | 
					import 'package:latlong2/latlong.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExifBottomSheet extends ConsumerWidget {
 | 
					class ExifBottomSheet extends ConsumerWidget {
 | 
				
			||||||
  final AssetResponseDto assetDetail;
 | 
					  final Asset assetDetail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ExifBottomSheet({Key? key, required this.assetDetail})
 | 
					  const ExifBottomSheet({Key? key, required this.assetDetail})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
@ -26,8 +27,8 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
          child: FlutterMap(
 | 
					          child: FlutterMap(
 | 
				
			||||||
            options: MapOptions(
 | 
					            options: MapOptions(
 | 
				
			||||||
              center: LatLng(
 | 
					              center: LatLng(
 | 
				
			||||||
                assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
 | 
					                assetDetail.latitude ?? 0,
 | 
				
			||||||
                assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
 | 
					                assetDetail.longitude ?? 0,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              zoom: 16.0,
 | 
					              zoom: 16.0,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@ -48,8 +49,8 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
                  Marker(
 | 
					                  Marker(
 | 
				
			||||||
                    anchorPos: AnchorPos.align(AnchorAlign.top),
 | 
					                    anchorPos: AnchorPos.align(AnchorAlign.top),
 | 
				
			||||||
                    point: LatLng(
 | 
					                    point: LatLng(
 | 
				
			||||||
                      assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
 | 
					                      assetDetail.latitude ?? 0,
 | 
				
			||||||
                      assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
 | 
					                      assetDetail.longitude ?? 0,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    builder: (ctx) => const Image(
 | 
					                    builder: (ctx) => const Image(
 | 
				
			||||||
                      image: AssetImage('assets/location-pin.png'),
 | 
					                      image: AssetImage('assets/location-pin.png'),
 | 
				
			||||||
@ -63,9 +64,11 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _buildLocationText() {
 | 
					    _buildLocationText() {
 | 
				
			||||||
      return Text(
 | 
					      return Text(
 | 
				
			||||||
        "${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
 | 
					        "${exifInfo?.city}, ${exifInfo?.state}",
 | 
				
			||||||
        style: TextStyle(
 | 
					        style: TextStyle(
 | 
				
			||||||
          fontSize: 12,
 | 
					          fontSize: 12,
 | 
				
			||||||
          color: Colors.grey[200],
 | 
					          color: Colors.grey[200],
 | 
				
			||||||
@ -78,10 +81,10 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
      padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
 | 
					      padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
 | 
				
			||||||
      child: ListView(
 | 
					      child: ListView(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          if (assetDetail.exifInfo?.dateTimeOriginal != null)
 | 
					          if (exifInfo?.dateTimeOriginal != null)
 | 
				
			||||||
            Text(
 | 
					            Text(
 | 
				
			||||||
              DateFormat('date_format'.tr()).format(
 | 
					              DateFormat('date_format'.tr()).format(
 | 
				
			||||||
                assetDetail.exifInfo!.dateTimeOriginal!.toLocal(),
 | 
					                exifInfo!.dateTimeOriginal!.toLocal(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              style: TextStyle(
 | 
					              style: TextStyle(
 | 
				
			||||||
                color: Colors.grey[400],
 | 
					                color: Colors.grey[400],
 | 
				
			||||||
@ -101,7 +104,7 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Location
 | 
					          // Location
 | 
				
			||||||
          if (assetDetail.exifInfo?.latitude != null)
 | 
					          if (assetDetail.latitude != null)
 | 
				
			||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.only(top: 32.0),
 | 
					              padding: const EdgeInsets.only(top: 32.0),
 | 
				
			||||||
              child: Column(
 | 
					              child: Column(
 | 
				
			||||||
@ -115,21 +118,22 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
                    "exif_bottom_sheet_location",
 | 
					                    "exif_bottom_sheet_location",
 | 
				
			||||||
                    style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
					                    style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
				
			||||||
                  ).tr(),
 | 
					                  ).tr(),
 | 
				
			||||||
                  if (assetDetail.exifInfo?.latitude != null &&
 | 
					                  if (assetDetail.latitude != null &&
 | 
				
			||||||
                      assetDetail.exifInfo?.longitude != null)
 | 
					                      assetDetail.longitude != null)
 | 
				
			||||||
                    _buildMap(),
 | 
					                    _buildMap(),
 | 
				
			||||||
                  if (assetDetail.exifInfo?.city != null &&
 | 
					                  if (exifInfo != null &&
 | 
				
			||||||
                      assetDetail.exifInfo?.state != null)
 | 
					                      exifInfo.city != null &&
 | 
				
			||||||
 | 
					                      exifInfo.state != null)
 | 
				
			||||||
                    _buildLocationText(),
 | 
					                    _buildLocationText(),
 | 
				
			||||||
                  Text(
 | 
					                  Text(
 | 
				
			||||||
                    "${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
 | 
					                    "${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
 | 
				
			||||||
                    style: TextStyle(fontSize: 12, color: Colors.grey[400]),
 | 
					                    style: TextStyle(fontSize: 12, color: Colors.grey[400]),
 | 
				
			||||||
                  )
 | 
					                  )
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          // Detail
 | 
					          // Detail
 | 
				
			||||||
          if (assetDetail.exifInfo != null)
 | 
					          if (exifInfo != null)
 | 
				
			||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.only(top: 32.0),
 | 
					              padding: const EdgeInsets.only(top: 32.0),
 | 
				
			||||||
              child: Column(
 | 
					              child: Column(
 | 
				
			||||||
@ -153,16 +157,16 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
                    iconColor: Colors.grey[300],
 | 
					                    iconColor: Colors.grey[300],
 | 
				
			||||||
                    leading: const Icon(Icons.image),
 | 
					                    leading: const Icon(Icons.image),
 | 
				
			||||||
                    title: Text(
 | 
					                    title: Text(
 | 
				
			||||||
                      "${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
 | 
					                      "${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
 | 
				
			||||||
                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
					                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    subtitle: assetDetail.exifInfo?.exifImageHeight != null
 | 
					                    subtitle: exifInfo.exifImageHeight != null
 | 
				
			||||||
                        ? Text(
 | 
					                        ? Text(
 | 
				
			||||||
                            "${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth}  ${assetDetail.exifInfo?.fileSizeInByte!}B ",
 | 
					                            "${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth}  ${exifInfo.fileSizeInByte!}B ",
 | 
				
			||||||
                          )
 | 
					                          )
 | 
				
			||||||
                        : null,
 | 
					                        : null,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  if (assetDetail.exifInfo?.make != null)
 | 
					                  if (exifInfo.make != null)
 | 
				
			||||||
                    ListTile(
 | 
					                    ListTile(
 | 
				
			||||||
                      contentPadding: const EdgeInsets.all(0),
 | 
					                      contentPadding: const EdgeInsets.all(0),
 | 
				
			||||||
                      dense: true,
 | 
					                      dense: true,
 | 
				
			||||||
@ -170,11 +174,11 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
				
			|||||||
                      iconColor: Colors.grey[300],
 | 
					                      iconColor: Colors.grey[300],
 | 
				
			||||||
                      leading: const Icon(Icons.camera),
 | 
					                      leading: const Icon(Icons.camera),
 | 
				
			||||||
                      title: Text(
 | 
					                      title: Text(
 | 
				
			||||||
                        "${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
 | 
					                        "${exifInfo.make} ${exifInfo.model}",
 | 
				
			||||||
                        style: const TextStyle(fontWeight: FontWeight.bold),
 | 
					                        style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      subtitle: Text(
 | 
					                      subtitle: Text(
 | 
				
			||||||
                        "ƒ/${assetDetail.exifInfo?.fNumber}   1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)}   ${assetDetail.exifInfo?.focalLength}mm   ISO${assetDetail.exifInfo?.iso} ",
 | 
					                        "ƒ/${exifInfo.fNumber}   1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)}   ${exifInfo.focalLength}mm   ISO${exifInfo.iso} ",
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,22 @@
 | 
				
			|||||||
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:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart'
 | 
				
			||||||
 | 
					    show AssetEntityImageProvider, ThumbnailSize;
 | 
				
			||||||
import 'package:photo_view/photo_view.dart';
 | 
					import 'package:photo_view/photo_view.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum _RemoteImageStatus { empty, thumbnail, preview, full }
 | 
					enum _RemoteImageStatus { empty, thumbnail, preview, full }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
					class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
				
			||||||
  late CachedNetworkImageProvider _imageProvider;
 | 
					  late ImageProvider _imageProvider;
 | 
				
			||||||
  _RemoteImageStatus _status = _RemoteImageStatus.empty;
 | 
					  _RemoteImageStatus _status = _RemoteImageStatus.empty;
 | 
				
			||||||
  bool _zoomedIn = false;
 | 
					  bool _zoomedIn = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  late CachedNetworkImageProvider fullProvider;
 | 
					  late ImageProvider _fullProvider;
 | 
				
			||||||
  late CachedNetworkImageProvider previewProvider;
 | 
					  late ImageProvider _previewProvider;
 | 
				
			||||||
  late CachedNetworkImageProvider thumbnailProvider;
 | 
					  late ImageProvider _thumbnailProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
@ -68,7 +73,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void _performStateTransition(
 | 
					  void _performStateTransition(
 | 
				
			||||||
    _RemoteImageStatus newStatus,
 | 
					    _RemoteImageStatus newStatus,
 | 
				
			||||||
    CachedNetworkImageProvider provider,
 | 
					    ImageProvider provider,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    if (_status == newStatus) return;
 | 
					    if (_status == newStatus) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -90,40 +95,58 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _loadImages() {
 | 
					  void _loadImages() {
 | 
				
			||||||
    thumbnailProvider = _authorizedImageProvider(
 | 
					    if (widget.asset.isLocal) {
 | 
				
			||||||
      widget.thumbnailUrl,
 | 
					      _imageProvider = AssetEntityImageProvider(
 | 
				
			||||||
      widget.cacheKey,
 | 
					        widget.asset.local!,
 | 
				
			||||||
    );
 | 
					        isOriginal: false,
 | 
				
			||||||
    _imageProvider = thumbnailProvider;
 | 
					        thumbnailSize: const ThumbnailSize.square(250),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      _fullProvider = AssetEntityImageProvider(widget.asset.local!);
 | 
				
			||||||
 | 
					      _fullProvider.resolve(const ImageConfiguration()).addListener(
 | 
				
			||||||
 | 
					        ImageStreamListener((ImageInfo image, _) {
 | 
				
			||||||
 | 
					          _performStateTransition(
 | 
				
			||||||
 | 
					            _RemoteImageStatus.full,
 | 
				
			||||||
 | 
					            _fullProvider,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    thumbnailProvider.resolve(const ImageConfiguration()).addListener(
 | 
					    _thumbnailProvider = _authorizedImageProvider(
 | 
				
			||||||
 | 
					      getThumbnailUrl(widget.asset.remote!),
 | 
				
			||||||
 | 
					      widget.asset.id,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    _imageProvider = _thumbnailProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _thumbnailProvider.resolve(const ImageConfiguration()).addListener(
 | 
				
			||||||
      ImageStreamListener((ImageInfo imageInfo, _) {
 | 
					      ImageStreamListener((ImageInfo imageInfo, _) {
 | 
				
			||||||
        _performStateTransition(
 | 
					        _performStateTransition(
 | 
				
			||||||
          _RemoteImageStatus.thumbnail,
 | 
					          _RemoteImageStatus.thumbnail,
 | 
				
			||||||
          thumbnailProvider,
 | 
					          _thumbnailProvider,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (widget.previewUrl != null) {
 | 
					    if (widget.threeStageLoading) {
 | 
				
			||||||
      previewProvider = _authorizedImageProvider(
 | 
					      _previewProvider = _authorizedImageProvider(
 | 
				
			||||||
        widget.previewUrl!,
 | 
					        getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
 | 
				
			||||||
        "${widget.cacheKey}_previewStage",
 | 
					        "${widget.asset.id}_previewStage",
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      previewProvider.resolve(const ImageConfiguration()).addListener(
 | 
					      _previewProvider.resolve(const ImageConfiguration()).addListener(
 | 
				
			||||||
        ImageStreamListener((ImageInfo imageInfo, _) {
 | 
					        ImageStreamListener((ImageInfo imageInfo, _) {
 | 
				
			||||||
          _performStateTransition(_RemoteImageStatus.preview, previewProvider);
 | 
					          _performStateTransition(_RemoteImageStatus.preview, _previewProvider);
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fullProvider = _authorizedImageProvider(
 | 
					    _fullProvider = _authorizedImageProvider(
 | 
				
			||||||
      widget.imageUrl,
 | 
					      getImageUrl(widget.asset.remote!),
 | 
				
			||||||
      "${widget.cacheKey}_fullStage",
 | 
					      "${widget.asset.id}_fullStage",
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    fullProvider.resolve(const ImageConfiguration()).addListener(
 | 
					    _fullProvider.resolve(const ImageConfiguration()).addListener(
 | 
				
			||||||
      ImageStreamListener((ImageInfo imageInfo, _) {
 | 
					      ImageStreamListener((ImageInfo imageInfo, _) {
 | 
				
			||||||
        _performStateTransition(_RemoteImageStatus.full, fullProvider);
 | 
					        _performStateTransition(_RemoteImageStatus.full, _fullProvider);
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -139,11 +162,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
				
			|||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (_status == _RemoteImageStatus.full) {
 | 
					    if (_status == _RemoteImageStatus.full) {
 | 
				
			||||||
      await fullProvider.evict();
 | 
					      await _fullProvider.evict();
 | 
				
			||||||
    } else if (_status == _RemoteImageStatus.preview) {
 | 
					    } else if (_status == _RemoteImageStatus.preview) {
 | 
				
			||||||
      await previewProvider.evict();
 | 
					      await _previewProvider.evict();
 | 
				
			||||||
    } else if (_status == _RemoteImageStatus.thumbnail) {
 | 
					    } else if (_status == _RemoteImageStatus.thumbnail) {
 | 
				
			||||||
      await thumbnailProvider.evict();
 | 
					      await _thumbnailProvider.evict();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await _imageProvider.evict();
 | 
					    await _imageProvider.evict();
 | 
				
			||||||
@ -153,23 +176,18 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
				
			|||||||
class RemotePhotoView extends StatefulWidget {
 | 
					class RemotePhotoView extends StatefulWidget {
 | 
				
			||||||
  const RemotePhotoView({
 | 
					  const RemotePhotoView({
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
    required this.thumbnailUrl,
 | 
					    required this.asset,
 | 
				
			||||||
    required this.imageUrl,
 | 
					 | 
				
			||||||
    required this.authToken,
 | 
					    required this.authToken,
 | 
				
			||||||
 | 
					    required this.threeStageLoading,
 | 
				
			||||||
    required this.isZoomedFunction,
 | 
					    required this.isZoomedFunction,
 | 
				
			||||||
    required this.isZoomedListener,
 | 
					    required this.isZoomedListener,
 | 
				
			||||||
    required this.onSwipeDown,
 | 
					    required this.onSwipeDown,
 | 
				
			||||||
    required this.onSwipeUp,
 | 
					    required this.onSwipeUp,
 | 
				
			||||||
    this.previewUrl,
 | 
					 | 
				
			||||||
    required this.cacheKey,
 | 
					 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final String thumbnailUrl;
 | 
					  final Asset asset;
 | 
				
			||||||
  final String imageUrl;
 | 
					 | 
				
			||||||
  final String authToken;
 | 
					  final String authToken;
 | 
				
			||||||
  final String? previewUrl;
 | 
					  final bool threeStageLoading;
 | 
				
			||||||
  final String cacheKey;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final void Function() onSwipeDown;
 | 
					  final void Function() onSwipeDown;
 | 
				
			||||||
  final void Function() onSwipeUp;
 | 
					  final void Function() onSwipeUp;
 | 
				
			||||||
  final void Function() isZoomedFunction;
 | 
					  final void Function() isZoomedFunction;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
					class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
				
			||||||
  const TopControlAppBar({
 | 
					  const TopControlAppBar({
 | 
				
			||||||
@ -13,9 +13,9 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
				
			|||||||
    this.loading = false,
 | 
					    this.loading = false,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
  final Function onMoreInfoPressed;
 | 
					  final Function onMoreInfoPressed;
 | 
				
			||||||
  final Function onDownloadPressed;
 | 
					  final VoidCallback? onDownloadPressed;
 | 
				
			||||||
  final Function onSharePressed;
 | 
					  final Function onSharePressed;
 | 
				
			||||||
  final bool loading;
 | 
					  final bool loading;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -47,17 +47,16 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
				
			|||||||
              child: const CircularProgressIndicator(strokeWidth: 2.0),
 | 
					              child: const CircularProgressIndicator(strokeWidth: 2.0),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        IconButton(
 | 
					        if (!asset.isLocal)
 | 
				
			||||||
          iconSize: iconSize,
 | 
					          IconButton(
 | 
				
			||||||
          splashRadius: iconSize,
 | 
					            iconSize: iconSize,
 | 
				
			||||||
          onPressed: () {
 | 
					            splashRadius: iconSize,
 | 
				
			||||||
            onDownloadPressed();
 | 
					            onPressed: onDownloadPressed,
 | 
				
			||||||
          },
 | 
					            icon: Icon(
 | 
				
			||||||
          icon: Icon(
 | 
					              Icons.cloud_download_rounded,
 | 
				
			||||||
            Icons.cloud_download_rounded,
 | 
					              color: Colors.grey[200],
 | 
				
			||||||
            color: Colors.grey[200],
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        IconButton(
 | 
					        IconButton(
 | 
				
			||||||
          iconSize: iconSize,
 | 
					          iconSize: iconSize,
 | 
				
			||||||
          splashRadius: iconSize,
 | 
					          splashRadius: iconSize,
 | 
				
			||||||
@ -69,17 +68,18 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
				
			|||||||
            color: Colors.grey[200],
 | 
					            color: Colors.grey[200],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        IconButton(
 | 
					        if (asset.isRemote)
 | 
				
			||||||
          iconSize: iconSize,
 | 
					          IconButton(
 | 
				
			||||||
          splashRadius: iconSize,
 | 
					            iconSize: iconSize,
 | 
				
			||||||
          onPressed: () {
 | 
					            splashRadius: iconSize,
 | 
				
			||||||
            onMoreInfoPressed();
 | 
					            onPressed: () {
 | 
				
			||||||
          },
 | 
					              onMoreInfoPressed();
 | 
				
			||||||
          icon: Icon(
 | 
					            },
 | 
				
			||||||
            Icons.more_horiz_rounded,
 | 
					            icon: Icon(
 | 
				
			||||||
            color: Colors.grey[200],
 | 
					              Icons.more_horiz_rounded,
 | 
				
			||||||
          ),
 | 
					              color: Colors.grey[200],
 | 
				
			||||||
        )
 | 
					            ),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -14,12 +14,12 @@ 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/modules/home/services/asset.service.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';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ignore: must_be_immutable
 | 
					// ignore: must_be_immutable
 | 
				
			||||||
class GalleryViewerPage extends HookConsumerWidget {
 | 
					class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			||||||
  late List<AssetResponseDto> assetList;
 | 
					  late List<Asset> assetList;
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  GalleryViewerPage({
 | 
					  GalleryViewerPage({
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
@ -27,7 +27,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    required this.asset,
 | 
					    required this.asset,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AssetResponseDto? assetDetail;
 | 
					  Asset? assetDetail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
@ -37,8 +37,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    final loading = useState(false);
 | 
					    final loading = useState(false);
 | 
				
			||||||
    final isZoomed = useState<bool>(false);
 | 
					    final isZoomed = useState<bool>(false);
 | 
				
			||||||
    ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
 | 
					    ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
 | 
				
			||||||
 | 
					    final indexOfAsset = useState(assetList.indexOf(asset));
 | 
				
			||||||
    int indexOfAsset = assetList.indexOf(asset);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PageController controller =
 | 
					    PageController controller =
 | 
				
			||||||
        PageController(initialPage: assetList.indexOf(asset));
 | 
					        PageController(initialPage: assetList.indexOf(asset));
 | 
				
			||||||
@ -52,15 +51,15 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      [],
 | 
					      [],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override
 | 
					 | 
				
			||||||
    initState(int index) {
 | 
					 | 
				
			||||||
      indexOfAsset = index;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    getAssetExif() async {
 | 
					    getAssetExif() async {
 | 
				
			||||||
      assetDetail = await ref
 | 
					      if (assetList[indexOfAsset.value].isRemote) {
 | 
				
			||||||
          .watch(assetServiceProvider)
 | 
					        assetDetail = await ref
 | 
				
			||||||
          .getAssetById(assetList[indexOfAsset].id);
 | 
					            .watch(assetServiceProvider)
 | 
				
			||||||
 | 
					            .getAssetById(assetList[indexOfAsset.value].id);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // TODO local exif parsing?
 | 
				
			||||||
 | 
					        assetDetail = assetList[indexOfAsset.value];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void showInfo() {
 | 
					    void showInfo() {
 | 
				
			||||||
@ -88,19 +87,20 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      backgroundColor: Colors.black,
 | 
					      backgroundColor: Colors.black,
 | 
				
			||||||
      appBar: TopControlAppBar(
 | 
					      appBar: TopControlAppBar(
 | 
				
			||||||
        loading: loading.value,
 | 
					        loading: loading.value,
 | 
				
			||||||
        asset: assetList[indexOfAsset],
 | 
					        asset: assetList[indexOfAsset.value],
 | 
				
			||||||
        onMoreInfoPressed: () {
 | 
					        onMoreInfoPressed: () {
 | 
				
			||||||
          showInfo();
 | 
					          showInfo();
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        onDownloadPressed: () {
 | 
					        onDownloadPressed: assetList[indexOfAsset.value].isLocal
 | 
				
			||||||
          ref
 | 
					            ? null
 | 
				
			||||||
              .watch(imageViewerStateProvider.notifier)
 | 
					            : () {
 | 
				
			||||||
              .downloadAsset(assetList[indexOfAsset], context);
 | 
					                ref.watch(imageViewerStateProvider.notifier).downloadAsset(
 | 
				
			||||||
        },
 | 
					                    assetList[indexOfAsset.value].remote!, context);
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
        onSharePressed: () {
 | 
					        onSharePressed: () {
 | 
				
			||||||
          ref
 | 
					          ref
 | 
				
			||||||
              .watch(imageViewerStateProvider.notifier)
 | 
					              .watch(imageViewerStateProvider.notifier)
 | 
				
			||||||
              .shareAsset(assetList[indexOfAsset], context);
 | 
					              .shareAsset(assetList[indexOfAsset.value], context);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SafeArea(
 | 
					      body: SafeArea(
 | 
				
			||||||
@ -113,14 +113,13 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
          itemCount: assetList.length,
 | 
					          itemCount: assetList.length,
 | 
				
			||||||
          scrollDirection: Axis.horizontal,
 | 
					          scrollDirection: Axis.horizontal,
 | 
				
			||||||
          onPageChanged: (value) {
 | 
					          onPageChanged: (value) {
 | 
				
			||||||
 | 
					            indexOfAsset.value = value;
 | 
				
			||||||
            HapticFeedback.selectionClick();
 | 
					            HapticFeedback.selectionClick();
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          itemBuilder: (context, index) {
 | 
					          itemBuilder: (context, index) {
 | 
				
			||||||
            initState(index);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            getAssetExif();
 | 
					            getAssetExif();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (assetList[index].type == AssetTypeEnum.IMAGE) {
 | 
					            if (assetList[index].isImage) {
 | 
				
			||||||
              return ImageViewerPage(
 | 
					              return ImageViewerPage(
 | 
				
			||||||
                authToken: 'Bearer ${box.get(accessTokenKey)}',
 | 
					                authToken: 'Bearer ${box.get(accessTokenKey)}',
 | 
				
			||||||
                isZoomedFunction: isZoomedMethod,
 | 
					                isZoomedFunction: isZoomedMethod,
 | 
				
			||||||
@ -139,11 +138,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                child: Hero(
 | 
					                child: Hero(
 | 
				
			||||||
                  tag: assetList[index].id,
 | 
					                  tag: assetList[index].id,
 | 
				
			||||||
                  child: VideoViewerPage(
 | 
					                  child: VideoViewerPage(asset: assetList[index]),
 | 
				
			||||||
                    asset: assetList[index],
 | 
					 | 
				
			||||||
                    videoUrl:
 | 
					 | 
				
			||||||
                        '${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}',
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -8,13 +8,12 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
 | 
				
			|||||||
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/remote_photo_view.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
 | 
					import 'package:immich_mobile/modules/home/services/asset.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ignore: must_be_immutable
 | 
					// ignore: must_be_immutable
 | 
				
			||||||
class ImageViewerPage extends HookConsumerWidget {
 | 
					class ImageViewerPage extends HookConsumerWidget {
 | 
				
			||||||
  final String heroTag;
 | 
					  final String heroTag;
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
  final String authToken;
 | 
					  final String authToken;
 | 
				
			||||||
  final ValueNotifier<bool> isZoomedListener;
 | 
					  final ValueNotifier<bool> isZoomedListener;
 | 
				
			||||||
  final void Function() isZoomedFunction;
 | 
					  final void Function() isZoomedFunction;
 | 
				
			||||||
@ -30,7 +29,7 @@ class ImageViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    required this.threeStageLoading,
 | 
					    required this.threeStageLoading,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AssetResponseDto? assetDetail;
 | 
					  Asset? assetDetail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
@ -38,8 +37,13 @@ class ImageViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
        ref.watch(imageViewerStateProvider).downloadAssetStatus;
 | 
					        ref.watch(imageViewerStateProvider).downloadAssetStatus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getAssetExif() async {
 | 
					    getAssetExif() async {
 | 
				
			||||||
      assetDetail =
 | 
					      if (asset.isRemote) {
 | 
				
			||||||
          await ref.watch(assetServiceProvider).getAssetById(asset.id);
 | 
					        assetDetail =
 | 
				
			||||||
 | 
					            await ref.watch(assetServiceProvider).getAssetById(asset.id);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // TODO local exif parsing?
 | 
				
			||||||
 | 
					        assetDetail = asset;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(
 | 
					    useEffect(
 | 
				
			||||||
@ -68,17 +72,13 @@ class ImageViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
          child: Hero(
 | 
					          child: Hero(
 | 
				
			||||||
            tag: heroTag,
 | 
					            tag: heroTag,
 | 
				
			||||||
            child: RemotePhotoView(
 | 
					            child: RemotePhotoView(
 | 
				
			||||||
              thumbnailUrl: getThumbnailUrl(asset),
 | 
					              asset: asset,
 | 
				
			||||||
              cacheKey: asset.id,
 | 
					 | 
				
			||||||
              imageUrl: getImageUrl(asset),
 | 
					 | 
				
			||||||
              previewUrl: threeStageLoading
 | 
					 | 
				
			||||||
                  ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
 | 
					 | 
				
			||||||
                  : null,
 | 
					 | 
				
			||||||
              authToken: authToken,
 | 
					              authToken: authToken,
 | 
				
			||||||
 | 
					              threeStageLoading: threeStageLoading,
 | 
				
			||||||
              isZoomedFunction: isZoomedFunction,
 | 
					              isZoomedFunction: isZoomedFunction,
 | 
				
			||||||
              isZoomedListener: isZoomedListener,
 | 
					              isZoomedListener: isZoomedListener,
 | 
				
			||||||
              onSwipeDown: () => AutoRouter.of(context).pop(),
 | 
					              onSwipeDown: () => AutoRouter.of(context).pop(),
 | 
				
			||||||
              onSwipeUp: () => showInfo(),
 | 
					              onSwipeUp: asset.isRemote ? showInfo : () {},
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.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';
 | 
				
			||||||
@ -6,24 +8,41 @@ import 'package:chewie/chewie.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
import 'package:video_player/video_player.dart';
 | 
					import 'package:video_player/video_player.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ignore: must_be_immutable
 | 
					// ignore: must_be_immutable
 | 
				
			||||||
class VideoViewerPage extends HookConsumerWidget {
 | 
					class VideoViewerPage extends HookConsumerWidget {
 | 
				
			||||||
  final String videoUrl;
 | 
					  final Asset asset;
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					 | 
				
			||||||
  AssetResponseDto? assetDetail;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
 | 
					  const VideoViewerPage({Key? key, required this.asset}) : super(key: key);
 | 
				
			||||||
      : super(key: key);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    if (asset.isLocal) {
 | 
				
			||||||
 | 
					      final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
 | 
				
			||||||
 | 
					      return videoFile.when(
 | 
				
			||||||
 | 
					        data: (data) => VideoThumbnailPlayer(file: data),
 | 
				
			||||||
 | 
					        error: (error, stackTrace) => Icon(
 | 
				
			||||||
 | 
					          Icons.image_not_supported_outlined,
 | 
				
			||||||
 | 
					          color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        loading: () => const Center(
 | 
				
			||||||
 | 
					          child: SizedBox(
 | 
				
			||||||
 | 
					            width: 75,
 | 
				
			||||||
 | 
					            height: 75,
 | 
				
			||||||
 | 
					            child: CircularProgressIndicator.adaptive(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    final downloadAssetStatus =
 | 
					    final downloadAssetStatus =
 | 
				
			||||||
        ref.watch(imageViewerStateProvider).downloadAssetStatus;
 | 
					        ref.watch(imageViewerStateProvider).downloadAssetStatus;
 | 
				
			||||||
 | 
					    final box = Hive.box(userInfoBox);
 | 
				
			||||||
    String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
 | 
					    final String jwtToken = box.get(accessTokenKey);
 | 
				
			||||||
 | 
					    final String videoUrl =
 | 
				
			||||||
 | 
					        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
@ -40,11 +59,21 @@ class VideoViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoThumbnailPlayer extends StatefulWidget {
 | 
					final _fileFamily =
 | 
				
			||||||
  final String url;
 | 
					    FutureProvider.family<File, AssetEntity>((ref, entity) async {
 | 
				
			||||||
  final String? jwtToken;
 | 
					  final file = await entity.file;
 | 
				
			||||||
 | 
					  if (file == null) {
 | 
				
			||||||
 | 
					    throw Exception();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return file;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
 | 
					class VideoThumbnailPlayer extends StatefulWidget {
 | 
				
			||||||
 | 
					  final String? url;
 | 
				
			||||||
 | 
					  final String? jwtToken;
 | 
				
			||||||
 | 
					  final File? file;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const VideoThumbnailPlayer({Key? key, this.url, this.jwtToken, this.file})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -63,10 +92,12 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Future<void> initializePlayer() async {
 | 
					  Future<void> initializePlayer() async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      videoPlayerController = VideoPlayerController.network(
 | 
					      videoPlayerController = widget.file == null
 | 
				
			||||||
        widget.url,
 | 
					          ? VideoPlayerController.network(
 | 
				
			||||||
        httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
 | 
					              widget.url!,
 | 
				
			||||||
      );
 | 
					              httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          : VideoPlayerController.file(widget.file!);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await videoPlayerController.initialize();
 | 
					      await videoPlayerController.initialize();
 | 
				
			||||||
      _createChewieController();
 | 
					      _createChewieController();
 | 
				
			||||||
 | 
				
			|||||||
@ -50,6 +50,11 @@ class BackgroundService {
 | 
				
			|||||||
      _Throttle(_updateProgress, notifyInterval);
 | 
					      _Throttle(_updateProgress, notifyInterval);
 | 
				
			||||||
  late final _Throttle _throttledDetailNotify =
 | 
					  late final _Throttle _throttledDetailNotify =
 | 
				
			||||||
      _Throttle(_updateDetailProgress, notifyInterval);
 | 
					      _Throttle(_updateDetailProgress, notifyInterval);
 | 
				
			||||||
 | 
					  Completer<bool> _hasAccessCompleter = Completer();
 | 
				
			||||||
 | 
					  late Future<bool> _hasAccess =
 | 
				
			||||||
 | 
					      Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<bool> get hasAccess => _hasAccess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool get isBackgroundInitialized {
 | 
					  bool get isBackgroundInitialized {
 | 
				
			||||||
    return _isBackgroundInitialized;
 | 
					    return _isBackgroundInitialized;
 | 
				
			||||||
@ -201,6 +206,15 @@ class BackgroundService {
 | 
				
			|||||||
    if (!Platform.isAndroid) {
 | 
					    if (!Platform.isAndroid) {
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (_hasLock) {
 | 
				
			||||||
 | 
					      debugPrint("WARNING: [acquireLock] called more than once");
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_hasAccessCompleter.isCompleted) {
 | 
				
			||||||
 | 
					      debugPrint("WARNING: [acquireLock] _hasAccessCompleter is completed");
 | 
				
			||||||
 | 
					      _hasAccessCompleter = Completer();
 | 
				
			||||||
 | 
					      _hasAccess = _hasAccessCompleter.future;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    final int lockTime = Timeline.now;
 | 
					    final int lockTime = Timeline.now;
 | 
				
			||||||
    _wantsLockTime = lockTime;
 | 
					    _wantsLockTime = lockTime;
 | 
				
			||||||
    final ReceivePort rp = ReceivePort(_portNameLock);
 | 
					    final ReceivePort rp = ReceivePort(_portNameLock);
 | 
				
			||||||
@ -219,6 +233,7 @@ class BackgroundService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    _hasLock = true;
 | 
					    _hasLock = true;
 | 
				
			||||||
    rp.listen(_heartbeatListener);
 | 
					    rp.listen(_heartbeatListener);
 | 
				
			||||||
 | 
					    _hasAccessCompleter.complete(true);
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -271,6 +286,8 @@ class BackgroundService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    _wantsLockTime = 0;
 | 
					    _wantsLockTime = 0;
 | 
				
			||||||
    if (_hasLock) {
 | 
					    if (_hasLock) {
 | 
				
			||||||
 | 
					      _hasAccessCompleter = Completer();
 | 
				
			||||||
 | 
					      _hasAccess = _hasAccessCompleter.future;
 | 
				
			||||||
      IsolateNameServer.removePortNameMapping(_portNameLock);
 | 
					      IsolateNameServer.removePortNameMapping(_portNameLock);
 | 
				
			||||||
      _waitingIsolate?.send(true);
 | 
					      _waitingIsolate?.send(true);
 | 
				
			||||||
      _waitingIsolate = null;
 | 
					      _waitingIsolate = null;
 | 
				
			||||||
 | 
				
			|||||||
@ -46,6 +46,17 @@ class HiveBackupAlbums {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a deep copy to allow safe modification without changing the global
 | 
				
			||||||
 | 
					  /// state of [HiveBackupAlbums] before actually saving the changes
 | 
				
			||||||
 | 
					  HiveBackupAlbums deepCopy() {
 | 
				
			||||||
 | 
					    return HiveBackupAlbums(
 | 
				
			||||||
 | 
					      selectedAlbumIds: selectedAlbumIds.toList(),
 | 
				
			||||||
 | 
					      excludedAlbumsIds: excludedAlbumsIds.toList(),
 | 
				
			||||||
 | 
					      lastSelectedBackupTime: lastSelectedBackupTime.toList(),
 | 
				
			||||||
 | 
					      lastExcludedBackupTime: lastExcludedBackupTime.toList(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toMap() {
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
    final result = <String, dynamic>{};
 | 
					    final result = <String, dynamic>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -565,11 +565,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
      state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
 | 
					      state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
 | 
				
			||||||
      final bool hasLock = await _backgroundService.acquireLock();
 | 
					      final bool hasLock = await _backgroundService.acquireLock();
 | 
				
			||||||
      if (!hasLock) {
 | 
					      if (!hasLock) {
 | 
				
			||||||
 | 
					        debugPrint("WARNING [resumeBackup] failed to acquireLock");
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      Box<HiveBackupAlbums> box =
 | 
					      await Future.wait([
 | 
				
			||||||
          await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
					        Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
 | 
				
			||||||
      HiveBackupAlbums? albums = box.get(backupInfoKey);
 | 
					        Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
 | 
				
			||||||
 | 
					        Hive.openBox(backgroundBackupInfoBox),
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					      final HiveBackupAlbums? albums =
 | 
				
			||||||
 | 
					          Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
 | 
				
			||||||
      Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
 | 
					      Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
 | 
				
			||||||
      Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
 | 
					      Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
 | 
				
			||||||
      if (albums != null) {
 | 
					      if (albums != null) {
 | 
				
			||||||
@ -584,8 +589,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
          albums.lastExcludedBackupTime,
 | 
					          albums.lastExcludedBackupTime,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
 | 
					      final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
 | 
				
			||||||
      final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
 | 
					 | 
				
			||||||
      state = state.copyWith(
 | 
					      state = state.copyWith(
 | 
				
			||||||
        backupProgress: previous,
 | 
					        backupProgress: previous,
 | 
				
			||||||
        selectedBackupAlbums: selectedAlbums,
 | 
					        selectedBackupAlbums: selectedAlbums,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,34 +1,90 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.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/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/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';
 | 
				
			||||||
 | 
					import 'package:photo_manager/src/types/entity.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final assetServiceProvider = Provider(
 | 
					final assetServiceProvider = Provider(
 | 
				
			||||||
  (ref) => AssetService(
 | 
					  (ref) => AssetService(
 | 
				
			||||||
    ref.watch(apiServiceProvider),
 | 
					    ref.watch(apiServiceProvider),
 | 
				
			||||||
 | 
					    ref.watch(backupServiceProvider),
 | 
				
			||||||
 | 
					    ref.watch(backgroundServiceProvider),
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetService {
 | 
					class AssetService {
 | 
				
			||||||
  final ApiService _apiService;
 | 
					  final ApiService _apiService;
 | 
				
			||||||
 | 
					  final BackupService _backupService;
 | 
				
			||||||
 | 
					  final BackgroundService _backgroundService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AssetService(this._apiService);
 | 
					  AssetService(this._apiService, this._backupService, this._backgroundService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<List<AssetResponseDto>?> getAllAsset() async {
 | 
					  /// Returns all local, remote assets in that order
 | 
				
			||||||
 | 
					  Future<List<Asset>> getAllAsset({bool urgent = false}) async {
 | 
				
			||||||
 | 
					    final List<Asset> assets = [];
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      return await _apiService.assetApi.getAllAssets();
 | 
					      // not using `await` here to fetch local & remote assets concurrently
 | 
				
			||||||
 | 
					      final Future<List<AssetResponseDto>?> remoteTask =
 | 
				
			||||||
 | 
					          _apiService.assetApi.getAllAssets();
 | 
				
			||||||
 | 
					      final Iterable<AssetEntity> newLocalAssets;
 | 
				
			||||||
 | 
					      final List<AssetEntity> localAssets = await _getLocalAssets(urgent);
 | 
				
			||||||
 | 
					      final List<AssetResponseDto> remoteAssets = await remoteTask ?? [];
 | 
				
			||||||
 | 
					      if (remoteAssets.isNotEmpty && localAssets.isNotEmpty) {
 | 
				
			||||||
 | 
					        final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | 
				
			||||||
 | 
					        final Set<String> existingIds = remoteAssets
 | 
				
			||||||
 | 
					            .where((e) => e.deviceId == deviceId)
 | 
				
			||||||
 | 
					            .map((e) => e.deviceAssetId)
 | 
				
			||||||
 | 
					            .toSet();
 | 
				
			||||||
 | 
					        newLocalAssets = localAssets.where((e) => !existingIds.contains(e.id));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        newLocalAssets = localAssets;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assets.addAll(newLocalAssets.map((e) => Asset.local(e)));
 | 
				
			||||||
 | 
					      // the order (first all local, then remote assets) is important!
 | 
				
			||||||
 | 
					      assets.addAll(remoteAssets.map((e) => Asset.remote(e)));
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      debugPrint("Error [getAllAsset]  ${e.toString()}");
 | 
					      debugPrint("Error [getAllAsset]  ${e.toString()}");
 | 
				
			||||||
      return null;
 | 
					    }
 | 
				
			||||||
 | 
					    return assets;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// if [urgent] is `true`, do not block by waiting on the background service
 | 
				
			||||||
 | 
					  /// to finish running. Returns an empty list instead after a timeout.
 | 
				
			||||||
 | 
					  Future<List<AssetEntity>> _getLocalAssets(bool urgent) 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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return backupAlbumInfo != null
 | 
				
			||||||
 | 
					          ? await _backupService
 | 
				
			||||||
 | 
					              .buildUploadCandidates(backupAlbumInfo.deepCopy())
 | 
				
			||||||
 | 
					          : [];
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      debugPrint("Error [_getLocalAssets] ${e.toString()}");
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<AssetResponseDto?> getAssetById(String assetId) async {
 | 
					  Future<Asset?> getAssetById(String assetId) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      return await _apiService.assetApi.getAssetById(assetId);
 | 
					      return Asset.remote(await _apiService.assetApi.getAssetById(assetId));
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      debugPrint("Error [getAssetById]  ${e.toString()}");
 | 
					      debugPrint("Error [getAssetById]  ${e.toString()}");
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
@ -36,12 +92,12 @@ class AssetService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<List<DeleteAssetResponseDto>?> deleteAssets(
 | 
					  Future<List<DeleteAssetResponseDto>?> deleteAssets(
 | 
				
			||||||
    Set<AssetResponseDto> deleteAssets,
 | 
					    Iterable<AssetResponseDto> deleteAssets,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      List<String> payload = [];
 | 
					      final List<String> payload = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (var asset in deleteAssets) {
 | 
					      for (final asset in deleteAssets) {
 | 
				
			||||||
        payload.add(asset.id);
 | 
					        payload.add(asset.id);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +1,24 @@
 | 
				
			|||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
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/services/json_cache.dart';
 | 
					import 'package:immich_mobile/shared/services/json_cache.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AssetCacheService extends JsonCache<List<Asset>> {
 | 
				
			||||||
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
 | 
					 | 
				
			||||||
  AssetCacheService() : super("asset_cache");
 | 
					  AssetCacheService() : super("asset_cache");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void put(List<AssetResponseDto> data) {
 | 
					  void put(List<Asset> data) {
 | 
				
			||||||
    putRawData(data.map((e) => e.toJson()).toList());
 | 
					    putRawData(data.map((e) => e.toJson()).toList());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<List<AssetResponseDto>> 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 = mapList
 | 
					      final responseData =
 | 
				
			||||||
          .map((e) => AssetResponseDto.fromJson(e))
 | 
					          mapList.map((e) => Asset.fromJson(e)).whereNotNull().toList();
 | 
				
			||||||
          .whereNotNull()
 | 
					 | 
				
			||||||
          .toList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return responseData;
 | 
					      return responseData;
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -33,5 +30,5 @@ class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final assetCacheServiceProvider = Provider(
 | 
					final assetCacheServiceProvider = Provider(
 | 
				
			||||||
      (ref) => AssetCacheService(),
 | 
					  (ref) => AssetCacheService(),
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import 'dart:math';
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum RenderAssetGridElementType {
 | 
					enum RenderAssetGridElementType {
 | 
				
			||||||
  assetRow,
 | 
					  assetRow,
 | 
				
			||||||
@ -9,7 +9,7 @@ enum RenderAssetGridElementType {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RenderAssetGridRow {
 | 
					class RenderAssetGridRow {
 | 
				
			||||||
  final List<AssetResponseDto> assets;
 | 
					  final List<Asset> assets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  RenderAssetGridRow(this.assets);
 | 
					  RenderAssetGridRow(this.assets);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -19,7 +19,7 @@ class RenderAssetGridElement {
 | 
				
			|||||||
  final RenderAssetGridRow? assetRow;
 | 
					  final RenderAssetGridRow? assetRow;
 | 
				
			||||||
  final String? title;
 | 
					  final String? title;
 | 
				
			||||||
  final DateTime date;
 | 
					  final DateTime date;
 | 
				
			||||||
  final List<AssetResponseDto>? relatedAssetList;
 | 
					  final List<Asset>? relatedAssetList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  RenderAssetGridElement(
 | 
					  RenderAssetGridElement(
 | 
				
			||||||
    this.type, {
 | 
					    this.type, {
 | 
				
			||||||
@ -31,13 +31,15 @@ class RenderAssetGridElement {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
List<RenderAssetGridElement> assetsToRenderList(
 | 
					List<RenderAssetGridElement> assetsToRenderList(
 | 
				
			||||||
    List<AssetResponseDto> assets, int assetsPerRow) {
 | 
					  List<Asset> assets,
 | 
				
			||||||
 | 
					  int assetsPerRow,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
  List<RenderAssetGridElement> elements = [];
 | 
					  List<RenderAssetGridElement> elements = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int cursor = 0;
 | 
					  int cursor = 0;
 | 
				
			||||||
  while (cursor < assets.length) {
 | 
					  while (cursor < assets.length) {
 | 
				
			||||||
    int rowElements = min(assets.length - cursor, assetsPerRow);
 | 
					    int rowElements = min(assets.length - cursor, assetsPerRow);
 | 
				
			||||||
    final date = DateTime.parse(assets[cursor].createdAt);
 | 
					    final date = assets[cursor].createdAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final rowElement = RenderAssetGridElement(
 | 
					    final rowElement = RenderAssetGridElement(
 | 
				
			||||||
      RenderAssetGridElementType.assetRow,
 | 
					      RenderAssetGridElementType.assetRow,
 | 
				
			||||||
@ -55,7 +57,9 @@ List<RenderAssetGridElement> assetsToRenderList(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
List<RenderAssetGridElement> assetGroupsToRenderList(
 | 
					List<RenderAssetGridElement> assetGroupsToRenderList(
 | 
				
			||||||
    Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
 | 
					  Map<String, List<Asset>> assetGroups,
 | 
				
			||||||
 | 
					  int assetsPerRow,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
  List<RenderAssetGridElement> elements = [];
 | 
					  List<RenderAssetGridElement> elements = [];
 | 
				
			||||||
  DateTime? lastDate;
 | 
					  DateTime? lastDate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -64,8 +68,11 @@ List<RenderAssetGridElement> assetGroupsToRenderList(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (lastDate == null || lastDate!.month != date.month) {
 | 
					    if (lastDate == null || lastDate!.month != date.month) {
 | 
				
			||||||
      elements.add(
 | 
					      elements.add(
 | 
				
			||||||
        RenderAssetGridElement(RenderAssetGridElementType.monthTitle,
 | 
					        RenderAssetGridElement(
 | 
				
			||||||
            title: groupName, date: date),
 | 
					          RenderAssetGridElementType.monthTitle,
 | 
				
			||||||
 | 
					          title: groupName,
 | 
				
			||||||
 | 
					          date: date,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import 'package:collection/collection.dart';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 | 
					import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 | 
				
			||||||
import 'asset_grid_data_structure.dart';
 | 
					import 'asset_grid_data_structure.dart';
 | 
				
			||||||
import 'daily_title_text.dart';
 | 
					import 'daily_title_text.dart';
 | 
				
			||||||
@ -13,7 +13,7 @@ import 'draggable_scrollbar_custom.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
typedef ImmichAssetGridSelectionListener = void Function(
 | 
					typedef ImmichAssetGridSelectionListener = void Function(
 | 
				
			||||||
  bool,
 | 
					  bool,
 | 
				
			||||||
  Set<AssetResponseDto>,
 | 
					  Set<Asset>,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
					class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			||||||
@ -24,20 +24,20 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			|||||||
  bool _scrolling = false;
 | 
					  bool _scrolling = false;
 | 
				
			||||||
  final Set<String> _selectedAssets = HashSet();
 | 
					  final Set<String> _selectedAssets = HashSet();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<AssetResponseDto> get _assets {
 | 
					  List<Asset> get _assets {
 | 
				
			||||||
    return widget.renderList
 | 
					    return widget.renderList
 | 
				
			||||||
        .map((e) {
 | 
					        .map((e) {
 | 
				
			||||||
          if (e.type == RenderAssetGridElementType.assetRow) {
 | 
					          if (e.type == RenderAssetGridElementType.assetRow) {
 | 
				
			||||||
            return e.assetRow!.assets;
 | 
					            return e.assetRow!.assets;
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            return List<AssetResponseDto>.empty();
 | 
					            return List<Asset>.empty();
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .flattened
 | 
					        .flattened
 | 
				
			||||||
        .toList();
 | 
					        .toList();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Set<AssetResponseDto> _getSelectedAssets() {
 | 
					  Set<Asset> _getSelectedAssets() {
 | 
				
			||||||
    return _selectedAssets
 | 
					    return _selectedAssets
 | 
				
			||||||
        .map((e) => _assets.firstWhereOrNull((a) => a.id == e))
 | 
					        .map((e) => _assets.firstWhereOrNull((a) => a.id == e))
 | 
				
			||||||
        .whereNotNull()
 | 
					        .whereNotNull()
 | 
				
			||||||
@ -48,7 +48,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			|||||||
    widget.listener?.call(selectionActive, _getSelectedAssets());
 | 
					    widget.listener?.call(selectionActive, _getSelectedAssets());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _selectAssets(List<AssetResponseDto> assets) {
 | 
					  void _selectAssets(List<Asset> assets) {
 | 
				
			||||||
    setState(() {
 | 
					    setState(() {
 | 
				
			||||||
      for (var e in assets) {
 | 
					      for (var e in assets) {
 | 
				
			||||||
        _selectedAssets.add(e.id);
 | 
					        _selectedAssets.add(e.id);
 | 
				
			||||||
@ -57,7 +57,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _deselectAssets(List<AssetResponseDto> assets) {
 | 
					  void _deselectAssets(List<Asset> assets) {
 | 
				
			||||||
    setState(() {
 | 
					    setState(() {
 | 
				
			||||||
      for (var e in assets) {
 | 
					      for (var e in assets) {
 | 
				
			||||||
        _selectedAssets.remove(e.id);
 | 
					        _selectedAssets.remove(e.id);
 | 
				
			||||||
@ -74,7 +74,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			|||||||
    _callSelectionListener(false);
 | 
					    _callSelectionListener(false);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _allAssetsSelected(List<AssetResponseDto> assets) {
 | 
					  bool _allAssetsSelected(List<Asset> assets) {
 | 
				
			||||||
    return widget.selectionActive &&
 | 
					    return widget.selectionActive &&
 | 
				
			||||||
        assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
 | 
					        assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -85,7 +85,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Widget _buildThumbnailOrPlaceholder(
 | 
					  Widget _buildThumbnailOrPlaceholder(
 | 
				
			||||||
    AssetResponseDto asset,
 | 
					    Asset asset,
 | 
				
			||||||
    bool placeholder,
 | 
					    bool placeholder,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    if (placeholder) {
 | 
					    if (placeholder) {
 | 
				
			||||||
@ -114,7 +114,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((AssetResponseDto asset) {
 | 
					      children: row.assets.map((Asset asset) {
 | 
				
			||||||
        bool last = asset == row.assets.last;
 | 
					        bool last = asset == row.assets.last;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Container(
 | 
					        return Container(
 | 
				
			||||||
@ -134,7 +134,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
 | 
				
			|||||||
  Widget _buildTitle(
 | 
					  Widget _buildTitle(
 | 
				
			||||||
    BuildContext context,
 | 
					    BuildContext context,
 | 
				
			||||||
    String title,
 | 
					    String title,
 | 
				
			||||||
    List<AssetResponseDto> assets,
 | 
					    List<Asset> assets,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return DailyTitleText(
 | 
					    return DailyTitleText(
 | 
				
			||||||
      isoDate: title,
 | 
					      isoDate: title,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,15 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.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:flutter/services.dart';
 | 
				
			||||||
import 'package:hive_flutter/hive_flutter.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/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ThumbnailImage extends HookConsumerWidget {
 | 
					class ThumbnailImage extends HookConsumerWidget {
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
  final List<AssetResponseDto> assetList;
 | 
					  final List<Asset> assetList;
 | 
				
			||||||
  final bool showStorageIndicator;
 | 
					  final bool showStorageIndicator;
 | 
				
			||||||
  final bool useGrayBoxPlaceholder;
 | 
					  final bool useGrayBoxPlaceholder;
 | 
				
			||||||
  final bool isSelected;
 | 
					  final bool isSelected;
 | 
				
			||||||
@ -34,12 +31,9 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var box = Hive.box(userInfoBox);
 | 
					 | 
				
			||||||
    var thumbnailRequestUrl = getThumbnailUrl(asset);
 | 
					 | 
				
			||||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
					    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget buildSelectionIcon(Asset asset) {
 | 
				
			||||||
    Widget buildSelectionIcon(AssetResponseDto asset) {
 | 
					 | 
				
			||||||
      if (isSelected) {
 | 
					      if (isSelected) {
 | 
				
			||||||
        return Icon(
 | 
					        return Icon(
 | 
				
			||||||
          Icons.check_circle,
 | 
					          Icons.check_circle,
 | 
				
			||||||
@ -87,41 +81,11 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
                      )
 | 
					                      )
 | 
				
			||||||
                    : const Border(),
 | 
					                    : const Border(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              child: CachedNetworkImage(
 | 
					              child: ImmichImage(
 | 
				
			||||||
                cacheKey: 'thumbnail-image-${asset.id}',
 | 
					                asset,
 | 
				
			||||||
                width: 300,
 | 
					                width: 300,
 | 
				
			||||||
                height: 300,
 | 
					                height: 300,
 | 
				
			||||||
                memCacheHeight: 200,
 | 
					                useGrayBoxPlaceholder: useGrayBoxPlaceholder,
 | 
				
			||||||
                maxWidthDiskCache: 200,
 | 
					 | 
				
			||||||
                maxHeightDiskCache: 200,
 | 
					 | 
				
			||||||
                fit: BoxFit.cover,
 | 
					 | 
				
			||||||
                imageUrl: thumbnailRequestUrl,
 | 
					 | 
				
			||||||
                httpHeaders: {
 | 
					 | 
				
			||||||
                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                fadeInDuration: const Duration(milliseconds: 250),
 | 
					 | 
				
			||||||
                progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
					 | 
				
			||||||
                  if (useGrayBoxPlaceholder) {
 | 
					 | 
				
			||||||
                    return const DecoratedBox(
 | 
					 | 
				
			||||||
                      decoration: BoxDecoration(color: Colors.grey),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                  return Transform.scale(
 | 
					 | 
				
			||||||
                    scale: 0.2,
 | 
					 | 
				
			||||||
                    child: CircularProgressIndicator(
 | 
					 | 
				
			||||||
                      value: downloadProgress.progress,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                errorWidget: (context, url, error) {
 | 
					 | 
				
			||||||
                  debugPrint("Error getting thumbnail $url = $error");
 | 
					 | 
				
			||||||
                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  return Icon(
 | 
					 | 
				
			||||||
                    Icons.image_not_supported_outlined,
 | 
					 | 
				
			||||||
                    color: Theme.of(context).primaryColor,
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            if (multiselectEnabled)
 | 
					            if (multiselectEnabled)
 | 
				
			||||||
@ -137,14 +101,16 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
                right: 10,
 | 
					                right: 10,
 | 
				
			||||||
                bottom: 5,
 | 
					                bottom: 5,
 | 
				
			||||||
                child: Icon(
 | 
					                child: Icon(
 | 
				
			||||||
                  (deviceId != asset.deviceId)
 | 
					                  asset.isRemote
 | 
				
			||||||
                      ? Icons.cloud_done_outlined
 | 
					                      ? (deviceId == asset.deviceId
 | 
				
			||||||
                      : Icons.photo_library_rounded,
 | 
					                          ? Icons.cloud_done_outlined
 | 
				
			||||||
 | 
					                          : Icons.cloud_outlined)
 | 
				
			||||||
 | 
					                      : Icons.cloud_off_outlined,
 | 
				
			||||||
                  color: Colors.white,
 | 
					                  color: Colors.white,
 | 
				
			||||||
                  size: 18,
 | 
					                  size: 18,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            if (asset.type != AssetTypeEnum.IMAGE)
 | 
					            if (!asset.isImage)
 | 
				
			||||||
              Positioned(
 | 
					              Positioned(
 | 
				
			||||||
                top: 5,
 | 
					                top: 5,
 | 
				
			||||||
                right: 5,
 | 
					                right: 5,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
					import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
				
			||||||
@ -14,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.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';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.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:immich_mobile/shared/providers/server_info.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
				
			||||||
@ -31,7 +33,7 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
    final multiselectEnabled = ref.watch(multiselectProvider.notifier);
 | 
					    final multiselectEnabled = ref.watch(multiselectProvider.notifier);
 | 
				
			||||||
    final selectionEnabledHook = useState(false);
 | 
					    final selectionEnabledHook = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final selection = useState(<AssetResponseDto>{});
 | 
					    final selection = useState(<Asset>{});
 | 
				
			||||||
    final albums = ref.watch(albumProvider);
 | 
					    final albums = ref.watch(albumProvider);
 | 
				
			||||||
    final albumService = ref.watch(albumServiceProvider);
 | 
					    final albumService = ref.watch(albumServiceProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -60,7 +62,7 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
    Widget buildBody() {
 | 
					    Widget buildBody() {
 | 
				
			||||||
      void selectionListener(
 | 
					      void selectionListener(
 | 
				
			||||||
        bool multiselect,
 | 
					        bool multiselect,
 | 
				
			||||||
        Set<AssetResponseDto> selectedAssets,
 | 
					        Set<Asset> selectedAssets,
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        selectionEnabledHook.value = multiselect;
 | 
					        selectionEnabledHook.value = multiselect;
 | 
				
			||||||
        selection.value = selectedAssets;
 | 
					        selection.value = selectedAssets;
 | 
				
			||||||
@ -76,9 +78,27 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
        selectionEnabledHook.value = false;
 | 
					        selectionEnabledHook.value = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Iterable<Asset> remoteOnlySelection() {
 | 
				
			||||||
 | 
					        final Set<Asset> assets = selection.value;
 | 
				
			||||||
 | 
					        final bool onlyRemote = assets.every((e) => e.isRemote);
 | 
				
			||||||
 | 
					        if (!onlyRemote) {
 | 
				
			||||||
 | 
					          ImmichToast.show(
 | 
				
			||||||
 | 
					            context: context,
 | 
				
			||||||
 | 
					            msg: "Can not add local assets to albums yet, skipping",
 | 
				
			||||||
 | 
					            gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          return assets.where((a) => a.isRemote);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return assets;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      void onAddToAlbum(AlbumResponseDto album) async {
 | 
					      void onAddToAlbum(AlbumResponseDto album) async {
 | 
				
			||||||
 | 
					        final Iterable<Asset> assets = remoteOnlySelection();
 | 
				
			||||||
 | 
					        if (assets.isEmpty) {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        final result = await albumService.addAdditionalAssetToAlbum(
 | 
					        final result = await albumService.addAdditionalAssetToAlbum(
 | 
				
			||||||
          selection.value,
 | 
					          assets,
 | 
				
			||||||
          album.id,
 | 
					          album.id,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -103,6 +123,7 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
                  "added": result.successfullyAdded.toString(),
 | 
					                  "added": result.successfullyAdded.toString(),
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					              toastType: ToastType.success,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -111,8 +132,11 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      void onCreateNewAlbum() async {
 | 
					      void onCreateNewAlbum() async {
 | 
				
			||||||
        final result =
 | 
					        final Iterable<Asset> assets = remoteOnlySelection();
 | 
				
			||||||
            await albumService.createAlbumWithGeneratedName(selection.value);
 | 
					        if (assets.isEmpty) {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        final result = await albumService.createAlbumWithGeneratedName(assets);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (result != null) {
 | 
					        if (result != null) {
 | 
				
			||||||
          ref.watch(albumProvider.notifier).getAllAlbums();
 | 
					          ref.watch(albumProvider.notifier).getAllAlbums();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,14 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchResultPageState {
 | 
					class SearchResultPageState {
 | 
				
			||||||
  final bool isLoading;
 | 
					  final bool isLoading;
 | 
				
			||||||
  final bool isSuccess;
 | 
					  final bool isSuccess;
 | 
				
			||||||
  final bool isError;
 | 
					  final bool isError;
 | 
				
			||||||
  final List<AssetResponseDto> searchResult;
 | 
					  final List<Asset> searchResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SearchResultPageState({
 | 
					  SearchResultPageState({
 | 
				
			||||||
    required this.isLoading,
 | 
					    required this.isLoading,
 | 
				
			||||||
@ -20,7 +21,7 @@ class SearchResultPageState {
 | 
				
			|||||||
    bool? isLoading,
 | 
					    bool? isLoading,
 | 
				
			||||||
    bool? isSuccess,
 | 
					    bool? isSuccess,
 | 
				
			||||||
    bool? isError,
 | 
					    bool? isError,
 | 
				
			||||||
    List<AssetResponseDto>? searchResult,
 | 
					    List<Asset>? searchResult,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return SearchResultPageState(
 | 
					    return SearchResultPageState(
 | 
				
			||||||
      isLoading: isLoading ?? this.isLoading,
 | 
					      isLoading: isLoading ?? this.isLoading,
 | 
				
			||||||
@ -44,8 +45,9 @@ 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<AssetResponseDto>.from(
 | 
					      searchResult: List<Asset>.from(
 | 
				
			||||||
        map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)),
 | 
					        map['searchResult']
 | 
				
			||||||
 | 
					            ?.map((x) => Asset.remote(AssetResponseDto.fromJson(x))),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,8 @@ import 'package:immich_mobile/modules/search/models/search_result_page_state.mod
 | 
				
			|||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
 | 
					import 'package:immich_mobile/modules/search/services/search.service.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';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:intl/intl.dart';
 | 
					import 'package:intl/intl.dart';
 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
 | 
					class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
 | 
				
			||||||
  SearchResultPageNotifier(this._searchService)
 | 
					  SearchResultPageNotifier(this._searchService)
 | 
				
			||||||
@ -30,8 +30,9 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
 | 
				
			|||||||
      isSuccess: false,
 | 
					      isSuccess: false,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<AssetResponseDto>? assets =
 | 
					    List<Asset>? assets = (await _searchService.searchAsset(searchTerm))
 | 
				
			||||||
        await _searchService.searchAsset(searchTerm);
 | 
					        ?.map((e) => Asset.remote(e))
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (assets != null) {
 | 
					    if (assets != null) {
 | 
				
			||||||
      state = state.copyWith(
 | 
					      state = state.copyWith(
 | 
				
			||||||
@ -61,12 +62,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
 | 
				
			|||||||
  var assets = ref.watch(searchResultPageProvider).searchResult;
 | 
					  var assets = ref.watch(searchResultPageProvider).searchResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  assets.sortByCompare<DateTime>(
 | 
					  assets.sortByCompare<DateTime>(
 | 
				
			||||||
    (e) => DateTime.parse(e.createdAt),
 | 
					    (e) => e.createdAt,
 | 
				
			||||||
    (a, b) => b.compareTo(a),
 | 
					    (a, b) => b.compareTo(a),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return assets.groupListsBy(
 | 
					  return assets.groupListsBy(
 | 
				
			||||||
    (element) => DateFormat('y-MM-dd')
 | 
					    (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
 | 
				
			||||||
        .format(DateTime.parse(element.createdAt).toLocal()),
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,7 @@ import 'package:immich_mobile/modules/settings/views/settings_page.dart';
 | 
				
			|||||||
import 'package:immich_mobile/routing/auth_guard.dart';
 | 
					import 'package:immich_mobile/routing/auth_guard.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
 | 
					import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.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:immich_mobile/shared/views/splash_screen.dart';
 | 
					import 'package:immich_mobile/shared/views/splash_screen.dart';
 | 
				
			||||||
 | 
				
			|||||||
@ -65,8 +65,7 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
      final args = routeData.argsAs<VideoViewerRouteArgs>();
 | 
					      final args = routeData.argsAs<VideoViewerRouteArgs>();
 | 
				
			||||||
      return MaterialPageX<dynamic>(
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
          routeData: routeData,
 | 
					          routeData: routeData,
 | 
				
			||||||
          child: VideoViewerPage(
 | 
					          child: VideoViewerPage(key: args.key, asset: args.asset));
 | 
				
			||||||
              key: args.key, videoUrl: args.videoUrl, asset: args.asset));
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    BackupControllerRoute.name: (routeData) {
 | 
					    BackupControllerRoute.name: (routeData) {
 | 
				
			||||||
      return MaterialPageX<dynamic>(
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
@ -258,9 +257,7 @@ class TabControllerRoute extends PageRouteInfo<void> {
 | 
				
			|||||||
/// [GalleryViewerPage]
 | 
					/// [GalleryViewerPage]
 | 
				
			||||||
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
 | 
					class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
 | 
				
			||||||
  GalleryViewerRoute(
 | 
					  GalleryViewerRoute(
 | 
				
			||||||
      {Key? key,
 | 
					      {Key? key, required List<Asset> assetList, required Asset asset})
 | 
				
			||||||
      required List<AssetResponseDto> assetList,
 | 
					 | 
				
			||||||
      required AssetResponseDto asset})
 | 
					 | 
				
			||||||
      : super(GalleryViewerRoute.name,
 | 
					      : super(GalleryViewerRoute.name,
 | 
				
			||||||
            path: '/gallery-viewer-page',
 | 
					            path: '/gallery-viewer-page',
 | 
				
			||||||
            args: GalleryViewerRouteArgs(
 | 
					            args: GalleryViewerRouteArgs(
 | 
				
			||||||
@ -275,9 +272,9 @@ class GalleryViewerRouteArgs {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final Key? key;
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final List<AssetResponseDto> assetList;
 | 
					  final List<Asset> assetList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@ -291,7 +288,7 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
 | 
				
			|||||||
  ImageViewerRoute(
 | 
					  ImageViewerRoute(
 | 
				
			||||||
      {Key? key,
 | 
					      {Key? key,
 | 
				
			||||||
      required String heroTag,
 | 
					      required String heroTag,
 | 
				
			||||||
      required AssetResponseDto asset,
 | 
					      required Asset asset,
 | 
				
			||||||
      required String authToken,
 | 
					      required String authToken,
 | 
				
			||||||
      required void Function() isZoomedFunction,
 | 
					      required void Function() isZoomedFunction,
 | 
				
			||||||
      required ValueNotifier<bool> isZoomedListener,
 | 
					      required ValueNotifier<bool> isZoomedListener,
 | 
				
			||||||
@ -324,7 +321,7 @@ class ImageViewerRouteArgs {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final String heroTag;
 | 
					  final String heroTag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final String authToken;
 | 
					  final String authToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -343,29 +340,24 @@ class ImageViewerRouteArgs {
 | 
				
			|||||||
/// generated route for
 | 
					/// generated route for
 | 
				
			||||||
/// [VideoViewerPage]
 | 
					/// [VideoViewerPage]
 | 
				
			||||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
 | 
					class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
 | 
				
			||||||
  VideoViewerRoute(
 | 
					  VideoViewerRoute({Key? key, required Asset asset})
 | 
				
			||||||
      {Key? key, required String videoUrl, required AssetResponseDto asset})
 | 
					 | 
				
			||||||
      : super(VideoViewerRoute.name,
 | 
					      : super(VideoViewerRoute.name,
 | 
				
			||||||
            path: '/video-viewer-page',
 | 
					            path: '/video-viewer-page',
 | 
				
			||||||
            args: VideoViewerRouteArgs(
 | 
					            args: VideoViewerRouteArgs(key: key, asset: asset));
 | 
				
			||||||
                key: key, videoUrl: videoUrl, asset: asset));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const String name = 'VideoViewerRoute';
 | 
					  static const String name = 'VideoViewerRoute';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoViewerRouteArgs {
 | 
					class VideoViewerRouteArgs {
 | 
				
			||||||
  const VideoViewerRouteArgs(
 | 
					  const VideoViewerRouteArgs({this.key, required this.asset});
 | 
				
			||||||
      {this.key, required this.videoUrl, required this.asset});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Key? key;
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final String videoUrl;
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  final AssetResponseDto asset;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
 | 
					    return 'VideoViewerRouteArgs{key: $key, asset: $asset}';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										117
									
								
								mobile/lib/shared/models/asset.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								mobile/lib/shared/models/asset.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					import 'package:hive/hive.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/constants/hive_box.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Asset (online or local)
 | 
				
			||||||
 | 
					class Asset {
 | 
				
			||||||
 | 
					  Asset.remote(this.remote) {
 | 
				
			||||||
 | 
					    local = null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Asset.local(this.local) {
 | 
				
			||||||
 | 
					    remote = null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late final AssetResponseDto? remote;
 | 
				
			||||||
 | 
					  late final AssetEntity? local;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get isRemote => remote != null;
 | 
				
			||||||
 | 
					  bool get isLocal => local != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get deviceId =>
 | 
				
			||||||
 | 
					      isRemote ? remote!.deviceId : Hive.box(userInfoBox).get(deviceIdKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get deviceAssetId => isRemote ? remote!.deviceAssetId : local!.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 =>
 | 
				
			||||||
 | 
					      isLocal ? local!.createDateTime : DateTime.parse(remote!.createdAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get isImage => isLocal
 | 
				
			||||||
 | 
					      ? local!.type == AssetType.image
 | 
				
			||||||
 | 
					      : remote!.type == AssetTypeEnum.IMAGE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get duration => isRemote
 | 
				
			||||||
 | 
					      ? remote!.duration
 | 
				
			||||||
 | 
					      : Duration(seconds: local!.duration).toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// use only for tests
 | 
				
			||||||
 | 
					  set createdAt(DateTime val) {
 | 
				
			||||||
 | 
					    if (isRemote) {
 | 
				
			||||||
 | 
					      remote!.createdAt = val.toIso8601String();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					    if (isLocal) {
 | 
				
			||||||
 | 
					      json["local"] = _assetEntityToJson(local!);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      json["remote"] = remote!.toJson();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Asset? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					      final l = json["local"];
 | 
				
			||||||
 | 
					      if (l != null) {
 | 
				
			||||||
 | 
					        return Asset.local(_assetEntityFromJson(l));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return Asset.remote(AssetResponseDto.fromJson(json["remote"]));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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"],
 | 
				
			||||||
 | 
					      longitude: json["longitude"],
 | 
				
			||||||
 | 
					      mimeType: json["mimeType"],
 | 
				
			||||||
 | 
					      subtype: json["subtype"],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,18 +1,23 @@
 | 
				
			|||||||
 | 
					import 'dart:collection';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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/modules/home/services/asset.service.dart';
 | 
					import 'package:immich_mobile/modules/home/services/asset.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
 | 
					import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
 | 
					import 'package:immich_mobile/shared/services/device_info.service.dart';
 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:intl/intl.dart';
 | 
					import 'package:intl/intl.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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
 | 
					class AssetNotifier extends StateNotifier<List<Asset>> {
 | 
				
			||||||
  final AssetService _assetService;
 | 
					  final AssetService _assetService;
 | 
				
			||||||
  final AssetCacheService _assetCacheService;
 | 
					  final AssetCacheService _assetCacheService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final DeviceInfoService _deviceInfoService = DeviceInfoService();
 | 
					  final DeviceInfoService _deviceInfoService = DeviceInfoService();
 | 
				
			||||||
 | 
					  bool _getAllAssetInProgress = false;
 | 
				
			||||||
 | 
					  bool _deleteInProgress = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AssetNotifier(this._assetService, this._assetCacheService) : super([]);
 | 
					  AssetNotifier(this._assetService, this._assetCacheService) : super([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,29 +26,38 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAllAsset() async {
 | 
					  getAllAsset() async {
 | 
				
			||||||
    final stopwatch = Stopwatch();
 | 
					    if (_getAllAssetInProgress || _deleteInProgress) {
 | 
				
			||||||
 | 
					      // guard against multiple calls to this method while it's still working
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
    if (await _assetCacheService.isValid() && state.isEmpty) {
 | 
					 | 
				
			||||||
      stopwatch.start();
 | 
					 | 
				
			||||||
      state = await _assetCacheService.get();
 | 
					 | 
				
			||||||
      debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
 | 
					 | 
				
			||||||
      stopwatch.reset();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    final stopwatch = Stopwatch();
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      _getAllAssetInProgress = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final bool isCacheValid = await _assetCacheService.isValid();
 | 
				
			||||||
 | 
					      if (isCacheValid && state.isEmpty) {
 | 
				
			||||||
 | 
					        stopwatch.start();
 | 
				
			||||||
 | 
					        state = await _assetCacheService.get();
 | 
				
			||||||
 | 
					        debugPrint(
 | 
				
			||||||
 | 
					            "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
 | 
				
			||||||
 | 
					        stopwatch.reset();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      stopwatch.start();
 | 
				
			||||||
 | 
					      var allAssets = await _assetService.getAllAsset(urgent: !isCacheValid);
 | 
				
			||||||
 | 
					      debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
 | 
				
			||||||
 | 
					      stopwatch.reset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      state = allAssets;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      _getAllAssetInProgress = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    debugPrint("[getAllAsset] setting new asset state");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    stopwatch.start();
 | 
					    stopwatch.start();
 | 
				
			||||||
    var allAssets = await _assetService.getAllAsset();
 | 
					    _cacheState();
 | 
				
			||||||
    debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
 | 
					    debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
 | 
				
			||||||
    stopwatch.reset();
 | 
					    stopwatch.reset();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (allAssets != null) {
 | 
					 | 
				
			||||||
      state = allAssets;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      stopwatch.start();
 | 
					 | 
				
			||||||
      _cacheState();
 | 
					 | 
				
			||||||
      debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
 | 
					 | 
				
			||||||
      stopwatch.reset();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  clearAllAsset() {
 | 
					  clearAllAsset() {
 | 
				
			||||||
@ -52,80 +66,113 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onNewAssetUploaded(AssetResponseDto newAsset) {
 | 
					  onNewAssetUploaded(AssetResponseDto newAsset) {
 | 
				
			||||||
    state = [...state, newAsset];
 | 
					    final int i = state.indexWhere(
 | 
				
			||||||
 | 
					      (a) =>
 | 
				
			||||||
 | 
					          a.isRemote ||
 | 
				
			||||||
 | 
					          (a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (i == -1 || state[i].deviceAssetId != newAsset.deviceAssetId) {
 | 
				
			||||||
 | 
					      state = [...state, Asset.remote(newAsset)];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // order is important to keep all local-only assets at the beginning!
 | 
				
			||||||
 | 
					      state = [
 | 
				
			||||||
 | 
					        ...state.slice(0, i),
 | 
				
			||||||
 | 
					        ...state.slice(i + 1),
 | 
				
			||||||
 | 
					        Asset.remote(newAsset),
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      // TODO here is a place to unify local/remote assets by replacing the
 | 
				
			||||||
 | 
					      // local-only asset in the state with a local&remote asset
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    _cacheState();
 | 
					    _cacheState();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  deleteAssets(Set<AssetResponseDto> deleteAssets) async {
 | 
					  deleteAssets(Set<Asset> deleteAssets) async {
 | 
				
			||||||
 | 
					    _deleteInProgress = true;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final localDeleted = await _deleteLocalAssets(deleteAssets);
 | 
				
			||||||
 | 
					      final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
 | 
				
			||||||
 | 
					      final Set<String> deleted = HashSet();
 | 
				
			||||||
 | 
					      deleted.addAll(localDeleted);
 | 
				
			||||||
 | 
					      deleted.addAll(remoteDeleted);
 | 
				
			||||||
 | 
					      if (deleted.isNotEmpty) {
 | 
				
			||||||
 | 
					        state = state.where((a) => !deleted.contains(a.id)).toList();
 | 
				
			||||||
 | 
					        _cacheState();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      _deleteInProgress = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<String>> _deleteLocalAssets(Set<Asset> assetsToDelete) async {
 | 
				
			||||||
    var deviceInfo = await _deviceInfoService.getDeviceInfo();
 | 
					    var deviceInfo = await _deviceInfoService.getDeviceInfo();
 | 
				
			||||||
    var deviceId = deviceInfo["deviceId"];
 | 
					    var deviceId = deviceInfo["deviceId"];
 | 
				
			||||||
    var deleteIdList = <String>[];
 | 
					    final List<String> local = [];
 | 
				
			||||||
    // Delete asset from device
 | 
					    // Delete asset from device
 | 
				
			||||||
    for (var asset in deleteAssets) {
 | 
					    for (final Asset asset in assetsToDelete) {
 | 
				
			||||||
      // Delete asset on device if present
 | 
					      if (asset.isLocal) {
 | 
				
			||||||
      if (asset.deviceId == deviceId) {
 | 
					        local.add(asset.id);
 | 
				
			||||||
 | 
					      } else if (asset.deviceId == deviceId) {
 | 
				
			||||||
 | 
					        // Delete asset on device if it is still present
 | 
				
			||||||
        var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
 | 
					        var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (localAsset != null) {
 | 
					        if (localAsset != null) {
 | 
				
			||||||
          deleteIdList.add(localAsset.id);
 | 
					          local.add(localAsset.id);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (local.isNotEmpty) {
 | 
				
			||||||
    try {
 | 
					      try {
 | 
				
			||||||
      await PhotoManager.editor.deleteWithIds(deleteIdList);
 | 
					        return await PhotoManager.editor.deleteWithIds(local);
 | 
				
			||||||
    } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
      debugPrint("Delete asset from device failed: $e");
 | 
					        debugPrint("Delete asset from device failed: $e");
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Delete asset on server
 | 
					 | 
				
			||||||
    List<DeleteAssetResponseDto>? deleteAssetResult =
 | 
					 | 
				
			||||||
        await _assetService.deleteAssets(deleteAssets);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (deleteAssetResult == null) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (var asset in deleteAssetResult) {
 | 
					 | 
				
			||||||
      if (asset.status == DeleteAssetStatus.SUCCESS) {
 | 
					 | 
				
			||||||
        state =
 | 
					 | 
				
			||||||
            state.where((immichAsset) => immichAsset.id != asset.id).toList();
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _cacheState();
 | 
					  Future<Iterable<String>> _deleteRemoteAssets(
 | 
				
			||||||
 | 
					    Set<Asset> assetsToDelete,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    final Iterable<AssetResponseDto> remote =
 | 
				
			||||||
 | 
					        assetsToDelete.where((e) => e.isRemote).map((e) => e.remote!);
 | 
				
			||||||
 | 
					    final List<DeleteAssetResponseDto> deleteAssetResult =
 | 
				
			||||||
 | 
					        await _assetService.deleteAssets(remote) ?? [];
 | 
				
			||||||
 | 
					    return deleteAssetResult
 | 
				
			||||||
 | 
					        .where((a) => a.status == DeleteAssetStatus.SUCCESS)
 | 
				
			||||||
 | 
					        .map((a) => a.id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final assetProvider =
 | 
					final assetProvider = StateNotifierProvider<AssetNotifier, List<Asset>>((ref) {
 | 
				
			||||||
    StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
 | 
					 | 
				
			||||||
  return AssetNotifier(
 | 
					  return AssetNotifier(
 | 
				
			||||||
      ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
 | 
					      ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
 | 
					final assetGroupByDateTimeProvider = StateProvider((ref) {
 | 
				
			||||||
  var assets = ref.watch(assetProvider);
 | 
					  final assets = ref.watch(assetProvider).toList();
 | 
				
			||||||
 | 
					  // `toList()` ist needed to make a copy as to NOT sort the original list/state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  assets.sortByCompare<DateTime>(
 | 
					  assets.sortByCompare<DateTime>(
 | 
				
			||||||
    (e) => DateTime.parse(e.createdAt),
 | 
					    (e) => e.createdAt,
 | 
				
			||||||
    (a, b) => b.compareTo(a),
 | 
					    (a, b) => b.compareTo(a),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return assets.groupListsBy(
 | 
					  return assets.groupListsBy(
 | 
				
			||||||
    (element) => DateFormat('y-MM-dd')
 | 
					    (element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
 | 
				
			||||||
        .format(DateTime.parse(element.createdAt).toLocal()),
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
 | 
					final assetGroupByMonthYearProvider = StateProvider((ref) {
 | 
				
			||||||
  var assets = ref.watch(assetProvider);
 | 
					  // TODO: remove `where` once temporary workaround is no longer needed (to only
 | 
				
			||||||
 | 
					  // allow remote assets to be added to album). Keep `toList()` as to NOT sort
 | 
				
			||||||
 | 
					  // the original list/state
 | 
				
			||||||
 | 
					  final assets = ref.watch(assetProvider).where((e) => e.isRemote).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  assets.sortByCompare<DateTime>(
 | 
					  assets.sortByCompare<DateTime>(
 | 
				
			||||||
    (e) => DateTime.parse(e.createdAt),
 | 
					    (e) => e.createdAt,
 | 
				
			||||||
    (a, b) => b.compareTo(a),
 | 
					    (a, b) => b.compareTo(a),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return assets.groupListsBy(
 | 
					  return assets.groupListsBy(
 | 
				
			||||||
    (element) => DateFormat('MMMM, y')
 | 
					    (element) => DateFormat('MMMM, y').format(element.createdAt.toLocal()),
 | 
				
			||||||
        .format(DateTime.parse(element.createdAt).toLocal()),
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,11 @@ import 'dart:io';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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:openapi/api.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 'package:path/path.dart' as p;
 | 
					 | 
				
			||||||
import 'api.service.dart';
 | 
					import 'api.service.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final shareServiceProvider =
 | 
					final shareServiceProvider =
 | 
				
			||||||
@ -17,26 +17,28 @@ class ShareService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ShareService(this._apiService);
 | 
					  ShareService(this._apiService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> shareAsset(AssetResponseDto asset) async {
 | 
					  Future<void> shareAsset(Asset asset) async {
 | 
				
			||||||
    await shareAssets([asset]);
 | 
					    await shareAssets([asset]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> shareAssets(List<AssetResponseDto> assets) async {
 | 
					  Future<void> shareAssets(List<Asset> assets) async {
 | 
				
			||||||
    final downloadedFilePaths = assets.map((asset) async {
 | 
					    final downloadedFilePaths = assets.map((asset) async {
 | 
				
			||||||
      final res = await _apiService.assetApi.downloadFileWithHttpInfo(
 | 
					      if (asset.isRemote) {
 | 
				
			||||||
        asset.deviceAssetId,
 | 
					        final tempDir = await getTemporaryDirectory();
 | 
				
			||||||
        asset.deviceId,
 | 
					        final fileName = basename(asset.remote!.originalPath);
 | 
				
			||||||
        isThumb: false,
 | 
					        final tempFile = await File('${tempDir.path}/$fileName').create();
 | 
				
			||||||
        isWeb: false,
 | 
					        final res = await _apiService.assetApi.downloadFileWithHttpInfo(
 | 
				
			||||||
      );
 | 
					          asset.remote!.deviceAssetId,
 | 
				
			||||||
 | 
					          asset.remote!.deviceId,
 | 
				
			||||||
      final fileName = p.basename(asset.originalPath);
 | 
					          isThumb: false,
 | 
				
			||||||
 | 
					          isWeb: false,
 | 
				
			||||||
      final tempDir = await getTemporaryDirectory();
 | 
					        );
 | 
				
			||||||
      final tempFile = await File('${tempDir.path}/$fileName').create();
 | 
					        tempFile.writeAsBytesSync(res.bodyBytes);
 | 
				
			||||||
      tempFile.writeAsBytesSync(res.bodyBytes);
 | 
					        return tempFile.path;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
      return tempFile.path;
 | 
					        File? f = await asset.local!.file;
 | 
				
			||||||
 | 
					        return f!.path;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Share.shareFiles(
 | 
					    Share.shareFiles(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										96
									
								
								mobile/lib/shared/ui/immich_image.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								mobile/lib/shared/ui/immich_image.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:hive_flutter/hive_flutter.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/constants/hive_box.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Renders an Asset using local data if available, else remote data
 | 
				
			||||||
 | 
					class ImmichImage extends StatelessWidget {
 | 
				
			||||||
 | 
					  const ImmichImage(
 | 
				
			||||||
 | 
					    this.asset, {
 | 
				
			||||||
 | 
					    required this.width,
 | 
				
			||||||
 | 
					    required this.height,
 | 
				
			||||||
 | 
					    this.useGrayBoxPlaceholder = false,
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  final Asset asset;
 | 
				
			||||||
 | 
					  final bool useGrayBoxPlaceholder;
 | 
				
			||||||
 | 
					  final double width;
 | 
				
			||||||
 | 
					  final double height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    if (asset.isLocal) {
 | 
				
			||||||
 | 
					      return Image(
 | 
				
			||||||
 | 
					        image: AssetEntityImageProvider(
 | 
				
			||||||
 | 
					          asset.local!,
 | 
				
			||||||
 | 
					          isOriginal: false,
 | 
				
			||||||
 | 
					          thumbnailSize: const ThumbnailSize.square(250), // like server thumbs
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        width: width,
 | 
				
			||||||
 | 
					        height: height,
 | 
				
			||||||
 | 
					        fit: BoxFit.cover,
 | 
				
			||||||
 | 
					        frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
 | 
				
			||||||
 | 
					          if (wasSynchronouslyLoaded || frame != null) {
 | 
				
			||||||
 | 
					            return child;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return (useGrayBoxPlaceholder
 | 
				
			||||||
 | 
					              ? const SizedBox.square(
 | 
				
			||||||
 | 
					                  dimension: 250,
 | 
				
			||||||
 | 
					                  child: DecoratedBox(
 | 
				
			||||||
 | 
					                    decoration: BoxDecoration(color: Colors.grey),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : Transform.scale(
 | 
				
			||||||
 | 
					                  scale: 0.2,
 | 
				
			||||||
 | 
					                  child: const CircularProgressIndicator(),
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        errorBuilder: (context, error, stackTrace) {
 | 
				
			||||||
 | 
					          debugPrint("Error getting thumb for assetId=${asset.id}: $error");
 | 
				
			||||||
 | 
					          return Icon(
 | 
				
			||||||
 | 
					            Icons.image_not_supported_outlined,
 | 
				
			||||||
 | 
					            color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final String token = Hive.box(userInfoBox).get(accessTokenKey);
 | 
				
			||||||
 | 
					    final String thumbnailRequestUrl = getThumbnailUrl(asset.remote!);
 | 
				
			||||||
 | 
					    return CachedNetworkImage(
 | 
				
			||||||
 | 
					      imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
 | 
					      httpHeaders: {"Authorization": "Bearer $token"},
 | 
				
			||||||
 | 
					      cacheKey: 'thumbnail-image-${asset.id}',
 | 
				
			||||||
 | 
					      width: width,
 | 
				
			||||||
 | 
					      height: height,
 | 
				
			||||||
 | 
					      // keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
 | 
				
			||||||
 | 
					      // maxHeightDiskCache = null allows to simply store the webp thumbnail
 | 
				
			||||||
 | 
					      // from the server and use it for all rendered thumbnail sizes
 | 
				
			||||||
 | 
					      fit: BoxFit.cover,
 | 
				
			||||||
 | 
					      fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
 | 
					      progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
				
			||||||
 | 
					        if (useGrayBoxPlaceholder) {
 | 
				
			||||||
 | 
					          return const DecoratedBox(
 | 
				
			||||||
 | 
					            decoration: BoxDecoration(color: Colors.grey),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Transform.scale(
 | 
				
			||||||
 | 
					          scale: 0.2,
 | 
				
			||||||
 | 
					          child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					            value: downloadProgress.progress,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      errorWidget: (context, url, error) {
 | 
				
			||||||
 | 
					        debugPrint("Error getting thumbnail $url = $error");
 | 
				
			||||||
 | 
					        CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
				
			||||||
 | 
					        return Icon(
 | 
				
			||||||
 | 
					          Icons.image_not_supported_outlined,
 | 
				
			||||||
 | 
					          color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
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:openapi/api.dart';
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() {
 | 
					void main() {
 | 
				
			||||||
  final List<AssetResponseDto> testAssets = [];
 | 
					  final List<Asset> testAssets = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (int i = 0; i < 150; i++) {
 | 
					  for (int i = 0; i < 150; i++) {
 | 
				
			||||||
    int month = i ~/ 31;
 | 
					    int month = i ~/ 31;
 | 
				
			||||||
@ -11,39 +12,43 @@ void main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    DateTime date = DateTime(2022, month, day);
 | 
					    DateTime date = DateTime(2022, month, day);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    testAssets.add(AssetResponseDto(
 | 
					    testAssets.add(
 | 
				
			||||||
      type: AssetTypeEnum.IMAGE,
 | 
					      Asset.remote(
 | 
				
			||||||
      id: '$i',
 | 
					        AssetResponseDto(
 | 
				
			||||||
      deviceAssetId: '',
 | 
					          type: AssetTypeEnum.IMAGE,
 | 
				
			||||||
      ownerId: '',
 | 
					          id: '$i',
 | 
				
			||||||
      deviceId: '',
 | 
					          deviceAssetId: '',
 | 
				
			||||||
      originalPath: '',
 | 
					          ownerId: '',
 | 
				
			||||||
      resizePath: '',
 | 
					          deviceId: '',
 | 
				
			||||||
      createdAt: date.toIso8601String(),
 | 
					          originalPath: '',
 | 
				
			||||||
      modifiedAt: date.toIso8601String(),
 | 
					          resizePath: '',
 | 
				
			||||||
      isFavorite: false,
 | 
					          createdAt: date.toIso8601String(),
 | 
				
			||||||
      mimeType: 'image/jpeg',
 | 
					          modifiedAt: date.toIso8601String(),
 | 
				
			||||||
      duration: '',
 | 
					          isFavorite: false,
 | 
				
			||||||
      webpPath: '',
 | 
					          mimeType: 'image/jpeg',
 | 
				
			||||||
      encodedVideoPath: '',
 | 
					          duration: '',
 | 
				
			||||||
    ));
 | 
					          webpPath: '',
 | 
				
			||||||
 | 
					          encodedVideoPath: '',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Map<String, List<AssetResponseDto>> groups = {
 | 
					  final Map<String, List<Asset>> groups = {
 | 
				
			||||||
    '2022-01-05': testAssets.sublist(0, 5).map((e) {
 | 
					    '2022-01-05': testAssets.sublist(0, 5).map((e) {
 | 
				
			||||||
      e.createdAt = DateTime(2022, 1, 5).toIso8601String();
 | 
					      e.createdAt = DateTime(2022, 1, 5);
 | 
				
			||||||
      return e;
 | 
					      return e;
 | 
				
			||||||
    }).toList(),
 | 
					    }).toList(),
 | 
				
			||||||
    '2022-01-10': testAssets.sublist(5, 10).map((e) {
 | 
					    '2022-01-10': testAssets.sublist(5, 10).map((e) {
 | 
				
			||||||
      e.createdAt = DateTime(2022, 1, 10).toIso8601String();
 | 
					      e.createdAt = DateTime(2022, 1, 10);
 | 
				
			||||||
      return e;
 | 
					      return e;
 | 
				
			||||||
    }).toList(),
 | 
					    }).toList(),
 | 
				
			||||||
    '2022-02-17': testAssets.sublist(10, 15).map((e) {
 | 
					    '2022-02-17': testAssets.sublist(10, 15).map((e) {
 | 
				
			||||||
      e.createdAt = DateTime(2022, 2, 17).toIso8601String();
 | 
					      e.createdAt = DateTime(2022, 2, 17);
 | 
				
			||||||
      return e;
 | 
					      return e;
 | 
				
			||||||
    }).toList(),
 | 
					    }).toList(),
 | 
				
			||||||
    '2022-10-15': testAssets.sublist(15, 30).map((e) {
 | 
					    '2022-10-15': testAssets.sublist(15, 30).map((e) {
 | 
				
			||||||
      e.createdAt = DateTime(2022, 10, 15).toIso8601String();
 | 
					      e.createdAt = DateTime(2022, 10, 15);
 | 
				
			||||||
      return e;
 | 
					      return e;
 | 
				
			||||||
    }).toList()
 | 
					    }).toList()
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user