master
jack ning 2 years ago
parent 4de8e2cb9c
commit c1eb8de4dc

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -39,7 +39,9 @@ class _HistoryThreadPageState extends State<HistoryThreadPage> {
subtitle: Text('${_historyThreadList[index].content}'),
onTap: () {
//
BytedeskKefu.startChatThread(context, _historyThreadList[index]);
// BytedeskKefu.startChatThread(context, _historyThreadList[index]);
BytedeskKefu.startChatThreadIM(
context, _historyThreadList[index]);
},
),
itemCount: _historyThreadList.length,

@ -46,7 +46,7 @@ dependencies:
# 请在ios/Podfile中添加use_frameworks!
vibration: ^1.7.3
# 在线客服 https://pub.dev/packages/bytedesk_kefu
bytedesk_kefu: ^1.3.3
bytedesk_kefu: ^1.4.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@ -80,6 +80,7 @@ flutter:
# - images/a_dot_ham.jpeg
assets:
- assets/audio/
- assets/images/chat/
- assets/images/feedback/
# An image asset can refer to one or more resolution-specific "variants", see

@ -1,5 +1,13 @@
# Upgrade Log
## 1.4.0
* optimize user experience
## 1.3.4
* optimize user experience
## 1.3.3
* optimize user experience

@ -27,6 +27,15 @@ bytedesk flutter helpdesk sdk
## Getting Started
### Zero Step: Copy Assets dir from bytedesk_demo
```dart
assets:
- assets/audio/
- assets/images/chat/
- assets/images/feedback/
```
### First Step: Register Account
- [Register](https://www.bytedesk.com/admin)

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -64,6 +64,8 @@ PODS:
- FMDB (>= 2.7.5)
- SwiftyGif (5.4.3)
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- wakelock (0.0.1):
@ -84,6 +86,7 @@ DEPENDENCIES:
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
@ -122,6 +125,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios"
wakelock:
@ -148,6 +153,7 @@ SPEC CHECKSUMS:
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162

@ -31,6 +31,7 @@ void main() {
//
BytedeskKefu.init(_appKey, _subDomain);
// 使usernamenickname使
// BytedeskKefu.logout()
// BytedeskKefu.initWithUsernameAndNicknameAndAvatar('myflutterusername', '我是美女', 'https://bytedesk.oss-cn-shenzhen.aliyuncs.com/avatars/girl.png', _appKey, _subDomain);
// BytedeskKefu.initWithUsername('myflutterusername', _appKey, _subDomain); // username
// /使 initWithUsernameAndNicknameinitWithUsernameAndNicknameAndAvatar

@ -58,6 +58,7 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/audio/
- assets/images/chat/
- assets/images/feedback/
# An image asset can refer to one or more resolution-specific "variants", see

@ -100,7 +100,7 @@ class MessageBloc extends Bloc<MessageEvent, MessageState> {
emit(SendMessageRestSuccess(jsonResult));
} catch (error) {
BytedeskUtils.printLog(error);
emit(SendMessageRestError());
emit(SendMessageRestError(event.json!));
}
}

@ -65,6 +65,13 @@ class UpLoadImageError extends MessageState {
}
class SendMessageRestError extends MessageState {
final String json;
const SendMessageRestError(this.json);
@override
List<Object> get props => [json];
@override
String toString() => 'SendMessageRestError';
}

@ -312,6 +312,7 @@ class BytedeskUserHttpApi extends BytedeskBaseHttpApi {
User user = User.fromJson(responseJson['data']);
//
SpUtil.putString(BytedeskConstants.uid, user.uid!);
SpUtil.putString(BytedeskConstants.username, user.username!);
SpUtil.putString(BytedeskConstants.nickname, user.nickname!);
SpUtil.putString(BytedeskConstants.avatar, user.avatar!);
SpUtil.putString(BytedeskConstants.mobile, user.mobile ?? '');

@ -984,8 +984,10 @@ class BytedeskMqtt {
//
void disconnect() {
if (mqttClient != null) {
mqttClient.disconnect();
}
}
/// The subscribed callback
void _onSubscribed(String? topic) {

@ -25,6 +25,12 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
// import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:bytedesk_kefu/ui/widget/chat_input.dart';
import 'package:bytedesk_kefu/ui/widget/extra_item.dart';
// import 'package:bytedesk_kefu/ui/widget/send_button_visibility_mode.dart';
// import 'package:bytedesk_kefu/ui/widget/voice_record/voice_widget.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart' as chat_ui;
// import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
// TODO: titleloading
//
@ -87,6 +93,7 @@ class _ChatKFPageState extends State<ChatKFPage>
BytedeskMqtt _bdMqtt = new BytedeskMqtt();
// uid
String? _currentUid = SpUtil.getString(BytedeskConstants.uid);
String? _currentUsername = SpUtil.getString(BytedeskConstants.username);
String? _currentNickname = SpUtil.getString(BytedeskConstants.nickname);
String? _currentAvatar = SpUtil.getString(BytedeskConstants.avatar);
//
@ -101,6 +108,8 @@ class _ChatKFPageState extends State<ChatKFPage>
Timer? _debounce;
//
Timer? _loadHistoryTimer;
//
Timer? _resendTimer;
//
// final _flutterVideoCompress = FlutterVideoCompress();
bool _isRequestingThread = true;
@ -138,6 +147,41 @@ class _ChatKFPageState extends State<ChatKFPage>
// // 1
// // timer.cancel();
});
_resendTimer = Timer.periodic(Duration(seconds: 2), (timer) {
// TODO: -
for (var i = 0; i < _messages.length; i++) {
Message? message = _messages[i].message;
// && ...
if (message!.isSend == 1 &&
message.status == BytedeskConstants.MESSAGE_STATUS_SENDING) {
var nowTime = DateTime.now();
var messageTime = DateTime.parse(message.timestamp!);
int diff = nowTime.difference(messageTime).inSeconds;
if (diff > 15) {
// 15error
_messageProvider.update(
message.mid, BytedeskConstants.MESSAGE_STATUS_ERROR);
} else if (diff > 5) {
// 5使http rest
String content = '';
if (message.type == BytedeskConstants.MESSAGE_TYPE_TEXT) {
content = message.content!;
} else if (message.type == BytedeskConstants.MESSAGE_TYPE_IMAGE) {
content = message.imageUrl!;
} else if (message.type == BytedeskConstants.MESSAGE_TYPE_FILE) {
content = message.fileUrl!;
} else if (message.type == BytedeskConstants.MESSAGE_TYPE_VOICE) {
content = message.voiceUrl!;
} else if (message.type == BytedeskConstants.MESSAGE_TYPE_VIDEO) {
content = message.videoUrl!;
} else {
content = message.content!;
}
this.sendMessageRest(message.mid!, message.type!, content);
}
}
}
});
// BlocProvider.of<MessageBloc>(context)
// ..add(LoadUnreadVisitorMessagesEvent(page: 0, size: 10));
}
@ -401,6 +445,18 @@ class _ChatKFPageState extends State<ChatKFPage>
message.mid!, _currentThread!);
}
}
} else if (state is SendMessageRestSuccess) {
// http rest
String jsonMessage = state.jsonResult.data!;
String mid = json.decode(jsonMessage);
_messageProvider.update(
mid, BytedeskConstants.MESSAGE_STATUS_STORED);
} else if (state is SendMessageRestError) {
// http rest
String jsonMessage = state.json;
String mid = json.decode(jsonMessage);
_messageProvider.update(
mid, BytedeskConstants.MESSAGE_STATUS_STORED);
}
},
),
@ -466,13 +522,81 @@ class _ChatKFPageState extends State<ChatKFPage>
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
),
child: _textComposerWidget(),
// child: _textComposerWidget(),
child: _chatInput(),
),
],
),
)));
}
Widget _chatInput() {
return ChatInput(
//
onSendPressed: _handleSendPressed,
sendButtonVisibilityMode: chat_ui.SendButtonVisibilityMode.editing,
// voiceWidget: VoiceRecord(),
// voiceWidget: VoiceWidget(
// startRecord: () {},
// stopRecord: _handleVoiceSelection,
// // Container
// height: 40.0,
// margin: EdgeInsets.zero,
// ),
extraWidget: ExtraItems(
//
handleImageSelection: _handleImageSelection,
//
handleFileSelection: _handleFileSelection,
//
handlePickerSelection: _handlePickerSelection,
//
handleUploadVideo: _handleUploadVideo,
//
handleCaptureVideo: _handleCaptureVideo),
);
}
//
// void _handleVoiceSelection(AudioFile? obj) async {
// print('_handleVoiceSelection');
// if (obj != null) {
// }
// }
//
Future<bool> _handleSendPressed(String content) async {
print('send: ${content}');
_handleSubmitted(content);
return true;
}
void _handleImageSelection() async {
print('_handleImageSelection');
_pickImage();
}
void _handleFileSelection() async {
print('_handleFileSelection');
}
Future<void> _handlePickerSelection() async {
print('_handlePickerSelection');
_takeImage();
return;
}
void _handleUploadVideo() async {
print('_handleUploadVideo');
_pickVideo();
}
void _handleCaptureVideo() async {
print('_handleCaptureVideo');
_captureVideo();
}
//
Widget _textComposerWidget() {
return IconTheme(
@ -541,7 +665,7 @@ class _ChatKFPageState extends State<ChatKFPage>
@override
bool get wantKeepAlive => true;
//
//
void _handleSubmitted(String? text) {
_textController.clear();
//
@ -572,76 +696,59 @@ class _ChatKFPageState extends State<ChatKFPage>
}
}
// http rest
// http rest
void sendTextMessageRest(String text) {
//
String? mid = BytedeskUuid.uuid();
sendMessageRest(mid, BytedeskConstants.MESSAGE_TYPE_TEXT, text);
}
// http rest
void sendImageMessageRest(String imageUrl) {
//
String? mid = BytedeskUuid.uuid();
sendMessageRest(mid, BytedeskConstants.MESSAGE_TYPE_IMAGE, imageUrl);
}
// http rest
void sendMessageRest(String mid, String type, String content) {
//
// String? mid = BytedeskUuid.uuid();
String? timestamp = BytedeskUtils.formatedDateNow();
String? client = BytedeskUtils.getClient();
String? type = BytedeskConstants.MESSAGE_TYPE_TEXT;
//
var jsonContent = {
var jsonContent;
if (type == BytedeskConstants.MESSAGE_TYPE_TEXT) {
jsonContent = {
"mid": mid,
"timestamp": timestamp,
"client": client,
"version": "1",
"type": type,
"status": "sending",
"status": BytedeskConstants.MESSAGE_STATUS_SENDING,
"user": {
"uid": this._currentUid,
"username": this._currentUsername,
"nickname": this._currentNickname,
"avatar": this._currentAvatar,
"extra": {"agent": false}
},
"text": {"content": text},
"text": {"content": content},
"thread": {
"tid": this._currentThread!.tid,
"type": this._currentThread!.type,
"content": text,
"content": content,
"nickname": this._currentThread!.nickname,
"avatar": this._currentThread!.avatar,
"topic": this._currentThread!.topic,
"client": client,
"timestamp": timestamp,
"unreadCount": 0
}
};
String? jsonString = json.encode(jsonContent);
BlocProvider.of<MessageBloc>(context)
..add(SendMessageRestEvent(json: jsonString));
//
Message message = new Message();
message.mid = mid;
message.type = type;
message.timestamp = timestamp;
message.client = client;
message.avatar = _currentAvatar;
message.topic = this._currentThread!.topic;
message.status = BytedeskConstants.MESSAGE_STATUS_SENDING;
message.isSend = 1;
message.currentUid = this._currentUid;
message.answersJson = '';
message.thread = this._currentThread;
message.user = User(
uid: this._currentUid,
avatar: this._currentAvatar,
nickname: this._currentNickname);
//
message.content = text;
//
_messageProvider.insert(message);
//
pushToMessageArray(message, true);
}
// http rest
void sendImageMessageRest(String imageUrl) {
//
String? mid = BytedeskUuid.uuid();
String? timestamp = BytedeskUtils.formatedDateNow();
String? client = BytedeskUtils.getClient();
String? type = BytedeskConstants.MESSAGE_TYPE_IMAGE;
//
var jsonContent = {
} else if (type == BytedeskConstants.MESSAGE_TYPE_IMAGE) {
jsonContent = {
"mid": mid,
"timestamp": timestamp,
"client": client,
@ -650,11 +757,12 @@ class _ChatKFPageState extends State<ChatKFPage>
"status": "sending",
"user": {
"uid": this._currentUid,
"username": this._currentUsername,
"nickname": this._currentNickname,
"avatar": this._currentAvatar,
"extra": {"agent": false}
},
"image": {"imageUrl": imageUrl},
"image": {"imageUrl": content},
"thread": {
"tid": this._currentThread!.tid,
"type": this._currentThread!.type,
@ -662,10 +770,101 @@ class _ChatKFPageState extends State<ChatKFPage>
"nickname": this._currentThread!.nickname,
"avatar": this._currentThread!.avatar,
"topic": this._currentThread!.topic,
"client": client,
"timestamp": timestamp,
"unreadCount": 0
}
};
} else if (type == BytedeskConstants.MESSAGE_TYPE_FILE) {
jsonContent = {
"mid": mid,
"timestamp": timestamp,
"client": client,
"version": "1",
"type": type,
"status": BytedeskConstants.MESSAGE_STATUS_SENDING,
"user": {
"uid": this._currentUid,
"username": this._currentUsername,
"nickname": this._currentNickname,
"avatar": this._currentAvatar,
"extra": {"agent": false}
},
"file": {"fileUrl": content},
"thread": {
"tid": this._currentThread!.tid,
"type": this._currentThread!.type,
"content": "[文件]",
"nickname": this._currentThread!.nickname,
"avatar": this._currentThread!.avatar,
"topic": this._currentThread!.topic,
"client": client,
"timestamp": timestamp,
"unreadCount": 0
}
};
} else if (type == BytedeskConstants.MESSAGE_TYPE_VOICE) {
jsonContent = {
"mid": mid,
"timestamp": timestamp,
"client": client,
"version": "1",
"type": type,
"status": BytedeskConstants.MESSAGE_STATUS_SENDING,
"user": {
"uid": this._currentUid,
"username": this._currentUsername,
"nickname": this._currentNickname,
"avatar": this._currentAvatar,
"extra": {"agent": false}
},
"voice": {
"voiceUrl": content,
"length": '0', // TODO:
"format": "wav",
},
"thread": {
"tid": this._currentThread!.tid,
"type": this._currentThread!.type,
"content": content,
"nickname": this._currentThread!.nickname,
"avatar": this._currentThread!.avatar,
"topic": this._currentThread!.topic,
"client": client,
"timestamp": timestamp,
"unreadCount": 0
}
};
} else if (type == BytedeskConstants.MESSAGE_TYPE_VIDEO) {
jsonContent = {
"mid": mid,
"timestamp": timestamp,
"client": client,
"version": "1",
"type": type,
"status": BytedeskConstants.MESSAGE_STATUS_SENDING,
"user": {
"uid": this._currentUid,
"username": this._currentUsername,
"nickname": this._currentNickname,
"avatar": this._currentAvatar,
"extra": {"agent": false}
},
"video": {"videoOrShortUrl": content},
"thread": {
"tid": this._currentThread!.tid,
"type": this._currentThread!.type,
"content": content,
"nickname": this._currentThread!.nickname,
"avatar": this._currentThread!.avatar,
"topic": this._currentThread!.topic,
"client": client,
"timestamp": timestamp,
"unreadCount": 0
}
};
}
String? jsonString = json.encode(jsonContent);
BlocProvider.of<MessageBloc>(context)
..add(SendMessageRestEvent(json: jsonString));
@ -674,7 +873,7 @@ class _ChatKFPageState extends State<ChatKFPage>
message.mid = mid;
message.type = type;
message.timestamp = timestamp;
// message.client = client;
message.client = client;
message.avatar = _currentAvatar;
message.topic = this._currentThread!.topic;
message.status = BytedeskConstants.MESSAGE_STATUS_SENDING;
@ -687,7 +886,7 @@ class _ChatKFPageState extends State<ChatKFPage>
avatar: this._currentAvatar,
nickname: this._currentNickname);
//
message.imageUrl = imageUrl;
message.content = content;
//
_messageProvider.insert(message);
//
@ -1174,6 +1373,7 @@ class _ChatKFPageState extends State<ChatKFPage>
WidgetsBinding.instance!.removeObserver(this);
_debounce?.cancel();
_loadHistoryTimer?.cancel();
_resendTimer?.cancel();
// bytedeskEventBus.destroy(); // FIXME: destroy
super.dispose();
}

@ -0,0 +1,531 @@
import 'dart:async';
import 'package:bytedesk_kefu/ui/widget/emoji_picker_view.dart';
import 'package:bytedesk_kefu/ui/widget/image_button.dart';
import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/emoji_picker_flutter.dart';
// import 'package:bytedesk_kefu/ui/widget/send_button_visibility_mode.dart';
// import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
// import 'package:emoji_picker_flutter/src/emoji_view_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
// import 'package:flutter_chat_ui/src/widgets/inherited_chat_theme.dart';
// import 'package:imboy/config/init.dart';
// import 'package:imboy/service/websocket.dart';
// import 'package:imboy/store/model/message_model.dart';
/**
* CaiJingLong https://github.com/CaiJingLong/flutter_like_wechat_input
*/
enum InputType {
text,
voice,
emoji,
extra,
}
Widget _buildVoiceButton(BuildContext context) {
return Container(
width: double.infinity,
child: TextButton(
// color: Colors.white70,
onPressed: () {
// Get.snackbar('Tips', '语音输入功能暂无实现');
},
child: Text(
'chat_hold_down_talk',
),
),
);
}
typedef void OnSend(String text);
InputType _initType = InputType.text;
double _softKeyHeight = 210;
class ChatInput extends StatefulWidget {
const ChatInput({
Key? key,
this.isAttachmentUploading,
this.onAttachmentPressed,
required this.onSendPressed,
this.onTextChanged,
this.onTextFieldTap,
required this.sendButtonVisibilityMode,
this.extraWidget,
this.voiceWidget,
}) : super(key: key);
/// See [AttachmentButton.onPressed]
final void Function()? onAttachmentPressed;
/// Whether attachment is uploading. Will replace attachment button with a
/// [CircularProgressIndicator]. Since we don't have libraries for
/// managing media in dependencies we have no way of knowing if
/// something is uploading so you need to set this manually.
final bool? isAttachmentUploading;
/// Will be called on [SendButton] tap. Has [types.PartialText] which can
/// be transformed to [types.TextMessage] and added to the messages list.
// final Future<bool> Function(types.PartialText) onSendPressed;
final Future<bool> Function(String) onSendPressed;
/// Will be called whenever the text inside [TextField] changes
final void Function(String)? onTextChanged;
/// Will be called on [TextField] tap
final void Function()? onTextFieldTap;
/// Controls the visibility behavior of the [SendButton] based on the
/// [TextField] state inside the [Input] widget.
/// Defaults to [SendButtonVisibilityMode.editing].
final SendButtonVisibilityMode sendButtonVisibilityMode;
final Widget? extraWidget;
final Widget? voiceWidget;
@override
_ChatInputState createState() => _ChatInputState();
}
/// [Input] widget state
class _ChatInputState extends State<ChatInput> with TickerProviderStateMixin {
InputType inputType = _initType;
final _inputFocusNode = FocusNode();
bool _sendButtonVisible = false;
final _textController = TextEditingController();
late AnimationController _bottomHeightController;
bool emojiShowing = false;
/**
* https://stackoverflow.com/questions/60057840/flutter-how-to-insert-text-in-middle-of-text-field-text
*/
void _setText(String val) {
String text = _textController.text;
TextSelection textSelection = _textController.selection;
int start = textSelection.start > -1 ? textSelection.start : 0;
String newText = text.replaceRange(
start,
textSelection.end > -1 ? textSelection.end : 0,
val,
);
_textController.text = newText;
int offset = start + val.length;
_textController.selection = textSelection.copyWith(
baseOffset: offset,
extentOffset: offset,
);
}
@override
void initState() {
super.initState();
if (!mounted) {
return;
}
if (widget.sendButtonVisibilityMode == SendButtonVisibilityMode.editing) {
_sendButtonVisible = _textController.text.trim() != '';
_textController.addListener(_handleTextControllerChange);
} else {
_sendButtonVisible = true;
}
_bottomHeightController = AnimationController(
vsync: this,
duration: Duration(
milliseconds: 150,
),
);
// "重新进入聊天页面的时候_bottomHeightController在开启状态"
_bottomHeightController.animateBack(0);
//
// eventBus.on<ReEditMessage>().listen((msg) async {
// if (_textController.text.toString() != msg.text) {
// _setText(msg.text);
// }
// });
//listener
//TextField
_inputFocusNode.addListener(() {
// debugPrint(">>> on chatinput ${_inputFocusNode.hasFocus}");
if (_inputFocusNode.hasFocus) {
updateState(InputType.text);
} else {}
});
}
@override
void dispose() {
// Get.delete<AnimationController>();
_inputFocusNode.dispose();
_textController.dispose();
super.dispose();
}
Future<void> _handleSendPressed() async {
final trimmedText = _textController.text.trim();
if (trimmedText != '') {
// final _partialText = types.PartialText(text: trimmedText);
// bool res = await widget.onSendPressed(_partialText);
bool res = await widget.onSendPressed(trimmedText);
if (res) {
_textController.clear();
} else {
// WSService.to.openSocket();
//
}
}
}
void _handleTextControllerChange() {
setState(() {
_sendButtonVisible = _textController.text.trim() != '';
});
}
void changeBottomHeight(final double height) {
if (height > 0) {
_bottomHeightController.animateTo(1);
} else {
_bottomHeightController.animateBack(0);
}
}
/**
*
*/
Future<void> _voiceBtnOnPressed(InputType type) async {
if (type == this.inputType) {
return;
}
if (type != InputType.text) {
hideSoftKey();
} else {
showSoftKey();
}
setState(() {
this.inputType = type;
});
}
Future<void> updateState(InputType type) async {
if (type == InputType.text || type == InputType.voice) {
_initType = type;
}
if (type == inputType) {
return;
}
this.inputType = type;
// InputTypeNotification(type).dispatch(context);
if (type != InputType.text) {
hideSoftKey();
} else {
showSoftKey();
}
if (type == InputType.emoji || type == InputType.extra) {
changeBottomHeight(1);
hideSoftKey();
} else {
changeBottomHeight(0);
}
setState(() {
this.emojiShowing = type == InputType.emoji;
this.inputType;
});
}
void showSoftKey() {
FocusScope.of(context).requestFocus(_inputFocusNode);
changeBottomHeight(0);
// debugPrint(">>> on chatinput showSoftKey");
}
void hideSoftKey() {
_inputFocusNode.unfocus();
// from https://developer.aliyun.com/article/763095
SystemChannels.textInput.invokeMethod('TextInput.hide');
}
Widget _buildBottomContainer({required Widget child}) {
return SizeTransition(
sizeFactor: _bottomHeightController,
child: Container(
child: child,
height: _softKeyHeight,
),
);
}
Widget _buildBottomItems() {
if (this.inputType == InputType.extra) {
return widget.extraWidget ?? Center(child: Text("其他item"));
} else if (this.inputType == InputType.emoji) {
return Offstage(
offstage: !emojiShowing,
child: SizedBox(
height: 400,
child: EmojiPicker(
onEmojiSelected: (Category category, Emoji emoji) {
_setText(emoji.emoji);
},
onBackspacePressed: () {
_textController
..text = _textController.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition(
TextPosition(offset: _textController.text.length),
);
},
config: Config(
columns: 7,
// Issue: https://github.com/flutter/flutter/issues/28894
// emojiSizeMax: 24 * (GetPlatform.isIOS ? 1.30 : 1.0),
verticalSpacing: 0,
horizontalSpacing: 0,
initCategory: Category.RECENT,
bgColor: const Color(0xFFF2F2F2),
indicatorColor: Colors.black87,
iconColorSelected: Colors.black87,
iconColor: Colors.grey,
progressIndicatorColor: Colors.blue,
backspaceColor: Colors.black54,
showRecentsTab: true,
recentsLimit: 19,
// noRecentsText: 'No Recents'.tr,
// noRecentsStyle: const TextStyle(
// fontSize: 20,
// color: Colors.black87,
// ),
tabIndicatorAnimDuration: kTabScrollDuration,
categoryIcons: const CategoryIcons(),
buttonMode: ButtonMode.MATERIAL,
),
customWidget: (Config config, EmojiViewState state) =>
EmojiPickerView(
config,
state,
_handleSendPressed,
),
),
),
);
} else {
return Container();
}
}
Widget _buildInputButton(BuildContext ctx) {
final voiceButton = widget.voiceWidget ?? _buildVoiceButton(ctx);
final inputButton = TextField(
controller: _textController,
// cursorColor: InheritedChatTheme.of(ctx).theme.inputTextCursorColor,
// decoration: InheritedChatTheme.of(ctx).theme.inputTextDecoration.copyWith(
// hintStyle: InheritedChatTheme.of(ctx).theme.inputTextStyle.copyWith(
// color: InheritedChatTheme.of(ctx)
// .theme
// .inputTextColor
// .withOpacity(0.5),
// ),
// hintText: '',
// ),
focusNode: _inputFocusNode,
// maxLength: 400,
maxLines: 6,
minLines: 1,
// //LengthLimitingTextInputFormatter
enableInteractiveSelection: true,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.newline,
onChanged: widget.onTextChanged,
onTap: () {
updateState(inputType);
widget.onTextFieldTap;
},
// style: InheritedChatTheme.of(ctx).theme.inputTextStyle.copyWith(
// color: InheritedChatTheme.of(ctx).theme.inputTextColor,
// ),
//
onSubmitted: (_) => _handleSendPressed(),
);
return Stack(
children: <Widget>[
Offstage(
child: inputButton,
offstage: inputType == InputType.voice,
),
Offstage(
child: voiceButton,
offstage: inputType != InputType.voice,
),
],
);
}
Widget buildLeftButton() {
return ImageButton(
onPressed: () {
if (inputType == InputType.voice) {
_voiceBtnOnPressed(InputType.text);
} else {
_voiceBtnOnPressed(InputType.voice);
}
changeBottomHeight(0);
},
image: AssetImage(
inputType != InputType.voice
? 'assets/images/chat/input_voice.png'
: 'assets/images/chat/input_keyboard.png',
),
);
}
/**
*
*/
Widget buildEmojiButton() {
return ImageButton(
image: AssetImage(inputType != InputType.emoji
? 'assets/images/chat/input_emoji.png'
: 'assets/images/chat/input_keyboard.png'),
onPressed: () {
if (inputType != InputType.emoji) {
updateState(InputType.emoji);
} else {
updateState(InputType.text);
}
},
);
}
/**
*
* More input message types entries
*/
Widget buildExtra() {
return ImageButton(
image: AssetImage('assets/images/chat/input_extra.png'),
onPressed: () {
if (inputType != InputType.extra) {
updateState(InputType.extra);
} else {
updateState(InputType.text);
}
},
);
}
/**
*
* Implement line breaks
*/
void _handleNewLine() {
final _newValue = '${_textController.text}\r\n';
_textController.value = TextEditingValue(
text: _newValue,
selection: TextSelection.fromPosition(
TextPosition(offset: _newValue.length),
),
);
}
@override
Widget build(BuildContext context) {
final _query = MediaQuery.of(context);
final isAndroid = Theme.of(context).platform == TargetPlatform.android;
final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
return InkWell(
child: Focus(
autofocus: true,
child: Padding(
padding: EdgeInsets.fromLTRB(15, 0, 0, 0), //InheritedChatTheme.of(context).theme.inputPadding,
child: Material(
borderRadius: const BorderRadius.vertical(top: Radius.circular(10),),
//InheritedChatTheme.of(context).theme.inputBorderRadius,
// color: Color.white, //Color(0xff1d1c21),//InheritedChatTheme.of(context).theme.inputBackgroundColor,
child: Container(
padding: EdgeInsets.fromLTRB(
_query.padding.left,
4,
_query.padding.right,
4 + _query.viewInsets.bottom + _query.padding.bottom,
),
child: Column(
children: <Widget>[
Row(
children: [
// TODO:
// buildLeftButton(),
// input
Expanded(
child: Shortcuts(
shortcuts: isAndroid || isIOS
? {
LogicalKeySet(LogicalKeyboardKey.enter):
const NewLineIntent(),
LogicalKeySet(LogicalKeyboardKey.enter,
LogicalKeyboardKey.alt):
const NewLineIntent(),
}
: {
LogicalKeySet(LogicalKeyboardKey.enter):
const SendMessageIntent(),
LogicalKeySet(LogicalKeyboardKey.enter,
LogicalKeyboardKey.alt):
const NewLineIntent(),
LogicalKeySet(LogicalKeyboardKey.enter,
LogicalKeyboardKey.shift):
const NewLineIntent(),
},
child: Actions(
actions: {
SendMessageIntent:
CallbackAction<SendMessageIntent>(
onInvoke: (SendMessageIntent intent) =>
_handleSendPressed(),
),
NewLineIntent: CallbackAction<NewLineIntent>(
onInvoke: (NewLineIntent intent) =>
_handleNewLine(),
),
},
child: _buildInputButton(context),
),
),
),
// emoji
buildEmojiButton(),
//extra
_textController.text.isEmpty
? buildExtra()
: IconButton(
icon: Icon(Icons.send),
onPressed: _handleSendPressed,
padding: EdgeInsets.only(left: 0),
),
],
),
this.inputType == InputType.emoji ||
this.inputType == InputType.extra
? Divider()
: SizedBox.shrink(), // 线
_buildBottomContainer(child: _buildBottomItems()),
],
),
),
),
),
),
);
}
}

@ -0,0 +1,198 @@
// import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
// import 'package:emoji_picker_flutter/src/category_emoji.dart';
// import 'package:emoji_picker_flutter/src/config.dart';
// import 'package:emoji_picker_flutter/src/emoji_picker_builder.dart';
// import 'package:emoji_picker_flutter/src/emoji_view_state.dart';
import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/src/category_emoji.dart';
// import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/src/config.dart';
// import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/src/emoji_picker_builder.dart';
// import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/src/emoji_view_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// EmojiPicker Implementation
class EmojiPickerView extends EmojiPickerBuilder {
/// Constructor
EmojiPickerView(
Config config,
EmojiViewState state,
this.handleSendPressed,
) : super(config, state);
@override
_EmojiPickerViewState createState() => _EmojiPickerViewState();
/// See [AttachmentButton.onPressed]
final void Function()? handleSendPressed;
}
class _EmojiPickerViewState extends State<EmojiPickerView>
with SingleTickerProviderStateMixin {
PageController? _pageController;
TabController? _tabController;
@override
void initState() {
var initCategory = widget.state.categoryEmoji.indexWhere(
(element) => element.category == widget.config.initCategory);
if (initCategory == -1) {
initCategory = 0;
}
_tabController = TabController(
initialIndex: initCategory,
length: widget.state.categoryEmoji.length,
vsync: this);
_pageController = PageController(initialPage: initCategory);
super.initState();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
return Container(
color: Color.fromRGBO(251, 251, 251, 1.0), //widget.config.bgColor,
child: Column(
children: [
Row(
children: [
Expanded(
child: TabBar(
labelColor: widget.config.iconColorSelected,
indicatorColor: widget.config.indicatorColor,
unselectedLabelColor: widget.config.iconColor,
controller: _tabController,
labelPadding: EdgeInsets.zero,
onTap: (index) {
_pageController!.jumpToPage(index);
},
tabs: widget.state.categoryEmoji
.asMap()
.entries
.map<Widget>((item) =>
_buildCategory(item.key, item.value.category))
.toList(),
),
),
IconButton(
padding: const EdgeInsets.only(bottom: 2),
icon: Icon(
Icons.backspace,
color: widget.config.backspaceColor,
),
onPressed: () {
widget.state.onBackspacePressed!();
},
),
// IconButton(
// // iconSize: 16,
// onPressed: () {
// widget.handleSendPressed!();
// },
// // backgroundColor: Colors.lightGreen,
// icon: Icon(Icons.send),
// ),
],
),
Flexible(
child: PageView.builder(
itemCount: widget.state.categoryEmoji.length,
controller: _pageController,
onPageChanged: (index) {
_tabController!.animateTo(
index,
duration: widget.config.tabIndicatorAnimDuration,
);
},
itemBuilder: (context, index) =>
_buildPage(emojiSize, widget.state.categoryEmoji[index]),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [],
),
],
),
);
},
);
}
Widget _buildCategory(int index, Category category) {
return Tab(
icon: Icon(
widget.config.getIconForCategory(category),
),
);
}
Widget _buildButtonWidget(
{required VoidCallback onPressed, required Widget child}) {
if (widget.config.buttonMode == ButtonMode.MATERIAL) {
return TextButton(
onPressed: onPressed,
child: child,
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)),
);
}
return CupertinoButton(
padding: EdgeInsets.zero, onPressed: onPressed, child: child);
}
Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
// Display notice if recent has no entries yet
if (categoryEmoji.category == Category.RECENT &&
categoryEmoji.emoji.isEmpty) {
return _buildNoRecent();
}
// Build page normally
return GridView.count(
scrollDirection: Axis.vertical,
physics: const ScrollPhysics(),
shrinkWrap: true,
primary: true,
padding: const EdgeInsets.all(0),
crossAxisCount: widget.config.columns,
mainAxisSpacing: widget.config.verticalSpacing,
crossAxisSpacing: widget.config.horizontalSpacing,
children: categoryEmoji.emoji
.map<Widget>((item) => _buildEmoji(emojiSize, categoryEmoji, item))
.toList(),
);
}
Widget _buildEmoji(
double emojiSize,
CategoryEmoji categoryEmoji,
Emoji emoji,
) {
return _buildButtonWidget(
onPressed: () {
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
},
child: FittedBox(
fit: BoxFit.fill,
child: Text(
emoji.emoji,
textScaleFactor: 1.0,
style: TextStyle(
fontSize: emojiSize,
backgroundColor: Colors.transparent,
),
),
));
}
Widget _buildNoRecent() {
return Center(
child: Text( '无最近使用表情',
// widget.config.noRecentsText,
// style: widget.config.noRecentsStyle,
textAlign: TextAlign.center,
));
}
}

