mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat: new upload (cont) (#20029)
* new upload button * wip * pr feedback * fix: updateAll override album selection value * feat: status box * feat: handle upload resume * re-enable websocket event * fix: update state condition and upload status * Better backup detail page --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
1dc62fce5f
commit
4d27f187ea
26 changed files with 1558 additions and 413 deletions
|
|
@ -1,39 +1,75 @@
|
|||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
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 EnqueueStatus {
|
||||
final int enqueueCount;
|
||||
final int totalCount;
|
||||
|
||||
const EnqueueStatus({
|
||||
required this.enqueueCount,
|
||||
required this.totalCount,
|
||||
});
|
||||
|
||||
EnqueueStatus copyWith({
|
||||
int? enqueueCount,
|
||||
int? totalCount,
|
||||
}) {
|
||||
return EnqueueStatus(
|
||||
enqueueCount: enqueueCount ?? this.enqueueCount,
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)';
|
||||
}
|
||||
|
||||
class DriftUploadStatus {
|
||||
final String taskId;
|
||||
final String filename;
|
||||
final double progress;
|
||||
final int fileSize;
|
||||
final String networkSpeedAsString;
|
||||
|
||||
const DriftUploadStatus({
|
||||
required this.taskId,
|
||||
required this.filename,
|
||||
required this.progress,
|
||||
required this.fileSize,
|
||||
required this.networkSpeedAsString,
|
||||
});
|
||||
|
||||
DriftUploadStatus copyWith({
|
||||
String? taskId,
|
||||
String? filename,
|
||||
double? progress,
|
||||
int? fileSize,
|
||||
String? networkSpeedAsString,
|
||||
}) {
|
||||
return DriftUploadStatus(
|
||||
taskId: taskId ?? this.taskId,
|
||||
filename: filename ?? this.filename,
|
||||
progress: progress ?? this.progress,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'ExpUploadStatus(taskId: $taskId, filename: $filename, progress: $progress)';
|
||||
String toString() {
|
||||
return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant DriftUploadStatus other) {
|
||||
|
|
@ -41,23 +77,65 @@ class DriftUploadStatus {
|
|||
|
||||
return other.taskId == taskId &&
|
||||
other.filename == filename &&
|
||||
other.progress == progress;
|
||||
other.progress == progress &&
|
||||
other.fileSize == fileSize &&
|
||||
other.networkSpeedAsString == networkSpeedAsString;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => taskId.hashCode ^ filename.hashCode ^ progress.hashCode;
|
||||
int get hashCode {
|
||||
return taskId.hashCode ^
|
||||
filename.hashCode ^
|
||||
progress.hashCode ^
|
||||
fileSize.hashCode ^
|
||||
networkSpeedAsString.hashCode;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'taskId': taskId,
|
||||
'filename': filename,
|
||||
'progress': progress,
|
||||
'fileSize': fileSize,
|
||||
'networkSpeedAsString': networkSpeedAsString,
|
||||
};
|
||||
}
|
||||
|
||||
factory DriftUploadStatus.fromMap(Map<String, dynamic> map) {
|
||||
return DriftUploadStatus(
|
||||
taskId: map['taskId'] as String,
|
||||
filename: map['filename'] as String,
|
||||
progress: map['progress'] as double,
|
||||
fileSize: map['fileSize'] as int,
|
||||
networkSpeedAsString: map['networkSpeedAsString'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory DriftUploadStatus.fromJson(String source) =>
|
||||
DriftUploadStatus.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
class DriftBackupState {
|
||||
final int totalCount;
|
||||
final int backupCount;
|
||||
final int remainderCount;
|
||||
|
||||
final int enqueueCount;
|
||||
final int enqueueTotalCount;
|
||||
|
||||
final bool isCanceling;
|
||||
|
||||
final Map<String, DriftUploadStatus> uploadItems;
|
||||
|
||||
const DriftBackupState({
|
||||
required this.totalCount,
|
||||
required this.backupCount,
|
||||
required this.remainderCount,
|
||||
required this.enqueueCount,
|
||||
required this.enqueueTotalCount,
|
||||
required this.isCanceling,
|
||||
required this.uploadItems,
|
||||
});
|
||||
|
||||
|
|
@ -65,19 +143,25 @@ class DriftBackupState {
|
|||
int? totalCount,
|
||||
int? backupCount,
|
||||
int? remainderCount,
|
||||
int? enqueueCount,
|
||||
int? enqueueTotalCount,
|
||||
bool? isCanceling,
|
||||
Map<String, DriftUploadStatus>? uploadItems,
|
||||
}) {
|
||||
return DriftBackupState(
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
backupCount: backupCount ?? this.backupCount,
|
||||
remainderCount: remainderCount ?? this.remainderCount,
|
||||
enqueueCount: enqueueCount ?? this.enqueueCount,
|
||||
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
||||
isCanceling: isCanceling ?? this.isCanceling,
|
||||
uploadItems: uploadItems ?? this.uploadItems,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, uploadItems: $uploadItems)';
|
||||
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -88,6 +172,9 @@ class DriftBackupState {
|
|||
return other.totalCount == totalCount &&
|
||||
other.backupCount == backupCount &&
|
||||
other.remainderCount == remainderCount &&
|
||||
other.enqueueCount == enqueueCount &&
|
||||
other.enqueueTotalCount == enqueueTotalCount &&
|
||||
other.isCanceling == isCanceling &&
|
||||
mapEquals(other.uploadItems, uploadItems);
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +183,9 @@ class DriftBackupState {
|
|||
return totalCount.hashCode ^
|
||||
backupCount.hashCode ^
|
||||
remainderCount.hashCode ^
|
||||
enqueueCount.hashCode ^
|
||||
enqueueTotalCount.hashCode ^
|
||||
isCanceling.hashCode ^
|
||||
uploadItems.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
@ -117,6 +207,9 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
totalCount: 0,
|
||||
backupCount: 0,
|
||||
remainderCount: 0,
|
||||
enqueueCount: 0,
|
||||
enqueueTotalCount: 0,
|
||||
isCanceling: false,
|
||||
uploadItems: {},
|
||||
),
|
||||
) {
|
||||
|
|
@ -131,13 +224,39 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
StreamSubscription<TaskStatusUpdate>? _statusSubscription;
|
||||
StreamSubscription<TaskProgressUpdate>? _progressSubscription;
|
||||
|
||||
/// Remove upload item from state
|
||||
void _removeUploadItem(String taskId) {
|
||||
if (state.uploadItems.containsKey(taskId)) {
|
||||
final updatedItems =
|
||||
Map<String, DriftUploadStatus>.from(state.uploadItems);
|
||||
updatedItems.remove(taskId);
|
||||
state = state.copyWith(uploadItems: updatedItems);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
|
||||
switch (update.status) {
|
||||
case TaskStatus.complete:
|
||||
state = state.copyWith(
|
||||
backupCount: state.backupCount + 1,
|
||||
remainderCount: state.remainderCount - 1,
|
||||
);
|
||||
if (update.task.group == kBackupGroup) {
|
||||
state = state.copyWith(
|
||||
backupCount: state.backupCount + 1,
|
||||
remainderCount: state.remainderCount - 1,
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the completed task from the upload items
|
||||
final taskId = update.task.taskId;
|
||||
if (state.uploadItems.containsKey(taskId)) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
_removeUploadItem(taskId);
|
||||
});
|
||||
}
|
||||
|
||||
case TaskStatus.failed:
|
||||
break;
|
||||
|
||||
case TaskStatus.canceled:
|
||||
_removeUploadItem(update.task.taskId);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -145,7 +264,48 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
}
|
||||
}
|
||||
|
||||
void _handleTaskProgressUpdate(TaskProgressUpdate update) {}
|
||||
void _handleTaskProgressUpdate(TaskProgressUpdate update) {
|
||||
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() async {
|
||||
final [totalCount, backupCount, remainderCount] = await Future.wait([
|
||||
|
|
@ -162,27 +322,50 @@ class ExpBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||
}
|
||||
|
||||
Future<void> backup() {
|
||||
return _backupService.backup();
|
||||
return _backupService.backup(_updateEnqueueCount);
|
||||
}
|
||||
|
||||
void _updateEnqueueCount(EnqueueStatus status) {
|
||||
state = state.copyWith(
|
||||
enqueueCount: status.enqueueCount,
|
||||
enqueueTotalCount: status.totalCount,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancel() async {
|
||||
state = state.copyWith(
|
||||
enqueueCount: 0,
|
||||
enqueueTotalCount: 0,
|
||||
isCanceling: true,
|
||||
);
|
||||
|
||||
await _backupService.cancel();
|
||||
await getDataInfo();
|
||||
|
||||
// Check if there are any tasks left in the queue
|
||||
final tasks = await FileDownloader().allTasks(group: kBackupGroup);
|
||||
|
||||
debugPrint("Tasks left to cancel: ${tasks.length}");
|
||||
|
||||
if (tasks.isNotEmpty) {
|
||||
await cancel();
|
||||
} else {
|
||||
// Clear all upload items when cancellation is complete
|
||||
state = state.copyWith(
|
||||
isCanceling: false,
|
||||
uploadItems: {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getDataInfo() async {
|
||||
final a = await FileDownloader().database.allRecordsWithStatus(
|
||||
TaskStatus.enqueued,
|
||||
group: kBackupGroup,
|
||||
);
|
||||
Future<void> handleBackupResume() async {
|
||||
final tasks = await FileDownloader().allTasks(group: kBackupGroup);
|
||||
if (tasks.isEmpty) {
|
||||
// Start a new backup queue
|
||||
await backup();
|
||||
}
|
||||
|
||||
final b = await FileDownloader().allTasks(
|
||||
group: kBackupGroup,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
"Enqueued tasks: ${a.length}, All tasks: ${b.length}",
|
||||
);
|
||||
debugPrint("Tasks to resume: ${tasks.length}");
|
||||
await FileDownloader().start();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue