💫 Animated the upload overlay

This commit is contained in:
2025-11-10 01:40:28 +08:00
parent 78a4022531
commit b40afde00f

View File

@@ -6,6 +6,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/upload_task.dart'; import 'package:island/models/upload_task.dart';
import 'package:island/pods/upload_tasks.dart'; import 'package:island/pods/upload_tasks.dart';
import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@@ -24,12 +25,44 @@ class UploadOverlay extends HookConsumerWidget {
task.status == UploadTaskStatus.paused || task.status == UploadTaskStatus.paused ||
task.status == UploadTaskStatus.completed, task.status == UploadTaskStatus.completed,
) )
.toList(); .toList()
if (activeTasks.isEmpty) { ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
final isVisible = activeTasks.isNotEmpty;
final slideController = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final slideAnimation = Tween<Offset>(
begin: const Offset(0, 1), // Start from below the screen
end: Offset.zero, // End at normal position
).animate(CurvedAnimation(parent: slideController, curve: Curves.easeOut));
// Animate when visibility changes
useEffect(() {
if (isVisible) {
slideController.forward();
} else {
slideController.reverse();
}
return null;
}, [isVisible]);
if (!isVisible && slideController.status == AnimationStatus.dismissed) {
// If not visible and animation is complete (back to start), don't show anything
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return _UploadOverlayContent(activeTasks: activeTasks); final isDesktop = isWideScreen(context);
return Positioned(
bottom: 16 + MediaQuery.of(context).padding.bottom,
left: isDesktop ? null : 0,
right: isDesktop ? 24 : 0,
child: SlideTransition(
position: slideAnimation,
child: _UploadOverlayContent(activeTasks: activeTasks),
),
);
} }
} }
@@ -198,11 +231,12 @@ class _UploadOverlayContent extends HookConsumerWidget {
), ),
), ),
), ),
child: Column( child: CustomScrollView(
children: [ slivers: [
// Clear completed tasks button // Clear completed tasks button
if (_hasCompletedTasks(activeTasks)) if (_hasCompletedTasks(activeTasks))
ListTile( SliverToBoxAdapter(
child: ListTile(
dense: true, dense: true,
title: const Text('Clear Completed'), title: const Text('Clear Completed'),
leading: Icon( leading: Icon(
@@ -223,22 +257,23 @@ class _UploadOverlayContent extends HookConsumerWidget {
context, context,
).colorScheme.surfaceContainerHighest, ).colorScheme.surfaceContainerHighest,
), ),
),
// Task list // Task list
Expanded( SliverList(
child: AnimatedOpacity( delegate: SliverChildBuilderDelegate((
opacity: opacityAnimation, context,
duration: const Duration(milliseconds: 150), index,
child: ListView.builder( ) {
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: activeTasks.length,
itemBuilder: (context, index) {
final task = activeTasks[index]; final task = activeTasks[index];
return UploadTaskTile(task: task); return AnimatedOpacity(
}, opacity: opacityAnimation,
), duration: const Duration(
milliseconds: 150,
), ),
child: UploadTaskTile(task: task),
);
}, childCount: activeTasks.length),
), ),
], ],
), ),
@@ -259,7 +294,13 @@ class _UploadOverlayContent extends HookConsumerWidget {
if (tasks.isEmpty) return 0.0; if (tasks.isEmpty) return 0.0;
final totalProgress = tasks.fold<double>( final totalProgress = tasks.fold<double>(
0.0, 0.0,
(sum, task) => sum + task.progress, (sum, task) =>
sum +
(task.status == UploadTaskStatus.inProgress
? task.progress
: task.status == UploadTaskStatus.completed
? 1
: 0),
); );
return totalProgress / tasks.length; return totalProgress / tasks.length;
} }