diff --git a/lib/pods/upload_tasks.dart b/lib/pods/upload_tasks.dart index 57504b38..d18895ce 100644 --- a/lib/pods/upload_tasks.dart +++ b/lib/pods/upload_tasks.dart @@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier> { }).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> { .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(); diff --git a/lib/screens/files/file_detail.dart b/lib/screens/files/file_detail.dart index db6505e3..69703c7a 100644 --- a/lib/screens/files/file_detail.dart +++ b/lib/screens/files/file_detail.dart @@ -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 _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); } } diff --git a/lib/widgets/upload_overlay.dart b/lib/widgets/upload_overlay.dart index badddb88..5ac9f39c 100644 --- a/lib/widgets/upload_overlay.dart +++ b/lib/widgets/upload_overlay.dart @@ -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(null); final pendingHide = useState(false); - final hideTimer = useState(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 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 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 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) { - return '${tasks.length} ${'uploading'.tr()}'; + 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 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: