diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index dac8af6376..10448d24de 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -85,10 +85,6 @@ class MessageContent extends StatelessWidget } return MessageDownloadContent( event, - onFileTapped: (event) => onFileTapped( - context: context, - event: event, - ), ); case MessageTypes.Video: if (event.isVideoAvailable) { @@ -105,10 +101,6 @@ class MessageContent extends StatelessWidget children: [ MessageDownloadContent( event, - onFileTapped: (event) => onFileTapped( - context: context, - event: event, - ), ), Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, @@ -124,10 +116,6 @@ class MessageContent extends StatelessWidget children: [ MessageDownloadContent( event, - onFileTapped: (event) => onFileTapped( - context: context, - event: event, - ), ), Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index c78349c80c..8208addb42 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,45 +1,191 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -class MessageDownloadContent extends StatelessWidget { +class MessageDownloadContent extends StatefulWidget { final Event event; - final void Function(Event event)? onFileTapped; final String? highlightText; const MessageDownloadContent( this.event, { Key? key, - this.onFileTapped, this.highlightText, }) : super(key: key); @override - Widget build(BuildContext context) { - final filename = event.filename; - final filetype = event.fileType; - final sizeString = event.sizeString; + State createState() => _MessageDownloadContentState(); +} + +class _MessageDownloadContentState extends State + with HandleDownloadAndPreviewFileMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + @override + void initState() { + super.initState(); + checkDownloadFileState(); + } + + void checkDownloadFileState() async { + if (!PlatformInfos.isWeb) { + final filePath = await widget.event.getFileNameInAppDownload(); + final file = File(filePath); + if (await file.exists() && + await file.length() == widget.event.getFileSize()) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePath, + ); + return; + } + } + setupListeningForStreamSubcription(); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } - Logs().i( - 'filename: $filename, filetype: $filetype, sizeString: $sizeString, content: ${event.content}', + void setupListeningForStreamSubcription() { + streamSubscription = downloadManager + .getDownloadStateStream(widget.event.eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either event) { + event.fold( + (failure) { + Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadNativeFileSuccessState) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: success.filePath, + ); + } else if (success is DownloadMatrixFileSuccessState) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + } + }, ); - return InkWell( - onTap: onFileTapped != null - ? () { - onFileTapped?.call(event); - } - : null, - child: FileTileWidget( - mimeType: event.mimeType, - fileType: filetype, - filename: filename, - highlightText: highlightText, - sizeString: sizeString, - style: MessageFileTileStyle(), - ), + } + + @override + void dispose() { + streamSubscription?.cancel(); + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final filename = widget.event.filename; + final filetype = widget.event.fileType; + final sizeString = widget.event.sizeString; + return ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: (context, DownloadPresentationState state, child) { + if (state is DownloadedPresentationState) { + return InkWell( + onTap: () async { + openDownloadedFileForPreview( + filePath: state.filePath, + mimeType: widget.event.mimeType, + ); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); + } else if (state is DownloadingPresentationState) { + return DownloadFileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + downloadFileStateNotifier: downloadFileStateNotifier, + onCancelDownload: () { + downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + downloadManager.cancelDownload(widget.event.eventId); + }, + ); + } else if (state is FileWebDownloadedPresentationState) { + return InkWell( + onTap: () { + handlePreviewWeb( + event: widget.event, + context: context, + ); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); + } + + return InkWell( + onTap: () { + downloadFileStateNotifier.value = + const DownloadingPresentationState(); + downloadManager.download( + event: widget.event, + ); + setupListeningForStreamSubcription(); + }, + child: DownloadFileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + downloadFileStateNotifier: downloadFileStateNotifier, + style: const MessageFileTileStyle(), + ), + ); + }, ); } } diff --git a/lib/presentation/model/chat/downloading_state_presentation_model.dart b/lib/presentation/model/chat/downloading_state_presentation_model.dart new file mode 100644 index 0000000000..4086a76175 --- /dev/null +++ b/lib/presentation/model/chat/downloading_state_presentation_model.dart @@ -0,0 +1,46 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +abstract class DownloadPresentationState with EquatableMixin { + const DownloadPresentationState(); + + @override + List get props => []; +} + +class NotDownloadPresentationState extends DownloadPresentationState { + const NotDownloadPresentationState() : super(); +} + +class DownloadedPresentationState extends DownloadPresentationState { + final String filePath; + + const DownloadedPresentationState({required this.filePath}) : super(); + + @override + List get props => [filePath]; +} + +class FileWebDownloadedPresentationState extends DownloadPresentationState { + final MatrixFile matrixFile; + + const FileWebDownloadedPresentationState({required this.matrixFile}) + : super(); + + @override + List get props => [matrixFile]; +} + +class DownloadingPresentationState extends DownloadPresentationState { + final int? receive; + + final int? total; + + const DownloadingPresentationState({ + this.receive, + this.total, + }); + + @override + List get props => [receive, total]; +} diff --git a/lib/utils/matrix_sdk_extensions/int_extension.dart b/lib/utils/matrix_sdk_extensions/int_extension.dart index c485c1b055..e6189cad72 100644 --- a/lib/utils/matrix_sdk_extensions/int_extension.dart +++ b/lib/utils/matrix_sdk_extensions/int_extension.dart @@ -1,5 +1,11 @@ -extension DoubleExtension on int { - String bytesToMB() { - return (this / (1024 * 1024)).toString(); +extension IntExtension on int { + String bytesToMB({int? placeDecimal}) { + return (this / (1024 * 1024)).toStringAsFixed(placeDecimal ?? 0); } + + String bytesToKB({int? placeDecimal}) { + return (this / 1024).toStringAsFixed(placeDecimal ?? 0); + } + + static const oneKB = 1024 * 1024; } diff --git a/lib/widgets/file_widget/download_file_tile_widget.dart b/lib/widgets/file_widget/download_file_tile_widget.dart new file mode 100644 index 0000000000..f57748e62a --- /dev/null +++ b/lib/widgets/file_widget/download_file_tile_widget.dart @@ -0,0 +1,166 @@ +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; + +class DownloadFileTileWidget extends StatefulWidget { + const DownloadFileTileWidget({ + super.key, + this.style = const MessageFileTileStyle(), + required this.mimeType, + required this.filename, + this.highlightText, + this.fileType, + this.sizeString, + required this.downloadFileStateNotifier, + this.onCancelDownload, + }); + + final TwakeMimeType mimeType; + final String filename; + final MessageFileTileStyle style; + final String? highlightText; + final String? sizeString; + final String? fileType; + final ValueNotifier downloadFileStateNotifier; + final VoidCallback? onCancelDownload; + + @override + State createState() => _DownloadFileTileWidgetState(); +} + +class _DownloadFileTileWidgetState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + AnimationController? _controller; + Animation? _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _animation = CurvedAnimation( + parent: _controller!, + curve: Curves.linear, + ); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Container( + padding: widget.style.paddingFileTileAll, + decoration: ShapeDecoration( + color: widget.style.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: widget.style.borderRadius, + ), + ), + child: Row( + crossAxisAlignment: widget.style.crossAxisAlignment, + children: [ + ValueListenableBuilder( + valueListenable: widget.downloadFileStateNotifier, + builder: (context, downloadFileState, child) { + double? downloadProgress; + if (downloadFileState is DownloadingPresentationState) { + if (downloadFileState.total == null || + downloadFileState.receive == null) { + downloadProgress = null; + } else { + downloadProgress = + downloadFileState.receive! / downloadFileState.total!; + } + } else if (downloadFileState is NotDownloadPresentationState) { + downloadProgress = 0; + } + if (downloadProgress == 0) { + return Padding( + padding: widget.style.paddingDownloadFileIcon, + child: Icon( + Icons.file_download_rounded, + size: widget.style.iconSize, + ), + ); + } + return Stack( + alignment: Alignment.center, + children: [ + RotationTransition( + turns: _animation!, + child: CircularProgressIndicator( + strokeWidth: widget.style.strokeWidthLoading, + value: downloadProgress, + ), + ), + IconButton( + onPressed: + PlatformInfos.isWeb ? null : widget.onCancelDownload, + icon: Icon( + Icons.close, + size: widget.style.cancelButtonSize, + ), + ), + ], + ); + }, + ), + widget.style.paddingRightIcon, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 4.0), + FileNameText( + filename: widget.filename, + highlightText: widget.highlightText, + style: widget.style, + ), + Row( + children: [ + if (widget.sizeString != null) + TextInformationOfFile( + value: widget.sizeString!, + style: widget.style, + downloadFileStateNotifier: + widget.downloadFileStateNotifier, + ), + TextInformationOfFile( + value: " · ", + style: widget.style, + ), + Flexible( + child: TextInformationOfFile( + value: widget.mimeType.getFileType( + context, + fileType: widget.fileType, + ), + style: widget.style, + ), + ), + ], + ), + widget.style.paddingBottomText, + ], + ), + ), + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/file_widget/file_tile_widget.dart b/lib/widgets/file_widget/file_tile_widget.dart index 5203f63e04..e086beffc7 100644 --- a/lib/widgets/file_widget/file_tile_widget.dart +++ b/lib/widgets/file_widget/file_tile_widget.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/int_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; import 'package:flutter/material.dart'; @@ -69,7 +71,7 @@ class FileTileWidget extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ const SizedBox(height: 4.0), - _FileNameText( + FileNameText( filename: filename, highlightText: highlightText, style: style, @@ -77,16 +79,16 @@ class FileTileWidget extends StatelessWidget { Row( children: [ if (sizeString != null) - _TextInformationOfFile( + TextInformationOfFile( value: sizeString!, style: style, ), - _TextInformationOfFile( + TextInformationOfFile( value: " · ", style: style, ), Flexible( - child: _TextInformationOfFile( + child: TextInformationOfFile( value: mimeType.getFileType( context, fileType: fileType, @@ -106,8 +108,9 @@ class FileTileWidget extends StatelessWidget { } } -class _FileNameText extends StatelessWidget { - const _FileNameText({ +class FileNameText extends StatelessWidget { + const FileNameText({ + super.key, required this.filename, this.highlightText, this.style = const FileTileWidgetStyle(), @@ -133,21 +136,47 @@ class _FileNameText extends StatelessWidget { } } -class _TextInformationOfFile extends StatelessWidget { +class TextInformationOfFile extends StatelessWidget { final String value; final FileTileWidgetStyle style; - const _TextInformationOfFile({ + final ValueNotifier? downloadFileStateNotifier; + + const TextInformationOfFile({ + super.key, required this.value, + this.downloadFileStateNotifier, this.style = const FileTileWidgetStyle(), }); @override Widget build(BuildContext context) { - return Text( - value, - style: style.textInformationStyle(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, + return Row( + children: [ + if (downloadFileStateNotifier != null) + ValueListenableBuilder( + valueListenable: downloadFileStateNotifier!, + builder: ((context, downloadFileState, child) { + if (downloadFileState is DownloadingPresentationState && + downloadFileState.total != null && + downloadFileState.receive != null && + downloadFileState.total! >= IntExtension.oneKB) { + return Text( + '${downloadFileState.receive!.bytesToMB(placeDecimal: 1)} MB / ', + style: style.textInformationStyle(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + return const SizedBox.shrink(); + }), + ), + Text( + value, + style: style.textInformationStyle(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ); } } diff --git a/lib/widgets/file_widget/message_file_tile_style.dart b/lib/widgets/file_widget/message_file_tile_style.dart index 6811a50d7c..1f747b2587 100644 --- a/lib/widgets/file_widget/message_file_tile_style.dart +++ b/lib/widgets/file_widget/message_file_tile_style.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'file_tile_widget_style.dart'; class MessageFileTileStyle extends FileTileWidgetStyle { + const MessageFileTileStyle(); + @override double get iconSize => 36; @@ -24,8 +26,17 @@ class MessageFileTileStyle extends FileTileWidgetStyle { } @override - Widget get paddingBottomText => const SizedBox(height: 4.0); + Widget get paddingBottomText => const SizedBox(height: 8.0); @override Widget get paddingRightIcon => const SizedBox(width: 4.0); + + EdgeInsets get paddingDownloadFileIcon => const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 4.0, + ); + + double get strokeWidthLoading => 2; + + double get cancelButtonSize => 32; }