diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index 504f936..e922bda 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -251,7 +251,8 @@ class _MessageInputState extends State { width: (100 - _cancelSlideOffset) % 101, ), Text( - context.lang.voiceMessageSlideToCancel), + context.lang.voiceMessageSlideToCancel, + ), ] else ...[ Expanded( child: Container(), @@ -265,7 +266,7 @@ class _MessageInputState extends State { ), ), ), - const SizedBox(width: 20) + const SizedBox(width: 20), ], ], ) diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 20a440b..5396397 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -22,6 +22,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:video_player/video_player.dart'; @@ -58,6 +59,7 @@ class _MediaViewerViewState extends State { bool imageSaved = false; bool imageSaving = false; bool displayTwonlyPresent = false; + bool _showDownloadingLoader = false; late String _currentMediaSender; final emojiKey = GlobalKey(); @@ -183,6 +185,9 @@ class _MediaViewerViewState extends State { downloadStateListener = stream.listen((updated) async { if (updated == null) return; if (updated.downloadState != DownloadState.ready) { + setState(() { + _showDownloadingLoader = true; + }); if (!downloadTriggered) { downloadTriggered = true; final mediaFile = await twonlyDB.mediaFilesDao @@ -204,6 +209,9 @@ class _MediaViewerViewState extends State { bool showTwonly, ) async { if (allMediaFiles.isEmpty) return; + setState(() { + _showDownloadingLoader = false; + }); final currentMediaLocal = await MediaFileService.fromMediaId(allMediaFiles.first.mediaId!); if (currentMediaLocal == null || !mounted) return; @@ -503,6 +511,17 @@ class _MediaViewerViewState extends State { child: Stack( fit: StackFit.expand, children: [ + if (_showDownloadingLoader) + Center( + child: SizedBox( + height: 60, + width: 60, + child: ThreeRotatingDots( + size: 40, + color: context.color.primary, + ), + ), + ), if ((currentMedia != null || videoController != null) && (canBeSeenUntil == null || progress >= 0)) GestureDetector( diff --git a/lib/src/views/components/loader.dart b/lib/src/views/components/loader.dart new file mode 100644 index 0000000..9fe6de9 --- /dev/null +++ b/lib/src/views/components/loader.dart @@ -0,0 +1,248 @@ +// Credits: https://github.com/watery-desert/loading_animation_widget/tree/main/lib/src/three_rotating_dots + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +extension LoadingAnimationControllerX on AnimationController { + T eval(Tween tween, {Curve curve = Curves.linear}) => + tween.transform(curve.transform(value)); + + double evalDouble({ + double from = 0, + double to = 1, + double begin = 0, + double end = 1, + Curve curve = Curves.linear, + }) { + return eval( + Tween(begin: from, end: to), + curve: Interval(begin, end, curve: curve), + ); + } +} + +class ThreeRotatingDots extends StatefulWidget { + const ThreeRotatingDots({ + required this.color, + required this.size, + super.key, + }); + final double size; + final Color color; + + @override + State createState() => _ThreeRotatingDotsState(); +} + +class _ThreeRotatingDotsState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), + )..repeat(); + } + + @override + Widget build(BuildContext context) { + final color = widget.color; + final size = widget.size; + final dotSize = size / 3; + final edgeOffset = (size - dotSize) / 2; + + const firstDotsInterval = Interval( + 0, + 0.50, + curve: Curves.easeInOutCubic, + ); + const secondDotsInterval = Interval( + 0.50, + 1, + curve: Curves.easeInOutCubic, + ); + + return SizedBox( + width: size, + height: size, + child: AnimatedBuilder( + animation: _animationController, + builder: (_, __) => Transform.translate( + offset: Offset(0, size / 12), + child: Stack( + alignment: Alignment.center, + children: [ + _BuildDot.first( + color: color, + size: dotSize, + controller: _animationController, + dotOffset: edgeOffset, + beginAngle: math.pi, + endAngle: 0, + interval: firstDotsInterval, + ), + + _BuildDot.first( + color: color, + size: dotSize, + controller: _animationController, + dotOffset: edgeOffset, + beginAngle: 5 * math.pi / 3, + endAngle: 2 * math.pi / 3, + interval: firstDotsInterval, + ), + + _BuildDot.first( + color: color, + size: dotSize, + controller: _animationController, + dotOffset: edgeOffset, + beginAngle: 7 * math.pi / 3, + endAngle: 4 * math.pi / 3, + interval: firstDotsInterval, + ), + + /// Next 3 dots + + _BuildDot.second( + controller: _animationController, + beginAngle: 0, + endAngle: -math.pi, + interval: secondDotsInterval, + dotOffset: edgeOffset, + color: color, + size: dotSize, + ), + + _BuildDot.second( + controller: _animationController, + beginAngle: 2 * math.pi / 3, + endAngle: -math.pi / 3, + interval: secondDotsInterval, + dotOffset: edgeOffset, + color: color, + size: dotSize, + ), + + _BuildDot.second( + controller: _animationController, + beginAngle: 4 * math.pi / 3, + endAngle: math.pi / 3, + interval: secondDotsInterval, + dotOffset: edgeOffset, + color: color, + size: dotSize, + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} + +class _BuildDot extends StatelessWidget { + const _BuildDot.second({ + required this.controller, + required this.beginAngle, + required this.endAngle, + required this.interval, + required this.dotOffset, + required this.color, + required this.size, + }) : first = false; + + const _BuildDot.first({ + required this.controller, + required this.beginAngle, + required this.endAngle, + required this.interval, + required this.dotOffset, + required this.color, + required this.size, + }) : first = true; + final AnimationController controller; + final double beginAngle; + final double endAngle; + final Interval interval; + final double dotOffset; + final Color color; + final double size; + final bool first; + + @override + Widget build(BuildContext context) { + return Visibility( + visible: first + ? controller.value <= interval.end + : controller.value >= interval.begin, + child: Transform.rotate( + angle: controller.eval( + Tween(begin: beginAngle, end: endAngle), + curve: interval, + ), + child: Transform.translate( + offset: controller.eval( + Tween( + begin: first ? Offset.zero : Offset(0, -dotOffset), + end: first ? Offset(0, -dotOffset) : Offset.zero, + ), + curve: interval, + ), + child: DrawDot.circular( + color: color, + dotSize: size, + ), + ), + ), + ); + } +} + +class DrawDot extends StatelessWidget { + const DrawDot.circular({ + required double dotSize, + required this.color, + super.key, + }) : width = dotSize, + height = dotSize, + circular = true; + + const DrawDot.elliptical({ + required this.width, + required this.height, + required this.color, + super.key, + }) : circular = false; + final double width; + final double height; + final bool circular; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: color, + shape: circular ? BoxShape.circle : BoxShape.rectangle, + borderRadius: circular + ? null + : BorderRadius.all(Radius.elliptical(width, height)), + ), + ); + } +}