Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 214 additions & 134 deletions example/pubspec.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/dash_chat_2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ library dash_chat_2;

import 'dart:math';

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
Expand Down Expand Up @@ -32,6 +33,7 @@ part 'src/widgets/input_toolbar/input_toolbar.dart';
part 'src/widgets/message_list/default_date_separator.dart';
part 'src/widgets/message_list/default_scroll_to_bottom.dart';
part 'src/widgets/message_list/message_list.dart';
part 'src/widgets/message_row/audio_player.dart';
part 'src/widgets/message_row/default_avatar.dart';
part 'src/widgets/message_row/default_message_decoration.dart';
part 'src/widgets/message_row/default_message_text.dart';
Expand Down
3 changes: 3 additions & 0 deletions lib/src/models/chat_media.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class MediaType {
return MediaType.video;
case 'file':
return MediaType.file;
case 'audio':
return MediaType.audio;
default:
throw UnsupportedError('$value is not a valid MediaType');
}
Expand All @@ -81,4 +83,5 @@ class MediaType {
static const MediaType image = MediaType._internal('image');
static const MediaType video = MediaType._internal('video');
static const MediaType file = MediaType._internal('file');
static const MediaType audio = MediaType._internal('audio');
}
14 changes: 14 additions & 0 deletions lib/src/models/message_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class MessageOptions {
this.onPressMention,
Color? currentUserContainerColor,
Color? currentUserTextColor,
Color? audioControlColor,
this.containerColor = const Color(0xFFF5F5F5),
this.textColor = Colors.black,
this.messagePadding = const EdgeInsets.all(11),
Expand Down Expand Up @@ -42,6 +43,7 @@ class MessageOptions {
Color? timeTextColor,
}) : _currentUserContainerColor = currentUserContainerColor,
_currentUserTextColor = currentUserTextColor,
_audioControlColor = audioControlColor,
_currentUserTimeTextColor = currentUserTimeTextColor,
_timeTextColor = timeTextColor;

Expand Down Expand Up @@ -122,6 +124,18 @@ class MessageOptions {
/// Default to: `Colors.grey.shade100`
final Color containerColor;

/// Color of the audio player button
///
/// Default to: `Colors.grey.shade100`
final Color? _audioControlColor;

/// Used to calculate [audioPlayerButtonColor]
///
/// Default to: `Colors.grey.shade100`
Color audioControlColor(BuildContext context) {
return _audioControlColor ?? Theme.of(context).colorScheme.primary;
}

/// Color of the other users text in chat bubbles
///
/// Default to: `Colors.black`
Expand Down
142 changes: 142 additions & 0 deletions lib/src/widgets/message_row/audio_player.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
part of '../../../dash_chat_2.dart';

class AudioPlayerWidget extends StatefulWidget {
const AudioPlayerWidget({
required this.url,
this.canPlay = true,
this.audioControlColor,
this.containerColor = const Color(0xFFF5F5F5),
Key? key,
}) : super(key: key);

/// URL of the audio file
final String url;

/// If the audio can be played
final bool canPlay;

/// Optional color of the play button
final Color? audioControlColor;

/// Background color of the audio message container
final Color containerColor;

@override
State<AudioPlayerWidget> createState() => _AudioPlayerWidgetState();
}

class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
late AudioPlayer _audioPlayer;
bool isPlaying = false;
bool hasError = false;
Duration duration = Duration.zero;
Duration position = Duration.zero;
final List<int> _randomWave =
List<int>.generate(30, (int index) => Random().nextInt(10) + 5);

@override
void initState() {
super.initState();
_audioPlayer = AudioPlayer()
..onDurationChanged.listen((Duration d) => setState(() => duration = d))
..onPositionChanged.listen((Duration p) => setState(() => position = p))
..onPlayerComplete.listen((_) => setState(() {
isPlaying = false;
position = Duration.zero;
}));
}

@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}

Future<void> _togglePlayPause() async {
if (hasError) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Error playing audio. Please check the file format.'),
backgroundColor: Colors.red,
),
);
return;
}

try {
if (isPlaying) {
await _audioPlayer.pause();
} else {
await _audioPlayer.play(UrlSource(widget.url));
}
setState(() => isPlaying = !isPlaying);
} catch (e) {
setState(() {
hasError = true;
isPlaying = false;
});
debugPrint('Exception while playing audio: $e');
}
}

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: widget.containerColor,
borderRadius: BorderRadius.circular(12),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: <Widget>[
GestureDetector(
onTap: widget.canPlay ? _togglePlayPause : null,
child: CircleAvatar(
backgroundColor: hasError
? Colors.red
: widget.audioControlColor ?? Colors.blue,
radius: 20,
child: Icon(
hasError
? Icons.error
: (isPlaying ? Icons.pause : Icons.play_arrow),
color: Colors.white,
),
),
),
const SizedBox(width: 10),
Expanded(
child: hasError
? const Text(
'Error: Invalid audio format',
style: TextStyle(color: Colors.red),
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _randomWave
.map((int h) => Container(
height: h.toDouble(),
width: 3,
margin:
const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color:
widget.audioControlColor ?? Colors.blue,
borderRadius: BorderRadius.circular(2),
),
))
.toList(),
),
),
],
),
),
);
}
}
27 changes: 23 additions & 4 deletions lib/src/widgets/message_row/media_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ class MediaContainer extends StatelessWidget {
child: const CircularProgressIndicator(),
);
switch (media.type) {
case MediaType.audio:
return Stack(
alignment: AlignmentDirectional.bottomEnd,
children: <Widget>[
AudioPlayerWidget(
url: media.url,
key: Key(media.url),
audioControlColor: messageOptions.audioControlColor(context),
),
if (media.isUploading) loading
],
);
case MediaType.video:
return Stack(
alignment: AlignmentDirectional.bottomEnd,
Expand Down Expand Up @@ -103,11 +115,16 @@ class MediaContainer extends StatelessWidget {
final double gallerySize =
(MediaQuery.of(context).size.width * 0.7) / 2 - 5;
final bool isImage = m.type == MediaType.image;
final bool isAudio = m.type == MediaType.audio;
return Container(
color: Colors.transparent,
margin: const EdgeInsets.only(top: 5, right: 5),
width: media.length > 1 && isImage ? gallerySize : null,
height: media.length > 1 && isImage ? gallerySize : null,
width: isAudio
? min(MediaQuery.of(context).size.width * 0.9, 250)
: (media.length > 1 && isImage ? gallerySize : null),
height: isAudio
? 70
: (media.length > 1 && isImage ? gallerySize : null),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.5,
maxWidth: MediaQuery.of(context).size.width * 0.7,
Expand All @@ -130,8 +147,10 @@ class MediaContainer extends StatelessWidget {
child: _getMedia(
context,
m,
media.length > 1 ? gallerySize : null,
media.length > 1 ? gallerySize : null,
isAudio ? 70 : (media.length > 1 ? gallerySize : null),
isAudio
? min(MediaQuery.of(context).size.width * 0.9, 250)
: (media.length > 1 ? gallerySize : null),
),
),
),
Expand Down
Loading