mirror of
https://github.com/immich-app/immich.git
synced 2025-11-30 02:05:40 -05:00
handle cloud id migration
This commit is contained in:
parent
d02d3b5472
commit
82c93cf325
@ -1921,6 +1921,7 @@
|
|||||||
"sync": "Sync",
|
"sync": "Sync",
|
||||||
"sync_albums": "Sync albums",
|
"sync_albums": "Sync albums",
|
||||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||||
|
"sync_cloud_ids": "Sync Cloud IDs",
|
||||||
"sync_local": "Sync Local",
|
"sync_local": "Sync Local",
|
||||||
"sync_remote": "Sync Remote",
|
"sync_remote": "Sync Remote",
|
||||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart';
|
||||||
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||||
import 'package:immich_mobile/utils/isolate.dart';
|
import 'package:immich_mobile/utils/isolate.dart';
|
||||||
@ -21,8 +22,13 @@ class BackgroundSyncManager {
|
|||||||
final SyncCallback? onHashingComplete;
|
final SyncCallback? onHashingComplete;
|
||||||
final SyncErrorCallback? onHashingError;
|
final SyncErrorCallback? onHashingError;
|
||||||
|
|
||||||
|
final SyncCallback? onCloudIdSyncStart;
|
||||||
|
final SyncCallback? onCloudIdSyncComplete;
|
||||||
|
final SyncErrorCallback? onCloudIdSyncError;
|
||||||
|
|
||||||
Cancelable<void>? _syncTask;
|
Cancelable<void>? _syncTask;
|
||||||
Cancelable<void>? _syncWebsocketTask;
|
Cancelable<void>? _syncWebsocketTask;
|
||||||
|
Cancelable<void>? _cloudIdSyncTask;
|
||||||
Cancelable<void>? _deviceAlbumSyncTask;
|
Cancelable<void>? _deviceAlbumSyncTask;
|
||||||
Cancelable<void>? _linkedAlbumSyncTask;
|
Cancelable<void>? _linkedAlbumSyncTask;
|
||||||
Cancelable<void>? _hashTask;
|
Cancelable<void>? _hashTask;
|
||||||
@ -37,6 +43,9 @@ class BackgroundSyncManager {
|
|||||||
this.onHashingStart,
|
this.onHashingStart,
|
||||||
this.onHashingComplete,
|
this.onHashingComplete,
|
||||||
this.onHashingError,
|
this.onHashingError,
|
||||||
|
this.onCloudIdSyncStart,
|
||||||
|
this.onCloudIdSyncComplete,
|
||||||
|
this.onCloudIdSyncError,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> cancel() async {
|
Future<void> cancel() async {
|
||||||
@ -54,6 +63,11 @@ class BackgroundSyncManager {
|
|||||||
_syncWebsocketTask?.cancel();
|
_syncWebsocketTask?.cancel();
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
|
|
||||||
|
if (_cloudIdSyncTask != null) {
|
||||||
|
futures.add(_cloudIdSyncTask!.future);
|
||||||
|
}
|
||||||
|
_cloudIdSyncTask?.cancel();
|
||||||
|
|
||||||
if (_linkedAlbumSyncTask != null) {
|
if (_linkedAlbumSyncTask != null) {
|
||||||
futures.add(_linkedAlbumSyncTask!.future);
|
futures.add(_linkedAlbumSyncTask!.future);
|
||||||
}
|
}
|
||||||
@ -173,6 +187,24 @@ class BackgroundSyncManager {
|
|||||||
return _linkedAlbumSyncTask!.whenComplete(() {
|
return _linkedAlbumSyncTask!.whenComplete(() {
|
||||||
_linkedAlbumSyncTask = null;
|
_linkedAlbumSyncTask = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<void> syncCloudIds() {
|
||||||
|
if (_cloudIdSyncTask != null) {
|
||||||
|
return _cloudIdSyncTask!.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloudIdSyncStart?.call();
|
||||||
|
|
||||||
|
_cloudIdSyncTask = runInIsolateGentle(computation: migrateCloudIds);
|
||||||
|
return _cloudIdSyncTask!
|
||||||
|
.whenComplete(() {
|
||||||
|
onCloudIdSyncComplete?.call();
|
||||||
|
_cloudIdSyncTask = null;
|
||||||
|
})
|
||||||
|
.catchError((error) {
|
||||||
|
onCloudIdSyncError?.call(error.toString());
|
||||||
|
_cloudIdSyncTask = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
70
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||||
|
// ignore: import_rule_openapi
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
Future<void> migrateCloudIds(ProviderContainer ref) async {
|
||||||
|
final db = ref.read(driftProvider);
|
||||||
|
// Populate cloud IDs for local assets that don't have one yet
|
||||||
|
await _populateCloudIds(db);
|
||||||
|
|
||||||
|
// Wait for remote sync to complete, so we have up-to-date asset metadata entries
|
||||||
|
await ref.read(syncStreamServiceProvider).sync();
|
||||||
|
|
||||||
|
// Fetch the mapping for backed up assets that have a cloud ID locally but do not have a cloud ID on the server
|
||||||
|
final mappingsToUpdate = await _fetchCloudIdMappings(db);
|
||||||
|
final assetApi = ref.read(apiServiceProvider).assetsApi;
|
||||||
|
for (final mapping in mappingsToUpdate) {
|
||||||
|
final mobileMeta = AssetMetadataUpsertItemDto(
|
||||||
|
key: AssetMetadataKey.mobileApp,
|
||||||
|
value: {"iCloudId": mapping.cloudId},
|
||||||
|
);
|
||||||
|
await assetApi.updateAssetMetadata(mapping.assetId, AssetMetadataUpsertDto(items: [mobileMeta]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _populateCloudIds(Drift drift) async {
|
||||||
|
final query = drift.localAssetEntity.selectOnly()
|
||||||
|
..addColumns([drift.localAssetEntity.id])
|
||||||
|
..where(drift.localAssetEntity.cloudId.isNull());
|
||||||
|
final ids = await query.map((row) => row.read(drift.localAssetEntity.id)!).get();
|
||||||
|
final cloudMapping = await NativeSyncApi().getCloudIdForAssetIds(ids);
|
||||||
|
await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef _CloudIdMapping = ({String assetId, String cloudId});
|
||||||
|
|
||||||
|
Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift) async {
|
||||||
|
final query =
|
||||||
|
drift.remoteAssetEntity.selectOnly().join([
|
||||||
|
leftOuterJoin(
|
||||||
|
drift.localAssetEntity,
|
||||||
|
drift.localAssetEntity.checksum.equalsExp(drift.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
drift.remoteAssetMetadataEntity,
|
||||||
|
drift.remoteAssetMetadataEntity.assetId.equalsExp(drift.remoteAssetEntity.id) &
|
||||||
|
drift.remoteAssetMetadataEntity.key.equalsValue(RemoteAssetMetadataKey.mobileApp),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..addColumns([drift.remoteAssetEntity.id, drift.localAssetEntity.cloudId])
|
||||||
|
..where(
|
||||||
|
drift.localAssetEntity.id.isNotNull() &
|
||||||
|
drift.localAssetEntity.cloudId.isNotNull() &
|
||||||
|
drift.remoteAssetMetadataEntity.cloudId.isNull(),
|
||||||
|
);
|
||||||
|
return query
|
||||||
|
.map(
|
||||||
|
(row) => (assetId: row.read(drift.remoteAssetEntity.id)!, cloudId: row.read(drift.localAssetEntity.cloudId)!),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
@ -15,6 +15,9 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
|||||||
onHashingStart: syncStatusNotifier.startHashJob,
|
onHashingStart: syncStatusNotifier.startHashJob,
|
||||||
onHashingComplete: syncStatusNotifier.completeHashJob,
|
onHashingComplete: syncStatusNotifier.completeHashJob,
|
||||||
onHashingError: syncStatusNotifier.errorHashJob,
|
onHashingError: syncStatusNotifier.errorHashJob,
|
||||||
|
onCloudIdSyncStart: syncStatusNotifier.startCloudIdSync,
|
||||||
|
onCloudIdSyncComplete: syncStatusNotifier.completeCloudIdSync,
|
||||||
|
onCloudIdSyncError: syncStatusNotifier.errorCloudIdSync,
|
||||||
);
|
);
|
||||||
ref.onDispose(manager.cancel);
|
ref.onDispose(manager.cancel);
|
||||||
return manager;
|
return manager;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class SyncStatusState {
|
|||||||
final SyncStatus remoteSyncStatus;
|
final SyncStatus remoteSyncStatus;
|
||||||
final SyncStatus localSyncStatus;
|
final SyncStatus localSyncStatus;
|
||||||
final SyncStatus hashJobStatus;
|
final SyncStatus hashJobStatus;
|
||||||
|
final SyncStatus cloudIdSyncStatus;
|
||||||
|
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ class SyncStatusState {
|
|||||||
this.remoteSyncStatus = SyncStatus.idle,
|
this.remoteSyncStatus = SyncStatus.idle,
|
||||||
this.localSyncStatus = SyncStatus.idle,
|
this.localSyncStatus = SyncStatus.idle,
|
||||||
this.hashJobStatus = SyncStatus.idle,
|
this.hashJobStatus = SyncStatus.idle,
|
||||||
|
this.cloudIdSyncStatus = SyncStatus.idle,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,12 +37,14 @@ class SyncStatusState {
|
|||||||
SyncStatus? remoteSyncStatus,
|
SyncStatus? remoteSyncStatus,
|
||||||
SyncStatus? localSyncStatus,
|
SyncStatus? localSyncStatus,
|
||||||
SyncStatus? hashJobStatus,
|
SyncStatus? hashJobStatus,
|
||||||
|
SyncStatus? cloudIdSyncStatus,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return SyncStatusState(
|
return SyncStatusState(
|
||||||
remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus,
|
remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus,
|
||||||
localSyncStatus: localSyncStatus ?? this.localSyncStatus,
|
localSyncStatus: localSyncStatus ?? this.localSyncStatus,
|
||||||
hashJobStatus: hashJobStatus ?? this.hashJobStatus,
|
hashJobStatus: hashJobStatus ?? this.hashJobStatus,
|
||||||
|
cloudIdSyncStatus: cloudIdSyncStatus ?? this.cloudIdSyncStatus,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -48,6 +52,7 @@ class SyncStatusState {
|
|||||||
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
|
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
|
||||||
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
|
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
|
||||||
bool get isHashing => hashJobStatus == SyncStatus.syncing;
|
bool get isHashing => hashJobStatus == SyncStatus.syncing;
|
||||||
|
bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
@ -56,11 +61,12 @@ class SyncStatusState {
|
|||||||
other.remoteSyncStatus == remoteSyncStatus &&
|
other.remoteSyncStatus == remoteSyncStatus &&
|
||||||
other.localSyncStatus == localSyncStatus &&
|
other.localSyncStatus == localSyncStatus &&
|
||||||
other.hashJobStatus == hashJobStatus &&
|
other.hashJobStatus == hashJobStatus &&
|
||||||
|
other.cloudIdSyncStatus == cloudIdSyncStatus &&
|
||||||
other.errorMessage == errorMessage;
|
other.errorMessage == errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, errorMessage);
|
int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, cloudIdSyncStatus, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
||||||
@ -71,6 +77,7 @@ class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
|||||||
remoteSyncStatus: SyncStatus.idle,
|
remoteSyncStatus: SyncStatus.idle,
|
||||||
localSyncStatus: SyncStatus.idle,
|
localSyncStatus: SyncStatus.idle,
|
||||||
hashJobStatus: SyncStatus.idle,
|
hashJobStatus: SyncStatus.idle,
|
||||||
|
cloudIdSyncStatus: SyncStatus.idle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +116,18 @@ class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
|||||||
void startHashJob() => setHashJobStatus(SyncStatus.syncing);
|
void startHashJob() => setHashJobStatus(SyncStatus.syncing);
|
||||||
void completeHashJob() => setHashJobStatus(SyncStatus.success);
|
void completeHashJob() => setHashJobStatus(SyncStatus.success);
|
||||||
void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error);
|
void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Cloud ID Sync Job
|
||||||
|
///
|
||||||
|
|
||||||
|
void setCloudIdSyncStatus(SyncStatus status, [String? errorMessage]) {
|
||||||
|
state = state.copyWith(cloudIdSyncStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startCloudIdSync() => setCloudIdSyncStatus(SyncStatus.syncing);
|
||||||
|
void completeCloudIdSync() => setCloudIdSyncStatus(SyncStatus.success);
|
||||||
|
void errorCloudIdSync(String error) => setCloudIdSyncStatus(SyncStatus.error, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
final syncStatusProvider = NotifierProvider<SyncStatusNotifier, SyncStatusState>(SyncStatusNotifier.new);
|
final syncStatusProvider = NotifierProvider<SyncStatusNotifier, SyncStatusState>(SyncStatusNotifier.new);
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import 'package:logging/logging.dart';
|
|||||||
// ignore: import_rule_photo_manager
|
// ignore: import_rule_photo_manager
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
const int targetVersion = 15;
|
const int targetVersion = 14;
|
||||||
|
|
||||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||||
@ -70,16 +70,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
|||||||
await Store.populateCache();
|
await Store.populateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version < 15) {
|
|
||||||
try {
|
|
||||||
await updateCloudId(drift);
|
|
||||||
} catch (error) {
|
|
||||||
Logger("Migration").warning("Error occurred while updating cloud ID: $error");
|
|
||||||
// do not update version when error occurs so this is retried the next time
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetVersion >= 12) {
|
if (targetVersion >= 12) {
|
||||||
await Store.put(StoreKey.version, targetVersion);
|
await Store.put(StoreKey.version, targetVersion);
|
||||||
return;
|
return;
|
||||||
@ -189,20 +179,6 @@ Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateCloudId(Drift drift) async {
|
|
||||||
// Android do not have a concept of cloud IDs
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final query = drift.localAssetEntity.selectOnly()
|
|
||||||
..addColumns([drift.localAssetEntity.id])
|
|
||||||
..where(drift.localAssetEntity.cloudId.isNull());
|
|
||||||
final ids = await query.map((row) => row.read(drift.localAssetEntity.id)!).get();
|
|
||||||
final cloudMapping = await NativeSyncApi().getCloudIdForAssetIds(ids);
|
|
||||||
await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
|
Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
|
||||||
try {
|
try {
|
||||||
final isarBackupAlbums = await db.backupAlbums.where().findAll();
|
final isarBackupAlbums = await db.backupAlbums.where().findAll();
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
@ -301,6 +301,16 @@ class BetaSyncSettings extends HookConsumerWidget {
|
|||||||
ref.read(backgroundSyncProvider).hashAssets();
|
ref.read(backgroundSyncProvider).hashAssets();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"sync_cloud_ids".t(context: context),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.keyboard_command_key_rounded),
|
||||||
|
subtitle: Text("tap_to_run_job".t(context: context)),
|
||||||
|
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus),
|
||||||
|
onTap: ref.read(backgroundSyncProvider).syncCloudIds,
|
||||||
|
),
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_SectionHeaderText(text: "actions".t(context: context)),
|
_SectionHeaderText(text: "actions".t(context: context)),
|
||||||
@ -347,7 +357,7 @@ class _SyncStatusIcon extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return switch (status) {
|
return switch (status) {
|
||||||
SyncStatus.idle => const Icon(Icons.pause_circle_outline_rounded),
|
SyncStatus.idle => const SizedBox.shrink(),
|
||||||
SyncStatus.syncing => const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2)),
|
SyncStatus.syncing => const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
SyncStatus.success => const Icon(Icons.check_circle_outline, color: Colors.green),
|
SyncStatus.success => const Icon(Icons.check_circle_outline, color: Colors.green),
|
||||||
SyncStatus.error => Icon(Icons.error_outline, color: context.colorScheme.error),
|
SyncStatus.error => Icon(Icons.error_outline, color: context.colorScheme.error),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user