@ -0,0 +1,297 @@
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
// import 'package:flutter/widgets.dart';
// import 'package:get/get.dart';
// import 'package:get/get_utils/src/extensions/internacionalization.dart';
// import 'package:imboy/config/const.dart';
class ExtraItem extends StatelessWidget {
const ExtraItem({
Key? key,
required this.onPressed,
required this.image,
double? this.width,
double? this.height,
required this.title,
}) : super(key: key);
final ImageProvider image;
final void Function()? onPressed;
final double? width;
final double? height;
final String title;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: this.onPressed ?? () => {
print('功能暂未实现')
},
child: Padding(
padding: EdgeInsets.only(left: 15, top: 13, right: 15, bottom: 0),
child: Column(
children: [
Container(
width: this.width ?? 56,
height: this.height ?? 56,
// margin: EdgeInsets.symmetric(horizontal: 10),
child: Material(
color: AppColors.ChatInputBackgroundColor,
// INK
child: new Ink(
// ink
decoration: BoxDecoration(
//
color: AppColors.ChatInputBackgroundColor,
//
borderRadius: BorderRadius.all(Radius.circular(16.0)),
//
border: Border.all(
width: 1,
color: AppColors.ChatInputBackgroundColor,
),
),
child: Image(
image: this.image,
),
),
),
),
Text(this.title),
],
),
),
);
}
}
class ExtraItems extends StatefulWidget {
const ExtraItems({
Key? key,
this.handleImageSelection,
this.handleFileSelection,
this.handlePickerSelection,
this.handleUploadVideo,
this.handleCaptureVideo,
}) : super(key: key);
final void Function()? handleImageSelection;
final void Function()? handleFileSelection;
final void Function()? handlePickerSelection;
final void Function()? handleUploadVideo;
final void Function()? handleCaptureVideo;
@override
_ExtraItemsState createState() => _ExtraItemsState();
}
class _ExtraItemsState extends State<ExtraItems> {
int _current = 0;
CarouselController _controller = CarouselController();
@override
Widget build(BuildContext context) {
var items = [
Column(
children: <Widget>[
Row(
children: [
ExtraItem(
title: "照片",
image: AssetImage('assets/images/chat/extra_photo.webp'),
onPressed: widget.handleImageSelection,
),
ExtraItem(
title: "拍摄",
image: AssetImage('assets/images/chat/extra_camera.webp'),
onPressed: widget.handlePickerSelection,
),
ExtraItem(
title: "上传视频",
image: AssetImage('assets/images/chat/extra_media.webp'),
onPressed: widget.handleUploadVideo,
),
ExtraItem(
title: "录制视频",
image: AssetImage('assets/images/chat/extra_videocall.webp'),
onPressed: widget.handleCaptureVideo,
),
// ExtraItem(
// title: "位置",
// image: AssetImage('assets/images/chat/extra_localtion.webp'),
// onPressed: null,
// ),
],
),
Row(
children: [
// TODO:
// ExtraItem(
// title: "文件",
// image: AssetImage('assets/images/chat/extra_file.webp'),
// onPressed: widget.handleFileSelection,
// ),
// ExtraItem(
// title: "语音输入",
// image: AssetImage('assets/images/chat/extra_voice.webp'),
// onPressed: null,
// ),
// ExtraItem(
// title: "收藏",
// image: AssetImage('assets/images/chat/extra_favorite.webp'),
// onPressed: null,
// ),
// ExtraItem(
// title: "个人名片",
// image: AssetImage('assets/images/chat/extra_card.webp'),
// onPressed: null,
// ),
],
)
],
),
// Column(
// children: <Widget>[
// Row(children: [
// ExtraItem(
// title: "文件",
// image: AssetImage('assets/images/chat/extra_file.webp'),
// onPressed: widget.handleFileSelection,
// ),
// ExtraItem(
// title: "卡券",
// image: AssetImage('assets/images/chat/extra_wallet.png'),
// onPressed: null,
// ),
// ]),
// ],
// ),
];
return Column(
children: <Widget>[
Expanded(
child: CarouselSlider(
options: CarouselOptions(
height: 50, // Get.height,
viewportFraction: 1.0,
aspectRatio: 2.0,
scrollDirection: Axis.horizontal,
disableCenter: true,
initialPage: 1,
enableInfiniteScroll: false,
onPageChanged: (index, reason) {
setState(() {
_current = index;
});
},
),
items: items.map((tab) {
return Padding(
padding: EdgeInsets.only(left: 8),
child: tab,
);
}).toList(),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items.asMap().entries.map((entry) {
return GestureDetector(
onTap: () => _controller.animateToPage(entry.key),
child: Container(
width: 10.0,
height: 10.0,
margin: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 6.0,
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black)
.withOpacity(_current == entry.key ? 0.7 : 0.2),
),
),
);
}).toList(),
),
],
);
}
}
class AppColors {
static const AppBarColor = Color.fromRGBO(237, 237, 237, 1);
static const BgColor = Color.fromRGBO(255, 255, 255, 1);
static const LineColor = Colors.grey;
static const TipColor = Color.fromRGBO(89, 96, 115, 1.0);
static const MainTextColor = Color.fromRGBO(115, 115, 115, 1.0);
static const LabelTextColor = Color.fromRGBO(144, 144, 144, 1.0);
static const ItemBgColor = Color.fromRGBO(75, 75, 75, 1.0);
static const ItemOnColor = Color.fromRGBO(68, 68, 68, 1.0);
static const ButtonTextColor = Color.fromRGBO(112, 113, 135, 1.0);
static const TitleColor = 0xff181818;
static const ButtonArrowColor = 0xffadadad;
///
///
static const Color primaryBackground = Color.fromARGB(255, 255, 255, 255);
///
static const Color primaryText = Color.fromARGB(255, 45, 45, 47);
/// - 绿
static const Color primaryElement = Color.fromARGB(255, 109, 192, 102);
/// -
static const Color primaryElementText = Color.fromARGB(255, 255, 255, 255);
// *****************************************
/// -
static const Color secondaryElement = Color.fromARGB(255, 246, 246, 246);
/// - 绿
static const Color secondaryElementText = Color.fromRGBO(169, 234, 122, 1.0);
// *****************************************
/// -
static const Color thirdElement = Color.fromARGB(255, 45, 45, 47);
/// - 2
static const Color thirdElementText = Color.fromARGB(255, 141, 141, 142);
// *****************************************
/// tabBar
static const Color tabBarElement = Color.fromARGB(255, 208, 208, 208);
/// tabCellSeparator
static const Color tabCellSeparator = Color.fromARGB(255, 230, 230, 231);
// for chat
static const ChatBg = Color.fromRGBO(243, 243, 243, 1.0);
static const ChatSendMessgeBgColor = Color.fromRGBO(169, 234, 122, 1.0);
static const ChatSentMessageBodyTextColor = Color.fromRGBO(19, 29, 13, 1.0);
static const ChatReceivedMessageBodyTextColor =
Color.fromRGBO(25, 25, 25, 1.0);
static const ChatReceivedMessageBodyBgColor =
Color.fromRGBO(255, 255, 255, 1.0);
static const ChatInputBackgroundColor = Color.fromRGBO(240, 240, 240, 1.0);
static const ChatInputFillGgColor = Color.fromRGBO(251, 251, 251, 1.0);
// end for chat
}

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
class ImageButton extends StatefulWidget {
const ImageButton({
Key? key,
required this.onPressed,
required this.image,
double? this.width,
double? this.height,
String? this.title,
}) : super(key: key);
final ImageProvider image;
final void Function()? onPressed;
final double? width;
final double? height;
final String? title;
@override
_ImageButtonState createState() => _ImageButtonState();
}
class _ImageButtonState extends State<ImageButton> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onPressed,
child: Container(
width: widget.width ?? 44,
height: widget.height ?? 44,
alignment: Alignment.center,
child: Image(
image: widget.image,
width: widget.width ?? 35,
height: widget.height ?? 35,
),
),
);
}
}

