feat(mobile): new upload (#18726)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user