merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy

This commit is contained in:
izzy
2026-01-16 09:43:30 +00:00
parent 28443a090d
commit e9dafefb02
46 changed files with 2234 additions and 1225 deletions
@@ -181,7 +181,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser != null) {
await _safeRun(
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
_ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id),
"handleBackupResume",
);
}
@@ -238,6 +238,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup();
}
} else {
await _ref.read(driftBackupProvider.notifier).stopForegroundBackup();
}
_ref.read(websocketProvider.notifier).disconnect();
@@ -1,37 +1,28 @@
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/share_intent_service.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path/path.dart' as p;
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
((ref) => ShareIntentUploadStateNotifier(
ref.watch(appRouterProvider),
ref.watch(uploadServiceProvider),
ref.watch(shareIntentServiceProvider),
ref.read(foregroundUploadServiceProvider),
ref.read(shareIntentServiceProvider),
)),
);
class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttachment>> {
final AppRouter router;
final UploadService _uploadService;
final ForegroundUploadService _foregroundUploadService;
final ShareIntentService _shareIntentService;
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) {
_uploadService.taskStatusStream.listen(_updateUploadStatus);
_uploadService.taskProgressStream.listen(_taskProgressCallback);
}
ShareIntentUploadStateNotifier(this.router, this._foregroundUploadService, this._shareIntentService) : super([]);
void init() {
_shareIntentService.onSharedMedia = onSharedMedia;
@@ -67,97 +58,44 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
state = [];
}
void _updateUploadStatus(TaskStatusUpdate task) async {
if (task.status == TaskStatus.canceled) {
return;
Future<void> uploadAll(List<File> files) async {
for (final file in files) {
final fileId = p.hash(file.path).toString();
_updateStatus(fileId, UploadStatus.running);
}
final taskId = task.task.taskId;
final uploadStatus = switch (task.status) {
TaskStatus.complete => UploadStatus.complete,
TaskStatus.failed => UploadStatus.failed,
TaskStatus.canceled => UploadStatus.canceled,
TaskStatus.enqueued => UploadStatus.enqueued,
TaskStatus.running => UploadStatus.running,
TaskStatus.paused => UploadStatus.paused,
TaskStatus.notFound => UploadStatus.notFound,
TaskStatus.waitingToRetry => UploadStatus.waitingToRetry,
};
state = [
for (final attachment in state)
if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment,
];
if (task.status == TaskStatus.failed) {
String? error;
final exception = task.exception;
if (exception != null && exception is TaskHttpException) {
final message = tryJsonDecode(exception.description)?['message'] as String?;
if (message != null) {
final responseCode = exception.httpResponseCode;
error = "${exception.exceptionType}, response code $responseCode: $message";
}
}
error ??= task.exception?.toString();
_logger.warning("Upload failed for asset: ${task.task.filename}, error: $error");
}
}
void _taskProgressCallback(TaskProgressUpdate update) {
// Ignore if the task is canceled or completed
if (update.progress == downloadFailed || update.progress == downloadCompleted) {
return;
}
final taskId = update.task.taskId;
state = [
for (final attachment in state)
if (attachment.id == taskId.toInt()) attachment.copyWith(uploadProgress: update.progress) else attachment,
];
}
Future<void> upload(File file) async {
final task = await _buildUploadTask(hash(file.path).toString(), file);
await _uploadService.enqueueTasks([task]);
}
Future<UploadTask> _buildUploadTask(String id, File file, {Map<String, String>? fields}) async {
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final url = Uri.parse('$serverEndpoint/assets').toString();
final headers = ApiService.getRequestHeaders();
final deviceId = Store.get(StoreKey.deviceId);
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
final stats = await file.stat();
final fileCreatedAt = stats.changed;
final fileModifiedAt = stats.modified;
final fieldsMap = {
'filename': filename,
'deviceAssetId': id,
'deviceId': deviceId,
'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
'isFavorite': 'false',
'duration': '0',
if (fields != null) ...fields,
};
return UploadTask(
taskId: id,
httpRequestMethod: 'POST',
url: url,
headers: headers,
filename: filename,
fields: fieldsMap,
baseDirectory: baseDirectory,
directory: directory,
fileField: 'assetData',
group: kManualUploadGroup,
updates: Updates.statusAndProgress,
await _foregroundUploadService.uploadShareIntent(
files,
onProgress: (fileId, bytes, totalBytes) {
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
_updateProgress(fileId, progress);
},
onSuccess: (fileId) {
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
},
onError: (fileId, errorMessage) {
_logger.warning("Upload failed for file: $fileId, error: $errorMessage");
_updateStatus(fileId, UploadStatus.failed);
},
);
}
void _updateStatus(String fileId, UploadStatus status, {double? progress}) {
final id = int.parse(fileId);
state = [
for (final attachment in state)
if (attachment.id == id)
attachment.copyWith(status: status, uploadProgress: progress ?? attachment.uploadProgress)
else
attachment,
];
}
void _updateProgress(String fileId, double progress) {
final id = int.parse(fileId);
state = [
for (final attachment in state)
if (attachment.id == id) attachment.copyWith(uploadProgress: progress) else attachment,
];
}
}
+6 -2
View File
@@ -11,8 +11,9 @@ import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/secure_storage.service.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/services/background_upload.service.dart';
import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/hash.dart';
@@ -34,6 +35,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
final AuthService _authService;
final ApiService _apiService;
final UserService _userService;
final SecureStorageService _secureStorageService;
final WidgetService _widgetService;
final Ref _ref;
@@ -45,6 +47,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
this._authService,
this._apiService,
this._userService,
this._secureStorageService,
this._widgetService,
this._ref,
@@ -87,7 +90,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _widgetService.clearCredentials();
await _authService.logout();
await _ref.read(uploadServiceProvider).cancelBackup();
await _ref.read(backgroundUploadServiceProvider).cancel();
_ref.read(foregroundUploadServiceProvider).cancel();
} finally {
await _cleanUp();
}
@@ -0,0 +1,33 @@
import 'package:cancellation_token_http/http.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
/// Tracks per-asset upload progress.
/// Key: local asset ID, Value: upload progress 0.0 to 1.0, or -1.0 for error
class AssetUploadProgressNotifier extends Notifier<Map<String, double>> {
static const double errorValue = -1.0;
@override
Map<String, double> build() => {};
void setProgress(String localAssetId, double progress) {
state = {...state, localAssetId: progress};
}
void setError(String localAssetId) {
state = {...state, localAssetId: errorValue};
}
void remove(String localAssetId) {
state = Map.from(state)..remove(localAssetId);
}
void clear() {
state = {};
}
}
final assetUploadProgressProvider = NotifierProvider<AssetUploadProgressNotifier, Map<String, double>>(
AssetUploadProgressNotifier.new,
);
final manualUploadCancelTokenProvider = StateProvider<CancellationToken?>((ref) => null);
@@ -1,19 +1,18 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/utils/upload_speed_calculator.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/background_upload.service.dart';
class EnqueueStatus {
final int enqueueCount;
@@ -106,26 +105,24 @@ class DriftBackupState {
final int remainderCount;
final int processingCount;
final int enqueueCount;
final int enqueueTotalCount;
final bool isSyncing;
final bool isCanceling;
final BackupError error;
final Map<String, DriftUploadStatus> uploadItems;
final CancellationToken? cancelToken;
final Map<String, double> iCloudDownloadProgress;
const DriftBackupState({
required this.totalCount,
required this.backupCount,
required this.remainderCount,
required this.processingCount,
required this.enqueueCount,
required this.enqueueTotalCount,
required this.isCanceling,
required this.isSyncing,
required this.uploadItems,
this.error = BackupError.none,
required this.uploadItems,
this.cancelToken,
this.iCloudDownloadProgress = const {},
});
DriftBackupState copyWith({
@@ -133,30 +130,28 @@ class DriftBackupState {
int? backupCount,
int? remainderCount,
int? processingCount,
int? enqueueCount,
int? enqueueTotalCount,
bool? isCanceling,
bool? isSyncing,
Map<String, DriftUploadStatus>? uploadItems,
BackupError? error,
Map<String, DriftUploadStatus>? uploadItems,
CancellationToken? cancelToken,
Map<String, double>? iCloudDownloadProgress,
}) {
return DriftBackupState(
totalCount: totalCount ?? this.totalCount,
backupCount: backupCount ?? this.backupCount,
remainderCount: remainderCount ?? this.remainderCount,
processingCount: processingCount ?? this.processingCount,
enqueueCount: enqueueCount ?? this.enqueueCount,
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
isCanceling: isCanceling ?? this.isCanceling,
isSyncing: isSyncing ?? this.isSyncing,
uploadItems: uploadItems ?? this.uploadItems,
error: error ?? this.error,
uploadItems: uploadItems ?? this.uploadItems,
cancelToken: cancelToken ?? this.cancelToken,
iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress,
);
}
@override
String toString() {
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, isSyncing: $isSyncing, uploadItems: $uploadItems, error: $error)';
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, cancelToken: $cancelToken, iCloudDownloadProgress: $iCloudDownloadProgress)';
}
@override
@@ -168,12 +163,11 @@ class DriftBackupState {
other.backupCount == backupCount &&
other.remainderCount == remainderCount &&
other.processingCount == processingCount &&
other.enqueueCount == enqueueCount &&
other.enqueueTotalCount == enqueueTotalCount &&
other.isCanceling == isCanceling &&
other.isSyncing == isSyncing &&
other.error == error &&
mapEquals(other.iCloudDownloadProgress, iCloudDownloadProgress) &&
mapEquals(other.uploadItems, uploadItems) &&
other.error == error;
other.cancelToken == cancelToken;
}
@override
@@ -182,44 +176,40 @@ class DriftBackupState {
backupCount.hashCode ^
remainderCount.hashCode ^
processingCount.hashCode ^
enqueueCount.hashCode ^
enqueueTotalCount.hashCode ^
isCanceling.hashCode ^
isSyncing.hashCode ^
error.hashCode ^
uploadItems.hashCode ^
error.hashCode;
cancelToken.hashCode ^
iCloudDownloadProgress.hashCode;
}
}
final driftBackupProvider = StateNotifierProvider<DriftBackupNotifier, DriftBackupState>((ref) {
return DriftBackupNotifier(ref.watch(uploadServiceProvider));
return DriftBackupNotifier(
ref.watch(foregroundUploadServiceProvider),
ref.watch(backgroundUploadServiceProvider),
UploadSpeedManager(),
);
});
class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
DriftBackupNotifier(this._uploadService)
DriftBackupNotifier(this._foregroundUploadService, this._backgroundUploadService, this._uploadSpeedManager)
: super(
const DriftBackupState(
totalCount: 0,
backupCount: 0,
remainderCount: 0,
processingCount: 0,
enqueueCount: 0,
enqueueTotalCount: 0,
isCanceling: false,
isSyncing: false,
uploadItems: {},
error: BackupError.none,
),
) {
{
_statusSubscription = _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
_progressSubscription = _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
}
}
);
final ForegroundUploadService _foregroundUploadService;
final BackgroundUploadService _backgroundUploadService;
final UploadSpeedManager _uploadSpeedManager;
final UploadService _uploadService;
StreamSubscription<TaskStatusUpdate>? _statusSubscription;
StreamSubscription<TaskProgressUpdate>? _progressSubscription;
final _logger = Logger("DriftBackupNotifier");
/// Remove upload item from state
@@ -235,120 +225,12 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
}
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
if (!mounted) {
_logger.warning("Skip _handleTaskStatusUpdate: notifier disposed");
return;
}
final taskId = update.task.taskId;
switch (update.status) {
case TaskStatus.complete:
if (update.task.group == kBackupGroup) {
if (update.responseStatusCode == 201) {
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
}
}
// Remove the completed task from the upload items
if (state.uploadItems.containsKey(taskId)) {
Future.delayed(const Duration(milliseconds: 1000), () {
_removeUploadItem(taskId);
});
}
case TaskStatus.failed:
// Ignore retry errors to avoid confusing users
if (update.exception?.description == 'Delayed or retried enqueue failed') {
_removeUploadItem(taskId);
return;
}
final currentItem = state.uploadItems[taskId];
if (currentItem == null) {
return;
}
String? error;
final exception = update.exception;
if (exception != null && exception is TaskHttpException) {
final message = tryJsonDecode(exception.description)?['message'] as String?;
if (message != null) {
final responseCode = exception.httpResponseCode;
error = "${exception.exceptionType}, response code $responseCode: $message";
}
}
error ??= update.exception?.toString();
state = state.copyWith(
uploadItems: {
...state.uploadItems,
taskId: currentItem.copyWith(isFailed: true, error: error),
},
);
_logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
break;
case TaskStatus.canceled:
_removeUploadItem(update.task.taskId);
break;
default:
break;
}
}
void _handleTaskProgressUpdate(TaskProgressUpdate update) {
if (!mounted) {
_logger.warning("Skip _handleTaskProgressUpdate: notifier disposed");
return;
}
final taskId = update.task.taskId;
final filename = update.task.displayName;
final progress = update.progress;
final currentItem = state.uploadItems[taskId];
if (currentItem != null) {
if (progress == kUploadStatusCanceled) {
_removeUploadItem(update.task.taskId);
return;
}
state = state.copyWith(
uploadItems: {
...state.uploadItems,
taskId: update.hasExpectedFileSize
? currentItem.copyWith(
progress: progress,
fileSize: update.expectedFileSize,
networkSpeedAsString: update.networkSpeedAsString,
)
: currentItem.copyWith(progress: progress),
},
);
return;
}
state = state.copyWith(
uploadItems: {
...state.uploadItems,
taskId: DriftUploadStatus(
taskId: taskId,
filename: filename,
progress: progress,
fileSize: update.expectedFileSize,
networkSpeedAsString: update.networkSpeedAsString,
),
},
);
}
Future<void> getBackupStatus(String userId) async {
if (!mounted) {
_logger.warning("Skip getBackupStatus (pre-call): notifier disposed");
return;
}
final counts = await _uploadService.getBackupCounts(userId);
final counts = await _foregroundUploadService.getBackupCounts(userId);
if (!mounted) {
_logger.warning("Skip getBackupStatus (post-call): notifier disposed");
return;
@@ -374,47 +256,126 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
state = state.copyWith(isSyncing: isSyncing);
}
Future<void> startBackup(String userId) {
Future<void> startForegroundBackup(String userId) async {
state = state.copyWith(error: BackupError.none);
return _uploadService.startBackup(userId, _updateEnqueueCount);
final cancelToken = CancellationToken();
state = state.copyWith(cancelToken: cancelToken);
return _foregroundUploadService.uploadCandidates(
userId,
cancelToken,
callbacks: UploadCallbacks(
onProgress: _handleForegroundBackupProgress,
onSuccess: _handleForegroundBackupSuccess,
onError: _handleForegroundBackupError,
onICloudProgress: _handleICloudProgress,
),
);
}
void _updateEnqueueCount(EnqueueStatus status) {
state = state.copyWith(enqueueCount: status.enqueueCount, enqueueTotalCount: status.totalCount);
Future<void> stopForegroundBackup() async {
state.cancelToken?.cancel();
_uploadSpeedManager.clear();
state = state.copyWith(cancelToken: null, uploadItems: {}, iCloudDownloadProgress: {});
}
Future<void> cancel() async {
if (!mounted) {
_logger.warning("Skip cancel (pre-call): notifier disposed");
return;
void _handleICloudProgress(String localAssetId, double progress) {
state = state.copyWith(iCloudDownloadProgress: {...state.iCloudDownloadProgress, localAssetId: progress});
if (progress >= 1.0) {
Future.delayed(const Duration(milliseconds: 250), () {
final updatedProgress = Map<String, double>.from(state.iCloudDownloadProgress);
updatedProgress.remove(localAssetId);
state = state.copyWith(iCloudDownloadProgress: updatedProgress);
});
}
dPrint(() => "Canceling backup tasks...");
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none);
}
final activeTaskCount = await _uploadService.cancelBackup();
if (!mounted) {
_logger.warning("Skip cancel (post-call): notifier disposed");
void _handleForegroundBackupProgress(String localAssetId, String filename, int bytes, int totalBytes) {
if (state.cancelToken == null) {
return;
}
if (activeTaskCount > 0) {
dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
await cancel();
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
final networkSpeedAsString = _uploadSpeedManager.updateProgress(localAssetId, bytes, totalBytes);
final currentItem = state.uploadItems[localAssetId];
if (currentItem != null) {
state = state.copyWith(
uploadItems: {
...state.uploadItems,
localAssetId: currentItem.copyWith(
filename: filename,
progress: progress,
fileSize: totalBytes,
networkSpeedAsString: networkSpeedAsString,
),
},
);
} else {
dPrint(() => "All tasks canceled successfully.");
// Clear all upload items when cancellation is complete
state = state.copyWith(isCanceling: false, uploadItems: {});
state = state.copyWith(
uploadItems: {
...state.uploadItems,
localAssetId: DriftUploadStatus(
taskId: localAssetId,
filename: filename,
progress: progress,
fileSize: totalBytes,
networkSpeedAsString: networkSpeedAsString,
),
},
);
}
}
Future<void> handleBackupResume(String userId) async {
void _handleForegroundBackupSuccess(String localAssetId, String remoteAssetId) {
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
_uploadSpeedManager.removeTask(localAssetId);
Future.delayed(const Duration(milliseconds: 1000), () {
_removeUploadItem(localAssetId);
});
}
void _handleForegroundBackupError(String localAssetId, String errorMessage) {
_logger.severe("Upload failed for $localAssetId: $errorMessage");
final currentItem = state.uploadItems[localAssetId];
if (currentItem != null) {
state = state.copyWith(
uploadItems: {
...state.uploadItems,
localAssetId: currentItem.copyWith(isFailed: true, error: errorMessage),
},
);
} else {
state = state.copyWith(
uploadItems: {
...state.uploadItems,
localAssetId: DriftUploadStatus(
taskId: localAssetId,
filename: 'Unknown',
progress: 0,
fileSize: 0,
networkSpeedAsString: '',
isFailed: true,
error: errorMessage,
),
},
);
}
_uploadSpeedManager.removeTask(localAssetId);
}
Future<void> startBackupWithURLSession(String userId) async {
if (!mounted) {
_logger.warning("Skip handleBackupResume (pre-call): notifier disposed");
return;
}
_logger.info("Resuming backup tasks...");
state = state.copyWith(error: BackupError.none);
final tasks = await _uploadService.getActiveTasks(kBackupGroup);
final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup);
if (!mounted) {
_logger.warning("Skip handleBackupResume (post-call): notifier disposed");
return;
@@ -422,20 +383,12 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
_logger.info("Found ${tasks.length} tasks");
if (tasks.isEmpty) {
// Start a new backup queue
_logger.info("Start a new backup queue");
return startBackup(userId);
_logger.info("Start backup with URLSession");
return _backgroundUploadService.uploadBackupCandidates(userId);
}
_logger.info("Tasks to resume: ${tasks.length}");
return _uploadService.resumeBackup();
}
@override
void dispose() {
_statusSubscription?.cancel();
_progressSubscription?.cancel();
super.dispose();
return _backgroundUploadService.resume();
}
}
@@ -445,7 +398,7 @@ final driftBackupCandidateProvider = FutureProvider.autoDispose<List<LocalAsset>
return [];
}
return ref.read(backupRepositoryProvider).getCandidates(user.id, onlyHashed: false);
return ref.read(foregroundUploadServiceProvider).getBackupCandidates(user.id, onlyHashed: false);
});
final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family<List<LocalAlbum>, String>((
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -13,10 +14,11 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/services/action.service.dart';
import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/services/timeline.service.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -40,7 +42,7 @@ class ActionResult {
class ActionNotifier extends Notifier<void> {
final Logger _logger = Logger('ActionNotifier');
late ActionService _service;
late UploadService _uploadService;
late ForegroundUploadService _foregroundUploadService;
late DownloadService _downloadService;
late AssetService _assetService;
@@ -48,7 +50,7 @@ class ActionNotifier extends Notifier<void> {
@override
void build() {
_uploadService = ref.watch(uploadServiceProvider);
_foregroundUploadService = ref.watch(foregroundUploadServiceProvider);
_service = ref.watch(actionServiceProvider);
_assetService = ref.watch(assetServiceProvider);
_downloadService = ref.watch(downloadServiceProvider);
@@ -411,14 +413,44 @@ class ActionNotifier extends Notifier<void> {
}
}
Future<ActionResult> upload(ActionSource source) async {
final assets = _getAssets(source).whereType<LocalAsset>().toList();
Future<ActionResult> upload(ActionSource source, {List<LocalAsset>? assets}) async {
final assetsToUpload = assets ?? _getAssets(source).whereType<LocalAsset>().toList();
final progressNotifier = ref.read(assetUploadProgressProvider.notifier);
final cancelToken = CancellationToken();
ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken;
// Initialize progress for all assets
for (final asset in assetsToUpload) {
progressNotifier.setProgress(asset.id, 0.0);
}
try {
await _uploadService.manualBackup(assets);
return ActionResult(count: assets.length, success: true);
await _foregroundUploadService.uploadManual(
assetsToUpload,
cancelToken,
callbacks: UploadCallbacks(
onProgress: (localAssetId, filename, bytes, totalBytes) {
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
progressNotifier.setProgress(localAssetId, progress);
},
onSuccess: (localAssetId, remoteAssetId) {
progressNotifier.remove(localAssetId);
},
onError: (localAssetId, errorMessage) {
progressNotifier.setError(localAssetId);
},
),
);
return ActionResult(count: assetsToUpload.length, success: true);
} catch (error, stack) {
_logger.severe('Failed manually upload assets', error, stack);
return ActionResult(count: assets.length, success: false, error: error.toString());
return ActionResult(count: assetsToUpload.length, success: false, error: error.toString());
} finally {
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
Future.delayed(const Duration(seconds: 2), () {
progressNotifier.clear();
});
}
}
}
@@ -1,4 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
final storageRepositoryProvider = Provider<StorageRepository>((ref) => const StorageRepository());
final storageRepositoryProvider = Provider<StorageRepository>((ref) => StorageRepository());