✨ Downloading file tasks
This commit is contained in:
@@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void updateDownloadProgress(
|
||||
String taskId,
|
||||
int downloadedBytes,
|
||||
int totalBytes,
|
||||
) {
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
fileSize: totalBytes,
|
||||
uploadedBytes: downloadedBytes,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void removeTask(String taskId) {
|
||||
state = state.where((task) => task.taskId != taskId).toList();
|
||||
}
|
||||
@@ -291,6 +309,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
String addLocalDownloadTask(SnCloudFile item) {
|
||||
final taskId =
|
||||
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||
final task = DriveTask(
|
||||
id: taskId,
|
||||
taskId: taskId,
|
||||
fileName: item.name,
|
||||
contentType: item.mimeType ?? '',
|
||||
fileSize: 0,
|
||||
uploadedBytes: 0,
|
||||
totalChunks: 1,
|
||||
uploadedChunks: 0,
|
||||
status: DriveTaskStatus.inProgress,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
type: 'FileDownload',
|
||||
);
|
||||
state = [...state, task];
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_websocketSubscription?.cancel();
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:island/models/drive_task.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
@@ -208,6 +210,9 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(WidgetRef ref) async {
|
||||
final taskId = ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.addLocalDownloadTask(item);
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
@@ -223,14 +228,34 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
onReceiveProgress: (count, total) {
|
||||
if (total > 0) {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateDownloadProgress(taskId, count, total);
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTransmissionProgress(taskId, count / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTaskStatus(
|
||||
taskId,
|
||||
DriveTaskStatus.failed,
|
||||
errorMessage: e.toString(),
|
||||
);
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -31,7 +30,6 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
|
||||
final isVisibleOverride = useState<bool?>(null);
|
||||
final pendingHide = useState(false);
|
||||
final hideTimer = useState<Timer?>(null);
|
||||
final isVisible =
|
||||
(isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
|
||||
!pendingHide.value;
|
||||
@@ -53,24 +51,6 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [isVisible]);
|
||||
|
||||
// Handle hide delay when tasks complete
|
||||
useEffect(() {
|
||||
if (activeTasks.isEmpty && (isVisibleOverride.value ?? false) == false) {
|
||||
// No active tasks and not manually visible (not expanded)
|
||||
hideTimer.value = Timer(const Duration(seconds: 2), () {
|
||||
pendingHide.value = true;
|
||||
});
|
||||
} else {
|
||||
// Cancel any pending hide and reset
|
||||
hideTimer.value?.cancel();
|
||||
hideTimer.value = null;
|
||||
pendingHide.value = false;
|
||||
}
|
||||
return () {
|
||||
hideTimer.value?.cancel();
|
||||
};
|
||||
}, [activeTasks.length, isVisibleOverride.value]);
|
||||
|
||||
if (!isVisible && slideController.status == AnimationStatus.dismissed) {
|
||||
// If not visible and animation is complete (back to start), don't show anything
|
||||
return const SizedBox.shrink();
|
||||
@@ -86,7 +66,6 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
position: slideAnimation,
|
||||
child: _UploadOverlayContent(
|
||||
activeTasks: activeTasks,
|
||||
onVisibilityChanged: (bool? v) => isVisibleOverride.value = v,
|
||||
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
);
|
||||
@@ -95,15 +74,12 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
|
||||
class _UploadOverlayContent extends HookConsumerWidget {
|
||||
final List<DriveTask> activeTasks;
|
||||
final void Function(bool?) onVisibilityChanged;
|
||||
|
||||
const _UploadOverlayContent({
|
||||
required this.activeTasks,
|
||||
required this.onVisibilityChanged,
|
||||
});
|
||||
const _UploadOverlayContent({required this.activeTasks});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
initialValue: 0.0,
|
||||
@@ -117,15 +93,12 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
final isExpanded = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
if (isExpanded.value) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
onVisibilityChanged.call(isExpanded.value);
|
||||
return null;
|
||||
}, [isExpanded.value]);
|
||||
|
||||
@@ -349,6 +322,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return Symbols.upload;
|
||||
|
||||
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||
final hasInProgress = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.inProgress,
|
||||
);
|
||||
@@ -370,6 +344,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
|
||||
// Priority order: in progress > pending > paused > failed > completed
|
||||
if (hasInProgress) {
|
||||
if (hasDownload) {
|
||||
return Symbols.download;
|
||||
}
|
||||
return Symbols.upload;
|
||||
} else if (hasPending) {
|
||||
return Symbols.schedule;
|
||||
@@ -387,6 +364,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return '0 tasks';
|
||||
|
||||
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||
final hasInProgress = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.inProgress,
|
||||
);
|
||||
@@ -408,7 +386,11 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
|
||||
// Priority order: in progress > pending > paused > failed > completed
|
||||
if (hasInProgress) {
|
||||
if (hasDownload) {
|
||||
return '${tasks.length} ${'downloading'.tr()}';
|
||||
} else {
|
||||
return '${tasks.length} ${'uploading'.tr()}';
|
||||
}
|
||||
} else if (hasPending) {
|
||||
return '${tasks.length} ${'pending'.tr()}';
|
||||
} else if (hasPaused) {
|
||||
@@ -550,7 +532,10 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
||||
color = Theme.of(context).colorScheme.secondary;
|
||||
break;
|
||||
case DriveTaskStatus.inProgress:
|
||||
icon = Symbols.upload;
|
||||
icon =
|
||||
widget.task.type == 'FileDownload'
|
||||
? Symbols.download
|
||||
: Symbols.upload;
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
break;
|
||||
case DriveTaskStatus.paused:
|
||||
|
||||
Reference in New Issue
Block a user