Commit 1610ec8c authored by skeyboy's avatar skeyboy

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

parent 1b13148f
library dash_chat_2;
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:animated_text_kit/animated_text_kit.dart';
import 'package:chart/package/markdown/flutter_markdown.dart';
......@@ -14,9 +18,11 @@ import 'package:gradient_borders/input_borders/gradient_outline_input_border.dar
import 'package:highlight/highlight.dart';
import 'package:intl/intl.dart' as intl;
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:video_player/video_player.dart' as vp;
import '../../pages/home/controller.dart';
import '../markdown/src/style_sheet.dart';
import 'src/widgets/image_provider/image_provider.dart';
......
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
class MessageList extends StatefulWidget {
const MessageList({
......@@ -42,6 +83,7 @@ class _MessageListState extends State<MessageList> {
bool scrollToBottomIsVisible = false;
bool isLoadingMore = false;
late ScrollController scrollController;
late ChartTTS chartTTS = ChartTTS();
@override
void initState() {
......@@ -134,7 +176,7 @@ class _MessageListState extends State<MessageList> {
color: Color(0xFF9c67f6),
),
]),
)
),
],
),
),
......
......@@ -3,7 +3,7 @@ part of dash_chat_2;
// part of animated_text_kit;
/// {@category Default widgets}
class DefaultMessageText extends StatelessWidget {
class DefaultMessageText extends StatefulWidget {
const DefaultMessageText({
required this.message,
required this.messageLength,
......@@ -27,28 +27,39 @@ class DefaultMessageText extends StatelessWidget {
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
@override
State<DefaultMessageText> createState() => _DefaultMessageTextState();
}
class _DefaultMessageTextState extends State<DefaultMessageText> {
CustomPopupMenuController controller = CustomPopupMenuController();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment:
isOwnMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
crossAxisAlignment: widget.isOwnMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: <Widget>[
Wrap(
children: getMessage(isOwnMessage, messageLength, index),
children: getMessage(
widget.isOwnMessage, widget.messageLength, widget.index),
),
if (messageOptions.showTime)
messageOptions.messageTimeBuilder != null
? messageOptions.messageTimeBuilder!(message, isOwnMessage)
if (widget.messageOptions.showTime)
widget.messageOptions.messageTimeBuilder != null
? widget.messageOptions.messageTimeBuilder!(
widget.message, widget.isOwnMessage)
: Padding(
padding: const EdgeInsets.only(top: 5),
child: Text(
(messageOptions.timeFormat ?? intl.DateFormat('HH:mm'))
.format(message.createdAt),
(widget.messageOptions.timeFormat ??
intl.DateFormat('HH:mm'))
.format(widget.message.createdAt),
style: TextStyle(
color: isOwnMessage
? (messageOptions.currentUserTextColor ??
color: widget.isOwnMessage
? (widget.messageOptions.currentUserTextColor ??
Colors.white70)
: (messageOptions.textColor ?? Colors.black54),
: (widget.messageOptions.textColor ?? Colors.black54),
fontSize: 10,
),
),
......@@ -59,10 +70,11 @@ class DefaultMessageText extends StatelessWidget {
List<Widget> getMessage(
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 stringMentionRegex = '';
for (final Mention mention in message.mentions!) {
for (final Mention mention in widget.message.mentions!) {
stringRegex += '(${mention.title})' r'([\s\S]*)';
stringMentionRegex += stringMentionRegex.isEmpty
? '(${mention.title})'
......@@ -71,14 +83,14 @@ class DefaultMessageText extends StatelessWidget {
final RegExp mentionRegex = RegExp(stringMentionRegex);
final RegExp regexp = RegExp(stringRegex);
RegExpMatch? match = regexp.firstMatch(message.text);
RegExpMatch? match = regexp.firstMatch(widget.message.text);
if (match != null) {
List<Widget> res = <Widget>[];
match
.groups(List<int>.generate(match.groupCount, (int i) => i + 1))
.forEach((String? part) {
if (mentionRegex.hasMatch(part!)) {
Mention mention = message.mentions!.firstWhere(
Mention mention = widget.message.mentions!.firstWhere(
(Mention m) => m.title == part,
);
res.add(getMention(mention));
......@@ -92,7 +104,7 @@ class DefaultMessageText extends StatelessWidget {
}
}
return <Widget>[
getParsePattern(message.text, isOwnMessage, messageLength, index),
getParsePattern(widget.message.text, isOwnMessage, messageLength, index),
];
}
......@@ -114,7 +126,6 @@ class DefaultMessageText extends StatelessWidget {
// height: 200,
// child: Markdown(data: text, selectable: true),
// );
return text == 'LOADING'
? Row(
children: [
......@@ -125,7 +136,14 @@ class DefaultMessageText extends StatelessWidget {
),
],
)
: MyMarkdown(
: CustomPopupMenu(
controller: controller,
position: PreferredPosition.top,
menuBuilder: () =>
_buildLongPressMenu(message: widget.message.text),
barrierColor: Colors.transparent,
pressType: PressType.longPress,
child: MyMarkdown(
// syntaxHighlighter: SyntaxHighlighter(),
// styleConfig: StyleConfig(),
data: text,
......@@ -133,7 +151,7 @@ class DefaultMessageText extends StatelessWidget {
p: TextStyle(color: Colors.white),
code: TextStyle(color: Colors.white),
),
);
));
// Column(
// children: [
// Expanded(
......@@ -149,7 +167,133 @@ class DefaultMessageText extends StatelessWidget {
// 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(
String text,
bool isOwnMessage,
......@@ -159,15 +303,15 @@ class DefaultMessageText extends StatelessWidget {
return !isOwnMessage
? getPostMessageBuild(text, messageLength, isOwnMessage, index)
: ParsedText(
parse: messageOptions.parsePatterns != null
? messageOptions.parsePatterns!
parse: widget.messageOptions.parsePatterns != null
? widget.messageOptions.parsePatterns!
: defaultPersePatterns,
text: text,
style: TextStyle(
fontSize: 16,
color: isOwnMessage
? (messageOptions.currentUserTextColor ?? Colors.white)
: (messageOptions.textColor ?? Colors.black),
? (widget.messageOptions.currentUserTextColor ?? Colors.white)
: (widget.messageOptions.textColor ?? Colors.black),
),
);
}
......@@ -177,13 +321,13 @@ class DefaultMessageText extends StatelessWidget {
text: TextSpan(
text: mention.title,
recognizer: TapGestureRecognizer()
..onTap = () => messageOptions.onPressMention != null
? messageOptions.onPressMention!(mention)
..onTap = () => widget.messageOptions.onPressMention != null
? widget.messageOptions.onPressMention!(mention)
: null,
style: TextStyle(
color: isOwnMessage
? (messageOptions.currentUserTextColor ?? Colors.white)
: (messageOptions.textColor ?? Colors.black),
color: widget.isOwnMessage
? (widget.messageOptions.currentUserTextColor ?? Colors.white)
: (widget.messageOptions.textColor ?? Colors.black),
decoration: TextDecoration.none,
fontWeight: FontWeight.w600,
),
......@@ -192,6 +336,13 @@ class DefaultMessageText extends StatelessWidget {
}
}
class ItemModel {
String title;
IconData icon;
ItemModel(this.title, this.icon);
}
class MyStyleSheet extends MarkdownStyleSheet {
@override
// TextStyle codeblockStyle = TextStyle(fontFamily: 'monospace', fontSize: 12.0);
......@@ -238,3 +389,38 @@ class MyStyleSheet extends MarkdownStyleSheet {
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,7 +131,8 @@ class MessageRow extends StatelessWidget {
// messageLength: messageLength,
),
if (message.text.isNotEmpty)
TextContainer(
(isOwnMessage
? TextContainer(
index: index,
messageOptions: messageOptions,
message: message,
......@@ -143,8 +144,34 @@ class MessageRow extends StatelessWidget {
isPreviousSameAuthor: isPreviousSameAuthor,
isAfterDateSeparator: isAfterDateSeparator,
isBeforeDateSeparator: isBeforeDateSeparator,
messageTextBuilder: messageOptions.messageTextBuilder,
),
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 &&
message.medias!.isNotEmpty &&
!messageOptions.textBeforeMedia)
......
......@@ -78,7 +78,14 @@ class HomeController extends GetxController {
);
_addMessage(textMessage);
final receiveMessage = Chat.ChatMessage(
user: receiveUser,
createdAt: DateTime.now(),
status: MessageStatus.pending,
// id: const Uuid().v4(),
text: "LOADING",
);
_addMessage(receiveMessage);
// return;
// _addMessage(loadingMessage);
......@@ -113,14 +120,7 @@ class HomeController extends GetxController {
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;
} else {
// EasyLoading.showError("网络异常");
......
......@@ -122,6 +122,9 @@ dependencies:
loading_animation_widget: ^1.2.0+4
flutter_keyboard_visibility: ^5.4.0
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
......
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