feat(mobile): new upload (#18726)

This commit is contained in:
Alex
2025-07-18 23:58:53 -05:00
committed by GitHub
parent f929dc0816
commit fafb88d31c
27 changed files with 1733 additions and 102 deletions
@@ -69,6 +69,9 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
}
await _ref.read(serverInfoProvider.notifier).getServerVersion();
// TODO: Need to decide on how we want to handle uploads once the app is resumed
// await FileDownloader().start();
}
if (!Store.isBetaTimelineEnabled) {
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
@@ -30,7 +29,7 @@ class ShareIntentUploadStateNotifier
this._uploadService,
this._shareIntentService,
) : super([]) {
_uploadService.onUploadStatus = _uploadStatusCallback;
_uploadService.onUploadStatus = _updateUploadStatus;
_uploadService.onTaskProgress = _taskProgressCallback;
}
@@ -69,8 +68,8 @@ class ShareIntentUploadStateNotifier
state = [];
}
void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async {
if (status == TaskStatus.canceled) {
void _updateUploadStatus(TaskStatusUpdate task) async {
if (task.status == TaskStatus.canceled) {
return;
}
@@ -83,7 +82,7 @@ class ShareIntentUploadStateNotifier
TaskStatus.running => UploadStatus.running,
TaskStatus.paused => UploadStatus.paused,
TaskStatus.notFound => UploadStatus.notFound,
TaskStatus.waitingToRetry => UploadStatus.waitingtoRetry
TaskStatus.waitingToRetry => UploadStatus.waitingToRetry
};
state = [
@@ -95,27 +94,6 @@ class ShareIntentUploadStateNotifier
];
}
void _uploadStatusCallback(TaskStatusUpdate update) {
_updateUploadStatus(update, update.status);
switch (update.status) {
case TaskStatus.complete:
if (update.responseStatusCode == 200) {
if (kDebugMode) {
debugPrint("[COMPLETE] ${update.task.taskId} - DUPLICATE");
}
} else {
if (kDebugMode) {
debugPrint("[COMPLETE] ${update.task.taskId}");
}
}
break;
default:
break;
}
}
void _taskProgressCallback(TaskProgressUpdate update) {
// Ignore if the task is canceled or completed
if (update.progress == downloadFailed ||
@@ -134,10 +112,6 @@ class ShareIntentUploadStateNotifier
}
Future<void> upload(File file) {
return _uploadService.upload(file);
}
Future<bool> cancelUpload(String id) {
return _uploadService.cancelUpload(id);
return _uploadService.buildUploadTask(file, group: kManualUploadGroup);
}
}
@@ -0,0 +1,62 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/services/local_album.service.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
final backupAlbumProvider =
StateNotifierProvider<BackupAlbumNotifier, List<LocalAlbum>>(
(ref) => BackupAlbumNotifier(
ref.watch(localAlbumServiceProvider),
),
);
class BackupAlbumNotifier extends StateNotifier<List<LocalAlbum>> {
BackupAlbumNotifier(this._localAlbumService) : super([]) {
getAll();
}
final LocalAlbumService _localAlbumService;
Future<void> getAll() async {
state = await _localAlbumService.getAll();
}
Future<void> selectAlbum(LocalAlbum album) async {
album = album.copyWith(backupSelection: BackupSelection.selected);
await _localAlbumService.update(album);
state = state
.map(
(currentAlbum) => currentAlbum.id == album.id
? currentAlbum.copyWith(backupSelection: BackupSelection.selected)
: currentAlbum,
)
.toList();
}
Future<void> deselectAlbum(LocalAlbum album) async {
album = album.copyWith(backupSelection: BackupSelection.none);
await _localAlbumService.update(album);
state = state
.map(
(currentAlbum) => currentAlbum.id == album.id
? currentAlbum.copyWith(backupSelection: BackupSelection.none)
: currentAlbum,
)
.toList();
}
Future<void> excludeAlbum(LocalAlbum album) async {
album = album.copyWith(backupSelection: BackupSelection.excluded);
await _localAlbumService.update(album);
state = state
.map(
(currentAlbum) => currentAlbum.id == album.id
? currentAlbum.copyWith(backupSelection: BackupSelection.excluded)
: currentAlbum,
)
.toList();
}
}
@@ -0,0 +1,194 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/services/drift_backup.service.dart';
import 'package:immich_mobile/services/upload.service.dart';
class DriftUploadStatus {
final String taskId;
final String filename;
final double progress;
const DriftUploadStatus({
required this.taskId,
required this.filename,
required this.progress,
});
DriftUploadStatus copyWith({
String? taskId,
String? filename,
double? progress,
}) {
return DriftUploadStatus(
taskId: taskId ?? this.taskId,
filename: filename ?? this.filename,
progress: progress ?? this.progress,
);
}
@override
String toString() =>
'ExpUploadStatus(taskId: $taskId, filename: $filename, progress: $progress)';
@override
bool operator ==(covariant DriftUploadStatus other) {
if (identical(this, other)) return true;
return other.taskId == taskId &&
other.filename == filename &&
other.progress == progress;
}
@override
int get hashCode => taskId.hashCode ^ filename.hashCode ^ progress.hashCode;
}
class DriftBackupState {
final int totalCount;
final int backupCount;
final int remainderCount;
final Map<String, DriftUploadStatus> uploadItems;
const DriftBackupState({
required this.totalCount,
required this.backupCount,
required this.remainderCount,
required this.uploadItems,
});
DriftBackupState copyWith({
int? totalCount,
int? backupCount,
int? remainderCount,
Map<String, DriftUploadStatus>? uploadItems,
}) {
return DriftBackupState(
totalCount: totalCount ?? this.totalCount,
backupCount: backupCount ?? this.backupCount,
remainderCount: remainderCount ?? this.remainderCount,
uploadItems: uploadItems ?? this.uploadItems,
);
}
@override
String toString() {
return 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, uploadItems: $uploadItems)';
}
@override
bool operator ==(covariant DriftBackupState other) {
if (identical(this, other)) return true;
final mapEquals = const DeepCollectionEquality().equals;
return other.totalCount == totalCount &&
other.backupCount == backupCount &&
other.remainderCount == remainderCount &&
mapEquals(other.uploadItems, uploadItems);
}
@override
int get hashCode {
return totalCount.hashCode ^
backupCount.hashCode ^
remainderCount.hashCode ^
uploadItems.hashCode;
}
}
final driftBackupProvider =
StateNotifierProvider<ExpBackupNotifier, DriftBackupState>((ref) {
return ExpBackupNotifier(
ref.watch(driftBackupServiceProvider),
ref.watch(uploadServiceProvider),
);
});
class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
ExpBackupNotifier(
this._backupService,
this._uploadService,
) : super(
const DriftBackupState(
totalCount: 0,
backupCount: 0,
remainderCount: 0,
uploadItems: {},
),
) {
{
_uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
_uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
}
}
final DriftBackupService _backupService;
final UploadService _uploadService;
StreamSubscription<TaskStatusUpdate>? _statusSubscription;
StreamSubscription<TaskProgressUpdate>? _progressSubscription;
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
switch (update.status) {
case TaskStatus.complete:
state = state.copyWith(
backupCount: state.backupCount + 1,
remainderCount: state.remainderCount - 1,
);
break;
default:
break;
}
}
void _handleTaskProgressUpdate(TaskProgressUpdate update) {}
Future<void> getBackupStatus() async {
final [totalCount, backupCount, remainderCount] = await Future.wait([
_backupService.getTotalCount(),
_backupService.getBackupCount(),
_backupService.getRemainderCount(),
]);
state = state.copyWith(
totalCount: totalCount,
backupCount: backupCount,
remainderCount: remainderCount,
);
}
Future<void> backup() {
return _backupService.backup();
}
Future<void> cancel() async {
await _backupService.cancel();
await getDataInfo();
}
Future<void> getDataInfo() async {
final a = await FileDownloader().database.allRecordsWithStatus(
TaskStatus.enqueued,
group: kBackupGroup,
);
final b = await FileDownloader().allTasks(
group: kBackupGroup,
);
debugPrint(
"Enqueued tasks: ${a.length}, All tasks: ${b.length}",
);
}
@override
void dispose() {
_statusSubscription?.cancel();
_progressSubscription?.cancel();
super.dispose();
}
}
+34 -8
View File
@@ -176,16 +176,14 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
);
});
socket.on('on_upload_success', _handleOnUploadSuccess);
if (!Store.isBetaTimelineEnabled) {
startListeningToOldEvents();
} else {
startListeningToBetaEvents();
}
socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_asset_delete', _handleOnAssetDelete);
socket.on('on_asset_trash', _handleOnAssetTrash);
socket.on('on_asset_restore', _handleServerUpdates);
socket.on('on_asset_update', _handleServerUpdates);
socket.on('on_asset_stack_update', _handleServerUpdates);
socket.on('on_asset_hidden', _handleOnAssetHidden);
socket.on('on_new_release', _handleReleaseUpdates);
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
@@ -213,6 +211,34 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
state.socket?.off(eventName);
}
void stopListenToOldEvents() {
state.socket?.off('on_upload_success');
state.socket?.off('on_asset_delete');
state.socket?.off('on_asset_trash');
state.socket?.off('on_asset_restore');
state.socket?.off('on_asset_update');
state.socket?.off('on_asset_stack_update');
state.socket?.off('on_asset_hidden');
}
void startListeningToOldEvents() {
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
state.socket?.on('on_asset_delete', _handleOnAssetDelete);
state.socket?.on('on_asset_trash', _handleOnAssetTrash);
state.socket?.on('on_asset_restore', _handleServerUpdates);
state.socket?.on('on_asset_update', _handleServerUpdates);
state.socket?.on('on_asset_stack_update', _handleServerUpdates);
state.socket?.on('on_asset_hidden', _handleOnAssetHidden);
}
void stopListeningToBetaEvents() {
state.socket?.off('AssetUploadReadyV1');
}
void startListeningToBetaEvents() {
state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
}
void listenUploadEvent() {
debugPrint("Start listening to event on_upload_success");
state.socket?.on('on_upload_success', _handleOnUploadSuccess);