chore: add sync indicator and better album state management (#20004)

* album list rerendering

* sync indicator

* sync indicator

* fix: lint
This commit is contained in:
Alex
2025-07-18 08:39:28 -05:00
committed by GitHub
parent 137f0d48c0
commit 2e63b9d951
7 changed files with 226 additions and 158 deletions
@@ -1,8 +1,14 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
final manager = BackgroundSyncManager();
final syncStatusNotifier = ref.read(syncStatusProvider.notifier);
final manager = BackgroundSyncManager(
onRemoteSyncStart: syncStatusNotifier.startRemoteSync,
onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync,
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
);
ref.onDispose(manager.cancel);
return manager;
});
@@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/utils/remote_album.utils.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'album.provider.dart';
@@ -13,33 +14,25 @@ import 'album.provider.dart';
class RemoteAlbumState {
final List<RemoteAlbum> albums;
final List<RemoteAlbum> filteredAlbums;
final bool isLoading;
final String? error;
const RemoteAlbumState({
required this.albums,
List<RemoteAlbum>? filteredAlbums,
this.isLoading = false,
this.error,
}) : filteredAlbums = filteredAlbums ?? albums;
RemoteAlbumState copyWith({
List<RemoteAlbum>? albums,
List<RemoteAlbum>? filteredAlbums,
bool? isLoading,
String? error,
}) {
return RemoteAlbumState(
albums: albums ?? this.albums,
filteredAlbums: filteredAlbums ?? this.filteredAlbums,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
@override
String toString() =>
'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length}, isLoading: $isLoading, error: $error)';
'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})';
@override
bool operator ==(covariant RemoteAlbumState other) {
@@ -47,47 +40,38 @@ class RemoteAlbumState {
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.albums, albums) &&
listEquals(other.filteredAlbums, filteredAlbums) &&
other.isLoading == isLoading &&
other.error == error;
listEquals(other.filteredAlbums, filteredAlbums);
}
@override
int get hashCode =>
albums.hashCode ^
filteredAlbums.hashCode ^
isLoading.hashCode ^
error.hashCode;
int get hashCode => albums.hashCode ^ filteredAlbums.hashCode;
}
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
late RemoteAlbumService _remoteAlbumService;
final _logger = Logger('RemoteAlbumNotifier');
@override
RemoteAlbumState build() {
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
return const RemoteAlbumState(albums: [], filteredAlbums: []);
}
Future<List<RemoteAlbum>> getAll() async {
state = state.copyWith(isLoading: true, error: null);
Future<List<RemoteAlbum>> _getAll() async {
try {
final albums = await _remoteAlbumService.getAll();
state = state.copyWith(
albums: albums,
filteredAlbums: albums,
isLoading: false,
);
return albums;
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
} catch (error, stack) {
_logger.severe('Failed to fetch albums', error, stack);
rethrow;
}
}
Future<void> refresh() async {
await getAll();
await _getAll();
}
void searchAlbums(
@@ -127,8 +111,6 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
String? description,
List<String> assetIds = const [],
}) async {
state = state.copyWith(isLoading: true, error: null);
try {
final album = await _remoteAlbumService.createAlbum(
title: title,
@@ -141,10 +123,9 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
filteredAlbums: [...state.filteredAlbums, album],
);
state = state.copyWith(isLoading: false);
return album;
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
} catch (error, stack) {
_logger.severe('Failed to create album', error, stack);
rethrow;
}
}
@@ -157,8 +138,6 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
bool? isActivityEnabled,
AlbumAssetOrder? order,
}) async {
state = state.copyWith(isLoading: true, error: null);
try {
final updatedAlbum = await _remoteAlbumService.updateAlbum(
albumId,
@@ -180,12 +159,11 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
state = state.copyWith(
albums: updatedAlbums,
filteredAlbums: updatedFilteredAlbums,
isLoading: false,
);
return updatedAlbum;
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
} catch (error, stack) {
_logger.severe('Failed to update album', error, stack);
rethrow;
}
}
@@ -0,0 +1,68 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
enum SyncStatus {
idle,
syncing,
success,
error,
}
class SyncStatusState {
final SyncStatus remoteSyncStatus;
final String? errorMessage;
const SyncStatusState({
this.remoteSyncStatus = SyncStatus.idle,
this.errorMessage,
});
SyncStatusState copyWith({
SyncStatus? remoteSyncStatus,
String? errorMessage,
}) {
return SyncStatusState(
remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus,
errorMessage: errorMessage ?? this.errorMessage,
);
}
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SyncStatusState &&
other.remoteSyncStatus == remoteSyncStatus &&
other.errorMessage == errorMessage;
}
@override
int get hashCode => Object.hash(remoteSyncStatus, errorMessage);
}
class SyncStatusNotifier extends Notifier<SyncStatusState> {
@override
SyncStatusState build() {
return const SyncStatusState(
errorMessage: null,
remoteSyncStatus: SyncStatus.idle,
);
}
void setRemoteSyncStatus(SyncStatus status, [String? errorMessage]) {
state = state.copyWith(
remoteSyncStatus: status,
errorMessage: status == SyncStatus.error ? errorMessage : null,
);
}
void startRemoteSync() => setRemoteSyncStatus(SyncStatus.syncing);
void completeRemoteSync() => setRemoteSyncStatus(SyncStatus.success);
void errorRemoteSync(String error) =>
setRemoteSyncStatus(SyncStatus.error, error);
}
final syncStatusProvider =
NotifierProvider<SyncStatusNotifier, SyncStatusState>(
SyncStatusNotifier.new,
);