@ -0,0 +1,12 @@
/// Used to toggle the visibility behavior of the [SendButton] based on the
/// [TextField] state inside the [Input] widget.
enum SendButtonVisibilityMode {
/// Always show the [SendButton] regardless of the [TextField] state.
always,
/// The [SendButton] will only appear when the [TextField] is not empty.
editing,
/// Always hide the [SendButton] regardless of the [TextField] state.
hidden,
}

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class CustomOverlay extends StatelessWidget {
final Widget? icon;
final BoxDecoration decoration;
final double width;
final double height;
const CustomOverlay({
Key? key,
this.icon,
this.decoration = const BoxDecoration(
color: Color(0xff77797A),
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
this.width = 160,
this.height = 160,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Positioned(
top: MediaQuery.of(context).size.height * 0.5 - width / 2,
left: MediaQuery.of(context).size.width * 0.5 - height / 2,
child: Material(
type: MaterialType.transparency,
child: Center(
child: Opacity(
opacity: 0.8,
child: Container(
width: width,
height: height,
decoration: decoration,
child: icon,
),
),
),
),
);
}
}

@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
class VoiceAnimation extends StatefulWidget {
final double width;
final double height;
int interval;
bool isStop = false;
bool userIsAuthor;
var callStart;
late VoiceAnimationState voiceAnimationImageState;
VoiceAnimation({
required this.width,
required this.height,
required this.isStop,
required this.userIsAuthor,
this.interval = 200,
});
@override
State<StatefulWidget> createState() {
voiceAnimationImageState = VoiceAnimationState();
return voiceAnimationImageState;
}
start() {
voiceAnimationImageState.start();
}
stop() {
voiceAnimationImageState.stop();
}
}
class VoiceAnimationState extends State<VoiceAnimation>
with SingleTickerProviderStateMixin {
//
late Animation<double> _animation;
late AnimationController _controller;
int interval = 200;
List<String> voicePlayingAsset = [];
@override
void initState() {
super.initState();
voicePlayingAsset.add("voice_playing_1.png");
voicePlayingAsset.add("voice_playing_2.png");
voicePlayingAsset.add("voice_playing_3.png");
if (widget.interval != null) {
interval = widget.interval;
}
final int imageCount = 3;
final int maxTime = interval * imageCount;
// controller
_controller = AnimationController(
duration: Duration(milliseconds: maxTime),
vsync: this,
);
_controller.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
_controller.forward(from: 2.0); //
}
});
_animation = Tween<double>(begin: -(imageCount - 2).toDouble(), end: 2)
.animate(_controller)
..addListener(() {
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
stop() {
_controller.stop();
}
start() {
_controller.forward();
}
@override
Widget build(BuildContext context) {
if (widget.isStop) {
start();
} else {
stop();
}
int ix = _animation.value.floor() % voicePlayingAsset.length;
List<Widget> images = [];
String prefix = "assets/images/chat/";
//
// receiverImages
for (int i = 0; i < voicePlayingAsset.length; ++i) {
if (i != ix) {
images.add(Image.asset(
prefix + voicePlayingAsset[i],
width: 0,
height: 0,
));
}
}
images.add(Image.asset(
prefix + voicePlayingAsset[ix],
width: widget.width,
height: widget.height,
color: Colors.black,
));
return Stack(
alignment: AlignmentDirectional.centerEnd,
children: images,
);
}
}

@ -0,0 +1,537 @@
// import 'dart:async';
// import 'dart:io';
// import 'package:audio_session/audio_session.dart';
// import 'package:flutter/foundation.dart' show kIsWeb;
// import 'package:flutter/material.dart';
// import 'package:flutter_sound/flutter_sound.dart';
// // import 'package:get/get.dart';
// // import 'package:imboy/component/helper/func.dart';
// import 'package:intl/intl.dart' show DateFormat;
// import 'package:path/path.dart';
// import 'package:path_provider/path_provider.dart';
// import 'package:permission_handler/permission_handler.dart';
// import 'custom_overlay.dart';
// bool strEmpty(String? val) {
// return !strNoEmpty(val);
// }
// ///
// bool strNoEmpty(String? value) {
// if (value == null) return false;
// return value.trim().isNotEmpty;
// }
// class AudioFile {
// const AudioFile({
// required this.file,
// required this.duration,
// required this.mimeType,
// });
// final File file;
// final Duration duration;
// final String mimeType;
// }
// class VoiceWidget extends StatefulWidget {
// final Function()? startRecord;
// final Function(AudioFile? obj)? stopRecord;
// final double? height;
// final EdgeInsets? margin;
// final Decoration? decoration;
// /// startRecord stopRecord
// VoiceWidget({
// Key? key,
// this.startRecord,
// this.stopRecord,
// this.height,
// this.decoration,
// this.margin,
// }) : super(key: key);
// @override
// _VoiceWidgetState createState() => _VoiceWidgetState();
// }
// class _VoiceWidgetState extends State<VoiceWidget> {
// //
// int _countTotal = 300;
// double starty = 0.0;
// double offset = 0.0;
// bool isUp = false;
// String textShow = "按住说话";
// String toastShow = "手指上滑,取消发送";
// String voiceIco = "assets/images/chat/voice_volume_1.png";
// Duration recordingDuration = const Duration();
// // final List<double> _levels = [];
// late String recordingMimeType;
// late Codec recordCodec;
// Timer? _timer;
// int _count = 0;
// OverlayEntry? overlayEntry;
// /////
// final FlutterSoundRecorder recorderModule = FlutterSoundRecorder();
// String recorderTxt = '00:00.000';
// String filePath = '';
// StreamSubscription? recorderSubscription;
// int pos = 0;
// double dbLevel = 0;
// late AudioSession session;
// @override
// void initState() {
// super.initState();
// debugPrint(">>> on chat _VoiceWidgetState initState");
// }
// ///
// buildOverLayView(BuildContext context) {
// if (overlayEntry == null) {
// overlayEntry = OverlayEntry(builder: (content) {
// return CustomOverlay(
// icon: Column(
// children: <Widget>[
// Container(
// margin: const EdgeInsets.only(top: 10),
// child: _countTotal - _count < 11
// ? Center(
// child: Padding(
// padding: const EdgeInsets.only(bottom: 15.0),
// child: Text(
// (_countTotal - _count).toString(),
// style: TextStyle(
// fontSize: 70.0,
// color: Colors.white,
// ),
// ),
// ),
// )
// : new Image.asset(
// voiceIco,
// width: 100,
// height: 100,
// // package: 'flutter_plugin_record',
// ),
// ),
// Container(
// child: Text(
// toastShow + "\n" + recorderTxt,
// textAlign: TextAlign.center,
// style: TextStyle(
// fontStyle: FontStyle.normal,
// color: Colors.white,
// fontSize: 14,
// ),
// ),
// )
// ],
// ),
// );
// });
// Overlay.of(context)!.insert(overlayEntry!);
// }
// }
// showVoiceView(BuildContext ctx) {
// setState(() {
// textShow = "松开结束";
// });
// ///
// buildOverLayView(ctx);
// debugPrint(">>> on record showVoiceView");
// recorderStart(ctx);
// }
// hideVoiceView(BuildContext ctx) async {
// if (_timer!.isActive) {
// if (_count < 1) {
// Toast.showView(
// context: ctx,
// msg: '说话时间太短',
// icon: Text(
// '!',
// style: TextStyle(
// fontSize: 60,
// color: Colors.white,
// ),
// ));
// isUp = true;
// }
// _timer?.cancel();
// _count = 0;
// }
// setState(() {
// textShow = "按住说话";
// });
// recorderStop(recorderModule);
// if (overlayEntry != null) {
// overlayEntry?.remove();
// overlayEntry = null;
// }
// debugPrint(
// ">>> on record hideVoiceView isUp ${isUp}, filepath: ${filePath}");
// if (isUp) {
// // print("取消发送");
// } else {
// debugPrint("进行发送");
// widget.stopRecord!.call(
// strEmpty(filePath)
// ? null
// : AudioFile(
// file: File(filePath),
// duration: this.recordingDuration,
// // waveForm: _levels,
// mimeType: recordingMimeType,
// ),
// );
// }
// }
// moveVoiceView() {
// // print(offset - start);
// setState(() {
// isUp = starty - offset > 100 ? true : false;
// if (isUp) {
// textShow = "松开手指,取消发送";
// toastShow = textShow;
// } else {
// textShow = "松开结束";
// toastShow = "手指上滑,取消发送";
// }
// });
// }
// void cancelRecorderSubscriptions() {
// if (recorderSubscription != null) {
// recorderSubscription!.cancel();
// recorderSubscription = null;
// }
// }
// /// Creates an path to a temporary file.
// Future<String> _createTempAacFilePath(String name) async {
// if (kIsWeb) {
// throw Exception(
// 'This method only works for mobile as it creates a temporary AAC file',
// );
// }
// String path;
// final tmpDir = await getTemporaryDirectory();
// path = '${join(tmpDir.path, name)}.aac';
// final parent = dirname(path);
// await Directory(parent).create(recursive: true);
// return path;
// }
// Future<void> openTheRecorder() async {
// if (!kIsWeb) {
// var status = await Permission.microphone.request();
// if (status != PermissionStatus.granted) {
// throw RecordingPermissionException('Microphone permission not granted');
// }
// }
// if (session == null) {
// session = await AudioSession.instance;
// await session.configure(AudioSessionConfiguration(
// avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
// avAudioSessionCategoryOptions:
// AVAudioSessionCategoryOptions.allowBluetooth |
// AVAudioSessionCategoryOptions.defaultToSpeaker,
// avAudioSessionMode: AVAudioSessionMode.spokenAudio,
// avAudioSessionRouteSharingPolicy:
// AVAudioSessionRouteSharingPolicy.defaultPolicy,
// avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
// androidAudioAttributes: const AndroidAudioAttributes(
// contentType: AndroidAudioContentType.speech,
// flags: AndroidAudioFlags.none,
// usage: AndroidAudioUsage.voiceCommunication,
// ),
// androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
// androidWillPauseWhenDucked: true,
// ));
// }
// }
// // ------- Here is the code to playback -----------------------
// ///
// void recorderStart(BuildContext ctx) async {
// debugPrint(">>> on record start");
// try {
// var status = await Permission.microphone.request();
// if (status != PermissionStatus.granted) {
// // Get.snackbar("", "未获取到麦克风权限");
// throw RecordingPermissionException("未获取到麦克风权限");
// }
// await recorderModule.openRecorder();
// // String name = "${Xid().toString()}";
// String name = "recardtmp";
// if (kIsWeb) {
// if (await recorderModule.isEncoderSupported(Codec.opusWebM)) {
// filePath = '$name.webm';
// recordCodec = Codec.opusWebM;
// recordingMimeType = 'audio/webm;codecs="opus"';
// } else {
// filePath = '$name.mp4';
// recordCodec = Codec.aacMP4;
// recordingMimeType = 'audio/aac';
// }
// } else {
// filePath = await _createTempAacFilePath(name);
// recordCodec = Codec.aacADTS;
// recordingMimeType = 'audio/aac';
// }
// //
// setSubscriptionDuration(40);
// await recorderModule.startRecorder(
// toFile: filePath,
// codec: recordCodec,
// bitRate: 8000,
// sampleRate: 8000,
// );
// ///
// recorderSubscription = recorderModule.onProgress!.listen((e) {
// // debugPrint(">>> on record listen e ${e.toString()}");
// setState(() {
// pos = e.duration.inMilliseconds;
// });
// // debugPrint(">>> on record listen pos: ${pos}, dbLevel: ${e.decibels};");
// if (e != null && e.duration != null) {
// recordingDuration = e.duration;
// // _levels.add(e.decibels ?? 0.0);
// DateTime date = new DateTime.fromMillisecondsSinceEpoch(
// e.duration.inMilliseconds,
// isUtc: true,
// );
// String txt = DateFormat('mm:ss.SSS').format(date);
// if (date.second >= _countTotal) {
// // recorderStop(recorderModule);
// hideVoiceView(ctx);
// }
// if (e.decibels != null) {
// setState(() {
// recorderTxt = txt.substring(0, 9);
// dbLevel = e.decibels!.toDouble();
// // debugPrint(">>> on record 当前振幅:$dbLevel");
// });
// }
// }
// if (e.decibels != null) {
// dbLevel = e.decibels as double;
// double voiceData = ((dbLevel * 100.0).floor()) / 10000;
// if (voiceData > 0 && voiceData < 0.1) {
// voiceIco = "assets/images/chat/voice_volume_2.png";
// } else if (voiceData > 0.2 && voiceData < 0.3) {
// voiceIco = "assets/images/chat/voice_volume_3.png";
// } else if (voiceData > 0.3 && voiceData < 0.4) {
// voiceIco = "assets/images/chat/voice_volume_4.png";
// } else if (voiceData > 0.4 && voiceData < 0.5) {
// voiceIco = "assets/images/chat/voice_volume_5.png";
// } else if (voiceData > 0.5 && voiceData < 0.6) {
// voiceIco = "assets/images/chat/voice_volume_6.png";
// } else if (voiceData > 0.6 && voiceData < 0.7) {
// voiceIco = "assets/images/chat/voice_volume_7.png";
// } else if (voiceData > 0.7 && voiceData < 1) {
// voiceIco = "assets/images/chat/voice_volume_7.png";
// } else {
// voiceIco = "assets/images/chat/voice_volume_1.png";
// }
// if (overlayEntry != null) {
// overlayEntry!.markNeedsBuild();
// }
// // debugPrint(
// // ">>> on record 振幅大小 " + voiceData.toString() + " " + voiceIco);
// setState(() {
// dbLevel = dbLevel;
// voiceIco = voiceIco;
// });
// }
// });
// setState(() {
// filePath = filePath;
// });
// } catch (err) {
// setState(() {
// recorderStop(recorderModule);
// cancelRecorderSubscriptions();
// });
// }
// }
// ///
// Future<String?> recorderStop(FlutterSoundRecorder recorder) async {
// try {
// String? filepath = await recorder.stopRecorder();
// cancelRecorderSubscriptions();
// setState(() {
// dbLevel = 0.0;
// pos = 0;
// recorderTxt = '00:00.000';
// });
// // _getDuration();
// return filepath;
// } catch (err) {
// print('stopRecorder error: $err');
// }
// }
// /**
// *
// */
// Future<void> setSubscriptionDuration(
// double d) async // d is between 0.0 and 2000 (milliseconds)
// {
// setState(() {});
// await recorderModule.setSubscriptionDuration(
// Duration(milliseconds: d.floor()),
// );
// }
// // --------------------- UI -------------------
// @override
// Widget build(BuildContext context) {
// return Container(
// child: GestureDetector(
// onLongPressStart: (details) {
// starty = details.globalPosition.dy;
// _timer = Timer.periodic(Duration(milliseconds: 1000), (t) {
// _count++;
// if (_count == _countTotal) {
// hideVoiceView(context);
// }
// });
// showVoiceView(context);
// },
// onLongPressEnd: (details) {
// hideVoiceView(context);
// },
// onLongPressMoveUpdate: (details) {
// offset = details.globalPosition.dy;
// moveVoiceView();
// },
// child: Container(
// height: widget.height ?? 60,
// decoration: widget.decoration ??
// BoxDecoration(
// borderRadius: BorderRadius.circular(6.0),
// border: Border.all(
// width: 1.0,
// color: Colors.white70,
// ),
// color: Colors.white70,
// ),
// margin: widget.margin ?? EdgeInsets.fromLTRB(50, 0, 50, 20),
// child: Center(
// child: Text(
// textShow,
// // '${textShow}(pos: ${pos})',
// ),
// ),
// ),
// ),
// );
// }
// @override
// void dispose() {
// recorderStop(recorderModule);
// cancelRecorderSubscriptions();
// // Be careful : you must `close` the audio session when you have finished with it.
// recorderModule.closeRecorder();
// // recordPlugin?.dispose();
// _timer?.cancel();
// super.dispose();
// }
// }
// class Toast {
// static showView({
// BuildContext? context,
// String? msg,
// TextStyle? style,
// Widget? icon,
// Duration duration = const Duration(seconds: 1),
// int count = 3,
// Function? onTap,
// }) {
// OverlayEntry? overlayEntry;
// int _count = 0;
// void removeOverlay() {
// overlayEntry?.remove();
// overlayEntry = null;
// }
// if (overlayEntry == null) {
// overlayEntry = OverlayEntry(builder: (content) {
// return Container(
// child: GestureDetector(
// onTap: () {
// if (onTap != null) {
// removeOverlay();
// onTap();
// }
// },
// child: CustomOverlay(
// icon: Column(
// children: [
// Padding(
// child: icon,
// padding: const EdgeInsets.only(
// bottom: 10.0,
// ),
// ),
// Container(
// child: Text(
// msg ?? '',
// style: style ??
// TextStyle(
// fontStyle: FontStyle.normal,
// color: Colors.white,
// fontSize: 16,
// ),
// ),
// )
// ],
// ),
// ),
// ),
// );
// });
// Overlay.of(context!)!.insert(overlayEntry!);
// if (onTap != null) return;
// Timer.periodic(duration, (timer) {
// _count++;
// if (_count == count) {
// _count = 0;
// timer.cancel();
// removeOverlay();
// }
// });
// }
// }
// }

@ -18,7 +18,7 @@ class BytedeskConstants {
// static const String httpUploadUrl = 'http://' + mqttHost + ':8000';
// static const String host = mqttHost + ':8000';
// static const int mqttPort = 3883; // not secure
// static const String mqttHost = '172.16.0.78';
// static const String mqttHost = '172.16.0.3';
//
// static const bool isDebug = true;
@ -45,7 +45,7 @@ class BytedeskConstants {
// static const String httpBaseUrliOS = 'http://' + mqttHost + ':8000';
// static const String httpUploadUrl = 'http://' + mqttHost + ':8000';
// static const String host = mqttHost + ':8000';
// static const String mqttHost = '192.168.0.106';
// static const String mqttHost = '192.168.0.104';
// 线
static const bool isDebug = false; // false;

@ -0,0 +1,9 @@
library emoji_picker_flutter;
export './src/category_icons.dart';
export './src/config.dart';
export './src/emoji.dart';
export './src/emoji_picker.dart';
export './src/emoji_picker_builder.dart';
export './src/emoji_picker_utils.dart';
export './src/emoji_view_state.dart';

@ -0,0 +1,13 @@
import '../emoji_picker_flutter.dart';
/// Container for Category and their emoji
class CategoryEmoji {
/// Constructor
CategoryEmoji(this.category, this.emoji);
/// Category instance
final Category category;
/// List of emoji of this category
List<Emoji> emoji;
}

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
/// Class that defines the icon representing a [Category]
class CategoryIcon {
/// Icon of Category
const CategoryIcon({
required this.icon,
this.color = const Color.fromRGBO(211, 211, 211, 1),
this.selectedColor = const Color.fromRGBO(178, 178, 178, 1),
});
/// The icon to represent the category
final IconData icon;
/// The default color of the icon
final Color color;
/// The color of the icon once the category is selected
final Color selectedColor;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save