mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feature(mobile): sync assets, albums & users to local database on device (#1759)
* feature(mobile): sync assets, albums & users to local database on device * try to fix tests * move DB sync operations to new SyncService * clear db on user logout * fix reason for endless loading timeline * fix error when deleting album * fix thumbnail of device albums * add a few comments * fix Hive box not open in album service when loading local assets * adjust tests to int IDs * fix bug: show all albums when Recent is selected * update generated api * reworked Recents album isAll handling * guard against wrongly interleaved sync operations * fix: timeline asset ordering (sort asset state by created at) * fix: sort assets in albums by created at
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							8f11529a75
						
					
				
				
					commit
					8708867c1c
				
			@@ -1,37 +1,43 @@
 | 
			
		||||
import 'package:collection/collection.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_cache.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/store.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/user.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
 | 
			
		||||
import 'package:isar/isar.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumNotifier extends StateNotifier<List<Album>> {
 | 
			
		||||
  AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
 | 
			
		||||
  AlbumNotifier(this._albumService, this._db) : super([]);
 | 
			
		||||
  final AlbumService _albumService;
 | 
			
		||||
  final AlbumCacheService _albumCacheService;
 | 
			
		||||
 | 
			
		||||
  void _cacheState() {
 | 
			
		||||
    _albumCacheService.put(state);
 | 
			
		||||
  }
 | 
			
		||||
  final Isar _db;
 | 
			
		||||
 | 
			
		||||
  Future<void> getAllAlbums() async {
 | 
			
		||||
    if (await _albumCacheService.isValid() && state.isEmpty) {
 | 
			
		||||
      final albums = await _albumCacheService.get();
 | 
			
		||||
      if (albums != null) {
 | 
			
		||||
        state = albums;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final albums = await _albumService.getAlbums(isShared: false);
 | 
			
		||||
 | 
			
		||||
    if (albums != null) {
 | 
			
		||||
    final User me = Store.get(StoreKey.currentUser);
 | 
			
		||||
    List<Album> albums = await _db.albums
 | 
			
		||||
        .filter()
 | 
			
		||||
        .owner((q) => q.isarIdEqualTo(me.isarId))
 | 
			
		||||
        .findAll();
 | 
			
		||||
    if (!const ListEquality().equals(albums, state)) {
 | 
			
		||||
      state = albums;
 | 
			
		||||
    }
 | 
			
		||||
    await Future.wait([
 | 
			
		||||
      _albumService.refreshDeviceAlbums(),
 | 
			
		||||
      _albumService.refreshRemoteAlbums(isShared: false),
 | 
			
		||||
    ]);
 | 
			
		||||
    albums = await _db.albums
 | 
			
		||||
        .filter()
 | 
			
		||||
        .owner((q) => q.isarIdEqualTo(me.isarId))
 | 
			
		||||
        .findAll();
 | 
			
		||||
    if (!const ListEquality().equals(albums, state)) {
 | 
			
		||||
      state = albums;
 | 
			
		||||
      _cacheState();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void deleteAlbum(Album album) {
 | 
			
		||||
  Future<bool> deleteAlbum(Album album) async {
 | 
			
		||||
    state = state.where((a) => a.id != album.id).toList();
 | 
			
		||||
    _cacheState();
 | 
			
		||||
    return _albumService.deleteAlbum(album);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Album?> createAlbum(
 | 
			
		||||
@@ -39,20 +45,16 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
 | 
			
		||||
    Set<Asset> assets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    Album? album = await _albumService.createAlbum(albumTitle, assets, []);
 | 
			
		||||
 | 
			
		||||
    if (album != null) {
 | 
			
		||||
      state = [...state, album];
 | 
			
		||||
      _cacheState();
 | 
			
		||||
 | 
			
		||||
      return album;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
    return album;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final albumProvider = StateNotifierProvider<AlbumNotifier, List<Album>>((ref) {
 | 
			
		||||
  return AlbumNotifier(
 | 
			
		||||
    ref.watch(albumServiceProvider),
 | 
			
		||||
    ref.watch(albumCacheServiceProvider),
 | 
			
		||||
    ref.watch(dbProvider),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addNewAssets(List<Asset> assets) {
 | 
			
		||||
  void addNewAssets(Iterable<Asset> assets) {
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
      selectedNewAssetsForAlbum: {
 | 
			
		||||
        ...state.selectedNewAssetsForAlbum,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,18 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/material.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_cache.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/user.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
 | 
			
		||||
import 'package:isar/isar.dart';
 | 
			
		||||
 | 
			
		||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
 | 
			
		||||
  SharedAlbumNotifier(this._albumService, this._sharedAlbumCacheService)
 | 
			
		||||
      : super([]);
 | 
			
		||||
  SharedAlbumNotifier(this._albumService, this._db) : super([]);
 | 
			
		||||
 | 
			
		||||
  final AlbumService _albumService;
 | 
			
		||||
  final SharedAlbumCacheService _sharedAlbumCacheService;
 | 
			
		||||
 | 
			
		||||
  void _cacheState() {
 | 
			
		||||
    _sharedAlbumCacheService.put(state);
 | 
			
		||||
  }
 | 
			
		||||
  final Isar _db;
 | 
			
		||||
 | 
			
		||||
  Future<Album?> createSharedAlbum(
 | 
			
		||||
    String albumName,
 | 
			
		||||
@@ -23,7 +20,7 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
 | 
			
		||||
    Iterable<User> sharedUsers,
 | 
			
		||||
  ) async {
 | 
			
		||||
    try {
 | 
			
		||||
      var newAlbum = await _albumService.createAlbum(
 | 
			
		||||
      final Album? newAlbum = await _albumService.createAlbum(
 | 
			
		||||
        albumName,
 | 
			
		||||
        assets,
 | 
			
		||||
        sharedUsers,
 | 
			
		||||
@@ -31,61 +28,44 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
 | 
			
		||||
 | 
			
		||||
      if (newAlbum != null) {
 | 
			
		||||
        state = [...state, newAlbum];
 | 
			
		||||
        _cacheState();
 | 
			
		||||
        return newAlbum;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return newAlbum;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error createSharedAlbum  ${e.toString()}");
 | 
			
		||||
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> getAllSharedAlbums() async {
 | 
			
		||||
    if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
 | 
			
		||||
      final albums = await _sharedAlbumCacheService.get();
 | 
			
		||||
      if (albums != null) {
 | 
			
		||||
        state = albums;
 | 
			
		||||
      }
 | 
			
		||||
    var albums = await _db.albums.filter().sharedEqualTo(true).findAll();
 | 
			
		||||
    if (!const ListEquality().equals(albums, state)) {
 | 
			
		||||
      state = albums;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    List<Album>? sharedAlbums = await _albumService.getAlbums(isShared: true);
 | 
			
		||||
 | 
			
		||||
    if (sharedAlbums != null) {
 | 
			
		||||
      state = sharedAlbums;
 | 
			
		||||
      _cacheState();
 | 
			
		||||
    await _albumService.refreshRemoteAlbums(isShared: true);
 | 
			
		||||
    albums = await _db.albums.filter().sharedEqualTo(true).findAll();
 | 
			
		||||
    if (!const ListEquality().equals(albums, state)) {
 | 
			
		||||
      state = albums;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void deleteAlbum(Album album) {
 | 
			
		||||
  Future<bool> deleteAlbum(Album album) {
 | 
			
		||||
    state = state.where((a) => a.id != album.id).toList();
 | 
			
		||||
    _cacheState();
 | 
			
		||||
    return _albumService.deleteAlbum(album);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> leaveAlbum(Album album) async {
 | 
			
		||||
    var res = await _albumService.leaveAlbum(album);
 | 
			
		||||
 | 
			
		||||
    if (res) {
 | 
			
		||||
      state = state.where((a) => a.id != album.id).toList();
 | 
			
		||||
      _cacheState();
 | 
			
		||||
      await deleteAlbum(album);
 | 
			
		||||
      return true;
 | 
			
		||||
    } else {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> removeAssetFromAlbum(
 | 
			
		||||
    Album album,
 | 
			
		||||
    Iterable<Asset> assets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    var res = await _albumService.removeAssetFromAlbum(album, assets);
 | 
			
		||||
 | 
			
		||||
    if (res) {
 | 
			
		||||
      return true;
 | 
			
		||||
    } else {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
 | 
			
		||||
    return _albumService.removeAssetFromAlbum(album, assets);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -93,13 +73,15 @@ final sharedAlbumProvider =
 | 
			
		||||
    StateNotifierProvider<SharedAlbumNotifier, List<Album>>((ref) {
 | 
			
		||||
  return SharedAlbumNotifier(
 | 
			
		||||
    ref.watch(albumServiceProvider),
 | 
			
		||||
    ref.watch(sharedAlbumCacheServiceProvider),
 | 
			
		||||
    ref.watch(dbProvider),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final sharedAlbumDetailProvider =
 | 
			
		||||
    FutureProvider.autoDispose.family<Album?, String>((ref, albumId) async {
 | 
			
		||||
    FutureProvider.autoDispose.family<Album?, int>((ref, albumId) async {
 | 
			
		||||
  final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
 | 
			
		||||
 | 
			
		||||
  return await sharedAlbumService.getAlbumDetail(albumId);
 | 
			
		||||
  final Album? a = await sharedAlbumService.getAlbumDetail(albumId);
 | 
			
		||||
  await a?.loadSortedAssets();
 | 
			
		||||
  return a;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@ import 'package:immich_mobile/shared/models/user.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/user.service.dart';
 | 
			
		||||
 | 
			
		||||
final suggestedSharedUsersProvider =
 | 
			
		||||
    FutureProvider.autoDispose<List<User>>((ref) async {
 | 
			
		||||
    FutureProvider.autoDispose<List<User>>((ref) {
 | 
			
		||||
  UserService userService = ref.watch(userServiceProvider);
 | 
			
		||||
 | 
			
		||||
  return await userService.getAllUsers(isAll: false) ?? [];
 | 
			
		||||
  return userService.getUsersInDb();
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,129 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.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/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/store.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/user.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/api.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/sync.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/user.service.dart';
 | 
			
		||||
import 'package:isar/isar.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:photo_manager/photo_manager.dart';
 | 
			
		||||
 | 
			
		||||
final albumServiceProvider = Provider(
 | 
			
		||||
  (ref) => AlbumService(
 | 
			
		||||
    ref.watch(apiServiceProvider),
 | 
			
		||||
    ref.watch(userServiceProvider),
 | 
			
		||||
    ref.watch(backgroundServiceProvider),
 | 
			
		||||
    ref.watch(syncServiceProvider),
 | 
			
		||||
    ref.watch(dbProvider),
 | 
			
		||||
  ),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
class AlbumService {
 | 
			
		||||
  final ApiService _apiService;
 | 
			
		||||
  final UserService _userService;
 | 
			
		||||
  final BackgroundService _backgroundService;
 | 
			
		||||
  final SyncService _syncService;
 | 
			
		||||
  final Isar _db;
 | 
			
		||||
  Completer<bool> _localCompleter = Completer()..complete(false);
 | 
			
		||||
  Completer<bool> _remoteCompleter = Completer()..complete(false);
 | 
			
		||||
 | 
			
		||||
  AlbumService(this._apiService);
 | 
			
		||||
  AlbumService(
 | 
			
		||||
    this._apiService,
 | 
			
		||||
    this._userService,
 | 
			
		||||
    this._backgroundService,
 | 
			
		||||
    this._syncService,
 | 
			
		||||
    this._db,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<List<Album>?> getAlbums({required bool isShared}) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final dto = await _apiService.albumApi
 | 
			
		||||
          .getAllAlbums(shared: isShared ? isShared : null);
 | 
			
		||||
      return dto?.map(Album.remote).toList();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error getAllSharedAlbum  ${e.toString()}");
 | 
			
		||||
      return null;
 | 
			
		||||
  /// Checks all selected device albums for changes of albums and their assets
 | 
			
		||||
  /// Updates the local database and returns `true` if there were any changes
 | 
			
		||||
  Future<bool> refreshDeviceAlbums() async {
 | 
			
		||||
    if (!_localCompleter.isCompleted) {
 | 
			
		||||
      // guard against concurrent calls
 | 
			
		||||
      return _localCompleter.future;
 | 
			
		||||
    }
 | 
			
		||||
    _localCompleter = Completer();
 | 
			
		||||
    final Stopwatch sw = Stopwatch()..start();
 | 
			
		||||
    bool changes = false;
 | 
			
		||||
    try {
 | 
			
		||||
      if (!await _backgroundService.hasAccess) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      final HiveBackupAlbums? infos =
 | 
			
		||||
          (await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox))
 | 
			
		||||
              .get(backupInfoKey);
 | 
			
		||||
      if (infos == null) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      final List<AssetPathEntity> onDevice =
 | 
			
		||||
          await PhotoManager.getAssetPathList(
 | 
			
		||||
        hasAll: true,
 | 
			
		||||
        filterOption: FilterOptionGroup(containsPathModified: true),
 | 
			
		||||
      );
 | 
			
		||||
      if (infos.excludedAlbumsIds.isNotEmpty) {
 | 
			
		||||
        // remove all excluded albums
 | 
			
		||||
        onDevice.removeWhere((e) => infos.excludedAlbumsIds.contains(e.id));
 | 
			
		||||
      }
 | 
			
		||||
      final hasAll = infos.selectedAlbumIds
 | 
			
		||||
          .map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
 | 
			
		||||
          .whereNotNull()
 | 
			
		||||
          .any((a) => a.isAll);
 | 
			
		||||
      if (hasAll) {
 | 
			
		||||
        // remove the virtual "Recents" album and keep and individual albums
 | 
			
		||||
        onDevice.removeWhere((e) => e.isAll);
 | 
			
		||||
      } else {
 | 
			
		||||
        // keep only the explicitly selected albums
 | 
			
		||||
        onDevice.removeWhere((e) => !infos.selectedAlbumIds.contains(e.id));
 | 
			
		||||
      }
 | 
			
		||||
      changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice);
 | 
			
		||||
    } finally {
 | 
			
		||||
      _localCompleter.complete(changes);
 | 
			
		||||
    }
 | 
			
		||||
    debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
 | 
			
		||||
    return changes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Checks remote albums (owned if `isShared` is false) for changes,
 | 
			
		||||
  /// updates the local database and returns `true` if there were any changes
 | 
			
		||||
  Future<bool> refreshRemoteAlbums({required bool isShared}) async {
 | 
			
		||||
    if (!_remoteCompleter.isCompleted) {
 | 
			
		||||
      // guard against concurrent calls
 | 
			
		||||
      return _remoteCompleter.future;
 | 
			
		||||
    }
 | 
			
		||||
    _remoteCompleter = Completer();
 | 
			
		||||
    final Stopwatch sw = Stopwatch()..start();
 | 
			
		||||
    bool changes = false;
 | 
			
		||||
    try {
 | 
			
		||||
      await _userService.refreshUsers();
 | 
			
		||||
      final List<AlbumResponseDto>? serverAlbums = await _apiService.albumApi
 | 
			
		||||
          .getAllAlbums(shared: isShared ? true : null);
 | 
			
		||||
      if (serverAlbums == null) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      changes = await _syncService.syncRemoteAlbumsToDb(
 | 
			
		||||
        serverAlbums,
 | 
			
		||||
        isShared: isShared,
 | 
			
		||||
        loadDetails: (dto) async => dto.assetCount == dto.assets.length
 | 
			
		||||
            ? dto
 | 
			
		||||
            : (await _apiService.albumApi.getAlbumInfo(dto.id)) ?? dto,
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      _remoteCompleter.complete(changes);
 | 
			
		||||
    }
 | 
			
		||||
    debugPrint("refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms");
 | 
			
		||||
    return changes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Album?> createAlbum(
 | 
			
		||||
@@ -37,56 +132,51 @@ class AlbumService {
 | 
			
		||||
    Iterable<User> sharedUsers = const [],
 | 
			
		||||
  ]) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final dto = await _apiService.albumApi.createAlbum(
 | 
			
		||||
      AlbumResponseDto? remote = await _apiService.albumApi.createAlbum(
 | 
			
		||||
        CreateAlbumDto(
 | 
			
		||||
          albumName: albumName,
 | 
			
		||||
          assetIds: assets.map((asset) => asset.remoteId!).toList(),
 | 
			
		||||
          sharedWithUserIds: sharedUsers.map((e) => e.id).toList(),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      return dto != null ? Album.remote(dto) : null;
 | 
			
		||||
      if (remote != null) {
 | 
			
		||||
        Album album = await Album.remote(remote);
 | 
			
		||||
        await _db.writeTxn(() => _db.albums.store(album));
 | 
			
		||||
        return album;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error createSharedAlbum  ${e.toString()}");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
   * Creates names like Untitled, Untitled (1), Untitled (2), ...
 | 
			
		||||
   */
 | 
			
		||||
  String _getNextAlbumName(List<Album>? albums) {
 | 
			
		||||
  Future<String> _getNextAlbumName() async {
 | 
			
		||||
    const baseName = "Untitled";
 | 
			
		||||
    for (int round = 0;; round++) {
 | 
			
		||||
      final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
 | 
			
		||||
 | 
			
		||||
    if (albums != null) {
 | 
			
		||||
      for (int round = 0; round < albums.length; round++) {
 | 
			
		||||
        final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
 | 
			
		||||
 | 
			
		||||
        if (albums.where((a) => a.name == proposedName).isEmpty) {
 | 
			
		||||
          return proposedName;
 | 
			
		||||
        }
 | 
			
		||||
      if (null ==
 | 
			
		||||
          await _db.albums.filter().nameEqualTo(proposedName).findFirst()) {
 | 
			
		||||
        return proposedName;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return baseName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Album?> createAlbumWithGeneratedName(
 | 
			
		||||
    Iterable<Asset> assets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    return createAlbum(
 | 
			
		||||
      _getNextAlbumName(await getAlbums(isShared: false)),
 | 
			
		||||
      await _getNextAlbumName(),
 | 
			
		||||
      assets,
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Album?> getAlbumDetail(String albumId) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final dto = await _apiService.albumApi.getAlbumInfo(albumId);
 | 
			
		||||
      return dto != null ? Album.remote(dto) : null;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint('Error [getAlbumDetail] ${e.toString()}');
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  Future<Album?> getAlbumDetail(int albumId) {
 | 
			
		||||
    return _db.albums.get(albumId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
 | 
			
		||||
@@ -98,6 +188,10 @@ class AlbumService {
 | 
			
		||||
        album.remoteId!,
 | 
			
		||||
        AddAssetsDto(assetIds: assets.map((asset) => asset.remoteId!).toList()),
 | 
			
		||||
      );
 | 
			
		||||
      if (result != null && result.successfullyAdded > 0) {
 | 
			
		||||
        album.assets.addAll(assets);
 | 
			
		||||
        await _db.writeTxn(() => album.assets.save());
 | 
			
		||||
      }
 | 
			
		||||
      return result;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}");
 | 
			
		||||
@@ -110,26 +204,53 @@ class AlbumService {
 | 
			
		||||
    Album album,
 | 
			
		||||
  ) async {
 | 
			
		||||
    try {
 | 
			
		||||
      var result = await _apiService.albumApi.addUsersToAlbum(
 | 
			
		||||
      final result = await _apiService.albumApi.addUsersToAlbum(
 | 
			
		||||
        album.remoteId!,
 | 
			
		||||
        AddUsersDto(sharedUserIds: sharedUserIds),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return result != null;
 | 
			
		||||
      if (result != null) {
 | 
			
		||||
        album.sharedUsers
 | 
			
		||||
            .addAll((await _db.users.getAllById(sharedUserIds)).cast());
 | 
			
		||||
        await _db.writeTxn(() => album.sharedUsers.save());
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error addAdditionalUserToAlbum  ${e.toString()}");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> deleteAlbum(Album album) async {
 | 
			
		||||
    try {
 | 
			
		||||
      await _apiService.albumApi.deleteAlbum(album.remoteId!);
 | 
			
		||||
      final userId = Store.get<User>(StoreKey.currentUser)!.isarId;
 | 
			
		||||
      if (album.owner.value?.isarId == userId) {
 | 
			
		||||
        await _apiService.albumApi.deleteAlbum(album.remoteId!);
 | 
			
		||||
      }
 | 
			
		||||
      if (album.shared) {
 | 
			
		||||
        final foreignAssets =
 | 
			
		||||
            await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
 | 
			
		||||
        await _db.writeTxn(() => _db.albums.delete(album.id));
 | 
			
		||||
        final List<Album> albums =
 | 
			
		||||
            await _db.albums.filter().sharedEqualTo(true).findAll();
 | 
			
		||||
        final List<Asset> existing = [];
 | 
			
		||||
        for (Album a in albums) {
 | 
			
		||||
          existing.addAll(
 | 
			
		||||
            await a.assets.filter().not().ownerIdEqualTo(userId).findAll(),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        final List<int> idsToRemove =
 | 
			
		||||
            _syncService.sharedAssetsToRemove(foreignAssets, existing);
 | 
			
		||||
        if (idsToRemove.isNotEmpty) {
 | 
			
		||||
          await _db.writeTxn(() => _db.assets.deleteAll(idsToRemove));
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        await _db.writeTxn(() => _db.albums.delete(album.id));
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("Error deleteAlbum  ${e.toString()}");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> leaveAlbum(Album album) async {
 | 
			
		||||
@@ -153,6 +274,8 @@ class AlbumService {
 | 
			
		||||
          assetIds: assets.map((e) => e.remoteId!).toList(growable: false),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      album.assets.removeAll(assets);
 | 
			
		||||
      await _db.writeTxn(() => album.assets.update(unlink: assets));
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@@ -173,6 +296,7 @@ class AlbumService {
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      album.name = newAlbumTitle;
 | 
			
		||||
      await _db.writeTxn(() => _db.albums.put(album));
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +1,23 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/json_cache.dart';
 | 
			
		||||
 | 
			
		||||
class BaseAlbumCacheService extends JsonCache<List<Album>> {
 | 
			
		||||
  BaseAlbumCacheService(super.cacheFileName);
 | 
			
		||||
@Deprecated("only kept to remove its files after migration")
 | 
			
		||||
class _BaseAlbumCacheService extends JsonCache<List<Album>> {
 | 
			
		||||
  _BaseAlbumCacheService(super.cacheFileName);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void put(List<Album> data) {
 | 
			
		||||
    putRawData(data.map((e) => e.toJson()).toList());
 | 
			
		||||
  }
 | 
			
		||||
  void put(List<Album> data) {}
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<List<Album>?> get() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final mapList = await readRawData() as List<dynamic>;
 | 
			
		||||
 | 
			
		||||
      final responseData =
 | 
			
		||||
          mapList.map((e) => Album.fromJson(e)).whereNotNull().toList();
 | 
			
		||||
 | 
			
		||||
      return responseData;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      await invalidate();
 | 
			
		||||
      debugPrint(e.toString());
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  Future<List<Album>?> get() => Future.value(null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class AlbumCacheService extends BaseAlbumCacheService {
 | 
			
		||||
@Deprecated("only kept to remove its files after migration")
 | 
			
		||||
class AlbumCacheService extends _BaseAlbumCacheService {
 | 
			
		||||
  AlbumCacheService() : super("album_cache");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SharedAlbumCacheService extends BaseAlbumCacheService {
 | 
			
		||||
@Deprecated("only kept to remove its files after migration")
 | 
			
		||||
class SharedAlbumCacheService extends _BaseAlbumCacheService {
 | 
			
		||||
  SharedAlbumCacheService() : super("shared_album_cache");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final albumCacheServiceProvider = Provider(
 | 
			
		||||
  (ref) => AlbumCacheService(),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
final sharedAlbumCacheServiceProvider = Provider(
 | 
			
		||||
  (ref) => SharedAlbumCacheService(),
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final albums = ref.watch(albumProvider);
 | 
			
		||||
    final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
 | 
			
		||||
    final albumService = ref.watch(albumServiceProvider);
 | 
			
		||||
    final sharedAlbums = ref.watch(sharedAlbumProvider);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumThumbnailCard extends StatelessWidget {
 | 
			
		||||
  final Function()? onTap;
 | 
			
		||||
@@ -20,7 +16,6 @@ class AlbumThumbnailCard extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    var box = Hive.box(userInfoBox);
 | 
			
		||||
    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
 | 
			
		||||
    return LayoutBuilder(
 | 
			
		||||
      builder: (context, constraints) {
 | 
			
		||||
@@ -42,21 +37,11 @@ class AlbumThumbnailCard extends StatelessWidget {
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        buildAlbumThumbnail() {
 | 
			
		||||
          return CachedNetworkImage(
 | 
			
		||||
            width: cardSize,
 | 
			
		||||
            height: cardSize,
 | 
			
		||||
            fit: BoxFit.cover,
 | 
			
		||||
            fadeInDuration: const Duration(milliseconds: 200),
 | 
			
		||||
            imageUrl: getAlbumThumbnailUrl(
 | 
			
		||||
              album,
 | 
			
		||||
              type: ThumbnailFormat.JPEG,
 | 
			
		||||
            ),
 | 
			
		||||
            httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
			
		||||
            cacheKey:
 | 
			
		||||
                getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        buildAlbumThumbnail() => ImmichImage(
 | 
			
		||||
              album.thumbnail.value,
 | 
			
		||||
              width: cardSize,
 | 
			
		||||
              height: cardSize,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        return GestureDetector(
 | 
			
		||||
          onTap: onTap,
 | 
			
		||||
@@ -72,7 +57,7 @@ class AlbumThumbnailCard extends StatelessWidget {
 | 
			
		||||
                      height: cardSize,
 | 
			
		||||
                      child: ClipRRect(
 | 
			
		||||
                        borderRadius: BorderRadius.circular(20),
 | 
			
		||||
                        child: album.albumThumbnailAssetId == null
 | 
			
		||||
                        child: album.thumbnail.value == null
 | 
			
		||||
                            ? buildEmptyThumbnail()
 | 
			
		||||
                            : buildAlbumThumbnail(),
 | 
			
		||||
                      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
 | 
			
		||||
          children: [
 | 
			
		||||
            ClipRRect(
 | 
			
		||||
              borderRadius: BorderRadius.circular(8),
 | 
			
		||||
              child: album.albumThumbnailAssetId == null
 | 
			
		||||
              child: album.thumbnail.value == null
 | 
			
		||||
                  ? buildEmptyThumbnail()
 | 
			
		||||
                  : buildAlbumThumbnail(),
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
			
		||||
@@ -35,19 +34,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
    void onDeleteAlbumPressed() async {
 | 
			
		||||
      ImmichLoadingOverlayController.appLoader.show();
 | 
			
		||||
 | 
			
		||||
      bool isSuccess = await ref.watch(albumServiceProvider).deleteAlbum(album);
 | 
			
		||||
 | 
			
		||||
      if (isSuccess) {
 | 
			
		||||
        if (album.shared) {
 | 
			
		||||
          ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
 | 
			
		||||
          AutoRouter.of(context)
 | 
			
		||||
              .navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
        } else {
 | 
			
		||||
          ref.watch(albumProvider.notifier).deleteAlbum(album);
 | 
			
		||||
          AutoRouter.of(context)
 | 
			
		||||
              .navigate(const TabControllerRoute(children: [LibraryRoute()]));
 | 
			
		||||
        }
 | 
			
		||||
      final bool success;
 | 
			
		||||
      if (album.shared) {
 | 
			
		||||
        success =
 | 
			
		||||
            await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
 | 
			
		||||
        AutoRouter.of(context)
 | 
			
		||||
            .navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
      } else {
 | 
			
		||||
        success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
 | 
			
		||||
        AutoRouter.of(context)
 | 
			
		||||
            .navigate(const TabControllerRoute(children: [LibraryRoute()]));
 | 
			
		||||
      }
 | 
			
		||||
      if (!success) {
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
          msg: "album_viewer_appbar_share_err_delete".tr(),
 | 
			
		||||
@@ -208,11 +206,12 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
          : null,
 | 
			
		||||
      centerTitle: false,
 | 
			
		||||
      actions: [
 | 
			
		||||
        IconButton(
 | 
			
		||||
          splashRadius: 25,
 | 
			
		||||
          onPressed: buildBottomSheet,
 | 
			
		||||
          icon: const Icon(Icons.more_horiz_rounded),
 | 
			
		||||
        ),
 | 
			
		||||
        if (album.isRemote)
 | 
			
		||||
          IconButton(
 | 
			
		||||
            splashRadius: 25,
 | 
			
		||||
            onPressed: buildBottomSheet,
 | 
			
		||||
            icon: const Icon(Icons.more_horiz_rounded),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/favorite/providers/favorite_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/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
			
		||||
@@ -22,7 +21,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
			
		||||
    final selectedAssetsInAlbumViewer =
 | 
			
		||||
        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
			
		||||
    final isMultiSelectionEnable =
 | 
			
		||||
@@ -88,7 +86,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
			
		||||
        bottom: 5,
 | 
			
		||||
        child: Icon(
 | 
			
		||||
          asset.isRemote
 | 
			
		||||
              ? (deviceId == asset.deviceId
 | 
			
		||||
              ? (asset.isLocal
 | 
			
		||||
                  ? Icons.cloud_done_outlined
 | 
			
		||||
                  : Icons.cloud_outlined)
 | 
			
		||||
              : Icons.cloud_off_outlined,
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegat
 | 
			
		||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
  final String albumId;
 | 
			
		||||
  final int albumId;
 | 
			
		||||
 | 
			
		||||
  const AlbumViewerPage({Key? key, required this.albumId}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
@@ -101,7 +101,7 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
    Widget buildTitle(Album album) {
 | 
			
		||||
      return Padding(
 | 
			
		||||
        padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
 | 
			
		||||
        child: userId == album.ownerId
 | 
			
		||||
        child: userId == album.ownerId && album.isRemote
 | 
			
		||||
            ? AlbumViewerEditableTitle(
 | 
			
		||||
                album: album,
 | 
			
		||||
                titleFocusNode: titleFocusNode,
 | 
			
		||||
@@ -122,9 +122,10 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
    Widget buildAlbumDateRange(Album album) {
 | 
			
		||||
      final DateTime startDate = album.assets.first.fileCreatedAt;
 | 
			
		||||
      final DateTime endDate = album.assets.last.fileCreatedAt; //Need default.
 | 
			
		||||
      final String startDateText =
 | 
			
		||||
          (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd())
 | 
			
		||||
              .format(startDate);
 | 
			
		||||
      final String startDateText = (startDate.year == endDate.year
 | 
			
		||||
              ? DateFormat.MMMd()
 | 
			
		||||
              : DateFormat.yMMMd())
 | 
			
		||||
          .format(startDate);
 | 
			
		||||
      final String endDateText = DateFormat.yMMMd().format(endDate);
 | 
			
		||||
 | 
			
		||||
      return Padding(
 | 
			
		||||
@@ -188,7 +189,7 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
      final bool showStorageIndicator =
 | 
			
		||||
          appSettingService.getSetting(AppSettingsEnum.storageIndicator);
 | 
			
		||||
 | 
			
		||||
      if (album.assets.isNotEmpty) {
 | 
			
		||||
      if (album.sortedAssets.isNotEmpty) {
 | 
			
		||||
        return SliverPadding(
 | 
			
		||||
          padding: const EdgeInsets.only(top: 10.0),
 | 
			
		||||
          sliver: SliverGrid(
 | 
			
		||||
@@ -201,8 +202,8 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
            delegate: SliverChildBuilderDelegate(
 | 
			
		||||
              (BuildContext context, int index) {
 | 
			
		||||
                return AlbumViewerThumbnail(
 | 
			
		||||
                  asset: album.assets[index],
 | 
			
		||||
                  assetList: album.assets,
 | 
			
		||||
                  asset: album.sortedAssets[index],
 | 
			
		||||
                  assetList: album.sortedAssets,
 | 
			
		||||
                  showStorageIndicator: showStorageIndicator,
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
@@ -267,17 +268,18 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
              controller: scrollController,
 | 
			
		||||
              slivers: [
 | 
			
		||||
                buildHeader(album),
 | 
			
		||||
                SliverPersistentHeader(
 | 
			
		||||
                  pinned: true,
 | 
			
		||||
                  delegate: ImmichSliverPersistentAppBarDelegate(
 | 
			
		||||
                    minHeight: 50,
 | 
			
		||||
                    maxHeight: 50,
 | 
			
		||||
                    child: Container(
 | 
			
		||||
                      color: Theme.of(context).scaffoldBackgroundColor,
 | 
			
		||||
                      child: buildControlButton(album),
 | 
			
		||||
                if (album.isRemote)
 | 
			
		||||
                  SliverPersistentHeader(
 | 
			
		||||
                    pinned: true,
 | 
			
		||||
                    delegate: ImmichSliverPersistentAppBarDelegate(
 | 
			
		||||
                      minHeight: 50,
 | 
			
		||||
                      maxHeight: 50,
 | 
			
		||||
                      child: Container(
 | 
			
		||||
                        color: Theme.of(context).scaffoldBackgroundColor,
 | 
			
		||||
                        child: buildControlButton(album),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                SliverSafeArea(
 | 
			
		||||
                  sliver: buildImageGrid(album),
 | 
			
		||||
                ),
 | 
			
		||||
 
 | 
			
		||||
@@ -44,9 +44,13 @@ class LibraryPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    List<Album> sortedAlbums() {
 | 
			
		||||
      if (selectedAlbumSortOrder.value == 0) {
 | 
			
		||||
        return albums.sortedBy((album) => album.createdAt).reversed.toList();
 | 
			
		||||
        return albums
 | 
			
		||||
            .where((a) => a.isRemote)
 | 
			
		||||
            .sortedBy((album) => album.createdAt)
 | 
			
		||||
            .reversed
 | 
			
		||||
            .toList();
 | 
			
		||||
      }
 | 
			
		||||
      return albums.sortedBy((album) => album.name);
 | 
			
		||||
      return albums.where((a) => a.isRemote).sortedBy((album) => album.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget buildSortButton() {
 | 
			
		||||
@@ -194,6 +198,8 @@ class LibraryPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    final sorted = sortedAlbums();
 | 
			
		||||
 | 
			
		||||
    final local = albums.where((a) => a.isLocal).toList();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: buildAppBar(),
 | 
			
		||||
      body: CustomScrollView(
 | 
			
		||||
@@ -270,6 +276,47 @@ class LibraryPage extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          SliverToBoxAdapter(
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(
 | 
			
		||||
                top: 12.0,
 | 
			
		||||
                left: 12.0,
 | 
			
		||||
                right: 12.0,
 | 
			
		||||
                bottom: 20.0,
 | 
			
		||||
              ),
 | 
			
		||||
              child: Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                children: [
 | 
			
		||||
                  const Text(
 | 
			
		||||
                    'library_page_device_albums',
 | 
			
		||||
                    style: TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                  ).tr(),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          SliverPadding(
 | 
			
		||||
            padding: const EdgeInsets.all(12.0),
 | 
			
		||||
            sliver: SliverGrid(
 | 
			
		||||
              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
			
		||||
                maxCrossAxisExtent: 250,
 | 
			
		||||
                mainAxisSpacing: 12,
 | 
			
		||||
                crossAxisSpacing: 12,
 | 
			
		||||
                childAspectRatio: .7,
 | 
			
		||||
              ),
 | 
			
		||||
              delegate: SliverChildBuilderDelegate(
 | 
			
		||||
                childCount: local.length,
 | 
			
		||||
                (context, index) => AlbumThumbnailCard(
 | 
			
		||||
                  album: local[index],
 | 
			
		||||
                  onTap: () => AutoRouter.of(context).push(
 | 
			
		||||
                    AlbumViewerRoute(
 | 
			
		||||
                      albumId: local[index].id,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,19 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/album.dart';
 | 
			
		||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
			
		||||
 | 
			
		||||
class SharingPage extends HookConsumerWidget {
 | 
			
		||||
  const SharingPage({Key? key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    var box = Hive.box(userInfoBox);
 | 
			
		||||
    final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
 | 
			
		||||
 | 
			
		||||
    useEffect(
 | 
			
		||||
@@ -39,16 +35,10 @@ class SharingPage extends HookConsumerWidget {
 | 
			
		||||
                  const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
 | 
			
		||||
              leading: ClipRRect(
 | 
			
		||||
                borderRadius: BorderRadius.circular(8),
 | 
			
		||||
                child: CachedNetworkImage(
 | 
			
		||||
                child: ImmichImage(
 | 
			
		||||
                  album.thumbnail.value,
 | 
			
		||||
                  width: 60,
 | 
			
		||||
                  height: 60,
 | 
			
		||||
                  fit: BoxFit.cover,
 | 
			
		||||
                  imageUrl: getAlbumThumbnailUrl(album),
 | 
			
		||||
                  cacheKey: getAlbumThumbNailCacheKey(album),
 | 
			
		||||
                  httpHeaders: {
 | 
			
		||||
                    "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
			
		||||
                  },
 | 
			
		||||
                  fadeInDuration: const Duration(milliseconds: 200),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              title: Text(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user