Commit 1610ec8c authored by skeyboy's avatar skeyboy

回答消息增加复制、分享菜单和tts语音播报

parent 1b13148f
library dash_chat_2; library dash_chat_2;
import 'dart:math'; import 'dart:math';
import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
import 'package:clipboard/clipboard.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:highlight/highlight.dart' show highlight, Node; import 'package:highlight/highlight.dart' show highlight, Node;
import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:chart/package/markdown/flutter_markdown.dart'; import 'package:chart/package/markdown/flutter_markdown.dart';
...@@ -14,9 +18,11 @@ import 'package:gradient_borders/input_borders/gradient_outline_input_border.dar ...@@ -14,9 +18,11 @@ import 'package:gradient_borders/input_borders/gradient_outline_input_border.dar
import 'package:highlight/highlight.dart'; import 'package:highlight/highlight.dart';
import 'package:intl/intl.dart' as intl; import 'package:intl/intl.dart' as intl;
import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart' as vp; import 'package:video_player/video_player.dart' as vp;
import '../../pages/home/controller.dart';
import '../markdown/src/style_sheet.dart'; import '../markdown/src/style_sheet.dart';
import 'src/widgets/image_provider/image_provider.dart'; import 'src/widgets/image_provider/image_provider.dart';
......
part of dash_chat_2; part of dash_chat_2;
enum TtsState { playing, stopped, paused, continued }
class ChartTTS {
String voiceName;
String locale;
double volume;
double speechRate;
String language = "en-US";
static ChartTTS tts = ChartTTS();
ChartTTS(
{this.voiceName = "Karen",
this.locale = 'en-AU',
this.volume = 1.0,
this.speechRate = 1.0}) {
flutterTts.setLanguage(language);
flutterTts.setVolume(volume);
flutterTts.setSpeechRate(speechRate);
flutterTts.setVoice({"name": voiceName, "locale": locale});
}
final FlutterTts _flutterTts = FlutterTts();
FlutterTts get flutterTts => _flutterTts;
Future<void> speak(String message, VoidCallback? onFinishedCallback) async {
//先停止
await stop();
_flutterTts.setCompletionHandler(() {
if (onFinishedCallback != null) {
onFinishedCallback();
}
});
var rev = await _flutterTts.speak(message);
}
Future<void> stop() async {
await _flutterTts.stop();
}
}
/// @nodoc /// @nodoc
class MessageList extends StatefulWidget { class MessageList extends StatefulWidget {
const MessageList({ const MessageList({
...@@ -42,6 +83,7 @@ class _MessageListState extends State<MessageList> { ...@@ -42,6 +83,7 @@ class _MessageListState extends State<MessageList> {
bool scrollToBottomIsVisible = false; bool scrollToBottomIsVisible = false;
bool isLoadingMore = false; bool isLoadingMore = false;
late ScrollController scrollController; late ScrollController scrollController;
late ChartTTS chartTTS = ChartTTS();
@override @override
void initState() { void initState() {
...@@ -118,23 +160,23 @@ class _MessageListState extends State<MessageList> { ...@@ -118,23 +160,23 @@ class _MessageListState extends State<MessageList> {
margin: const EdgeInsets.only(top: 20), margin: const EdgeInsets.only(top: 20),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
message.text, message.text,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
style: const TextStyle( style: const TextStyle(
color: Color.fromARGB( color: Color.fromARGB(
255, 170, 170, 194)), 255, 170, 170, 194)),
)), )),
const Icon( const Icon(
Icons.arrow_forward, Icons.arrow_forward,
color: Color(0xFF9c67f6), color: Color(0xFF9c67f6),
), ),
]), ]),
) ),
], ],
), ),
), ),
......
...@@ -3,7 +3,7 @@ part of dash_chat_2; ...@@ -3,7 +3,7 @@ part of dash_chat_2;
// part of animated_text_kit; // part of animated_text_kit;
/// {@category Default widgets} /// {@category Default widgets}
class DefaultMessageText extends StatelessWidget { class DefaultMessageText extends StatefulWidget {
const DefaultMessageText({ const DefaultMessageText({
required this.message, required this.message,
required this.messageLength, required this.messageLength,
...@@ -27,28 +27,39 @@ class DefaultMessageText extends StatelessWidget { ...@@ -27,28 +27,39 @@ class DefaultMessageText extends StatelessWidget {
/// Options to customize the behaviour and design of the messages /// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions; final MessageOptions messageOptions;
@override
State<DefaultMessageText> createState() => _DefaultMessageTextState();
}
class _DefaultMessageTextState extends State<DefaultMessageText> {
CustomPopupMenuController controller = CustomPopupMenuController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: crossAxisAlignment: widget.isOwnMessage
isOwnMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, ? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Wrap( Wrap(
children: getMessage(isOwnMessage, messageLength, index), children: getMessage(
widget.isOwnMessage, widget.messageLength, widget.index),
), ),
if (messageOptions.showTime) if (widget.messageOptions.showTime)
messageOptions.messageTimeBuilder != null widget.messageOptions.messageTimeBuilder != null
? messageOptions.messageTimeBuilder!(message, isOwnMessage) ? widget.messageOptions.messageTimeBuilder!(
widget.message, widget.isOwnMessage)
: Padding( : Padding(
padding: const EdgeInsets.only(top: 5), padding: const EdgeInsets.only(top: 5),
child: Text( child: Text(
(messageOptions.timeFormat ?? intl.DateFormat('HH:mm')) (widget.messageOptions.timeFormat ??
.format(message.createdAt), intl.DateFormat('HH:mm'))
.format(widget.message.createdAt),
style: TextStyle( style: TextStyle(
color: isOwnMessage color: widget.isOwnMessage
? (messageOptions.currentUserTextColor ?? ? (widget.messageOptions.currentUserTextColor ??
Colors.white70) Colors.white70)
: (messageOptions.textColor ?? Colors.black54), : (widget.messageOptions.textColor ?? Colors.black54),
fontSize: 10, fontSize: 10,
), ),
), ),
...@@ -59,10 +70,11 @@ class DefaultMessageText extends StatelessWidget { ...@@ -59,10 +70,11 @@ class DefaultMessageText extends StatelessWidget {
List<Widget> getMessage( List<Widget> getMessage(
bool isOwnMessage, List<ChatMessage> messageLength, int index) { bool isOwnMessage, List<ChatMessage> messageLength, int index) {
if (message.mentions != null && message.mentions!.isNotEmpty) { if (widget.message.mentions != null &&
widget.message.mentions!.isNotEmpty) {
String stringRegex = r'([\s\S]*)'; String stringRegex = r'([\s\S]*)';
String stringMentionRegex = ''; String stringMentionRegex = '';
for (final Mention mention in message.mentions!) { for (final Mention mention in widget.message.mentions!) {
stringRegex += '(${mention.title})' r'([\s\S]*)'; stringRegex += '(${mention.title})' r'([\s\S]*)';
stringMentionRegex += stringMentionRegex.isEmpty stringMentionRegex += stringMentionRegex.isEmpty
? '(${mention.title})' ? '(${mention.title})'
...@@ -71,14 +83,14 @@ class DefaultMessageText extends StatelessWidget { ...@@ -71,14 +83,14 @@ class DefaultMessageText extends StatelessWidget {
final RegExp mentionRegex = RegExp(stringMentionRegex); final RegExp mentionRegex = RegExp(stringMentionRegex);
final RegExp regexp = RegExp(stringRegex); final RegExp regexp = RegExp(stringRegex);
RegExpMatch? match = regexp.firstMatch(message.text); RegExpMatch? match = regexp.firstMatch(widget.message.text);
if (match != null) { if (match != null) {
List<Widget> res = <Widget>[]; List<Widget> res = <Widget>[];
match match
.groups(List<int>.generate(match.groupCount, (int i) => i + 1)) .groups(List<int>.generate(match.groupCount, (int i) => i + 1))
.forEach((String? part) { .forEach((String? part) {
if (mentionRegex.hasMatch(part!)) { if (mentionRegex.hasMatch(part!)) {
Mention mention = message.mentions!.firstWhere( Mention mention = widget.message.mentions!.firstWhere(
(Mention m) => m.title == part, (Mention m) => m.title == part,
); );
res.add(getMention(mention)); res.add(getMention(mention));
...@@ -92,7 +104,7 @@ class DefaultMessageText extends StatelessWidget { ...@@ -92,7 +104,7 @@ class DefaultMessageText extends StatelessWidget {
} }
} }
return <Widget>[ return <Widget>[
getParsePattern(message.text, isOwnMessage, messageLength, index), getParsePattern(widget.message.text, isOwnMessage, messageLength, index),
]; ];
} }
...@@ -114,7 +126,6 @@ class DefaultMessageText extends StatelessWidget { ...@@ -114,7 +126,6 @@ class DefaultMessageText extends StatelessWidget {
// height: 200, // height: 200,
// child: Markdown(data: text, selectable: true), // child: Markdown(data: text, selectable: true),
// ); // );
return text == 'LOADING' return text == 'LOADING'
? Row( ? Row(
children: [ children: [
...@@ -125,15 +136,22 @@ class DefaultMessageText extends StatelessWidget { ...@@ -125,15 +136,22 @@ class DefaultMessageText extends StatelessWidget {
), ),
], ],
) )
: MyMarkdown( : CustomPopupMenu(
// syntaxHighlighter: SyntaxHighlighter(), controller: controller,
// styleConfig: StyleConfig(), position: PreferredPosition.top,
data: text, menuBuilder: () =>
styleSheet: MarkdownStyleSheet( _buildLongPressMenu(message: widget.message.text),
p: TextStyle(color: Colors.white), barrierColor: Colors.transparent,
code: TextStyle(color: Colors.white), pressType: PressType.longPress,
), child: MyMarkdown(
); // syntaxHighlighter: SyntaxHighlighter(),
// styleConfig: StyleConfig(),
data: text,
styleSheet: MarkdownStyleSheet(
p: TextStyle(color: Colors.white),
code: TextStyle(color: Colors.white),
),
));
// Column( // Column(
// children: [ // children: [
// Expanded( // Expanded(
...@@ -149,7 +167,133 @@ class DefaultMessageText extends StatelessWidget { ...@@ -149,7 +167,133 @@ class DefaultMessageText extends StatelessWidget {
// Markdown(data: text, selectable: true); // Markdown(data: text, selectable: true);
} }
// {buildPostMessage = getPostMessageBuild} /**
* Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () =>
{
FlutterClipboard.copy(
message.text)
.then((value) =>
EasyLoading.showToast(
"复制成功"))
},
child: Chip(
labelStyle:
TextStyle(fontSize: 11),
label: Text("复制"),
avatar: Icon(
Icons.copy,
size: 15,
),
backgroundColor:
Colors.deepPurpleAccent,
),
),
SizedBox(
width: 5,
),
GestureDetector(
onTap: () async =>
{
if (message.text.isNotEmpty)
{
await Share.share(
"${message.text}\n\n",
)
}
},
child: const RawChip(
labelStyle:
TextStyle(fontSize: 11),
padding: EdgeInsets.only(left: 5),
label: Text("分享"),
avatar: Icon(
Icons.share,
size: 15,
),
backgroundColor:
Colors.deepPurpleAccent,
),
),
],
)
*/
Widget _buildLongPressMenu({String message = ""}) {
return ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
width: 100,
padding: EdgeInsets.only(left: 5,top:5,right: 5,bottom: 5),
decoration: BoxDecoration(gradient: LinearGradient(
colors: [Color(0xFF3d3f54), Color(0xFF333450), Color(0xFF2b2b4d)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
GestureDetector(
onTap: () {
FlutterClipboard.copy(message)
.then((value) => EasyLoading.showToast("复制成功"));
controller.hideMenu();
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
Icons.content_copy,
size: 20,
color: Colors.white,
),
Container(
margin: EdgeInsets.only(top: 2),
child: Text(
"复制",
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
],
)),
GestureDetector(
onTap: () async {
if (message.isNotEmpty) {
await Share.share(
"${message}\n\n",
);
}
controller.hideMenu();
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
Icons.share,
size: 20,
color: Colors.white,
),
Container(
margin: EdgeInsets.only(top: 2),
child: const Text(
"分享",
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
],
))
],
),
),
);
}
// {buildPostMessage = getPostMessageBuild}
Widget getParsePattern( Widget getParsePattern(
String text, String text,
bool isOwnMessage, bool isOwnMessage,
...@@ -159,15 +303,15 @@ class DefaultMessageText extends StatelessWidget { ...@@ -159,15 +303,15 @@ class DefaultMessageText extends StatelessWidget {
return !isOwnMessage return !isOwnMessage
? getPostMessageBuild(text, messageLength, isOwnMessage, index) ? getPostMessageBuild(text, messageLength, isOwnMessage, index)
: ParsedText( : ParsedText(
parse: messageOptions.parsePatterns != null parse: widget.messageOptions.parsePatterns != null
? messageOptions.parsePatterns! ? widget.messageOptions.parsePatterns!
: defaultPersePatterns, : defaultPersePatterns,
text: text, text: text,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: isOwnMessage color: isOwnMessage
? (messageOptions.currentUserTextColor ?? Colors.white) ? (widget.messageOptions.currentUserTextColor ?? Colors.white)
: (messageOptions.textColor ?? Colors.black), : (widget.messageOptions.textColor ?? Colors.black),
), ),
); );
} }
...@@ -177,13 +321,13 @@ class DefaultMessageText extends StatelessWidget { ...@@ -177,13 +321,13 @@ class DefaultMessageText extends StatelessWidget {
text: TextSpan( text: TextSpan(
text: mention.title, text: mention.title,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => messageOptions.onPressMention != null ..onTap = () => widget.messageOptions.onPressMention != null
? messageOptions.onPressMention!(mention) ? widget.messageOptions.onPressMention!(mention)
: null, : null,
style: TextStyle( style: TextStyle(
color: isOwnMessage color: widget.isOwnMessage
? (messageOptions.currentUserTextColor ?? Colors.white) ? (widget.messageOptions.currentUserTextColor ?? Colors.white)
: (messageOptions.textColor ?? Colors.black), : (widget.messageOptions.textColor ?? Colors.black),
decoration: TextDecoration.none, decoration: TextDecoration.none,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
...@@ -192,6 +336,13 @@ class DefaultMessageText extends StatelessWidget { ...@@ -192,6 +336,13 @@ class DefaultMessageText extends StatelessWidget {
} }
} }
class ItemModel {
String title;
IconData icon;
ItemModel(this.title, this.icon);
}
class MyStyleSheet extends MarkdownStyleSheet { class MyStyleSheet extends MarkdownStyleSheet {
@override @override
// TextStyle codeblockStyle = TextStyle(fontFamily: 'monospace', fontSize: 12.0); // TextStyle codeblockStyle = TextStyle(fontFamily: 'monospace', fontSize: 12.0);
...@@ -238,3 +389,38 @@ class MyStyleSheet extends MarkdownStyleSheet { ...@@ -238,3 +389,38 @@ class MyStyleSheet extends MarkdownStyleSheet {
return result; return result;
} }
} }
class ChartTTSWave extends StatefulWidget {
String text;
ChartTTSWave({Key? key, required this.text}) : super(key: key);
@override
State<ChartTTSWave> createState() => _ChartTTSWaveState();
}
class _ChartTTSWaveState extends State<ChartTTSWave> {
var isPlaying = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
ChartTTS.tts.speak(widget.text, () {
setState(() {
isPlaying = false;
});
});
setState(() {
isPlaying = true;
});
},
child: isPlaying
? LoadingAnimationWidget.staggeredDotsWave(
color: Colors.white,
size: 20,
)
: Icon(Icons.record_voice_over_sharp),
);
}
}
...@@ -131,20 +131,47 @@ class MessageRow extends StatelessWidget { ...@@ -131,20 +131,47 @@ class MessageRow extends StatelessWidget {
// messageLength: messageLength, // messageLength: messageLength,
), ),
if (message.text.isNotEmpty) if (message.text.isNotEmpty)
TextContainer( (isOwnMessage
index: index, ? TextContainer(
messageOptions: messageOptions, index: index,
message: message, messageOptions: messageOptions,
messageLength: messageLength, message: message,
previousMessage: previousMessage, messageLength: messageLength,
nextMessage: nextMessage, previousMessage: previousMessage,
isOwnMessage: isOwnMessage, nextMessage: nextMessage,
isNextSameAuthor: isNextSameAuthor, isOwnMessage: isOwnMessage,
isPreviousSameAuthor: isPreviousSameAuthor, isNextSameAuthor: isNextSameAuthor,
isAfterDateSeparator: isAfterDateSeparator, isPreviousSameAuthor: isPreviousSameAuthor,
isBeforeDateSeparator: isBeforeDateSeparator, isAfterDateSeparator: isAfterDateSeparator,
messageTextBuilder: messageOptions.messageTextBuilder, isBeforeDateSeparator: isBeforeDateSeparator,
), messageTextBuilder:
messageOptions.messageTextBuilder,
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextContainer(
index: index,
messageOptions: messageOptions,
message: message,
messageLength: messageLength,
previousMessage: previousMessage,
nextMessage: nextMessage,
isOwnMessage: isOwnMessage,
isNextSameAuthor: isNextSameAuthor,
isPreviousSameAuthor: isPreviousSameAuthor,
isAfterDateSeparator: isAfterDateSeparator,
isBeforeDateSeparator: isBeforeDateSeparator,
messageTextBuilder:
messageOptions.messageTextBuilder,
)),
Padding(
padding: EdgeInsets.only(left: 10, top: 15),
child: ChartTTSWave(text: message.text)),
],
)),
if (message.medias != null && if (message.medias != null &&
message.medias!.isNotEmpty && message.medias!.isNotEmpty &&
!messageOptions.textBeforeMedia) !messageOptions.textBeforeMedia)
......
...@@ -78,7 +78,14 @@ class HomeController extends GetxController { ...@@ -78,7 +78,14 @@ class HomeController extends GetxController {
); );
_addMessage(textMessage); _addMessage(textMessage);
final receiveMessage = Chat.ChatMessage(
user: receiveUser,
createdAt: DateTime.now(),
status: MessageStatus.pending,
// id: const Uuid().v4(),
text: "LOADING",
);
_addMessage(receiveMessage);
// return; // return;
// _addMessage(loadingMessage); // _addMessage(loadingMessage);
...@@ -113,14 +120,7 @@ class HomeController extends GetxController { ...@@ -113,14 +120,7 @@ class HomeController extends GetxController {
integral: value.integral)); integral: value.integral));
}); });
final receiveMessage = Chat.ChatMessage(
user: receiveUser,
createdAt: DateTime.now(),
status: MessageStatus.pending,
// id: const Uuid().v4(),
text: "LOADING",
);
_addMessage(receiveMessage);
// index.value = state.messageList.length; // index.value = state.messageList.length;
} else { } else {
// EasyLoading.showError("网络异常"); // EasyLoading.showError("网络异常");
......
...@@ -122,6 +122,9 @@ dependencies: ...@@ -122,6 +122,9 @@ dependencies:
loading_animation_widget: ^1.2.0+4 loading_animation_widget: ^1.2.0+4
flutter_keyboard_visibility: ^5.4.0 flutter_keyboard_visibility: ^5.4.0
flutter_barrage: ^0.5.2 flutter_barrage: ^0.5.2
clipboard: ^0.1.3
flutter_tts: ^3.6.3
custom_pop_up_menu: ^1.2.4
# package:bubble/bubble.dart # package:bubble/bubble.dart
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment