refactor(mobile): services and providers (#9232)

* refactor(mobile): services and provider

* providers
This commit is contained in:
Alex
2024-05-02 15:59:14 -05:00
committed by GitHub
parent ec4eb7cd19
commit c1253663b7
242 changed files with 497 additions and 503 deletions
@@ -1,78 +0,0 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart';
class AlbumNotifier extends StateNotifier<List<Album>> {
AlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums
.filter()
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService _albumService;
late final StreamSubscription<List<Album>> _streamSub;
Future<void> getAllAlbums() => Future.wait([
_albumService.refreshDeviceAlbums(),
_albumService.refreshRemoteAlbums(isShared: false),
]);
Future<void> getDeviceAlbums() => _albumService.refreshDeviceAlbums();
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
Future<Album?> createAlbum(
String albumTitle,
Set<Asset> assets,
) =>
_albumService.createAlbum(albumTitle, assets, []);
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final albumProvider =
StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
return AlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
);
});
final albumWatcher =
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
final db = ref.watch(dbProvider);
final a = await db.albums.get(albumId);
if (a != null) yield a;
await for (final a in db.albums.watchObject(albumId, fireImmediately: true)) {
if (a != null) yield a;
}
});
final albumRenderlistProvider =
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
final album = ref.watch(albumWatcher(albumId)).value;
if (album != null) {
final query =
album.assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc();
return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
}
return const Stream.empty();
});
@@ -1,131 +0,0 @@
import 'package:collection/collection.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/entities/album.entity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'album_sort_by_options.provider.g.dart';
typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
class _AlbumSortHandlers {
const _AlbumSortHandlers._();
static const AlbumSortFn created = _sortByCreated;
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
final sorted = albums.sortedBy((album) => album.createdAt);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn title = _sortByTitle;
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
final sorted = albums.sortedBy((album) => album.name);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn lastModified = _sortByLastModified;
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
final sorted = albums.sortedBy((album) => album.modifiedAt);
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn assetCount = _sortByAssetCount;
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
final sorted =
albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn mostRecent = _sortByMostRecent;
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
final sorted = albums.sorted((a, b) {
if (a.endDate != null && b.endDate != null) {
return a.endDate!.compareTo(b.endDate!);
}
if (a.endDate == null) return 1;
if (b.endDate == null) return -1;
return 0;
});
return (isReverse ? sorted.reversed : sorted).toList();
}
static const AlbumSortFn mostOldest = _sortByMostOldest;
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
final sorted = albums.sorted((a, b) {
if (a.startDate != null && b.startDate != null) {
return a.startDate!.compareTo(b.startDate!);
}
if (a.startDate == null) return 1;
if (b.startDate == null) return -1;
return 0;
});
return (isReverse ? sorted.reversed : sorted).toList();
}
}
// Store index allows us to re-arrange the values without affecting the saved prefs
enum AlbumSortMode {
title(1, "library_page_sort_title", _AlbumSortHandlers.title),
assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount),
lastModified(
3,
"library_page_sort_last_modified",
_AlbumSortHandlers.lastModified,
),
created(0, "library_page_sort_created", _AlbumSortHandlers.created),
mostRecent(
2,
"library_page_sort_most_recent_photo",
_AlbumSortHandlers.mostRecent,
),
mostOldest(
5,
"library_page_sort_most_oldest_photo",
_AlbumSortHandlers.mostOldest,
);
final int storeIndex;
final String label;
final AlbumSortFn sortFn;
const AlbumSortMode(this.storeIndex, this.label, this.sortFn);
}
@riverpod
class AlbumSortByOptions extends _$AlbumSortByOptions {
@override
AlbumSortMode build() {
final sortOpt = ref
.watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.selectedAlbumSortOrder);
return AlbumSortMode.values.firstWhere(
(e) => e.storeIndex == sortOpt,
orElse: () => AlbumSortMode.title,
);
}
void changeSortMode(AlbumSortMode sortOption) {
state = sortOption;
ref.watch(appSettingsServiceProvider).setSetting(
AppSettingsEnum.selectedAlbumSortOrder,
sortOption.storeIndex,
);
}
}
@riverpod
class AlbumSortOrder extends _$AlbumSortOrder {
@override
bool build() {
return ref
.watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.selectedAlbumSortReverse);
}
void changeSortDirection(bool isReverse) {
state = isReverse;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse);
}
}
@@ -1,43 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'album_sort_by_options.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$albumSortByOptionsHash() =>
r'dd8da5e730af555de1b86c3b157b6c93183523ac';
/// See also [AlbumSortByOptions].
@ProviderFor(AlbumSortByOptions)
final albumSortByOptionsProvider =
AutoDisposeNotifierProvider<AlbumSortByOptions, AlbumSortMode>.internal(
AlbumSortByOptions.new,
name: r'albumSortByOptionsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$albumSortByOptionsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AlbumSortByOptions = AutoDisposeNotifier<AlbumSortMode>;
String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440';
/// See also [AlbumSortOrder].
@ProviderFor(AlbumSortOrder)
final albumSortOrderProvider =
AutoDisposeNotifierProvider<AlbumSortOrder, bool>.internal(
AlbumSortOrder.new,
name: r'albumSortOrderProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$albumSortOrderHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -1,17 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
class AlbumTitleNotifier extends StateNotifier<String> {
AlbumTitleNotifier() : super("");
setAlbumTitle(String title) {
state = title;
}
clearAlbumTitle() {
state = "";
}
}
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
(ref) => AlbumTitleNotifier(),
);
@@ -1,56 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.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/entities/album.entity.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref)
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
final Ref ref;
void enableEditAlbum() {
state = state.copyWith(isEditAlbum: true);
}
void disableEditAlbum() {
state = state.copyWith(isEditAlbum: false);
}
void setEditTitleText(String newTitle) {
state = state.copyWith(editTitleText: newTitle);
}
void remoteEditTitleText() {
state = state.copyWith(editTitleText: "");
}
void resetState() {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
}
Future<bool> changeAlbumTitle(
Album album,
String newAlbumTitle,
) async {
AlbumService service = ref.watch(albumServiceProvider);
bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle);
if (isSuccess) {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return true;
}
state = state.copyWith(editTitleText: "", isEditAlbum: false);
return false;
}
}
final albumViewerProvider =
StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
return AlbumViewerNotifier(ref);
});
@@ -1,15 +0,0 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'current_album.provider.g.dart';
@riverpod
class CurrentAlbum extends _$CurrentAlbum {
@override
Album? build() => null;
void set(Album? a) => state = a;
}
/// Mock class for testing
abstract class CurrentAlbumInternal extends _$CurrentAlbum {}
@@ -1,25 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'current_album.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$currentAlbumHash() => r'61f00273d6b69da45add1532cc3d3a076ee55110';
/// See also [CurrentAlbum].
@ProviderFor(CurrentAlbum)
final currentAlbumProvider =
AutoDisposeNotifierProvider<CurrentAlbum, Album?>.internal(
CurrentAlbum.new,
name: r'currentAlbumProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$currentAlbumHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -1,90 +0,0 @@
import 'dart:async';
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/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService _albumService;
late final StreamSubscription<List<Album>> _streamSub;
Future<Album?> createSharedAlbum(
String albumName,
Iterable<Asset> assets,
Iterable<User> sharedUsers,
) async {
try {
return await _albumService.createAlbum(
albumName,
assets,
sharedUsers,
);
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
}
return null;
}
Future<void> getAllSharedAlbums() =>
_albumService.refreshRemoteAlbums(isShared: true);
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
Future<bool> leaveAlbum(Album album) async {
var res = await _albumService.leaveAlbum(album);
if (res) {
await deleteAlbum(album);
return true;
} else {
return false;
}
}
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
return _albumService.removeAssetFromAlbum(album, assets);
}
Future<bool> removeUserFromAlbum(Album album, User user) async {
final result = await _albumService.removeUserFromAlbum(album, user);
if (result && album.sharedUsers.isEmpty) {
state = state.where((element) => element.id != album.id).toList();
}
return result;
}
Future<bool> setActivityEnabled(Album album, bool activityEnabled) {
return _albumService.setActivityEnabled(album, activityEnabled);
}
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final sharedAlbumProvider =
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
return SharedAlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
);
});
@@ -1,9 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
UserService userService = ref.watch(userServiceProvider);
return userService.getUsersInDb();
});
@@ -1,433 +0,0 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.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:logging/logging.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(syncServiceProvider),
ref.watch(dbProvider),
ref.watch(backupServiceProvider),
),
);
class AlbumService {
final ApiService _apiService;
final UserService _userService;
final SyncService _syncService;
final Isar _db;
final BackupService _backupService;
final Logger _log = Logger('AlbumService');
Completer<bool> _localCompleter = Completer()..complete(false);
Completer<bool> _remoteCompleter = Completer()..complete(false);
AlbumService(
this._apiService,
this._userService,
this._syncService,
this._db,
this._backupService,
);
/// 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
_log.info("refreshDeviceAlbums is already in progress");
return _localCompleter.future;
}
_localCompleter = Completer();
final Stopwatch sw = Stopwatch()..start();
bool changes = false;
try {
final List<String> excludedIds =
await _backupService.excludedAlbumsQuery().idProperty().findAll();
final List<String> selectedIds =
await _backupService.selectedAlbumsQuery().idProperty().findAll();
if (selectedIds.isEmpty) {
final numLocal = await _db.albums.where().localIdIsNotNull().count();
if (numLocal > 0) {
_syncService.removeAllLocalAlbumsAndAssets();
}
return false;
}
final List<AssetPathEntity> onDevice =
await PhotoManager.getAssetPathList(
hasAll: true,
filterOption: FilterOptionGroup(containsPathModified: true),
);
_log.info("Found ${onDevice.length} device albums");
Set<String>? excludedAssets;
if (excludedIds.isNotEmpty) {
if (Platform.isIOS) {
// iOS and Android device album working principle differ significantly
// on iOS, an asset can be in multiple albums
// on Android, an asset can only be in exactly one album (folder!) at the same time
// thus, on Android, excluding an album can be done by ignoring that album
// however, on iOS, it it necessary to load the assets from all excluded
// albums and check every asset from any selected album against the set
// of excluded assets
excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds);
_log.info("Found ${excludedAssets.length} assets to exclude");
}
// remove all excluded albums
onDevice.removeWhere((e) => excludedIds.contains(e.id));
_log.info(
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
);
}
final hasAll = selectedIds
.map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
.whereNotNull()
.any((a) => a.isAll);
if (hasAll) {
if (Platform.isAndroid) {
// remove the virtual "Recent" album and keep and individual albums
// on Android, the virtual "Recent" `lastModified` value is always null
onDevice.removeWhere((e) => e.isAll);
_log.info("'Recents' is selected, keeping all individual albums");
}
} else {
// keep only the explicitly selected albums
onDevice.removeWhere((e) => !selectedIds.contains(e.id));
_log.info("'Recents' is not selected, keeping only selected albums");
}
changes =
await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets);
_log.info("Syncing completed. Changes: $changes");
} finally {
_localCompleter.complete(changes);
}
debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
return changes;
}
Future<Set<String>> _loadExcludedAssetIds(
List<AssetPathEntity> albums,
List<String> excludedAlbumIds,
) async {
final Set<String> result = HashSet<String>();
for (AssetPathEntity a in albums) {
if (excludedAlbumIds.contains(a.id)) {
final List<AssetEntity> assets =
await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
result.addAll(assets.map((e) => e.id));
}
}
return result;
}
/// 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(
String albumName,
Iterable<Asset> assets, [
Iterable<User> sharedUsers = const [],
]) async {
try {
AlbumResponseDto? remote = await _apiService.albumApi.createAlbum(
CreateAlbumDto(
albumName: albumName,
assetIds: assets.map((asset) => asset.remoteId!).toList(),
sharedWithUserIds: sharedUsers.map((e) => e.id).toList(),
),
);
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;
}
/*
* Creates names like Untitled, Untitled (1), Untitled (2), ...
*/
Future<String> _getNextAlbumName() async {
const baseName = "Untitled";
for (int round = 0;; round++) {
final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
if (null ==
await _db.albums.filter().nameEqualTo(proposedName).findFirst()) {
return proposedName;
}
}
}
Future<Album?> createAlbumWithGeneratedName(
Iterable<Asset> assets,
) async {
return createAlbum(
await _getNextAlbumName(),
assets,
[],
);
}
Future<AlbumAddAssetsResponse?> addAdditionalAssetToAlbum(
Iterable<Asset> assets,
Album album,
) async {
try {
var response = await _apiService.albumApi.addAssetsToAlbum(
album.remoteId!,
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
);
if (response != null) {
List<Asset> successAssets = [];
List<String> duplicatedAssets = [];
for (final result in response) {
if (result.success) {
successAssets
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
} else if (!result.success &&
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
duplicatedAssets.add(result.id);
}
}
await _updateAssets(album.id, add: successAssets);
return AlbumAddAssetsResponse(
alreadyInAlbum: duplicatedAssets,
successfullyAdded: successAssets.length,
);
}
} catch (e) {
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
}
return null;
}
Future<void> _updateAssets(
int albumId, {
Iterable<Asset> add = const [],
Iterable<Asset> remove = const [],
}) {
return _db.writeTxn(() async {
final album = await _db.albums.get(albumId);
if (album == null) return;
await album.assets.update(link: add, unlink: remove);
album.startDate =
await album.assets.filter().fileCreatedAtProperty().min();
album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
album.lastModifiedAssetTimestamp =
await album.assets.filter().updatedAtProperty().max();
await _db.albums.put(album);
});
}
Future<bool> addAdditionalUserToAlbum(
List<String> sharedUserIds,
Album album,
) async {
try {
final result = await _apiService.albumApi.addUsersToAlbum(
album.remoteId!,
AddUsersDto(sharedUserIds: sharedUserIds),
);
if (result != null) {
album.sharedUsers
.addAll((await _db.users.getAllById(sharedUserIds)).cast());
album.shared = result.shared;
await _db.writeTxn(() async {
await _db.albums.put(album);
await album.sharedUsers.save();
});
return true;
}
} catch (e) {
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
}
return false;
}
Future<bool> setActivityEnabled(Album album, bool enabled) async {
try {
final result = await _apiService.albumApi.updateAlbumInfo(
album.remoteId!,
UpdateAlbumDto(isActivityEnabled: enabled),
);
if (result != null) {
album.activityEnabled = enabled;
await _db.writeTxn(() => _db.albums.put(album));
return true;
}
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
}
return false;
}
Future<bool> deleteAlbum(Album album) async {
try {
final userId = Store.get(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;
}
Future<bool> leaveAlbum(Album album) async {
try {
await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
return true;
} catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}");
return false;
}
}
Future<bool> removeAssetFromAlbum(
Album album,
Iterable<Asset> assets,
) async {
try {
final response = await _apiService.albumApi.removeAssetFromAlbum(
album.remoteId!,
BulkIdsDto(
ids: assets.map((asset) => asset.remoteId!).toList(),
),
);
if (response != null) {
final toRemove = response.every((e) => e.success)
? assets
: response
.where((e) => e.success)
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
await _updateAssets(album.id, remove: toRemove);
return true;
}
} catch (e) {
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
}
return false;
}
Future<bool> removeUserFromAlbum(
Album album,
User user,
) async {
try {
await _apiService.albumApi.removeUserFromAlbum(
album.remoteId!,
user.id,
);
album.sharedUsers.remove(user);
await _db.writeTxn(() async {
await album.sharedUsers.update(unlink: [user]);
final a = await _db.albums.get(album.id);
// trigger watcher
await _db.albums.put(a!);
});
return true;
} catch (e) {
debugPrint("Error removeUserFromAlbum ${e.toString()}");
return false;
}
}
Future<bool> changeTitleAlbum(
Album album,
String newAlbumTitle,
) async {
try {
await _apiService.albumApi.updateAlbumInfo(
album.remoteId!,
UpdateAlbumDto(
albumName: newAlbumTitle,
),
);
album.name = newAlbumTitle;
await _db.writeTxn(() => _db.albums.put(album));
return true;
} catch (e) {
debugPrint("Error changeTitleAlbum ${e.toString()}");
return false;
}
}
}
@@ -4,9 +4,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album.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/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
class AlbumTitleTextField extends ConsumerWidget {
const AlbumTitleTextField({
@@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
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/shared_album.provider.dart';
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
import 'package:immich_mobile/entities/album.entity.dart';
class AlbumViewerEditableTitle extends HookConsumerWidget {
@@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
@@ -9,19 +9,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/current_album.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/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
@@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/render_list.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
@@ -5,14 +5,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
@RoutePage()
// ignore: must_be_immutable
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
@RoutePage()
@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
@@ -5,9 +5,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
@@ -4,13 +4,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';