diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml deleted file mode 100644 index b2800858..00000000 --- a/.github/workflows/merge.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Merge - -on: - push: - branches: [main] - workflow_dispatch: - -jobs: - merge-main-to-develop: - name: Merge main back to develop - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set Git config - run: | - git config --local user.email "actions@github.com" - git config --local user.name "Github Actions" - - - name: Merge command - run: | - git fetch --unshallow - git checkout develop - git pull - git merge --no-ff origin/main -m "build: merge main back to develop" - git push \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dba9c20d..aa37039a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.84 + +- [DSImageMessageBubble] Improved the performance of the image bubble +- [DSVideoMessageBubble] Improved the video storage + ## 0.0.83 - [DSCard] Accept reply messages diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index da2c6a89..7f2b52e5 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -50,6 +50,7 @@ export 'src/themes/texts/styles/ds_headline_small_text_style.theme.dart' export 'src/themes/texts/utils/ds_font_families.theme.dart' show DSFontFamilies; export 'src/themes/texts/utils/ds_font_weights.theme.dart' show DSFontWeights; export 'src/utils/ds_animate.util.dart' show DSAnimate; +export 'src/utils/ds_directory_formatter.util.dart' show DSDirectoryFormatter; export 'src/utils/ds_linkify.util.dart' show DSLinkify; export 'src/utils/ds_utils.util.dart' show DSUtils; export 'src/widgets/animations/ds_animated_size.widget.dart' @@ -77,10 +78,10 @@ export 'src/widgets/buttons/ds_icon_button.widget.dart' show DSIconButton; export 'src/widgets/buttons/ds_pause_button.widget.dart' show DSPauseButton; export 'src/widgets/buttons/ds_play_button.widget.dart' show DSPlayButton; export 'src/widgets/buttons/ds_primary_button.widget.dart' show DSPrimaryButton; -export 'src/widgets/buttons/ds_secondary_button.widget.dart' - show DSSecondaryButton; export 'src/widgets/buttons/ds_request_location_button.widget.dart' show DSRequestLocationButton; +export 'src/widgets/buttons/ds_secondary_button.widget.dart' + show DSSecondaryButton; export 'src/widgets/buttons/ds_send_button.widget.dart' show DSSendButton; export 'src/widgets/buttons/ds_tertiary_button.widget.dart' show DSTertiaryButton; @@ -101,6 +102,8 @@ export 'src/widgets/chat/ds_location_message_bubble.widget.dart' export 'src/widgets/chat/ds_message_bubble.widget.dart' show DSMessageBubble; export 'src/widgets/chat/ds_message_bubble_detail.widget.dart' show DSMessageBubbleDetail; +export 'src/widgets/chat/ds_request_location_bubble.widget.dart' + show DSRequestLocationBubble; export 'src/widgets/chat/ds_survey_message_bubble.widget.dart' show DSSurveyMessageBubble; export 'src/widgets/chat/ds_text_message_bubble.widget.dart' @@ -155,5 +158,3 @@ export 'src/widgets/utils/ds_group_card.widget.dart' show DSGroupCard; export 'src/widgets/utils/ds_header.widget.dart' show DSHeader; export 'src/widgets/utils/ds_progress_bar.widget.dart' show DSProgressBar; export 'src/widgets/utils/ds_user_avatar.widget.dart' show DSUserAvatar; -export 'src/widgets/chat/ds_request_location_bubble.widget.dart' - show DSRequestLocationBubble; diff --git a/lib/src/controllers/chat/ds_audio_player.controller.dart b/lib/src/controllers/chat/ds_audio_player.controller.dart index df88bad7..0f12ee37 100644 --- a/lib/src/controllers/chat/ds_audio_player.controller.dart +++ b/lib/src/controllers/chat/ds_audio_player.controller.dart @@ -5,8 +5,7 @@ import 'package:rxdart/rxdart.dart' as rx_dart; class DSAudioPlayerController extends GetxController { final audioSpeed = RxDouble(1.0); final player = AudioPlayer(); - - bool isInitialized = false; + final isInitialized = RxBool(false); /// Collects the data useful for displaying in a SeekBar widget. /// diff --git a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart index 2e497c86..38baf0f5 100644 --- a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart @@ -1,34 +1,69 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; -import 'package:flutter/material.dart'; +import 'package:blip_ds/src/utils/ds_directory_formatter.util.dart'; +import 'package:crypto/crypto.dart'; import 'package:get/get.dart'; import '../../services/ds_auth.service.dart'; +import '../../services/ds_file.service.dart'; class DSImageMessageBubbleController extends GetxController { - Future getImageInfo({ - required final String url, - final bool shouldAuthenticate = false, - }) async { - final Image img = Image.network( - url, - headers: shouldAuthenticate ? DSAuthService.httpHeaders : null, - ); + final maximumProgress = RxInt(0); + final downloadProgress = RxInt(0); + final localPath = RxnString(); + + final String url; + final String? mediaType; + final bool shouldAuthenticate; - final completer = Completer(); + DSImageMessageBubbleController( + this.url, { + this.mediaType, + this.shouldAuthenticate = false, + }) { + _downloadImage(); + } + + void _onReceiveProgress(final int currentProgress, final int maxProgres) { + downloadProgress.value = currentProgress; + maximumProgress.value = maxProgres; + } - final ImageStream imageStream = - img.image.resolve(const ImageConfiguration()); + Future _downloadImage() async { + if (mediaType == null || !url.startsWith('http')) { + localPath.value = url; + return; + } - imageStream.addListener( - ImageStreamListener( - (ImageInfo i, bool _) { - completer.complete(i); - }, - onError: (exception, stackTrace) => completer.completeError(exception), - ), + final uri = Uri.parse(url); + + final fullPath = await DSDirectoryFormatter.getPath( + type: mediaType!, + fileName: md5.convert(utf8.encode(uri.path)).toString(), ); - return completer.future; + if (await File(fullPath).exists()) { + localPath.value = fullPath; + return; + } + + final fileName = fullPath.split('/').last; + final path = fullPath.substring(0, fullPath.lastIndexOf('/')); + + try { + final savedFilePath = await DSFileService.download( + url, + fileName, + path: path, + onReceiveProgress: _onReceiveProgress, + httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null, + ); + + localPath.value = savedFilePath; + } catch (_) { + localPath.value = url; + } } } diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index e4dad29b..9a73ee87 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -1,34 +1,37 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; import 'package:file_sizes/file_sizes.dart'; import 'package:get/get.dart'; -import 'package:path_provider/path_provider.dart'; import '../../models/ds_toast_props.model.dart'; import '../../services/ds_file.service.dart'; import '../../services/ds_toast.service.dart'; +import '../../utils/ds_directory_formatter.util.dart'; import '../../widgets/chat/video/ds_video_error.dialog.dart'; class DSVideoMessageBubbleController { - final String uniqueId; final String url; final int mediaSize; final Map? httpHeaders; + final String type; DSVideoMessageBubbleController({ - required this.uniqueId, required this.url, required this.mediaSize, + required this.type, this.httpHeaders, }) { - setThumbnail(); + getStoredVideo(); } final isDownloading = RxBool(false); final thumbnail = RxString(''); final hasError = RxBool(false); + final isLoadingThumbnail = RxBool(false); String size() { return mediaSize > 0 @@ -40,32 +43,49 @@ class DSVideoMessageBubbleController { : 'Download'; } - Future setThumbnail() async { - final thumbnailFile = File(await getFullThumbnailPath()); - if (await thumbnailFile.exists()) { - thumbnail.value = thumbnailFile.path; + Future getStoredVideo() async { + try { + isLoadingThumbnail.value = true; + final fileName = md5.convert(utf8.encode(Uri.parse(url).path)).toString(); + final fullPath = await DSDirectoryFormatter.getPath( + type: type, + fileName: fileName, + ); + final fullThumbnailPath = await DSDirectoryFormatter.getPath( + type: 'image/png', + fileName: '$fileName-thumbnail', + ); + final file = File(fullPath); + final thumbnailfile = File(fullThumbnailPath); + if (await thumbnailfile.exists()) { + thumbnail.value = thumbnailfile.path; + } else if (await file.exists() && thumbnail.value.isEmpty) { + await _generateThumbnail(file.path); + } + } finally { + isLoadingThumbnail.value = false; } } Future getFullThumbnailPath() async { - final temporaryPath = (await getTemporaryDirectory()).path; - return "$temporaryPath/VIDEO-Thumbnail-$uniqueId.png"; + final fileName = md5.convert(utf8.encode(Uri.parse(url).path)).toString(); + final mediaPath = await DSDirectoryFormatter.getPath( + type: 'image/png', + fileName: '$fileName-thumbnail', + ); + return mediaPath; } Future downloadVideo() async { + final fileName = md5.convert(utf8.encode(Uri.parse(url).path)).toString(); isDownloading.value = true; try { - final path = Uri.parse(url).path; - - var fileName = path.substring(path.lastIndexOf('/')).substring(1); - - if (fileName.isEmpty) { - fileName = DateTime.now().toIso8601String(); - } - - final temporaryPath = (await getTemporaryDirectory()).path; - final outputFile = File('$temporaryPath/VIDEO-$uniqueId.mp4'); + final fullPath = await DSDirectoryFormatter.getPath( + type: 'video/mp4', + fileName: fileName, + ); + final outputFile = File(fullPath); if (!await outputFile.exists()) { final inputFilePath = await DSFileService.download( @@ -77,6 +97,8 @@ class DSVideoMessageBubbleController { final session = await FFmpegKit.execute( '-hide_banner -y -i "$inputFilePath" "${outputFile.path}"'); + File(inputFilePath!).delete(); + final returnCode = await session.getReturnCode(); if (!ReturnCode.isSuccess(returnCode)) { @@ -89,13 +111,7 @@ class DSVideoMessageBubbleController { } } - final thumbnailPath = await getFullThumbnailPath(); - - await FFmpegKit.execute( - '-hide_banner -y -i "${outputFile.path}" -vframes 1 "$thumbnailPath"', - ); - - thumbnail.value = thumbnailPath; + _generateThumbnail(outputFile.path); } catch (_) { hasError.value = true; @@ -110,4 +126,14 @@ class DSVideoMessageBubbleController { isDownloading.value = false; } } + + Future _generateThumbnail(String path) async { + final thumbnailPath = await getFullThumbnailPath(); + + await FFmpegKit.execute( + '-hide_banner -y -i "$path" -vframes 1 "$thumbnailPath"', + ); + + thumbnail.value = thumbnailPath; + } } diff --git a/lib/src/controllers/ds_video_player.controller.dart b/lib/src/controllers/ds_video_player.controller.dart index e8a3a0d4..00f50711 100644 --- a/lib/src/controllers/ds_video_player.controller.dart +++ b/lib/src/controllers/ds_video_player.controller.dart @@ -13,15 +13,12 @@ class DSVideoPlayerController extends GetxController { /// the management of video controls. DSVideoPlayerController({ required this.url, - required this.uniqueId, this.shouldAuthenticate = false, }); // External URL containing the video to be played final String url; - final String uniqueId; - /// Indicates if the HTTP Requests should be authenticated or not. final bool shouldAuthenticate; diff --git a/lib/src/services/ds_file.service.dart b/lib/src/services/ds_file.service.dart index 640a2fd2..1952ca00 100644 --- a/lib/src/services/ds_file.service.dart +++ b/lib/src/services/ds_file.service.dart @@ -47,6 +47,7 @@ abstract class DSFileService { final String? path, final void Function(bool)? onDownloadStateChange, final Map? httpHeaders, + final Function(int, int)? onReceiveProgress, }) async { try { onDownloadStateChange?.call(true); @@ -63,6 +64,10 @@ abstract class DSFileService { options: Options( headers: httpHeaders, ), + onReceiveProgress: onReceiveProgress != null + ? (currentProgress, maximumProgress) => + onReceiveProgress(currentProgress, maximumProgress) + : null, ); if (response.statusCode == 200) return savedFilePath; diff --git a/lib/src/utils/ds_directory_formatter.util.dart b/lib/src/utils/ds_directory_formatter.util.dart new file mode 100644 index 00000000..49fb68c0 --- /dev/null +++ b/lib/src/utils/ds_directory_formatter.util.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; + +abstract class DSDirectoryFormatter { + static Future getPath({ + required final String type, + required final String fileName, + }) async { + final cachePath = (await getApplicationCacheDirectory()).path; + + final typeFolder = '${type.split('/').first.capitalizeFirst}'; + final extension = type.split('/').last; + + final typePrefix = '${typeFolder.substring(0, 3).toUpperCase()}-'; + + final newFileName = + '${!fileName.startsWith(typePrefix) ? typePrefix : ''}$fileName'; + + final path = await _formatDirectory( + type: typeFolder, + directory: cachePath, + ); + + return '$path/$newFileName.$extension'; + } + + static Future _formatDirectory({ + required final String type, + required final String directory, + }) async { + final formattedDirectory = '$directory/$type'; + final directoryExists = await Directory(formattedDirectory).exists(); + + if (!directoryExists) { + await Directory(formattedDirectory).create(recursive: true); + } + + return formattedDirectory; + } +} diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index 7ad7d55b..ef01775a 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -126,9 +126,9 @@ class _DSAudioPlayerState extends State ), ); - _controller.isInitialized = true; + _controller.isInitialized.value = true; } catch (_) { - _controller.isInitialized = false; + _controller.isInitialized.value = false; } } @@ -176,15 +176,17 @@ class _DSAudioPlayerState extends State final processingState = playerState?.processingState; final playing = playerState?.playing; if (playing != true) { - return DSPlayButton( - onPressed: _controller.isInitialized - ? _controller.player.play - : () => {}, - isLoading: [ProcessingState.loading, ProcessingState.buffering] - .contains(processingState), - color: _controller.isInitialized - ? widget.controlForegroundColor - : DSColors.contentDisable, + return Obx( + () => DSPlayButton( + onPressed: _controller.isInitialized.value + ? _controller.player.play + : () => {}, + isLoading: [ProcessingState.loading, ProcessingState.buffering] + .contains(processingState), + color: _controller.isInitialized.value + ? widget.controlForegroundColor + : DSColors.contentDisable, + ), ); } else if (processingState != ProcessingState.completed) { return DSPauseButton( @@ -202,19 +204,24 @@ class _DSAudioPlayerState extends State stream: _controller.positionDataStream, builder: (context, snapshot) { final positionData = snapshot.data; - return DSAudioSeekBar( - duration: positionData?.duration ?? Duration.zero, - position: positionData?.position ?? Duration.zero, - bufferedPosition: positionData?.bufferedPosition ?? Duration.zero, - onChangeEnd: - _controller.isInitialized ? _controller.player.play : null, - onChanged: _controller.isInitialized ? _controller.player.seek : null, - onChangeStart: _controller.player.pause, - labelColor: widget.labelColor, - bufferActiveTrackColor: widget.bufferActiveTrackColor, - bufferInactiveTrackColor: widget.bufferInactiveTrackColor, - sliderActiveTrackColor: widget.sliderActiveTrackColor, - sliderThumbColor: widget.sliderThumbColor, + return Obx( + () => DSAudioSeekBar( + duration: positionData?.duration ?? Duration.zero, + position: positionData?.position ?? Duration.zero, + bufferedPosition: positionData?.bufferedPosition ?? Duration.zero, + onChangeEnd: _controller.isInitialized.value + ? _controller.player.play + : null, + onChanged: _controller.isInitialized.value + ? _controller.player.seek + : null, + onChangeStart: _controller.player.pause, + labelColor: widget.labelColor, + bufferActiveTrackColor: widget.bufferActiveTrackColor, + bufferInactiveTrackColor: widget.bufferInactiveTrackColor, + sliderActiveTrackColor: widget.sliderActiveTrackColor, + sliderThumbColor: widget.sliderThumbColor, + ), ); }, ); diff --git a/lib/src/widgets/chat/ds_carrousel.widget.dart b/lib/src/widgets/chat/ds_carrousel.widget.dart index d398b7fc..388563d1 100644 --- a/lib/src/widgets/chat/ds_carrousel.widget.dart +++ b/lib/src/widgets/chat/ds_carrousel.widget.dart @@ -7,7 +7,6 @@ import '../../models/ds_message_bubble_avatar_config.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; import '../../utils/ds_utils.util.dart'; import '../utils/ds_card.widget.dart'; - import 'ds_image_message_bubble.widget.dart'; /// A Design System widget used to display multiple cards. @@ -95,6 +94,7 @@ class DSCarrousel extends StatelessWidget { onSelected: onSelected, onOpenLink: onOpenLink, style: style, + mediaType: header["value"]["type"], ), ), ); diff --git a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart index 51759838..92311f76 100644 --- a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../controllers/chat/ds_image_message_bubble.controller.dart'; import '../../enums/ds_align.enum.dart'; @@ -6,7 +7,6 @@ import '../../enums/ds_border_radius.enum.dart'; import '../../models/ds_document_select.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; -import '../../utils/ds_utils.util.dart'; import '../texts/ds_caption_text.widget.dart'; import '../utils/ds_expanded_image.widget.dart'; import 'ds_document_select.widget.dart'; @@ -30,6 +30,9 @@ class DSImageMessageBubble extends StatefulWidget { this.onSelected, this.onOpenLink, this.shouldAuthenticate = false, + this.mediaType, + this.imageMaxHeight, + this.imageMinHeight, }) : style = style ?? DSMessageBubbleStyle(); final DSAlign align; @@ -46,6 +49,9 @@ class DSImageMessageBubble extends StatefulWidget { final void Function(String, Map)? onSelected; final void Function(Map)? onOpenLink; final bool shouldAuthenticate; + final String? mediaType; + final double? imageMaxHeight; + final double? imageMinHeight; @override State createState() => _DSImageMessageBubbleState(); @@ -58,7 +64,12 @@ class _DSImageMessageBubbleState extends State @override initState() { super.initState(); - _controller = DSImageMessageBubbleController(); + + _controller = DSImageMessageBubbleController( + widget.url, + mediaType: widget.mediaType, + shouldAuthenticate: widget.shouldAuthenticate, + ); } @override @@ -77,86 +88,100 @@ class _DSImageMessageBubbleState extends State padding: EdgeInsets.zero, hasSpacer: widget.hasSpacer, style: widget.style, - child: FutureBuilder( - future: _controller.getImageInfo( - url: widget.url, - shouldAuthenticate: widget.shouldAuthenticate, - ), - builder: (buildContext, snapshot) { - final isLoadingImage = !(snapshot.hasData || snapshot.hasError); - - final ImageInfo? data = - snapshot.hasData ? snapshot.data as ImageInfo : null; - - final width = - snapshot.hasData && data!.image.width > DSUtils.bubbleMinSize - ? data.image.width.toDouble() - : DSUtils.bubbleMinSize; - - return LayoutBuilder( - builder: (_, constraints) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DSExpandedImage( - appBarText: widget.appBarText, - appBarPhotoUri: widget.appBarPhotoUri, - url: widget.url, - width: width, - maxHeight: DSUtils.bubbleMaxSize, - align: widget.align, - style: widget.style, - isLoading: isLoadingImage, - shouldAuthenticate: widget.shouldAuthenticate, - ), - if ((widget.title?.isNotEmpty ?? false) || - (widget.text?.isNotEmpty ?? false)) - SizedBox( - width: width, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.title?.isNotEmpty ?? false) - DSCaptionText( - widget.title!, - color: foregroundColor, - isSelectable: true, - ), - if ((widget.text?.isNotEmpty ?? false) && - (widget.title?.isNotEmpty ?? false)) - const SizedBox( - height: 6.0, - ), - if (widget.text?.isNotEmpty ?? false) - DSShowMoreText( - text: widget.text!, - maxWidth: constraints.maxWidth, - align: widget.align, - style: widget.style, - ) - ], - ), - ), - ), - if (widget.showSelect) - DSDocumentSelect( - align: widget.align, - options: widget.selectOptions, - onSelected: widget.onSelected, - onOpenLink: widget.onOpenLink, - style: widget.style, + child: LayoutBuilder( + builder: (_, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx( + () => _controller.localPath.value != null + ? DSExpandedImage( + appBarText: widget.appBarText, + appBarPhotoUri: widget.appBarPhotoUri, + url: _controller.localPath.value!, + maxHeight: widget.imageMaxHeight != null + ? widget.imageMaxHeight! + : widget.showSelect + ? 200.0 + : double.infinity, + minHeight: widget.imageMinHeight != null + ? widget.imageMinHeight! + : widget.showSelect + ? 200.0 + : 0.0, + align: widget.align, + style: widget.style, + isLoading: false, + shouldAuthenticate: widget.shouldAuthenticate, + ) + : _buildDownloadProgress(), + ), + if ((widget.title?.isNotEmpty ?? false) || + (widget.text?.isNotEmpty ?? false)) + SizedBox( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title?.isNotEmpty ?? false) + DSCaptionText( + widget.title!, + color: foregroundColor, + isSelectable: true, + ), + if ((widget.text?.isNotEmpty ?? false) && + (widget.title?.isNotEmpty ?? false)) + const SizedBox( + height: 6.0, + ), + if (widget.text?.isNotEmpty ?? false) + DSShowMoreText( + text: widget.text!, + maxWidth: constraints.maxWidth, + align: widget.align, + style: widget.style, + ) + ], ), - ], - ); - }, + ), + ), + if (widget.showSelect) + DSDocumentSelect( + align: widget.align, + options: widget.selectOptions, + onSelected: widget.onSelected, + onOpenLink: widget.onOpenLink, + style: widget.style, + ), + ], ); }, ), ); } + Widget _buildDownloadProgress() { + final foregroundColor = widget.style.isLightBubbleBackground(widget.align) + ? DSColors.neutralDarkCity + : DSColors.neutralLightSnow; + + final double percent = _controller.maximumProgress.value > 0 + ? _controller.downloadProgress.value / _controller.maximumProgress.value + : 0; + + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator( + color: foregroundColor, + backgroundColor: Colors.grey, + value: percent, + ), + ), + ); + } + @override bool get wantKeepAlive => true; } diff --git a/lib/src/widgets/chat/video/ds_video_body.widget.dart b/lib/src/widgets/chat/video/ds_video_body.widget.dart index d4ffdcbe..63fb81cb 100644 --- a/lib/src/widgets/chat/video/ds_video_body.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_body.widget.dart @@ -10,7 +10,6 @@ class DSVideoBody extends StatelessWidget { required this.appBarText, required this.appBarPhotoUri, required this.url, - required this.uniqueId, required this.thumbnail, this.shouldAuthenticate = false, }); @@ -19,7 +18,6 @@ class DSVideoBody extends StatelessWidget { final String appBarText; final Uri? appBarPhotoUri; final String url; - final String uniqueId; final Widget thumbnail; final bool shouldAuthenticate; @@ -43,7 +41,6 @@ class DSVideoBody extends StatelessWidget { appBarText: appBarText, appBarPhotoUri: appBarPhotoUri, url: url, - uniqueId: uniqueId, shouldAuthenticate: shouldAuthenticate, ), ); diff --git a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart index 24129f8c..33a3916b 100644 --- a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart @@ -39,12 +39,12 @@ class DSVideoMessageBubble extends StatefulWidget { /// Style for bubble final DSMessageBubbleStyle style; - // Unique id to message bubble - final String uniqueId; - /// The video size final int mediaSize; + /// The video type + final String type; + /// Indicates if the HTTP Requests should be authenticated or not. final bool shouldAuthenticate; @@ -58,9 +58,9 @@ class DSVideoMessageBubble extends StatefulWidget { required this.align, required this.url, required this.appBarText, - required this.uniqueId, required this.mediaSize, this.appBarPhotoUri, + this.type = 'video/mp4', this.text, this.borderRadius = const [DSBorderRadius.all], this.shouldAuthenticate = false, @@ -79,10 +79,10 @@ class _DSVideoMessageBubbleState extends State void initState() { super.initState(); _controller = DSVideoMessageBubbleController( - uniqueId: widget.uniqueId, url: widget.url, mediaSize: widget.mediaSize, httpHeaders: widget.shouldAuthenticate ? DSAuthService.httpHeaders : null, + type: widget.type, ); } @@ -135,13 +135,15 @@ class _DSVideoMessageBubbleState extends State size: 80.0, color: DSColors.neutralDarkRooftop, ) - : _controller.isDownloading.value + : (_controller.isDownloading.value || + _controller.isLoadingThumbnail.value) ? Center( child: Container( height: 50.0, decoration: BoxDecoration( - shape: BoxShape.circle, - color: foregroundColor), + shape: BoxShape.circle, + color: foregroundColor, + ), child: DSFadingCircleLoading( color: backgroundLoadingColor, size: 45.0, @@ -169,7 +171,6 @@ class _DSVideoMessageBubbleState extends State align: widget.align, appBarPhotoUri: widget.appBarPhotoUri, appBarText: widget.appBarText, - uniqueId: widget.uniqueId, url: widget.url, shouldAuthenticate: widget.shouldAuthenticate, thumbnail: Center( diff --git a/lib/src/widgets/chat/video/ds_video_player.widget.dart b/lib/src/widgets/chat/video/ds_video_player.widget.dart index e251ab32..264855a0 100644 --- a/lib/src/widgets/chat/video/ds_video_player.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_player.widget.dart @@ -4,6 +4,8 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import '../../../controllers/ds_video_player.controller.dart'; +import '../../../themes/colors/ds_colors.theme.dart'; +import '../../../themes/icons/ds_icons.dart'; import '../../../themes/system_overlay/ds_system_overlay.style.dart'; import '../../utils/ds_header.widget.dart'; @@ -16,8 +18,6 @@ class DSVideoPlayer extends StatelessWidget { /// Avatar to be displayed in the appBarr final Uri? appBarPhotoUri; - final String uniqueId; - /// Indicates if the HTTP Requests should be authenticated or not. final bool shouldAuthenticate; @@ -29,13 +29,11 @@ class DSVideoPlayer extends StatelessWidget { Key? key, required this.appBarText, required String url, - required this.uniqueId, this.appBarPhotoUri, this.shouldAuthenticate = false, }) : controller = Get.put( DSVideoPlayerController( url: url, - uniqueId: uniqueId, ), ), super(key: key); @@ -73,9 +71,15 @@ class DSVideoPlayer extends StatelessWidget { 8.0, 8.0 + MediaQuery.of(context).padding.bottom, ), - child: Chewie( - controller: controller.chewieController!, - ), + child: controller.chewieController == null + ? const Icon( + DSIcons.video_broken_outline, + size: 80.0, + color: DSColors.neutralDarkRooftop, + ) + : Chewie( + controller: controller.chewieController!, + ), ), ), ), diff --git a/lib/src/widgets/utils/ds_card.widget.dart b/lib/src/widgets/utils/ds_card.widget.dart index 596ce460..9744fd57 100644 --- a/lib/src/widgets/utils/ds_card.widget.dart +++ b/lib/src/widgets/utils/ds_card.widget.dart @@ -181,6 +181,7 @@ class DSCard extends StatelessWidget { showSelect: true, onSelected: onSelected, onOpenLink: onOpenLink, + mediaType: documentSelectModel.header.mediaLink.type, ); } @@ -273,6 +274,8 @@ class DSCard extends StatelessWidget { borderRadius: borderRadius, style: style, shouldAuthenticate: shouldAuthenticate, + mediaType: media.type, + imageMaxHeight: 300.0, ); } else if (media.type.contains('video')) { return DSVideoMessageBubble( @@ -288,7 +291,6 @@ class DSCard extends StatelessWidget { text: media.text, borderRadius: borderRadius, style: style, - uniqueId: messageId ?? DateTime.now().toIso8601String(), mediaSize: size, shouldAuthenticate: shouldAuthenticate, ); diff --git a/lib/src/widgets/utils/ds_expanded_image.widget.dart b/lib/src/widgets/utils/ds_expanded_image.widget.dart index 99e2851b..e411d8d4 100644 --- a/lib/src/widgets/utils/ds_expanded_image.widget.dart +++ b/lib/src/widgets/utils/ds_expanded_image.widget.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -10,6 +12,7 @@ class DSExpandedImage extends StatelessWidget { final String url; final BoxFit fit; final double width; + final double minHeight; final double maxHeight; final bool isLoading; final Uri? appBarPhotoUri; @@ -27,6 +30,7 @@ class DSExpandedImage extends StatelessWidget { this.fit = BoxFit.cover, this.width = double.infinity, this.maxHeight = double.infinity, + this.minHeight = 0.0, this.isLoading = false, this.appBarPhotoUri, this.shouldAuthenticate = false, @@ -49,21 +53,52 @@ class DSExpandedImage extends StatelessWidget { child: Container( constraints: BoxConstraints( maxHeight: maxHeight, + minHeight: minHeight, ), - child: DSCachedNetworkImageView( - fit: fit, - width: width, - url: url, - placeholder: (_, __) => _buildLoading(), - onError: () => _error.value = true, - align: align, - style: style, - shouldAuthenticate: shouldAuthenticate, - ), + child: url.startsWith('http') + ? DSCachedNetworkImageView( + fit: fit, + width: width, + url: url, + placeholder: (_, __) => _buildLoading(), + onError: () => _error.value = true, + align: align, + style: style, + shouldAuthenticate: shouldAuthenticate, + ) + : Image.file( + File(url), + width: width, + fit: fit, + cacheWidth: 360, + errorBuilder: (_, __, ___) => _defaultErrorWidget(), + ), ), ), ); + Widget _defaultErrorWidget() { + _error.value = true; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Icon( + DSIcons.file_image_broken_outline, + color: style.isLightBubbleBackground(align) + ? DSColors.neutralMediumElephant + : DSColors.neutralMediumCloud, + size: 75, + ), + ), + ), + ], + ); + } + Future _expandImage() => showGeneralDialog( context: Get.context!, barrierDismissible: false, @@ -99,14 +134,19 @@ class DSExpandedImage extends StatelessWidget { child: Container( padding: const EdgeInsets.all(8.0), child: PinchZoom( - child: DSCachedNetworkImageView( - url: url, - fit: BoxFit.contain, - placeholder: (context, _) => _buildLoading(), - align: align, - style: style, - shouldAuthenticate: shouldAuthenticate, - ), + child: url.startsWith('http') + ? DSCachedNetworkImageView( + url: url, + fit: BoxFit.contain, + placeholder: (context, _) => _buildLoading(), + align: align, + style: style, + shouldAuthenticate: shouldAuthenticate, + ) + : Image.file( + File(url), + fit: BoxFit.contain, + ), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 941fa970..e4986f77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: blip_ds description: Blip Design System for Flutter. -version: 0.0.83 +version: 0.0.84 homepage: https://github.com/takenet/blip-ds-flutter#readme repository: https://github.com/takenet/blip-ds-flutter @@ -18,7 +18,7 @@ dependencies: get: ^4.6.5 filesize: ^2.0.1 open_filex: ^4.3.2 - path_provider: ^2.0.11 + path_provider: ^2.1.1 dio: ^5.2.1+1 url_launcher: ^6.1.5 path: ^1.8.1 @@ -39,6 +39,7 @@ dependencies: dotted_border: ^2.0.0+3 map_launcher: ^2.5.0+1 mime: ^1.0.4 + crypto: ^3.0.3 dev_dependencies: flutter_test: diff --git a/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart b/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart index 62b571ab..a0f33e2a 100644 --- a/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart +++ b/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart @@ -250,7 +250,6 @@ class SampleMessageBubbleShowcase extends StatelessWidget { url: _srcsVideo[0], text: '.mov', appBarText: 'Unknown User', - uniqueId: 'video1', mediaSize: 10000, ), DSVideoMessageBubble( @@ -258,7 +257,6 @@ class SampleMessageBubbleShowcase extends StatelessWidget { url: _srcsVideo[1], text: '.avi!', appBarText: 'Unknown User', - uniqueId: 'video2', mediaSize: 10000, ), DSVideoMessageBubble( @@ -266,7 +264,6 @@ class SampleMessageBubbleShowcase extends StatelessWidget { url: _srcsVideo[2], text: '.mpeg', appBarText: 'Unknown User', - uniqueId: 'video3', mediaSize: 10000, ), DSVideoMessageBubble( @@ -274,7 +271,6 @@ class SampleMessageBubbleShowcase extends StatelessWidget { url: _srcsVideo[3], text: '.mpg', appBarText: 'Unknown User', - uniqueId: 'video4', mediaSize: 10000, ), DSContactMessageBubble( diff --git a/sample/pubspec.lock b/sample/pubspec.lock index 5735e40c..67d875ba 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -108,10 +108,10 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" csslib: dependency: transitive description: @@ -499,50 +499,50 @@ packages: dependency: transitive description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" pedantic: dependency: transitive description: @@ -974,4 +974,4 @@ packages: version: "6.3.0" sdks: dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.7.0-0" + flutter: ">=3.7.0"