♻️ Refactored the upload files according to new backend tasks
This commit is contained in:
@@ -13,6 +13,7 @@ import 'package:island/route.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/upload_overlay.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -198,6 +199,7 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
_WebSocketIndicator(),
|
||||
const UploadOverlay(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -213,7 +215,11 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [Positioned.fill(child: child), _WebSocketIndicator()],
|
||||
children: [
|
||||
Positioned.fill(child: child),
|
||||
_WebSocketIndicator(),
|
||||
const UploadOverlay(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97';
|
||||
String _$postListNotifierHash() => r'bfc3d652dffc5ff3a94a6c3d04aac65354fe63b5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
327
lib/widgets/upload_overlay.dart
Normal file
327
lib/widgets/upload_overlay.dart
Normal file
@@ -0,0 +1,327 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/upload_task.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class UploadOverlay extends HookConsumerWidget {
|
||||
const UploadOverlay({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final uploadTasks = ref.watch(uploadTasksProvider);
|
||||
final activeTasks =
|
||||
uploadTasks
|
||||
.where(
|
||||
(task) =>
|
||||
task.status == UploadTaskStatus.pending ||
|
||||
task.status == UploadTaskStatus.inProgress ||
|
||||
task.status == UploadTaskStatus.paused,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (activeTasks.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Container(
|
||||
width: 320,
|
||||
constraints: BoxConstraints(maxHeight: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.upload,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'uploadTasks'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${activeTasks.length}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Task list
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: activeTasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = activeTasks[index];
|
||||
return UploadTaskTile(task: task);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadTaskTile extends HookConsumerWidget {
|
||||
final UploadTask task;
|
||||
|
||||
const UploadTaskTile({super.key, required this.task});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => isExpanded.value = !isExpanded.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Status icon
|
||||
_buildStatusIcon(context),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// File info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
task.fileName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatFileSize(task.fileSize),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Progress indicator
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
value: task.progress,
|
||||
strokeWidth: 3,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
|
||||
// Expand/collapse button
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isExpanded.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () => isExpanded.value = !isExpanded.value,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Expanded details
|
||||
if (isExpanded.value) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildExpandedDetails(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(BuildContext context) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
switch (task.status) {
|
||||
case UploadTaskStatus.pending:
|
||||
icon = Symbols.schedule;
|
||||
color = Theme.of(context).colorScheme.secondary;
|
||||
break;
|
||||
case UploadTaskStatus.inProgress:
|
||||
icon = Symbols.upload;
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
break;
|
||||
case UploadTaskStatus.paused:
|
||||
icon = Symbols.pause_circle;
|
||||
color = Theme.of(context).colorScheme.tertiary;
|
||||
break;
|
||||
case UploadTaskStatus.completed:
|
||||
icon = Symbols.check_circle;
|
||||
color = Colors.green;
|
||||
break;
|
||||
case UploadTaskStatus.failed:
|
||||
icon = Symbols.error;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case UploadTaskStatus.cancelled:
|
||||
icon = Symbols.cancel;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case UploadTaskStatus.expired:
|
||||
icon = Symbols.timer_off;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
}
|
||||
|
||||
return Icon(icon, size: 16, color: color);
|
||||
}
|
||||
|
||||
Widget _buildExpandedDetails(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Progress text
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(task.progress * 100).toStringAsFixed(1)}%',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
'${task.uploadedChunks}/${task.totalChunks} chunks',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Progress bar
|
||||
LinearProgressIndicator(
|
||||
value: task.progress,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Speed and ETA
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatBytesPerSecond(task),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (task.status == UploadTaskStatus.inProgress)
|
||||
Text(
|
||||
'ETA: ${_formatDuration(task.estimatedTimeRemaining)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Error message if failed
|
||||
if (task.errorMessage != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
task.errorMessage!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes >= 1073741824) {
|
||||
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
|
||||
} else if (bytes >= 1048576) {
|
||||
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
|
||||
} else if (bytes >= 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else {
|
||||
return '$bytes bytes';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatBytesPerSecond(UploadTask task) {
|
||||
if (task.uploadedBytes == 0) return '0 B/s';
|
||||
|
||||
final elapsedSeconds = DateTime.now().difference(task.createdAt).inSeconds;
|
||||
if (elapsedSeconds == 0) return '0 B/s';
|
||||
|
||||
final bytesPerSecond = task.uploadedBytes / elapsedSeconds;
|
||||
return '${_formatFileSize(bytesPerSecond.toInt())}/s';
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
if (duration.inHours > 0) {
|
||||
return '${duration.inHours}h ${duration.inMinutes.remainder(60)}m';
|
||||
} else if (duration.inMinutes > 0) {
|
||||
return '${duration.inMinutes}m ${duration.inSeconds.remainder(60)}s';
|
||||
} else {
|
||||
return '${duration.inSeconds}s';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user