Commit c7740f49 authored by 关振斌's avatar 关振斌

chat

parent a36cde6a
......@@ -126,6 +126,8 @@ PODS:
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
- vibration (1.7.5):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
......@@ -152,6 +154,7 @@ DEPENDENCIES:
- smart_auth (from `.symlinks/plugins/smart_auth/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- vibration (from `.symlinks/plugins/vibration/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
......@@ -214,6 +217,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
vibration:
:path: ".symlinks/plugins/vibration/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios"
webview_flutter_wkwebview:
......@@ -255,6 +260,7 @@ SPEC CHECKSUMS:
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
......
......@@ -24,6 +24,15 @@ class NewsAPI {
return response['data'];
}
static Future<String> sendMessageByDetailId(
dynamic params, String detailId) async {
var response =
await HttpUtil().post('/openAi/aiAnswer/${detailId}', data: params);
return response['data'];
}
// /api/openAi/aiAnswer/{detailId}
// /// 翻页
// /// refresh 是否刷新
// static Future<NewsPageListResponseEntity> newsPageList({
......
......@@ -60,6 +60,7 @@ class ClassifyDetialEntity {
required this.isShow,
required this.template,
required this.detailParamsEntityList,
required this.icon,
});
final int id;
......@@ -67,6 +68,7 @@ class ClassifyDetialEntity {
final String detailDesc;
final int isShow;
final String template;
final String icon;
final List<detailParamsEntity> detailParamsEntityList;
factory ClassifyDetialEntity.fromJson(String str) =>
......@@ -77,6 +79,7 @@ class ClassifyDetialEntity {
factory ClassifyDetialEntity.fromMap(Map<String, dynamic> json) =>
ClassifyDetialEntity(
id: json["id"],
icon: json['icon'],
detailName: json["detailName"],
detailDesc: json["detailDesc"],
template: json['template'],
......@@ -93,6 +96,7 @@ class ClassifyDetialEntity {
// detailParamsEntityList:json['detailParamsEntityList'];
Map<String, dynamic> toMap() => {
"id": id,
"icon": icon,
"detailName": detailName,
"detailDesc": detailDesc,
"isShow": isShow,
......
......@@ -44,8 +44,8 @@ class UserStore extends GetxController {
handleLogin(LoginEntity res) async {
await setToken(res.token);
await saveProfile(
IntegralEntity(id: res.id, username: res.username, token: res.token));
IntegralEntity userInfo = await UserAPI.getUserIntegral();
await UserStore.to.getUserInfo(userInfo);
_isLogin.value = true;
Get.offAndToNamed(AppRoutes.Application);
}
......@@ -98,7 +98,7 @@ class UserStore extends GetxController {
// 注销
Future<void> onLogout() async {
if (_isLogin.value) await UserAPI.logout();
// if (_isLogin.value) await UserAPI.logout();
await StorageService.to.remove(STORAGE_USER_TOKEN_KEY);
_isLogin.value = false;
token = '';
......
// baidu yapi
// const SERVER_API_URL = 'https://yapi.baidu.com/mock/41008';
// const SERVER_API_URL = 'https://yapi.ducafecat.tech/mock/11';
const SERVER_API_URL = 'http://101.34.153.228:8083/api';
// const SERVER_API_URL = 'http://101.34.153.228:8083/api';
const SERVER_API_URL = 'http://192.168.2.178:8083/api';
// AppRoutes.PRODUCT_PAGE
//http://192.168.2.178:8083/api/doc.html
// static const String baseUrl = 'http://101.34.153.228:8083'; // 基础接口地址
// static const String baseApiUrl = '${baseUrl}/api'; // 基础接口地址
......
......@@ -10,7 +10,7 @@ AppBar transparentAppBar({
}) {
return AppBar(
backgroundColor: Color.fromRGBO(41, 45, 62, 1.00),
elevation: 0,
// elevation: 2,
title: title,
leading: leading ?? null,
// actions: actions,
......
......@@ -141,12 +141,14 @@ class ClassifyEntity {
required this.classifyName,
required this.classifyDesc,
required this.isShow,
required this.icon,
});
final int id;
final String classifyName;
final String classifyDesc;
final int isShow;
final String icon;
factory ClassifyEntity.fromJson(String str) =>
ClassifyEntity.fromMap(json.decode(str));
......@@ -157,12 +159,15 @@ class ClassifyEntity {
id: json["id"],
classifyName: json["classifyName"],
classifyDesc: json["classifyDesc"],
icon: json['icon'],
isShow: json['isShow']);
Map<String, dynamic> toMap() => {
"id": id,
"classifyName": classifyName,
"classifyDesc": classifyDesc,
"isShow": isShow
"isShow": isShow,
"icon": icon,
};
}
......
library dash_chat_2;
import 'dart:math';
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_parsed_text/flutter_parsed_text.dart';
import 'package:gradient_borders/input_borders/gradient_outline_input_border.dart';
import 'package:intl/intl.dart' as intl;
import 'package:url_launcher/url_launcher.dart';
import 'package:video_player/video_player.dart' as vp;
import 'src/widgets/image_provider/image_provider.dart';
export 'package:flutter_parsed_text/flutter_parsed_text.dart';
part 'src/dash_chat.dart';
part 'src/models/chat_media.dart';
part 'src/models/chat_message.dart';
part 'src/models/chat_user.dart';
part 'src/models/cursor_style.dart';
part 'src/models/input_options.dart';
part 'src/models/mention.dart';
part 'src/models/message_list_options.dart';
part 'src/models/message_options.dart';
part 'src/models/quick_reply.dart';
part 'src/models/quick_reply_options.dart';
part 'src/models/scroll_to_bottom_options.dart';
part 'src/widgets/input_toolbar/default_input_decoration.dart';
part 'src/widgets/input_toolbar/default_send_button.dart';
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/default_avatar.dart';
part 'src/widgets/message_row/default_message_decoration.dart';
part 'src/widgets/message_row/default_message_text.dart';
part 'src/widgets/message_row/default_parse_patterns.dart';
part 'src/widgets/message_row/default_user_name.dart';
part 'src/widgets/message_row/media_container.dart';
part 'src/widgets/message_row/message_row.dart';
part 'src/widgets/message_row/text_container.dart';
part 'src/widgets/message_row/video_player.dart';
part 'src/widgets/quick_replies/default_quick_reply.dart';
part 'src/widgets/quick_replies/quick_replies.dart';
part 'src/widgets/typing_users/default_typing_builder.dart';
part 'src/widgets/typing_users/typing_indicator.dart';
part of dash_chat_2;
/// {@category Entry point}
class DashChat extends StatelessWidget {
const DashChat({
required this.currentUser,
required this.onSend,
required this.messages,
this.inputOptions = const InputOptions(),
this.messageOptions = const MessageOptions(),
this.messageListOptions = const MessageListOptions(),
this.quickReplyOptions = const QuickReplyOptions(),
this.scrollToBottomOptions = const ScrollToBottomOptions(),
this.readOnly = false,
this.typingUsers,
Key? key,
}) : super(key: key);
/// The current user of the chat
final ChatUser currentUser;
/// Function to call when the user sends a message
final void Function(ChatMessage message) onSend;
/// List of messages visible in the chat
final List<ChatMessage> messages;
/// Options to customize the behaviour and design of the chat input
final InputOptions inputOptions;
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
/// Options to customize the behaviour and design of the overall list of message
final MessageListOptions messageListOptions;
/// Options to customize the behaviour and design of the quick replies
final QuickReplyOptions quickReplyOptions;
/// Options to customize the behaviour and design of the scroll-to-bottom button
final ScrollToBottomOptions scrollToBottomOptions;
/// Option to make the chat read only, it will hide the input field
final bool readOnly;
/// List of users currently typing in the chat
final List<ChatUser>? typingUsers;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: MessageList(
currentUser: currentUser,
messages: messages,
messageOptions: messageOptions,
messageListOptions: messageListOptions,
quickReplyOptions: quickReplyOptions,
scrollToBottomOptions: scrollToBottomOptions,
typingUsers: typingUsers,
),
),
if (!readOnly)
InputToolbar(
inputOptions: inputOptions,
currentUser: currentUser,
onSend: onSend,
),
],
);
}
}
part of dash_chat_2;
/// {@category Models}
class ChatMedia {
ChatMedia({
required this.url,
required this.fileName,
required this.type,
this.isUploading = false,
this.uploadedDate,
this.customProperties,
});
/// Create a ChatMedia instance from json data
factory ChatMedia.fromJson(Map<String, dynamic> jsonData) {
return ChatMedia(
url: jsonData['url'].toString(),
fileName: jsonData['fileName'].toString(),
type: MediaType.parse(jsonData['type'].toString()),
isUploading: jsonData['isUploading'] == true,
uploadedDate: jsonData['uploadedDate'] != null
? DateTime.parse(jsonData['uploadedDate'].toString()).toLocal()
: null,
customProperties: jsonData['customProperties'] as Map<String, dynamic>,
);
}
/// URL of the media, can local (will use FileImage) or remote (will use NetworkImage)
String url;
/// Name of the file that will be shown in some cases
String fileName;
/// Type of media
MediaType type;
/// If the media is still uploading, usefull to add a visual feedback
bool isUploading;
/// Uploaded date of the media
DateTime? uploadedDate;
/// A list of custom properties to extend the existing ones
/// in case you need to store more things.
/// Can be useful to extend existing features
Map<String, dynamic>? customProperties;
/// Convert a ChatMedia into a json
Map<String, dynamic> toJson() {
return <String, dynamic>{
'url': url,
'type': type.toString(),
'fileName': fileName,
'isUploading': isUploading,
'uploadedDate': uploadedDate?.toUtc().toIso8601String(),
'customProperties': customProperties,
};
}
}
class MediaType {
const MediaType._internal(this._value);
final String _value;
@override
String toString() => _value;
static MediaType parse(String value) {
switch (value) {
case 'image':
return MediaType.image;
case 'video':
return MediaType.video;
case 'file':
return MediaType.file;
default:
throw UnsupportedError('$value is not a valid MediaType');
}
}
static const MediaType image = MediaType._internal('image');
static const MediaType video = MediaType._internal('video');
static const MediaType file = MediaType._internal('file');
}
part of dash_chat_2;
/// {@category Models}
class ChatMessage {
ChatMessage({
required this.user,
required this.createdAt,
this.text = '',
this.medias,
this.quickReplies,
this.customProperties,
this.mentions,
this.status = MessageStatus.none,
this.replyTo,
});
/// Create a ChatMessage instance from json data
factory ChatMessage.fromJson(Map<String, dynamic> jsonData) {
return ChatMessage(
user: ChatUser.fromJson(jsonData['user'] as Map<String, dynamic>),
createdAt: DateTime.parse(jsonData['createdAt'].toString()).toLocal(),
text: jsonData['text']?.toString() ?? '',
medias: jsonData['medias'] != null
? (jsonData['medias'] as List<dynamic>)
.map((dynamic media) =>
ChatMedia.fromJson(media as Map<String, dynamic>))
.toList()
: <ChatMedia>[],
quickReplies: jsonData['quickReplies'] != null
? (jsonData['quickReplies'] as List<dynamic>)
.map((dynamic quickReply) =>
QuickReply.fromJson(quickReply as Map<String, dynamic>))
.toList()
: <QuickReply>[],
customProperties: jsonData['customProperties'] as Map<String, dynamic>,
mentions: jsonData['mentions'] != null
? (jsonData['mentions'] as List<dynamic>)
.map((dynamic mention) =>
Mention.fromJson(mention as Map<String, dynamic>))
.toList()
: <Mention>[],
status: MessageStatus.parse(jsonData['status'].toString()),
replyTo: jsonData['replyTo'] != null
? ChatMessage.fromJson(jsonData['replyTo'] as Map<String, dynamic>)
: null,
);
}
/// Text of the message (optional because you can also just send a media)
String text;
/// Author of the message
ChatUser user;
/// List of medias of the message
List<ChatMedia>? medias;
/// A list of quick replies that users can use to reply to this message
List<QuickReply>? quickReplies;
/// A list of custom properties to extend the existing ones
/// in case you need to store more things.
/// Can be useful to extend existing features
Map<String, dynamic>? customProperties;
/// Date of the message
DateTime createdAt;
/// Mentionned elements in the message
List<Mention>? mentions;
/// Status of the message TODO:
MessageStatus? status;
/// If the message is a reply of another one TODO:
ChatMessage? replyTo;
/// Convert a ChatMessage into a json
Map<String, dynamic> toJson() {
return <String, dynamic>{
'user': user.toJson(),
'createdAt': createdAt.toUtc().toIso8601String(),
'text': text,
'medias': medias?.map((ChatMedia media) => media.toJson()).toList(),
'quickReplies': quickReplies
?.map((QuickReply quickReply) => quickReply.toJson())
.toList(),
'customProperties': customProperties,
'mentions': mentions,
'status': status.toString(),
'replyTo': replyTo?.toJson(),
};
}
}
class MessageStatus {
const MessageStatus._internal(this._value);
final String _value;
@override
String toString() => _value;
static MessageStatus parse(String value) {
switch (value) {
case 'none':
return MessageStatus.none;
case 'read':
return MessageStatus.read;
case 'received':
return MessageStatus.received;
case 'pending':
return MessageStatus.pending;
default:
return MessageStatus.none;
}
}
static const MessageStatus none = MessageStatus._internal('none');
static const MessageStatus read = MessageStatus._internal('read');
static const MessageStatus received = MessageStatus._internal('received');
static const MessageStatus pending = MessageStatus._internal('pending');
}
part of dash_chat_2;
/// {@category Models}
class ChatUser {
ChatUser({
required this.id,
this.profileImage,
this.customProperties,
this.firstName,
this.lastName,
});
/// Create a ChatUser instance from json data
factory ChatUser.fromJson(Map<String, dynamic> jsonData) {
return ChatUser(
id: jsonData['id'].toString(),
profileImage: jsonData['profileImage']?.toString(),
firstName: jsonData['firstName']?.toString(),
lastName: jsonData['lastName']?.toString(),
customProperties: jsonData['customProperties'] as Map<String, dynamic>,
);
}
/// Id of the user
String id;
/// Profile image of the user
String? profileImage;
/// A list of custom properties to extend the existing ones
/// in case you need to store more things.
/// Can be useful to extend existing features
Map<String, dynamic>? customProperties;
/// First name of the user,
/// if you only have the name as one string
/// you can put the entire value in the [fristName] field
String? firstName;
/// Last name of the user
String? lastName;
/// Get the full name (firstName + lastName) of the user
String getFullName() {
return (firstName ?? '') +
(firstName != null && lastName != null
? ' ' + lastName!
: lastName ?? '');
}
/// Convert a ChatUser into a json
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'profileImage': profileImage,
'firstName': firstName,
'lastName': lastName,
'customProperties': customProperties,
};
}
}
part of dash_chat_2;
/// {@category Customization}
class CursorStyle {
const CursorStyle({
this.color,
this.hide = false,
this.width = 2.0,
});
/// Color of the cursor
final Color? color;
/// Hide or not the cursor
final bool hide;
/// Width of the cursor
final double width;
}
part of dash_chat_2;
/// {@category Customization}
class InputOptions {
const InputOptions({
this.textController,
this.focusNode,
this.inputTextDirection = TextDirection.ltr,
this.onMention,
this.onMentionTriggers = const <String>['@'],
this.onTextChange,
this.inputDisabled = false,
this.inputDecoration,
this.textCapitalization = TextCapitalization.none,
this.alwaysShowSend = false,
this.sendOnEnter = false,
this.textInputAction,
this.maxInputLength,
this.leading,
this.trailing,
this.sendButtonBuilder,
this.inputTextStyle,
this.inputToolbarStyle,
this.inputMaxLines = 5,
this.showTraillingBeforeSend = false,
this.inputToolbarPadding = const EdgeInsets.all(8.0),
this.inputToolbarMargin = const EdgeInsets.only(top: 8.0),
this.cursorStyle = const CursorStyle(),
this.autocorrect = true,
});
/// Function to call when a mention is triggered in the input,
/// ie: typing ' @'
/// You need to return a list of widget that will be shown inside the selection overlay,
/// for instance user ListTiles
final Future<List<Widget>> Function(String trigger, String value,
void Function(String value) onMentionClick)? onMention;
/// The list of string triggers for the onMention callback
/// By default it only includes '@' character
final List<String> onMentionTriggers;
/// Function to call when the input text changee
final void Function(String value)? onTextChange;
/// Always show the send button, will be hidden when the text is empty otherwise
final bool alwaysShowSend;
/// Send the message when the user presses the enter key
final bool sendOnEnter;
/// Builder to create your own send button widget
/// You can use defaultSendButton to only override some variables
final Widget Function(void Function() send)? sendButtonBuilder;
/// Text controller for the input field
final TextEditingController? textController;
/// Focus node of the input field
final FocusNode? focusNode;
/// Use to change the direction of the text
final TextDirection inputTextDirection;
/// To make the input disabled
final bool inputDisabled;
/// Input decoration to customize the design of the input
/// You can use defaultInputDecoration to only orride some variables
final InputDecoration? inputDecoration;
/// Use to override the default TextCapitalization
final TextCapitalization textCapitalization;
/// An action the user has requested the text input control to perform
final TextInputAction? textInputAction;
/// If you want to limit the length of the text
final int? maxInputLength;
/// A list of widget to show before the input
final List<Widget>? leading;
/// A list of widget to show after the input
final List<Widget>? trailing;
/// To customize the text style of the inpu
final TextStyle? inputTextStyle;
/// To customize the overall container of the input
final BoxDecoration? inputToolbarStyle;
/// Max number of visible lines of the input, it will grow until this value and then scroll
final int inputMaxLines;
/// If [trailing] should be shown before or after the send button
final bool showTraillingBeforeSend;
/// Padding of the overall container of the input
final EdgeInsets? inputToolbarPadding;
/// Margin of the overall container of the input
final EdgeInsets? inputToolbarMargin;
/// Style of the cursor
final CursorStyle cursorStyle;
/// Whether to enable autocorrection. Defaults to true.
final bool autocorrect;
}
part of dash_chat_2;
/// {@category Models}
class Mention {
Mention({
required this.title,
this.customProperties,
});
/// Create a Mention instance from json data
factory Mention.fromJson(Map<String, dynamic> jsonData) {
return Mention(
title: jsonData['title'].toString(),
customProperties: jsonData['customProperties'] as Map<String, dynamic>,
);
}
/// Title of the mention,
/// it's what is visible in the message: @userName
String title;
/// A list of custom properties to save any data you might need
/// For instance a user Id
Map<String, dynamic>? customProperties;
/// Convert a Mention into a json
Map<String, dynamic> toJson() {
return <String, dynamic>{
'title': title,
'customProperties': customProperties,
};
}
}
part of dash_chat_2;
/// {@category Customization}
class MessageListOptions {
const MessageListOptions({
this.showDateSeparator = true,
this.dateSeparatorFormat,
this.dateSeparatorBuilder,
this.separatorFrequency = SeparatorFrequency.days,
this.scrollController,
this.chatFooterBuilder,
this.showFooterBeforeQuickReplies = false,
this.loadEarlierBuilder,
this.onLoadEarlier,
this.typingBuilder,
this.scrollPhysics,
});
/// If you want to who a date separator between messages of different dates
final bool showDateSeparator;
/// The formating of the date in the date separator.
/// By default it will adapt according to the difference with today
final intl.DateFormat? dateSeparatorFormat;
/// If you want to create you own separator widget
/// You can use DefaultDateSeparator to only override some variables
final Widget Function(DateTime date)? dateSeparatorBuilder;
/// The frequency of the separator
final SeparatorFrequency separatorFrequency;
/// Scroll controller of the list of message
final ScrollController? scrollController;
/// A widget to show at the bottom of the chat
/// (between the input and the chat content)
final Widget? chatFooterBuilder;
/// If you wnat to show [chatFooterBuilder] before or after the quick replies
final bool showFooterBeforeQuickReplies;
/// If you want to show a widget when the top of the list is reached
final Widget? loadEarlierBuilder;
/// Function to call when the top of the list is reached
/// Usefull to load more messages
final Future<void> Function()? onLoadEarlier;
/// Builder to create your own typing widget
final Widget Function(ChatUser user)? typingBuilder;
/// Scroll physics of the ListView
final ScrollPhysics? scrollPhysics;
}
enum SeparatorFrequency { days, hours }
part of dash_chat_2;
/// {@category Customization}
class MessageOptions {
const MessageOptions({
this.showCurrentUserAvatar = false,
this.showOtherUsersAvatar = true,
this.showOtherUsersName = true,
this.userNameBuilder,
this.avatarBuilder,
this.onPressAvatar,
this.onLongPressAvatar,
this.onLongPressMessage,
this.onPressMessage,
this.onPressMention,
this.currentUserContainerColor,
this.currentUserTextColor,
this.containerColor,
this.textColor,
this.messagePadding,
this.maxWidth,
this.messageDecorationBuilder,
this.top,
this.bottom,
this.messageRowBuilder,
this.messageTextBuilder,
this.parsePatterns,
this.textBeforeMedia = true,
this.onTapMedia,
this.showTime = false,
this.timeFormat,
this.messageTimeBuilder,
this.messageMediaBuilder,
});
/// Format of the time if [showTime] is true
/// Default to: DateFormat('HH:mm')
final intl.DateFormat? timeFormat;
/// If you want to show the time under the text of each message
final bool showTime;
/// If you want to show the avatar of the current user
final bool showCurrentUserAvatar;
/// If you want to show the avatar of the other users
final bool showOtherUsersAvatar;
/// If you want to show the name of the other users above the messages
/// Usefull in group chats
final bool showOtherUsersName;
/// If you want to create your own userName widget when [showOtherUsersName] is true
/// You can use DefaultUserName to only override some variables
final Widget Function(ChatUser user)? userNameBuilder;
/// Builder to create your own avatar
/// You can use DefaultAvatar to only override some varibales
final Widget Function(
ChatUser, Function? onPressAvatar, Function? onLongPressAvatar)?
avatarBuilder;
/// Function to call when the user press on an avatar
final Function(ChatUser)? onPressAvatar;
/// Function to call when the user long press on an avatar
final Function(ChatUser)? onLongPressAvatar;
/// Function to call when the user long press on a message
final Function(ChatMessage)? onLongPressMessage;
/// Function to call when the user press on a message
final Function(ChatMessage)? onPressMessage;
/// Function to call when the user press on a message mention
final Function(Mention)? onPressMention;
/// Color of the current user chat bubbles
/// Default to primary color
final Color? currentUserContainerColor;
/// Color of the current user text in chat bubbles
/// Default to white
final Color? currentUserTextColor;
/// Color of the other users chat bubbles
/// Default to Colors.grey[100]
final Color? containerColor;
/// Color of the other users text in chat bubbles
/// Default to black
final Color? textColor;
/// Builder to create the entire message row yourself
final Widget Function(
ChatMessage message,
ChatMessage? previousMessage,
ChatMessage? nextMessage,
bool isAfterDateSeparator,
bool isBeforeDateSeparator,
)? messageRowBuilder;
/// Builder to create own message text widget
final Widget Function(ChatMessage message, ChatMessage? previousMessage,
ChatMessage? nextMessage)? messageTextBuilder;
/// Builder to create your own media container widget
final Widget Function(ChatMessage message, ChatMessage? previousMessage,
ChatMessage? nextMessage)? messageMediaBuilder;
/// Builder to create your own time widget
/// (shown under the text when [showTime] is true)
final Widget Function(ChatMessage message, bool isOwnMessage)?
messageTimeBuilder;
/// List of MatchText using flutter_parsed_text library
/// to parse and customize accordingly some part of the text
/// By default ParsedType.URL is set and will use launchUrl to open the link
final List<MatchText>? parsePatterns;
/// Padding arround the message
/// Default to: EdgeInsets.all(11)
final EdgeInsets? messagePadding;
/// Max message width
/// Default to: null, MediaQuery.of(context).size.width * 0.7
final double? maxWidth;
/// When a message have both an text and a list of media
/// it will determine which one th show first
final bool textBeforeMedia;
/// To create your own BoxDecoration fot the chat bubble
/// You can use defaultMessageDecoration to only override some variables
final BoxDecoration Function(
ChatMessage message,
ChatMessage? previousMessage,
ChatMessage? nextMessage)? messageDecorationBuilder;
/// A widget to show above the chat bubble
final Widget Function(ChatMessage message, ChatMessage? previousMessage,
ChatMessage? nextMessage)? top;
/// A widget to show under the chat bubble
final Widget Function(ChatMessage message, ChatMessage? previousMessage,
ChatMessage? nextMessage)? bottom;
/// Function to call when the user clicks on a media
/// Will not work with the default video player
final void Function(ChatMedia media)? onTapMedia;
}
part of dash_chat_2;
/// {@category Models}
class QuickReply {
QuickReply({
required this.title,
this.value,
this.customProperties,
});
/// Create a QuickReply instance from json data
factory QuickReply.fromJson(Map<String, dynamic> jsonData) {
return QuickReply(
title: jsonData['title'].toString(),
value: jsonData['value']?.toString(),
customProperties: jsonData['customProperties'] as Map<String, dynamic>,
);
}
/// Title of the quick reply,
/// it's what will be visible in the quick replies list
String title;
/// Actual value of the quick reply
/// Use that if you want to have a message text different from the title
String? value;
/// A list of custom properties to extend the existing ones
/// in case you need to store more things.
/// Can be useful to extend existing features
Map<String, dynamic>? customProperties;
/// Convert a QuickReply into a json
Map<String, dynamic> toJson() {
return <String, dynamic>{
'title': title,
'value': value,
'customProperties': customProperties,
};
}
}
part of dash_chat_2;
/// {@category Customization}
class QuickReplyOptions {
const QuickReplyOptions({
this.onTapQuickReply,
this.quickReplyPadding,
this.quickReplyMargin,
this.quickReplyStyle,
this.quickReplyTextStyle,
this.quickReplyBuilder,
});
/// Function to call when the user click on a quick reply
/// Use that to create a message and send it
final Function(QuickReply)? onTapQuickReply;
/// Padding of a quick reply container
final EdgeInsets? quickReplyPadding;
/// Margin of a quick reply container
final EdgeInsets? quickReplyMargin;
/// BoxDecoration of a quick reply container
final BoxDecoration? quickReplyStyle;
/// TextStyle of a quick reply
final TextStyle? quickReplyTextStyle;
/// Builder to create your own quickReply builder
final Widget Function(QuickReply)? quickReplyBuilder;
}
part of dash_chat_2;
/// {@category Customization}
class ScrollToBottomOptions {
const ScrollToBottomOptions({
this.disabled = false,
this.scrollToBottomBuilder,
this.onScrollToBottomPress,
});
/// If you don't want to show the scroll-to-bottom widget
final bool disabled;
/// Builder to create your own scroll-to-bottom widget
/// You can use DefaultScrollToBottom to only override some variables
final Widget Function(ScrollController scrollController)?
scrollToBottomBuilder;
/// Function to call when the scroll-to-bottom widget is pressed
/// It will scroll down in any case
final void Function()? onScrollToBottomPress;
}
export 'image_provider_mobile.dart'
if (dart.library.html) 'image_provider_web.dart';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
ImageProvider getImageProvider(String url) {
if (url.startsWith('http')) {
return CachedNetworkImageProvider(url);
} else if (url.startsWith('assets')) {
return AssetImage(url);
} else {
return FileImage(
File(url),
);
}
}
import 'package:flutter/material.dart';
ImageProvider getImageProvider(String url) {
if (url.startsWith('http')) {
return NetworkImage(url);
} else {
return const AssetImage(
'assets/placeholder.png',
package: 'dash_chat_2',
);
}
}
part of dash_chat_2;
// 'package:gradient_borders/gradient_borders.dart';
/// {@category Default widgets}
InputDecoration defaultInputDecoration(
void Function() sendMessage, {
String hintText = '请输入问题...',
TextStyle hintStyle = const TextStyle(color: Colors.grey),
Color? fillColor,
}) =>
InputDecoration(
isDense: true,
hintText: hintText,
hintStyle: hintStyle,
filled: true,
// fillColor: fillColor ?? Colors.grey[100],
contentPadding: const EdgeInsets.only(
left: 18,
top: 15,
bottom: 15,
),
suffixIcon: IconButton(
onPressed: sendMessage,
icon: Icon(
Icons.send,
size: 30,
),
// rgba(123, 238, 251, 1.00)
// rgba(122, 239, 251, 1.00)
color: Color.fromRGBO(123, 238, 251, 1),
),
// Icon(Icons.abc_rounded),
// border: const InputBorder()
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(10),
// borderSide: const BorderSide(
// width: 10,
// style: BorderStyle.solid,
// ),
// ),
// rgba(116, 112, 249, 1.00)
enabledBorder: GradientOutlineInputBorder(
// rgba(140, 197, 208, 1.00)
borderRadius: BorderRadius.circular(10),
gradient: LinearGradient(colors: [
Color.fromRGBO(116, 112, 249, 1.00),
Color.fromRGBO(149, 197, 208, 1.00)
]),
width: 5,
),
// OutlineInputBorder(
// borderRadius:
// BorderRadius.circular(10),
// borderSide: const BorderSide(
// width: 5,
// style: BorderStyle.solid,
// ),
// ),
focusedBorder: GradientOutlineInputBorder(
// rgba(140, 197, 208, 1.00)
borderRadius: BorderRadius.circular(10),
gradient: LinearGradient(colors: [
Color.fromRGBO(116, 112, 249, 1.00),
Color.fromRGBO(149, 197, 208, 1.00)
]),
width: 5,
),
// focusedBorder: OutlineInputBorder(
// borderRadius: BorderRadius.circular(10),
// borderSide: const BorderSide(
// width: 5,
// style: BorderStyle.solid,
// ),
// ),
);
part of dash_chat_2;
/// {@category Default widgets}
Widget Function(Function send) defaultSendButton({
required Color color,
IconData icon = Icons.send,
EdgeInsets? padding,
}) =>
(Function fct) => InkWell(
onTap: () => fct(),
child: Padding(
padding: padding ??
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Icon(
Icons.send,
color: color,
),
),
);
part of dash_chat_2;
/// @nodoc
class InputToolbar extends StatefulWidget {
const InputToolbar({
required this.currentUser,
required this.onSend,
this.inputOptions = const InputOptions(),
Key? key,
}) : super(key: key);
/// Options to custom the toolbar
final InputOptions inputOptions;
/// Function to call when the message is sent (click on the send button)
final Function(ChatMessage) onSend;
/// Current user using the chat
final ChatUser currentUser;
@override
_InputToolbarState createState() => _InputToolbarState();
}
class _InputToolbarState extends State<InputToolbar>
with WidgetsBindingObserver {
late TextEditingController textController;
OverlayEntry? _overlayEntry;
int currentMentionIndex = -1;
String currentTrigger = '';
late FocusNode focusNode;
@override
void initState() {
textController =
widget.inputOptions.textController ?? TextEditingController();
focusNode = widget.inputOptions.focusNode ?? FocusNode();
focusNode.addListener(() {
if (!focusNode.hasFocus) {
_clearOverlay();
}
});
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangeMetrics() {
final double bottomInset = WidgetsBinding.instance.window.viewInsets.bottom;
final bool isKeyboardActive = bottomInset > 0.0;
if (!isKeyboardActive) {
_clearOverlay();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_clearOverlay();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: widget.inputOptions.inputToolbarPadding,
margin: widget.inputOptions.inputToolbarMargin,
decoration: widget.inputOptions.inputToolbarStyle,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (widget.inputOptions.leading != null)
...widget.inputOptions.leading!,
Expanded(
child: Directionality(
textDirection: widget.inputOptions.inputTextDirection,
child: TextField(
focusNode: focusNode,
controller: textController,
enabled: !widget.inputOptions.inputDisabled,
textCapitalization: widget.inputOptions.textCapitalization,
textInputAction: widget.inputOptions.textInputAction,
decoration: widget.inputOptions.inputDecoration ??
defaultInputDecoration(_sendMessage),
maxLength: widget.inputOptions.maxInputLength,
minLines: 1,
maxLines: widget.inputOptions.sendOnEnter
? 1
: widget.inputOptions.inputMaxLines,
cursorColor: widget.inputOptions.cursorStyle.color,
cursorWidth: widget.inputOptions.cursorStyle.width,
showCursor: !widget.inputOptions.cursorStyle.hide,
style: widget.inputOptions.inputTextStyle,
onSubmitted: (String value) {
if (widget.inputOptions.sendOnEnter) {
_sendMessage();
}
},
onChanged: (String value) async {
setState(() {});
if (widget.inputOptions.onTextChange != null) {
widget.inputOptions.onTextChange!(value);
}
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (widget.inputOptions.onMention != null) {
await _checkMentions(value);
}
});
},
autocorrect: widget.inputOptions.autocorrect,
),
),
),
if (widget.inputOptions.trailing != null &&
widget.inputOptions.showTraillingBeforeSend)
...widget.inputOptions.trailing!,
//发送按钮重写
// if (widget.inputOptions.alwaysShowSend ||
// textController.text.isNotEmpty)
// widget.inputOptions.sendButtonBuilder != null
// ? widget.inputOptions.sendButtonBuilder!(_sendMessage)
// : defaultSendButton(color: Theme.of(context).primaryColor)(
// _sendMessage,
// ),
if (widget.inputOptions.trailing != null &&
!widget.inputOptions.showTraillingBeforeSend)
...widget.inputOptions.trailing!,
],
),
);
}
Future<void> _checkMentions(String text) async {
bool hasMatch = false;
for (final String trigger in widget.inputOptions.onMentionTriggers) {
final RegExp regexp = RegExp(r'(?<![^\s<>])' + trigger + r'([^\s<>]+)$');
if (regexp.hasMatch(text)) {
hasMatch = true;
currentMentionIndex = textController.text.indexOf(regexp);
currentTrigger = trigger;
List<Widget> children = await widget.inputOptions.onMention!(
trigger,
regexp.firstMatch(text)!.group(1)!,
_onMentionClick,
);
_showMentionModal(children);
}
}
if (!hasMatch) {
_clearOverlay();
}
}
void _onMentionClick(String value) {
textController.text = textController.text.replaceRange(
currentMentionIndex,
textController.text.length,
currentTrigger + value,
);
textController.selection = TextSelection.collapsed(
offset: textController.text.length,
);
_clearOverlay();
}
void _clearOverlay() {
if (_overlayEntry != null && _overlayEntry!.mounted) {
_overlayEntry?.remove();
_overlayEntry?.dispose();
}
}
void _showMentionModal(List<Widget> children) {
final OverlayState overlay = Overlay.of(context)!;
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final Offset topLeftCornerOffset = renderBox.localToGlobal(Offset.zero);
double bottomPosition =
MediaQuery.of(context).size.height - topLeftCornerOffset.dy;
if (widget.inputOptions.inputToolbarMargin != null) {
bottomPosition -= widget.inputOptions.inputToolbarMargin!.top -
widget.inputOptions.inputToolbarMargin!.bottom;
}
_clearOverlay();
_overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return Positioned(
width: renderBox.size.width,
bottom: bottomPosition,
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height -
bottomPosition -
MediaQuery.of(context).padding.top -
kToolbarHeight,
),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 0.2,
color: Theme.of(context).dividerColor,
),
),
),
child: Material(
color: Theme.of(context).selectedRowColor,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
),
),
);
},
);
overlay.insert(_overlayEntry!);
}
void _sendMessage() {
if (textController.text.isNotEmpty) {
final ChatMessage message = ChatMessage(
text: textController.text,
user: widget.currentUser,
createdAt: DateTime.now(),
);
widget.onSend(message);
textController.text = '';
if (widget.inputOptions.onTextChange != null) {
widget.inputOptions.onTextChange!('');
}
}
}
}
part of dash_chat_2;
/// {@category Default widgets}
class DefaultDateSeparator extends StatelessWidget {
const DefaultDateSeparator({
required this.date,
this.messageListOptions = const MessageListOptions(),
this.padding = const EdgeInsets.symmetric(vertical: 20),
this.textStyle = const TextStyle(color: Colors.grey),
Key? key,
}) : super(key: key);
/// Date to show
final DateTime date;
/// Options to customize the behaviour and design of the overall list of message
final MessageListOptions messageListOptions;
/// Padding of the separator
final EdgeInsets padding;
/// Style of the text
final TextStyle textStyle;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: Text(
_formatDateSeparator(date),
style: textStyle,
),
);
}
String _formatDateSeparator(DateTime date) {
if (messageListOptions.dateSeparatorFormat != null) {
return messageListOptions.dateSeparatorFormat!.format(date);
}
final DateTime today = DateTime.now();
if (date.year != today.year) {
return intl.DateFormat('dd MMM yyyy, HH:mm').format(date);
} else if (date.month != today.month ||
_getWeekOfYear(date) != _getWeekOfYear(today)) {
return intl.DateFormat('dd MMM HH:mm').format(date);
} else if (date.day != today.day) {
return intl.DateFormat('E HH:mm').format(date);
}
return intl.DateFormat('HH:mm').format(date);
}
int _getWeekOfYear(DateTime date) {
final int dayOfYear = int.parse(intl.DateFormat('D').format(date));
return ((dayOfYear - date.weekday + 10) / 7).floor();
}
}
part of dash_chat_2;
/// {@category Default widgets}
class DefaultScrollToBottom extends StatelessWidget {
const DefaultScrollToBottom({
required this.scrollController,
this.backgroundColor,
this.textColor,
this.bottom = 10.0,
this.left = 0.0,
this.right = 0.0,
this.top,
this.height = 30.0,
this.width = 30.0,
this.elevation = 5,
this.icon = Icons.arrow_downward,
this.iconSize = 18,
this.onScrollToBottomPress,
Key? key,
}) : super(key: key);
/// Scroll controller of the chat list
final ScrollController scrollController;
/// Background color of the button
final Color? backgroundColor;
/// Icon color of the button
final Color? textColor;
/// The distance that the child's bottom edge is inset from the bottom of the stack
final double? bottom;
/// The distance that the child's left edge is inset from the left of the stack
final double? left;
/// The distance that the child's right edge is inset from the right of the stack
final double? right;
/// The distance that the child's top edge is inset from the top of the stack
final double? top;
/// Height of the button
final double height;
/// Width of the button
final double width;
/// Elevation of the button
final double elevation;
/// Icon of the button
final IconData icon;
/// Icon size
final double iconSize;
/// Function to call when the scroll-to-bottom widget is pressed
/// It will scroll down in any case
final void Function()? onScrollToBottomPress;
@override
Widget build(BuildContext context) {
return Positioned(
right: right,
left: left,
top: top,
bottom: bottom,
child: SizedBox(
width: width,
height: height,
child: RawMaterialButton(
elevation: elevation,
fillColor: backgroundColor,
shape: const CircleBorder(),
child: Icon(
icon,
size: iconSize,
color: textColor,
),
onPressed: () {
if (onScrollToBottomPress != null) {
onScrollToBottomPress!();
}
scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
),
);
}
}
part of dash_chat_2;
/// @nodoc
class MessageList extends StatefulWidget {
const MessageList({
required this.currentUser,
required this.messages,
this.messageOptions = const MessageOptions(),
this.messageListOptions = const MessageListOptions(),
this.quickReplyOptions = const QuickReplyOptions(),
this.scrollToBottomOptions = const ScrollToBottomOptions(),
this.typingUsers,
Key? key,
}) : super(key: key);
/// The current user of the chat
final ChatUser currentUser;
/// List of messages visible in the chat
final List<ChatMessage> messages;
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
/// Options to customize the behaviour and design of the overall list of message
final MessageListOptions messageListOptions;
/// Options to customize the behaviour and design of the quick replies
final QuickReplyOptions quickReplyOptions;
/// Options to customize the behaviour and design of the scroll-to-bottom button
final ScrollToBottomOptions scrollToBottomOptions;
/// List of users currently typing in the chat
final List<ChatUser>? typingUsers;
@override
_MessageListState createState() => _MessageListState();
}
class _MessageListState extends State<MessageList> {
bool scrollToBottomIsVisible = false;
bool isLoadingMore = false;
late ScrollController scrollController;
@override
void initState() {
scrollController =
widget.messageListOptions.scrollController ?? ScrollController();
scrollController.addListener(() => _onScroll());
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
child: Stack(
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: ListView.builder(
physics: widget.messageListOptions.scrollPhysics,
controller: scrollController,
reverse: true,
itemCount: widget.messages.length,
itemBuilder: (BuildContext context, int i) {
final ChatMessage? previousMessage =
i < widget.messages.length - 1
? widget.messages[i + 1]
: null;
final ChatMessage? nextMessage =
i > 0 ? widget.messages[i - 1] : null;
final ChatMessage message = widget.messages[i];
final bool isAfterDateSeparator = _shouldShowDateSeparator(
previousMessage, message, widget.messageListOptions);
bool isBeforeDateSeparator = false;
if (nextMessage != null) {
isBeforeDateSeparator = _shouldShowDateSeparator(
message, nextMessage, widget.messageListOptions);
}
return Column(
children: <Widget>[
if (isAfterDateSeparator)
widget.messageListOptions.dateSeparatorBuilder != null
? widget.messageListOptions
.dateSeparatorBuilder!(message.createdAt)
: DefaultDateSeparator(
date: message.createdAt,
messageListOptions: widget.messageListOptions,
),
if (widget.messageOptions.messageRowBuilder !=
null) ...<Widget>[
widget.messageOptions.messageRowBuilder!(
message,
previousMessage,
nextMessage,
isAfterDateSeparator,
isBeforeDateSeparator,
),
] else
MessageRow(
message: widget.messages[i],
nextMessage: nextMessage,
previousMessage: previousMessage,
currentUser: widget.currentUser,
isAfterDateSeparator: isAfterDateSeparator,
isBeforeDateSeparator: isBeforeDateSeparator,
messageOptions: widget.messageOptions,
messageLength: widget.messages,
),
],
);
},
),
),
if (widget.typingUsers != null && widget.typingUsers!.isNotEmpty)
...widget.typingUsers!.map((ChatUser user) {
if (widget.messageListOptions.typingBuilder != null) {
return widget.messageListOptions.typingBuilder!(user);
}
return DefaultTypingBuilder(user: user);
}).toList(),
if (widget.messageListOptions.showFooterBeforeQuickReplies &&
widget.messageListOptions.chatFooterBuilder != null)
widget.messageListOptions.chatFooterBuilder!,
if (widget.messages.isNotEmpty &&
widget.messages.first.quickReplies != null &&
widget.messages.first.quickReplies!.isNotEmpty &&
widget.messages.first.user.id != widget.currentUser.id)
QuickReplies(
quickReplies: widget.messages.first.quickReplies!,
quickReplyOptions: widget.quickReplyOptions,
),
if (!widget.messageListOptions.showFooterBeforeQuickReplies &&
widget.messageListOptions.chatFooterBuilder != null)
widget.messageListOptions.chatFooterBuilder!,
],
),
if (isLoadingMore)
Positioned(
top: 8.0,
right: 0,
left: 0,
child: widget.messageListOptions.loadEarlierBuilder ??
const Center(
child: SizedBox(
child: CircularProgressIndicator(),
),
),
),
if (!widget.scrollToBottomOptions.disabled && scrollToBottomIsVisible)
widget.scrollToBottomOptions.scrollToBottomBuilder != null
? widget.scrollToBottomOptions
.scrollToBottomBuilder!(scrollController)
: DefaultScrollToBottom(
scrollController: scrollController,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
textColor: Theme.of(context).primaryColor,
),
],
),
);
}
/// Check if a date separator needs to be shown
bool _shouldShowDateSeparator(ChatMessage? previousMessage,
ChatMessage message, MessageListOptions messageListOptions) {
if (!messageListOptions.showDateSeparator) {
return false;
}
if (previousMessage == null) {
// Means this is the first message
return true;
}
switch (messageListOptions.separatorFrequency) {
case SeparatorFrequency.days:
final DateTime previousDate = DateTime(
previousMessage.createdAt.year,
previousMessage.createdAt.month,
previousMessage.createdAt.day,
);
final DateTime messageDate = DateTime(
message.createdAt.year,
message.createdAt.month,
message.createdAt.day,
);
return previousDate.difference(messageDate).inDays.abs() > 0;
case SeparatorFrequency.hours:
final DateTime previousDate = DateTime(
previousMessage.createdAt.year,
previousMessage.createdAt.month,
previousMessage.createdAt.day,
previousMessage.createdAt.hour,
);
final DateTime messageDate = DateTime(
message.createdAt.year,
message.createdAt.month,
message.createdAt.day,
message.createdAt.hour,
);
return previousDate.difference(messageDate).inHours.abs() > 0;
default:
return false;
}
}
/// Sroll listener to trigger different actions:
/// show scroll-to-bottom btn and LoadEarlier behaviour
Future<void> _onScroll() async {
bool topReached =
scrollController.offset >= scrollController.position.maxScrollExtent &&
!scrollController.position.outOfRange;
if (topReached &&
widget.messageListOptions.onLoadEarlier != null &&
!isLoadingMore) {
setState(() {
isLoadingMore = true;
});
showScrollToBottom();
await widget.messageListOptions.onLoadEarlier!();
setState(() {
isLoadingMore = false;
});
} else if (scrollController.offset > 200) {
showScrollToBottom();
} else {
hideScrollToBottom();
}
}
void showScrollToBottom() {
if (!scrollToBottomIsVisible) {
setState(() {
scrollToBottomIsVisible = true;
});
}
}
void hideScrollToBottom() {
if (scrollToBottomIsVisible) {
setState(() {
scrollToBottomIsVisible = false;
});
}
}
}
part of dash_chat_2;
/// {@category Default widgets}
class DefaultAvatar extends StatelessWidget {
const DefaultAvatar({
required this.user,
this.size = 35,
this.fallbackImage,
this.onPressAvatar,
this.onLongPressAvatar,
});
/// The URL of the user's profile picture
final ChatUser user;
/// Size of the avatar
final double size;
/// Placeholder image in case there is no initials ot he profile image do not load
final ImageProvider? fallbackImage;
/// Function to call when the user long press on the avatar
final void Function(ChatUser)? onLongPressAvatar;
/// Function to call when the user press on the avatar
final void Function(ChatUser)? onPressAvatar;
/// Get the initials of the user
String getInitials() {
return (user.firstName == null || user.firstName!.isEmpty
? ''
: user.firstName![0]) +
(user.lastName == null || user.lastName!.isEmpty
? ''
: user.lastName![0]);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressAvatar != null ? () => onPressAvatar!(user) : null,
onLongPress:
onLongPressAvatar != null ? () => onLongPressAvatar!(user) : null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: SizedBox(
height: size,
width: size,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ClipOval(
child: Container(
color: Colors.grey[200],
child: getInitials().isNotEmpty
? Center(
child: Text(
getInitials(),
style: TextStyle(
color: Colors.black,
fontSize: size * 0.35,
),
),
)
: Image(
image: fallbackImage ??
const AssetImage(
'assets/profile_placeholder.png',
package: 'dash_chat_2',
),
),
),
),
if (user.profileImage != null && user.profileImage!.isNotEmpty)
Center(
child: ClipOval(
child: FadeInImage(
width: size,
height: size,
fit: BoxFit.cover,
image: getImageProvider(user.profileImage!),
placeholder: fallbackImage ??
const AssetImage(
'assets/profile_placeholder.png',
package: 'dash_chat_2',
),
),
),
),
],
),
),
),
);
}
}
part of dash_chat_2;
/// {@category Default widgets}
BoxDecoration defaultMessageDecoration({
required Color color,
required double borderTopLeft,
required double borderTopRight,
required double borderBottomLeft,
required double borderBottomRight,
}) =>
BoxDecoration(
color: color,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderTopLeft),
topRight: Radius.circular(borderTopRight),
bottomLeft: Radius.circular(borderBottomLeft),
bottomRight: Radius.circular(borderBottomRight),
),
);
part of dash_chat_2;
// part of animated_text_kit;
/// {@category Default widgets}
class DefaultMessageText extends StatelessWidget {
const DefaultMessageText({
required this.message,
required this.messageLength,
required this.isOwnMessage,
this.messageOptions = const MessageOptions(),
Key? key,
}) : super(key: key);
final List<ChatMessage> messageLength;
/// Message tha contains the text to show
final ChatMessage message;
/// If the message is from the current user
final bool isOwnMessage;
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment:
isOwnMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
Wrap(
children: getMessage(isOwnMessage, messageLength),
),
if (messageOptions.showTime)
messageOptions.messageTimeBuilder != null
? messageOptions.messageTimeBuilder!(message, isOwnMessage)
: Padding(
padding: const EdgeInsets.only(top: 5),
child: Text(
(messageOptions.timeFormat ?? intl.DateFormat('HH:mm'))
.format(message.createdAt),
style: TextStyle(
color: isOwnMessage
? (messageOptions.currentUserTextColor ??
Colors.white70)
: (messageOptions.textColor ?? Colors.black54),
fontSize: 10,
),
),
),
],
);
}
List<Widget> getMessage(bool isOwnMessage, List<ChatMessage> messageLength) {
if (message.mentions != null && message.mentions!.isNotEmpty) {
String stringRegex = r'([\s\S]*)';
String stringMentionRegex = '';
for (final Mention mention in message.mentions!) {
stringRegex += '(${mention.title})' r'([\s\S]*)';
stringMentionRegex += stringMentionRegex.isEmpty
? '(${mention.title})'
: '|(${mention.title})';
}
final RegExp mentionRegex = RegExp(stringMentionRegex);
final RegExp regexp = RegExp(stringRegex);
RegExpMatch? match = regexp.firstMatch(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 m) => m.title == part,
);
res.add(getMention(mention));
} else {
res.add(getParsePattern(part, false, messageLength));
}
});
if (res.isNotEmpty) {
return res;
}
}
}
return <Widget>[getParsePattern(message.text, isOwnMessage, messageLength)];
}
Widget getPostMessageBuild(
String text, List<ChatMessage> messageLength, bool isOwnMessage) {
var lastMessage = messageLength.last;
// if(lastMessage.) {
// }
// messageLength[messageLength.length - 1];
// if(a.is)
// if()
return AnimatedTextKit(
animatedTexts: [
TypewriterAnimatedText(
text,
textStyle: const TextStyle(
// fontSize: 32.0,
// fontWeight: FontWeight.bold,
),
speed: const Duration(milliseconds: 100),
)
],
totalRepeatCount: 1,
// displayFullTextOnTap: true,
// stopPauseOnTap: true,
);
}
// {buildPostMessage = getPostMessageBuild}
Widget getParsePattern(
String text,
bool isOwnMessage,
List<ChatMessage> messageLength,
) {
return !isOwnMessage
? getPostMessageBuild(text, messageLength, isOwnMessage)
: ParsedText(
parse: messageOptions.parsePatterns != null
? messageOptions.parsePatterns!
: defaultPersePatterns,
text: text,
style: TextStyle(
color: isOwnMessage
? (messageOptions.currentUserTextColor ?? Colors.white)
: (messageOptions.textColor ?? Colors.black),
),
);
}
Widget getMention(Mention mention) {
return RichText(
text: TextSpan(
text: mention.title,
recognizer: TapGestureRecognizer()
..onTap = () => messageOptions.onPressMention != null
? messageOptions.onPressMention!(mention)
: null,
style: TextStyle(
color: isOwnMessage
? (messageOptions.currentUserTextColor ?? Colors.white)
: (messageOptions.textColor ?? Colors.black),
decoration: TextDecoration.none,
fontWeight: FontWeight.w600,
),
),
);
}
}
part of dash_chat_2;
/// {@category Default widgets}
List<MatchText> defaultPersePatterns = <MatchText>[
MatchText(
type: ParsedType.URL,
style: const TextStyle(
decoration: TextDecoration.underline,
),
onTap: (String url) {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
},
),
];
part of dash_chat_2;
/// {@category Default widgets}
class DefaultUserName extends StatelessWidget {
const DefaultUserName({
required this.user,
this.style,
this.padding,
Key? key,
}) : super(key: key);
/// User to show
final ChatUser user;
/// Style of the text
final TextStyle? style;
/// Padding around the text
final EdgeInsets? padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 10),
child: Text(
user.getFullName(),
style: style ??
const TextStyle(
fontSize: 10,
color: Colors.grey,
),
),
);
}
}
part of dash_chat_2;
/// @nodoc
class MediaContainer extends StatelessWidget {
const MediaContainer({
required this.message,
required this.isOwnMessage,
required this.messageLength,
this.messageOptions = const MessageOptions(),
Key? key,
}) : super(key: key);
/// Message that contains the media to show
final ChatMessage message;
final List<ChatMessage> messageLength;
/// If the message is from the current user
final bool isOwnMessage;
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
/// Get the right media widget according to its type
Widget _getMedia(ChatMedia media, double? height, double? width) {
final Widget loading = Container(
width: 15,
height: 15,
margin: const EdgeInsets.all(10),
child: const CircularProgressIndicator(),
);
switch (media.type) {
case MediaType.video:
return Stack(
alignment: AlignmentDirectional.bottomEnd,
children: <Widget>[
VideoPlayer(url: media.url, key: GlobalKey()),
if (media.isUploading) loading
],
);
case MediaType.image:
return Stack(
alignment: AlignmentDirectional.bottomEnd,
children: <Widget>[
Image(
height: height,
width: width,
fit: BoxFit.cover,
alignment: isOwnMessage ? Alignment.topRight : Alignment.topLeft,
image: getImageProvider(media.url),
),
if (media.isUploading) loading
],
);
default:
return TextContainer(
isOwnMessage: isOwnMessage,
messageOptions: messageOptions,
message: message,
messageLength: messageLength,
messageTextBuilder: (ChatMessage m, ChatMessage? p, ChatMessage? n) {
return Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: !media.isUploading
? Icon(
Icons.description,
size: 18,
color: isOwnMessage
? (messageOptions.currentUserTextColor ??
Colors.white)
: (messageOptions.textColor ?? Colors.black),
)
: loading,
),
Flexible(
child: Text(
media.fileName,
style: TextStyle(
decoration: TextDecoration.underline,
color: isOwnMessage
? (messageOptions.currentUserTextColor ??
Colors.white)
: (messageOptions.textColor ?? Colors.black),
),
),
),
],
);
},
);
}
}
@override
Widget build(BuildContext context) {
if (message.medias != null && message.medias!.isNotEmpty) {
final List<ChatMedia> media = message.medias!;
return Wrap(
alignment: isOwnMessage ? WrapAlignment.end : WrapAlignment.start,
children: media.map(
(ChatMedia m) {
final double gallerySize =
(MediaQuery.of(context).size.width * 0.7) / 2 - 5;
final bool isImage = m.type == MediaType.image;
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,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.5,
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: GestureDetector(
onTap: messageOptions.onTapMedia != null
? () => messageOptions.onTapMedia!(m)
: null,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: ColorFiltered(
colorFilter: ColorFilter.mode(
m.isUploading
? Colors.white54
: Colors.white.withOpacity(
0.1,
), // Because transparent is causing an issue on flutter web
BlendMode.srcATop,
),
child: _getMedia(
m,
media.length > 1 ? gallerySize : null,
media.length > 1 ? gallerySize : null,
),
),
),
),
);
},
).toList(),
);
}
return const SizedBox();
}
}
part of dash_chat_2;
/// @nodoc
class MessageRow extends StatelessWidget {
const MessageRow({
required this.message,
required this.currentUser,
required this.messageLength,
this.previousMessage,
this.nextMessage,
this.isAfterDateSeparator = false,
this.isBeforeDateSeparator = false,
this.messageOptions = const MessageOptions(),
Key? key,
}) : super(key: key);
final List<ChatMessage> messageLength;
/// Current message to show
final ChatMessage message;
/// Previous message in the list
final ChatMessage? previousMessage;
/// Next message in the list
final ChatMessage? nextMessage;
/// Current user of the chat
final ChatUser currentUser;
/// If the message is preceded by a date separator
final bool isAfterDateSeparator;
/// If the message is before a date separator
final bool isBeforeDateSeparator;
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
/// Get the avatar widget
Widget getAvatar() {
return messageOptions.avatarBuilder != null
? messageOptions.avatarBuilder!(
message.user,
messageOptions.onPressAvatar,
messageOptions.onLongPressAvatar,
)
: DefaultAvatar(
user: message.user,
onLongPressAvatar: messageOptions.onLongPressAvatar,
onPressAvatar: messageOptions.onPressAvatar,
);
}
@override
Widget build(BuildContext context) {
print("messageLengthmessageLength$messageLength");
final bool isOwnMessage = message.user.id == currentUser.id;
bool isPreviousSameAuthor = false;
bool isNextSameAuthor = false;
if (previousMessage != null &&
previousMessage!.user.id == message.user.id) {
isPreviousSameAuthor = true;
}
if (nextMessage != null && nextMessage!.user.id == message.user.id) {
isNextSameAuthor = true;
}
return Padding(
padding: EdgeInsets.only(top: isPreviousSameAuthor ? 2 : 15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
isOwnMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
children: <Widget>[
if (messageOptions.showOtherUsersAvatar)
Opacity(
opacity:
!isOwnMessage && (!isNextSameAuthor || isBeforeDateSeparator)
? 1
: 0,
child: getAvatar(),
),
if (!messageOptions.showOtherUsersAvatar)
const Padding(padding: EdgeInsets.only(left: 10)),
GestureDetector(
onLongPress: messageOptions.onLongPressMessage != null
? () => messageOptions.onLongPressMessage!(message)
: null,
onTap: messageOptions.onPressMessage != null
? () => messageOptions.onPressMessage!(message)
: null,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: messageOptions.maxWidth ??
MediaQuery.of(context).size.width * 0.7,
),
child: Column(
crossAxisAlignment: isOwnMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
if (messageOptions.top != null)
messageOptions.top!(message, previousMessage, nextMessage),
if (!isOwnMessage &&
messageOptions.showOtherUsersName &&
(!isPreviousSameAuthor || isAfterDateSeparator))
messageOptions.userNameBuilder != null
? messageOptions.userNameBuilder!(message.user)
: DefaultUserName(user: message.user),
if (message.medias != null &&
message.medias!.isNotEmpty &&
messageOptions.textBeforeMedia)
messageOptions.messageMediaBuilder != null
? messageOptions.messageMediaBuilder!(
message, previousMessage, nextMessage)
: MediaContainer(
message: message,
isOwnMessage: isOwnMessage,
messageOptions: messageOptions,
messageLength: messageLength,
// messageLength: messageLength,
),
if (message.text.isNotEmpty)
TextContainer(
messageOptions: messageOptions,
message: message,
messageLength: messageLength,
previousMessage: previousMessage,
nextMessage: nextMessage,
isOwnMessage: isOwnMessage,
isNextSameAuthor: isNextSameAuthor,
isPreviousSameAuthor: isPreviousSameAuthor,
isAfterDateSeparator: isAfterDateSeparator,
isBeforeDateSeparator: isBeforeDateSeparator,
messageTextBuilder: messageOptions.messageTextBuilder,
),
if (message.medias != null &&
message.medias!.isNotEmpty &&
!messageOptions.textBeforeMedia)
messageOptions.messageMediaBuilder != null
? messageOptions.messageMediaBuilder!(
message, previousMessage, nextMessage)
: MediaContainer(
message: message,
isOwnMessage: isOwnMessage,
messageOptions: messageOptions,
messageLength: messageLength,
),
if (messageOptions.bottom != null)
messageOptions.bottom!(
message, previousMessage, nextMessage),
],
),
),
),
if (messageOptions.showCurrentUserAvatar)
Opacity(
opacity: isOwnMessage && !isNextSameAuthor ? 1 : 0,
child: getAvatar(),
),
if (!messageOptions.showCurrentUserAvatar)
const Padding(padding: EdgeInsets.only(left: 10))
],
),
);
}
}
part of dash_chat_2;
/// @nodoc
class TextContainer extends StatelessWidget {
const TextContainer({
required this.message,
this.messageOptions = const MessageOptions(),
this.previousMessage,
this.nextMessage,
required this.messageLength,
this.isOwnMessage = false,
this.isPreviousSameAuthor = false,
this.isNextSameAuthor = false,
this.isAfterDateSeparator = false,
this.isBeforeDateSeparator = false,
this.messageTextBuilder,
Key? key,
}) : super(key: key);
final List<ChatMessage> messageLength;
/// Options to customize the behaviour and design of the messages
final MessageOptions messageOptions;
/// Message that contains the text to show
final ChatMessage message;
/// Previous message in the list
final ChatMessage? previousMessage;
/// Next message in the list
final ChatMessage? nextMessage;
/// If the message is from the current user
final bool isOwnMessage;
/// If the previous message is from the same author as the current one
final bool isPreviousSameAuthor;
/// If the next message is from the same author as the current one
final bool isNextSameAuthor;
/// If the message is preceded by a date separator
final bool isAfterDateSeparator;
/// If the message is before by a date separator
final bool isBeforeDateSeparator;
/// We could acces that from messageOptions but we want to reuse this widget
/// for media and be able to override the text builder
final Widget Function(ChatMessage, ChatMessage?, ChatMessage?)?
messageTextBuilder;
@override
Widget build(BuildContext context) {
return Container(
decoration: messageOptions.messageDecorationBuilder != null
? messageOptions.messageDecorationBuilder!(
message, previousMessage, nextMessage)
: defaultMessageDecoration(
color: isOwnMessage
? (messageOptions.currentUserContainerColor ??
Theme.of(context).primaryColor)
: (messageOptions.containerColor ?? Colors.grey[100])!,
borderTopLeft:
isPreviousSameAuthor && !isOwnMessage && !isAfterDateSeparator
? 0.0
: 18.0,
borderTopRight:
isPreviousSameAuthor && isOwnMessage && !isAfterDateSeparator
? 0.0
: 18.0,
borderBottomLeft:
!isOwnMessage && !isBeforeDateSeparator && isNextSameAuthor
? 0.0
: 18.0,
borderBottomRight:
isOwnMessage && !isBeforeDateSeparator && isNextSameAuthor
? 0.0
: 18.0,
),
padding: messageOptions.messagePadding ?? const EdgeInsets.all(11),
child: messageTextBuilder != null
? messageTextBuilder!(message, previousMessage, nextMessage)
: DefaultMessageText(
messageLength: messageLength,
message: message,
isOwnMessage: isOwnMessage,
messageOptions: messageOptions,
),
);
}
}
part of dash_chat_2;
/// @nodoc
class VideoPlayer extends StatefulWidget {
const VideoPlayer({
required this.url,
this.aspectRatio = 1,
this.canPlay = true,
Key? key,
}) : super(key: key);
/// Link of the video
final String url;
/// The Aspect Ratio of the Video. Important to get the correct size of the video
final double aspectRatio;
/// If the video can be played
final bool canPlay;
@override
_VideoPlayerState createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
late vp.VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = vp.VideoPlayerController.network(widget.url)
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized,
// even before the play button has been pressed.
setState(() {});
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return _controller.value.isInitialized
? Container(
color: Colors.black,
child: Stack(
alignment: _controller.value.isPlaying
? AlignmentDirectional.bottomStart
: AlignmentDirectional.center,
children: <Widget>[
AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: vp.VideoPlayer(_controller),
),
IconButton(
iconSize: _controller.value.isPlaying ? 24 : 60,
onPressed: widget.canPlay
? () {
setState(() {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
});
}
: null,
icon: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
// size: 60,
),
),
],
),
)
: Container(color: Colors.black);
}
}
part of dash_chat_2;
/// {@category Default widgets}
class DefaultQuickReply extends StatelessWidget {
const DefaultQuickReply({
required this.quickReply,
this.quickReplyOptions = const QuickReplyOptions(),
Key? key,
}) : super(key: key);
/// Options used to customize quick replies behaviour and design
final QuickReplyOptions quickReplyOptions;
/// Quick reply to show
final QuickReply quickReply;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: quickReplyOptions.onTapQuickReply != null
? () => quickReplyOptions.onTapQuickReply!(quickReply)
: null,
child: Container(
margin: quickReplyOptions.quickReplyMargin ??
const EdgeInsets.symmetric(horizontal: 5),
padding: quickReplyOptions.quickReplyPadding ??
const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: quickReplyOptions.quickReplyStyle ??
BoxDecoration(
border: Border.all(color: Theme.of(context).primaryColor),
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
),
child: Text(
quickReply.title,
style: quickReplyOptions.quickReplyTextStyle ??
TextStyle(color: Theme.of(context).primaryColor),
),
),
);
}
}
part of dash_chat_2;
/// @nodoc
class QuickReplies extends StatelessWidget {
const QuickReplies({
required this.quickReplies,
this.quickReplyOptions = const QuickReplyOptions(),
Key? key,
}) : super(key: key);
/// List of quick replies to show
final List<QuickReply> quickReplies;
/// Options used to customize quick replies behaviour and design
final QuickReplyOptions quickReplyOptions;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: quickReplies.map((QuickReply r) {
return quickReplyOptions.quickReplyBuilder != null
? quickReplyOptions.quickReplyBuilder!(r)
: DefaultQuickReply(
quickReply: r,
quickReplyOptions: quickReplyOptions,
);
}).toList(),
),
),
),
],
);
}
}
part of dash_chat_2;
/// {@category Default widgets}
class DefaultTypingBuilder extends StatelessWidget {
const DefaultTypingBuilder({
required this.user,
this.text = 'is typing',
Key? key,
}) : super(key: key);
/// User that is typing
final ChatUser user;
/// Text to show after user's name in the indicator
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 15, top: 25),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
const Padding(
padding: EdgeInsets.only(right: 2),
child: TypingIndicator(),
),
Text(
user.getFullName(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
' $text',
style: const TextStyle(fontSize: 12),
),
],
),
);
}
}
part of dash_chat_2;
/// @nodoc
class TypingIndicator extends StatefulWidget {
const TypingIndicator({
Key? key,
this.flashingCircleDarkColor = const Color(0xFF333333),
this.flashingCircleBrightColor = const Color(0xFFaec1dd),
}) : super(key: key);
/// Dark color in the animation
final Color flashingCircleDarkColor;
/// Light color in the animation
final Color flashingCircleBrightColor;
@override
_TypingIndicatorState createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<TypingIndicator>
with TickerProviderStateMixin {
late AnimationController _repeatingController;
final List<Interval> _dotIntervals = const <Interval>[
Interval(0.25, 0.8),
Interval(0.35, 0.9),
Interval(0.45, 1.0),
];
@override
void initState() {
super.initState();
_repeatingController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_repeatingController.repeat();
}
@override
void dispose() {
_repeatingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: 30,
height: 15,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildFlashingCircle(0),
_buildFlashingCircle(1),
_buildFlashingCircle(2),
],
),
);
}
Widget _buildFlashingCircle(int index) {
return AnimatedBuilder(
animation: _repeatingController,
builder: (BuildContext context, Widget? child) {
final double circleFlashPercent =
_dotIntervals[index].transform(_repeatingController.value);
final double circleColorPercent = sin(pi * circleFlashPercent);
return Container(
width: 5,
height: 5,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color.lerp(widget.flashingCircleDarkColor,
widget.flashingCircleBrightColor, circleColorPercent),
),
);
},
);
}
}
import 'dart:async';
import 'package:vibration/vibration.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
......@@ -33,7 +33,10 @@ class ApplicationController extends GetxController {
/// 事件
// tab栏动画
void handleNavBarTap(int index) {
void handleNavBarTap(int index) async {
// if (await Vibration.hasVibrator() != null) {
// Vibration.vibrate(duration: 10, amplitude: 128);
// }
pageController.animateToPage(index,
duration: const Duration(milliseconds: 200), curve: Curves.ease);
}
......
import 'package:chart/common/apis/apis.dart';
import 'package:chart/common/store/store.dart';
import 'package:dash_chat_2/dash_chat_2.dart';
import 'package:chart/package/chat_dash/dash_chat_2.dart' as Chat;
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
......@@ -22,13 +22,13 @@ class ChatNewController extends GetxController {
// id: '82091008-a4aa-4a89-ae75-a22bf8d6f3aa',
// );
ChatUser _user = ChatUser(
Chat.ChatUser _user = Chat.ChatUser(
id: '1',
// firstName: 'Charles',
// lastName: 'Leclerc',
);
final receiveUser = ChatUser(
final receiveUser = Chat.ChatUser(
id: '82091008-a484-4a89-ae75-a22bf8d6f3aa',
firstName: "GPT",
lastName: '大师',
......@@ -39,7 +39,7 @@ class ChatNewController extends GetxController {
/// 事件
void sendMessage(ChatMessage message) async {
void sendMessage(Chat.ChatMessage message) async {
// if (state.messageList.isNotEmpty) {
// // data = !_messages.every((element) => element.status != Status.sending);
// }
......@@ -56,7 +56,7 @@ class ChatNewController extends GetxController {
// return;
// }
final textMessage = ChatMessage(
final textMessage = Chat.ChatMessage(
user: _user,
createdAt: DateTime.now(),
// id: const Uuid().v4(),
......@@ -75,14 +75,14 @@ class ChatNewController extends GetxController {
_addMessage(textMessage);
// _addMessage(loadingMessage);
EasyLoading.showProgress(0.5, status: "正在思考中");
EasyLoading.show(status: "正在思考中...");
// ("正在思考中...");
try {
String? result = await NewsAPI.sendMessage(
{"question": message.text, "id": "${UserStore.to.profile.id}"});
// _cancelLoading();
final receiveMessage = ChatMessage(
final receiveMessage = Chat.ChatMessage(
user: receiveUser,
createdAt: DateTime.now(),
// id: const Uuid().v4(),
......@@ -93,7 +93,7 @@ class ChatNewController extends GetxController {
} catch (e) {
print("eeeeeeee$e");
// _cancelLoading();
final receiveErrorMessage = ChatMessage(
final receiveErrorMessage = Chat.ChatMessage(
user: receiveUser,
createdAt: DateTime.now(),
// id: const Uuid().v4(),
......@@ -105,7 +105,7 @@ class ChatNewController extends GetxController {
}
}
void _addMessage(ChatMessage message) {
void _addMessage(Chat.ChatMessage message) {
state.messageList.insert(0, message);
// state.messageList = [message]
// state.messageList.add(message);
......
// import 'dart:ffi';
import 'package:chart/common/entities/entities.dart';
import 'package:dash_chat_2/dash_chat_2.dart';
import 'package:chart/package/chat_dash/dash_chat_2.dart' as Chat;
import 'package:get/get.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
class ChatPageState {
// 新闻翻页 // List<types.Message>
RxList<ChatMessage> messageList = <ChatMessage>[].obs;
RxList<Chat.ChatMessage> messageList = <Chat.ChatMessage>[].obs;
// get _messageList =>
final _page = "hello".obs;
final _page = "与人工智能机器人的对话".obs;
set page(value) => this._page.value = value;
get page => this._page.value;
// RxList<NewsItem> newsList = <NewsItem>[].obs;
......
import 'package:chart/common/routers/routes.dart';
import 'package:chart/common/values/values.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import '../../../common/widgets/app.dart';
import '../../application/view.dart';
import 'package:dash_chat_2/dash_chat_2.dart';
import 'index.dart';
import 'package:chart/package/chat_dash/dash_chat_2.dart' as Chat;
import 'widgets/widgets.dart';
class ChatNewPage extends GetView<ChatNewController> {
......@@ -21,11 +21,12 @@ class ChatNewPage extends GetView<ChatNewController> {
@override
Widget build(BuildContext context) {
// final c = Get.put(ChatPageController());
final c = Get.put(ChatNewController());
// final c = Get.put(ChatNewController());
return Obx(() => Scaffold(
appBar: transparentAppBar(
title: Text(
"${c.state.messageList.length}",
"${controller.state.page}${controller.state.messageList.length}",
style: TextStyle(
color: AppColors.primaryElementText,
fontFamily: 'Montserrat',
......@@ -51,17 +52,95 @@ class ChatNewPage extends GetView<ChatNewController> {
body: Container(
width: double.infinity,
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 0),
// padding: EdgeInsets.symmetric(horizontal: 0),
decoration: BoxDecoration(
// rgba(36, 40, 64, 1.00)
// color: Color.fromRGBO(36, 40, 64, 1.00),
image: DecorationImage(
image: Image.asset("assets/images/bg.png").image,
fit: BoxFit.cover),
),
child: DashChat(
child: Chat.DashChat(
inputOptions: const Chat.InputOptions(
// alwaysShowSend: true,
// sendButtonBuilder: (send) => const Text("data"),
// showTraillingBeforeSend: true,
inputTextStyle: TextStyle(color: Colors.white),
inputToolbarStyle: BoxDecoration(
color: Color.fromRGBO(36, 40, 64, 1.00),
//
// color: LinearGradient(
// width: 5,
// colors: [Colors.red, Colors.yellow],
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// ),
// Color.fromRGBO(36, 40, 64, 1.00),
// border: Border.all(
// width: 5,
// color: LinearGradient(
// colors: [Colors.red, Colors.yellow],
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// ),
// ),
// c
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16)),
),
inputToolbarMargin: EdgeInsets.all(0),
sendButtonBuilder: null,
inputToolbarPadding:
// symmetric(horizontal: 10, vertical: 20)
EdgeInsets.only(top: 15, right: 15, left: 15, bottom: 35),
// inputToolbarPadding : const EdgeInsets.all(8.0),
// this.inputToolbarMargin = const EdgeInsets.only(top: 8.0),
// textController:
// sendButtonBuilder:
// () => TextButton(
// child: const Text("13213"),
// onPressed: (send) => {send()},
// )
// inputTextStyle: TextStyle(color: Colors.red),
// inputDecoration: InputDecoration(
// isDense: true,
// filled: true,
// fillColor: Colors.red,
// // contentPadding: const EdgeInsets.only(
// // left: 18,
// // top: 10,
// // bottom: 10,
// // ),
// // border: OutlineInputBorder(
// // borderRadius: BorderRadius.circular(25),
// // borderSide: const BorderSide(
// // width: 0,
// // style: BorderStyle.none,
// // ),
// // ),
// ),
),
currentUser: _user,
onSend: c.sendMessage,
messages: c.state.messageList,
onSend: controller.sendMessage,
// messageListOptions:
// const MessageListOptions(loadEarlierBuilder: Text("2131")),
messages: controller.state.messageList,
// messageListOptions: MessageListOptions(
// chatFooterBuilder: Container(
// color: Colors.red,
// width: double.infinity,
// height: 100.00,
// child: Text("footer")),
// ),
messageOptions: Chat.MessageOptions(
// containerColor: Colors.black,
),
),
// Column(children: [
// ]),
......@@ -98,7 +177,7 @@ class ChatNewPage extends GetView<ChatNewController> {
}
}
ChatUser _user = ChatUser(
Chat.ChatUser _user = Chat.ChatUser(
id: '1',
// firstName: 'Charles',
// lastName: 'Leclerc',
......
......@@ -3,6 +3,7 @@ import 'package:chart/common/store/user.dart';
// import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get_connect/http/src/utils/utils.dart';
import 'package:share_plus/share_plus.dart';
import 'package:get/get.dart';
......@@ -19,6 +20,7 @@ class ProductController extends GetxController {
TextEditingController controller1 = TextEditingController();
TextEditingController controller2 = TextEditingController();
ScrollController _scrollController = ScrollController();
/// 成员变量
......@@ -70,6 +72,8 @@ class ProductController extends GetxController {
void handleGenText() async {
final params = Get.parameters;
// detailId
if (controller1.text.isNotEmpty) {
// 包含${controller2.text}的${params['title']}
String str = "帮我写一篇${params['firstLabel']}${controller1.text}";
......@@ -80,15 +84,40 @@ class ProductController extends GetxController {
String str3 = "的${params['title']}";
print(str + str1 + str3);
// return
try {
// GetX.pop.pop()
Get.back();
state.loading = true;
EasyLoading.show(status: "AI正在生成中。。。");
String result = await NewsAPI.sendMessage({
"question": str + str1 + str3,
"id": "${UserStore.to.profile.id}"
});
EasyLoading.show(status: "AI正在生成中...");
// "question": str + str1 + str3, "id": "${UserStore.to.profile.id}
// [
// {
// "id": 0,
// "label": "",
// "placeHolder": "",
// "value": ""
// }
// ]
// json.encode(map);
String result = await NewsAPI.sendMessageByDetailId([
{
"label": params['firstLabel'],
"value": controller1.text,
"placeHolder": params['firstValue'],
"sort": 1,
},
{
"label": params['lastLabel'],
"value": controller2.text,
"placeHolder": params['lastValue'],
"sort": 2,
}
], params['detailId']!);
// state.genText = result;
state.messageQueenItemQueen.add(MessageQueenItem(
......@@ -102,7 +131,7 @@ class ProductController extends GetxController {
state.loading = false;
}
// print("$str");
print("$str");
} else {
Get.snackbar("错误提示", "您的问题还没有填完", colorText: Colors.white);
}
......@@ -124,7 +153,9 @@ class ProductController extends GetxController {
print("awaitawaitawaitawaitawaitawaitawait");
// Share.share('Text I wish to share');
Get.bottomSheet(FormWidget());
Get.bottomSheet(
FormWidget(),
);
// useRootNavigator: false,
// // backgroundColor: Colors.white,
// shape: const RoundedRectangleBorder(
......@@ -141,7 +172,7 @@ class ProductController extends GetxController {
String str = a + '\n\n' + "继续";
state.loading = true;
EasyLoading.show(status: "AI正在生成中。。。");
EasyLoading.show(status: "AI正在生成中...");
String result = await NewsAPI.sendMessage(
{"question": str, "id": "${UserStore.to.profile.id}"});
state.loading = false;
......
......@@ -161,18 +161,20 @@ class ProductPage extends GetView<ProductController> {
"${idx.text}",
speed: const Duration(milliseconds: 100),
textStyle: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold),
fontSize: 18,
color: Colors.white,
// fontWeight: FontWeight.bold
),
)
],
)
: Text(
"${idx.text}",
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold),
fontSize: 18,
color: Colors.white,
// fontWeight: FontWeight.bold
),
);
return text;
......
......@@ -49,11 +49,12 @@ class MainController extends GetxController {
asyncLoadBannerData() async {
final token = StorageService.to.getString(STORAGE_USER_TOKEN_KEY);
// if (token.isEmpty) {
IntegralEntity userInfo = await UserAPI.getUserIntegral();
await UserStore.to.getUserInfo(userInfo);
// }
// await UserStore.to.setToken('');
// (res.token);
if (token.isEmpty) {
IntegralEntity userInfo = await UserAPI.getUserIntegral();
await UserStore.to.getUserInfo(userInfo);
}
List<MessageEntity>? list = await NewsAPI.bannerList();
......
......@@ -85,12 +85,20 @@ class BannerPageWidget extends GetView<MainController> {
width: 300,
height: 130,
padding: EdgeInsets.only(top: 16),
child: Image.asset(
doctorsList[index].image,
child: Image.network(
'${controller.state.bannerPage![index].icon}',
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
// height: double.infinity,
),
// controller.state.bannerPage![index]
// .classifyDesc,
// Image.asset(
// doctorsList[index].image,
// fit: BoxFit.contain,
// width: double.infinity,
// height: double.infinity,
// ),
),
],
),
......
......@@ -13,6 +13,7 @@ class NewsCategoriesWidget extends GetView<MainController> {
@override
Widget build(BuildContext context) {
final userC = Get.put(UserStore());
return Obx(() => controller.state.bannerList == null
? Container()
: Container(
......@@ -30,14 +31,14 @@ class NewsCategoriesWidget extends GetView<MainController> {
// ignore: prefer_const_literals_to_create_immutables
children: [
// ignore: prefer_const_constructors
Text(
"Hi, ${UserStore.to.profile.username}",
// ignore: prefer_const_constructors
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 20.sp),
),
Obx(() => Text(
"Hi, ${userC.profile.username}",
// ignore: prefer_const_constructors
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 20.sp),
)),
SizedBox(height: 30),
// ignore: prefer_const_constructors
Text(
......
......@@ -8,6 +8,18 @@ const kPrimarylightColor = Color(0xff77E2FE);
const kBackgroundColor = Color(0xffEFF2F7);
const List<Color> kCategoriesPrimaryColor = [
Color(0xffFFCA8C),
Color(0xff5DF9D3),
Color(0xFF85E4FD),
Color(0xffB8ACFF),
Color(0xffFFCA8C),
Color(0xff5DF9D3),
Color(0xFF85E4FD),
Color(0xffB8ACFF),
Color(0xffFFCA8C),
Color(0xff5DF9D3),
Color(0xFF85E4FD),
Color(0xffB8ACFF),
Color(0xffFFCA8C),
Color(0xff5DF9D3),
Color(0xFF85E4FD),
......@@ -15,6 +27,22 @@ const List<Color> kCategoriesPrimaryColor = [
];
const List<Color> kCategoriesSecondryColor = [
Color(0xffFEA741),
Color(0xff31DFB5),
Color(0xff45BAFB),
Color(0xff9182F9),
Color(0xffFEA741),
Color(0xff31DFB5),
Color(0xff45BAFB),
Color(0xff9182F9),
Color(0xffFEA741),
Color(0xff31DFB5),
Color(0xff45BAFB),
Color(0xff9182F9),
Color(0xffFEA741),
Color(0xff31DFB5),
Color(0xff45BAFB),
Color(0xff9182F9),
Color(0xffFEA741),
Color(0xff31DFB5),
Color(0xff45BAFB),
......
......@@ -159,7 +159,7 @@ class ListPageWidget extends GetView<MainController> {
child: Text("使用模版"),
onPressed: () async {
// await Get.put(ProductController());
// final a = ;
var firstLabel = controller
.state
.bannerPageDetail![index]
......@@ -183,13 +183,17 @@ class ListPageWidget extends GetView<MainController> {
var title = controller.state
.bannerPageDetail![index].detailName;
var detailId =
'${controller.state.bannerPageDetail![index].id}';
Get.toNamed(AppRoutes.PRODUCT_PAGE,
parameters: {
"title": title,
"firstValue": firstValue,
"firstLabel": firstLabel,
"lastValue": lastValue,
"lastLabel": lastLabel
"lastLabel": lastLabel,
"detailId": detailId
});
Future.delayed(Duration(milliseconds: 100),
() {
......@@ -245,24 +249,25 @@ class ListPageWidget extends GetView<MainController> {
child: Row(
children: [
Container(
padding: EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
kCategoriesPrimaryColor[index],
kCategoriesSecondryColor[index],
],
),
borderRadius:
BorderRadius.circular(16)),
child: Icon(
Icons.headset_mic,
size: 20,
color: Colors.white,
),
),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
kCategoriesPrimaryColor[index],
kCategoriesSecondryColor[index],
],
),
borderRadius:
BorderRadius.circular(16)),
child: Image.network(
'${controller.state.bannerPageDetail![index].icon}',
// "https://cdn-icons-png.flaticon.com/128/6784/6784128.png",
fit: BoxFit.cover,
width: 30.0,
height: 30.0,
)),
SizedBox(width: 11),
Column(
mainAxisAlignment: MainAxisAlignment.center,
......
import 'package:chart/common/routers/routes.dart';
import 'package:chart/entity/plan_entity.dart';
import 'package:chart/pages/frame/notfound/index.dart';
import 'package:dash_chat_2/dash_chat_2.dart';
import 'package:chart/package/chat_dash/dash_chat_2.dart' as Chat;
import 'package:flutter/material.dart';
import 'package:chart/common/values/values.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
......@@ -101,7 +101,7 @@ class SiperBannerWidget extends GetView<MainController> {
child: InkWell(
onTap: () {
// message.question
ChatUser _user = ChatUser(
Chat.ChatUser _user = Chat.ChatUser(
id: '1',
// firstName: 'Charles',
// lastName: 'Leclerc',
......@@ -110,7 +110,7 @@ class SiperBannerWidget extends GetView<MainController> {
Get.toNamed(
"${AppRoutes.CHAT_PAGE}?question=${message.question}");
c.sendMessage(ChatMessage(
c.sendMessage(Chat.ChatMessage(
text: "${message.question}",
user: _user,
createdAt: DateTime.now(),
......
import 'package:chart/common/values/values.dart';
import 'package:chart/common/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/sockets/src/socket_notifier.dart';
import 'package:glassmorphism/glassmorphism.dart';
import 'index.dart';
import 'widgets/widgets.dart';
......@@ -11,7 +13,47 @@ class TemplatePage extends GetView<TemplateController> {
// 主视图
Widget _buildView() {
return const HelloWidget();
return Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: Image.asset("assets/images/bg.png").image,
fit: BoxFit.cover),
),
child: const HelloWidget(),
// GlassmorphicContainer(
// width: Get.mediaQuery.size.width * 1,
// height: Get.mediaQuery.size.height * 1,
// // margin: EdgeInsets.only(bottom: 60),
// // padding: EdgeInsets.all(20),
// // ignore: sort_child_properties_last
// child:
// borderRadius: 14,
// blur: 14,
// alignment: Alignment.bottomCenter,
// border: 2,
// linearGradient: LinearGradient(
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// colors: [
// const Color(0xFF0FFFF).withOpacity(0.0),
// const Color(0xFF0FFFF).withOpacity(0.0),
// ],
// ),
// borderGradient: LinearGradient(
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// colors: [
// const Color(0xFF0FFFF).withOpacity(1),
// const Color(0xFFFFFFF),
// const Color(0xFF0FFFF).withOpacity(1),
// ],
// ),
// // child: ,
// ),
);
}
@override
......@@ -20,10 +62,28 @@ class TemplatePage extends GetView<TemplateController> {
builder: (_) {
return Scaffold(
appBar: transparentAppBar(
leading: IconButton(
tooltip: '返回上一页',
icon: const Icon(
Icons.arrow_back,
color: AppColors.primaryElementText,
),
onPressed: () async {
// Get.back();
// Get.offAll(ApplicationPage());
// await Get.off(ApplicationPage());
// Get.toNamed(AppRoutes.Application);
Get.back();
// await Get.toNamed();
// Navigator.of(context).pop();
//_nextPage(-1);
},
),
title: const Text(
"template",
style: TextStyle(color: Colors.white),
)),
"模版中心",
style: TextStyle(color: Colors.white),
)),
body: _buildView());
},
);
......
This diff is collapsed.
import 'package:chart/common/store/user.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:babstrap_settings_screen/babstrap_settings_screen.dart';
......@@ -6,6 +7,7 @@ import 'index.dart';
import 'widgets/widgets.dart';
class UserDetailPage extends GetView<UserDetailController> {
final c = Get.put(UserStore());
@override
Widget build(BuildContext context) {
return Scaffold(
......@@ -78,19 +80,19 @@ class UserDetailPage extends GetView<UserDetailController> {
// textBaseline: TextBaseline.alphabetic,
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
height: 30,
// color: Colors.white,
child: Text(
"一颗大白菜",
style: TextStyle(
color: Colors.white,
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.bold),
),
margin: EdgeInsets.only(right: 16),
),
Obx(() => Container(
height: 30,
// color: Colors.white,
child: Text(
'${c.profile.username}',
style: TextStyle(
color: Colors.white,
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.bold),
),
margin: EdgeInsets.only(right: 16),
)),
Container(
width: 60,
height: 30,
......
......@@ -460,7 +460,7 @@ Widget _bubbleBuilder(Widget child,
totalRepeatCount: 1,
animatedTexts: identical(message.status, Status.sending)
? [
TypewriterAnimatedText('正在思考中......'),
TypewriterAnimatedText('正在思考中...'),
TypewriterAnimatedText('正在使劲思考中......'),
TypewriterAnimatedText('正在拼命思考中......'),
TypewriterAnimatedText('您的问题太有深度,请稍等......'),
......
......@@ -450,7 +450,7 @@ Widget _bubbleBuilder(
totalRepeatCount: 1,
animatedTexts: identical(message.status, Status.sending)
? [
WavyAnimatedText('正在思考中......'),
WavyAnimatedText('正在思考中...'),
WavyAnimatedText('正在使劲思考中......'),
WavyAnimatedText('正在拼命思考中......'),
WavyAnimatedText('您的问题太有深度,回答很困难,如果不想等待,请问下一个问题......'),
......
......@@ -560,6 +560,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.6"
gradient_borders:
dependency: "direct main"
description:
name: gradient_borders
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
gradient_widgets:
dependency: "direct main"
description:
......@@ -1125,6 +1132,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
vibration:
dependency: "direct main"
description:
name: vibration
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.6"
video_player:
dependency: transitive
description:
......
......@@ -105,6 +105,8 @@ dependencies:
glassy: ^0.0.6
dash_chat_2: ^0.0.15
flutter_staggered_animations: ^1.1.1
vibration: ^1.7.6
gradient_borders: ^1.0.0
# package:bubble/bubble.dart
dev_dependencies:
......
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