diff --git a/.DS_Store b/.DS_Store index c029c5b..884dd73 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/bytedesk_demo/assets/images/chat/extra_camera.webp b/bytedesk_demo/assets/images/chat/extra_camera.webp new file mode 100644 index 0000000..fe83d04 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_camera.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_card.webp b/bytedesk_demo/assets/images/chat/extra_card.webp new file mode 100644 index 0000000..d99a68b Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_card.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_favorite.webp b/bytedesk_demo/assets/images/chat/extra_favorite.webp new file mode 100644 index 0000000..190ae40 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_favorite.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_file.webp b/bytedesk_demo/assets/images/chat/extra_file.webp new file mode 100644 index 0000000..c8d75ab Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_file.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_localtion.webp b/bytedesk_demo/assets/images/chat/extra_localtion.webp new file mode 100644 index 0000000..fcd628d Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_localtion.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_media.webp b/bytedesk_demo/assets/images/chat/extra_media.webp new file mode 100644 index 0000000..2806229 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_media.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_photo.webp b/bytedesk_demo/assets/images/chat/extra_photo.webp new file mode 100644 index 0000000..27607e2 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_photo.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_red.webp b/bytedesk_demo/assets/images/chat/extra_red.webp new file mode 100644 index 0000000..5a0b66b Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_red.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_videocall.webp b/bytedesk_demo/assets/images/chat/extra_videocall.webp new file mode 100644 index 0000000..44a6db6 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_videocall.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_voice.webp b/bytedesk_demo/assets/images/chat/extra_voice.webp new file mode 100644 index 0000000..0ebfbe2 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_voice.webp differ diff --git a/bytedesk_demo/assets/images/chat/extra_wallet.png b/bytedesk_demo/assets/images/chat/extra_wallet.png new file mode 100755 index 0000000..972274f Binary files /dev/null and b/bytedesk_demo/assets/images/chat/extra_wallet.png differ diff --git a/bytedesk_demo/assets/images/chat/input_emoji.png b/bytedesk_demo/assets/images/chat/input_emoji.png new file mode 100755 index 0000000..db61921 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/input_emoji.png differ diff --git a/bytedesk_demo/assets/images/chat/input_extra.png b/bytedesk_demo/assets/images/chat/input_extra.png new file mode 100755 index 0000000..eddec89 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/input_extra.png differ diff --git a/bytedesk_demo/assets/images/chat/input_keyboard.png b/bytedesk_demo/assets/images/chat/input_keyboard.png new file mode 100755 index 0000000..5c53793 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/input_keyboard.png differ diff --git a/bytedesk_demo/assets/images/chat/input_voice.png b/bytedesk_demo/assets/images/chat/input_voice.png new file mode 100755 index 0000000..537bc29 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/input_voice.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_playing_1.png b/bytedesk_demo/assets/images/chat/voice_playing_1.png new file mode 100755 index 0000000..009de38 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_playing_1.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_playing_2.png b/bytedesk_demo/assets/images/chat/voice_playing_2.png new file mode 100755 index 0000000..3999990 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_playing_2.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_playing_3.png b/bytedesk_demo/assets/images/chat/voice_playing_3.png new file mode 100755 index 0000000..6e4cec1 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_playing_3.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_1.png b/bytedesk_demo/assets/images/chat/voice_volume_1.png new file mode 100644 index 0000000..8ed01f5 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_1.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_2.png b/bytedesk_demo/assets/images/chat/voice_volume_2.png new file mode 100644 index 0000000..701f614 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_2.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_3.png b/bytedesk_demo/assets/images/chat/voice_volume_3.png new file mode 100644 index 0000000..2b57dc2 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_3.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_4.png b/bytedesk_demo/assets/images/chat/voice_volume_4.png new file mode 100644 index 0000000..7eabb92 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_4.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_5.png b/bytedesk_demo/assets/images/chat/voice_volume_5.png new file mode 100644 index 0000000..3ee78cf Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_5.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_6.png b/bytedesk_demo/assets/images/chat/voice_volume_6.png new file mode 100644 index 0000000..74a1cf6 Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_6.png differ diff --git a/bytedesk_demo/assets/images/chat/voice_volume_7.png b/bytedesk_demo/assets/images/chat/voice_volume_7.png new file mode 100644 index 0000000..e33ebbb Binary files /dev/null and b/bytedesk_demo/assets/images/chat/voice_volume_7.png differ diff --git a/bytedesk_demo/lib/page/history_thread_page.dart b/bytedesk_demo/lib/page/history_thread_page.dart index ce5cd5f..40711ef 100755 --- a/bytedesk_demo/lib/page/history_thread_page.dart +++ b/bytedesk_demo/lib/page/history_thread_page.dart @@ -39,7 +39,9 @@ class _HistoryThreadPageState extends State { subtitle: Text('${_historyThreadList[index].content}'), onTap: () { // 进入客服页面 - BytedeskKefu.startChatThread(context, _historyThreadList[index]); + // BytedeskKefu.startChatThread(context, _historyThreadList[index]); + BytedeskKefu.startChatThreadIM( + context, _historyThreadList[index]); }, ), itemCount: _historyThreadList.length, diff --git a/bytedesk_demo/pubspec.yaml b/bytedesk_demo/pubspec.yaml index 3b7446f..503ca76 100644 --- a/bytedesk_demo/pubspec.yaml +++ b/bytedesk_demo/pubspec.yaml @@ -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 diff --git a/bytedesk_kefu/CHANGELOG.md b/bytedesk_kefu/CHANGELOG.md index ff1d6e2..8e93d04 100644 --- a/bytedesk_kefu/CHANGELOG.md +++ b/bytedesk_kefu/CHANGELOG.md @@ -1,5 +1,13 @@ # Upgrade Log +## 1.4.0 + +* optimize user experience + +## 1.3.4 + +* optimize user experience + ## 1.3.3 * optimize user experience diff --git a/bytedesk_kefu/README.md b/bytedesk_kefu/README.md index 76de2d2..40e4167 100644 --- a/bytedesk_kefu/README.md +++ b/bytedesk_kefu/README.md @@ -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) diff --git a/bytedesk_kefu/assets/images/chat/extra_camera.webp b/bytedesk_kefu/assets/images/chat/extra_camera.webp new file mode 100644 index 0000000..fe83d04 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_camera.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_card.webp b/bytedesk_kefu/assets/images/chat/extra_card.webp new file mode 100644 index 0000000..d99a68b Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_card.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_favorite.webp b/bytedesk_kefu/assets/images/chat/extra_favorite.webp new file mode 100644 index 0000000..190ae40 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_favorite.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_file.webp b/bytedesk_kefu/assets/images/chat/extra_file.webp new file mode 100644 index 0000000..c8d75ab Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_file.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_localtion.webp b/bytedesk_kefu/assets/images/chat/extra_localtion.webp new file mode 100644 index 0000000..fcd628d Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_localtion.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_media.webp b/bytedesk_kefu/assets/images/chat/extra_media.webp new file mode 100644 index 0000000..2806229 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_media.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_photo.webp b/bytedesk_kefu/assets/images/chat/extra_photo.webp new file mode 100644 index 0000000..27607e2 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_photo.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_red.webp b/bytedesk_kefu/assets/images/chat/extra_red.webp new file mode 100644 index 0000000..5a0b66b Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_red.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_videocall.webp b/bytedesk_kefu/assets/images/chat/extra_videocall.webp new file mode 100644 index 0000000..44a6db6 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_videocall.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_voice.webp b/bytedesk_kefu/assets/images/chat/extra_voice.webp new file mode 100644 index 0000000..0ebfbe2 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_voice.webp differ diff --git a/bytedesk_kefu/assets/images/chat/extra_wallet.png b/bytedesk_kefu/assets/images/chat/extra_wallet.png new file mode 100755 index 0000000..972274f Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/extra_wallet.png differ diff --git a/bytedesk_kefu/assets/images/chat/input_emoji.png b/bytedesk_kefu/assets/images/chat/input_emoji.png new file mode 100755 index 0000000..db61921 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/input_emoji.png differ diff --git a/bytedesk_kefu/assets/images/chat/input_extra.png b/bytedesk_kefu/assets/images/chat/input_extra.png new file mode 100755 index 0000000..eddec89 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/input_extra.png differ diff --git a/bytedesk_kefu/assets/images/chat/input_keyboard.png b/bytedesk_kefu/assets/images/chat/input_keyboard.png new file mode 100755 index 0000000..5c53793 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/input_keyboard.png differ diff --git a/bytedesk_kefu/assets/images/chat/input_voice.png b/bytedesk_kefu/assets/images/chat/input_voice.png new file mode 100755 index 0000000..537bc29 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/input_voice.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_playing_1.png b/bytedesk_kefu/assets/images/chat/voice_playing_1.png new file mode 100755 index 0000000..009de38 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_playing_1.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_playing_2.png b/bytedesk_kefu/assets/images/chat/voice_playing_2.png new file mode 100755 index 0000000..3999990 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_playing_2.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_playing_3.png b/bytedesk_kefu/assets/images/chat/voice_playing_3.png new file mode 100755 index 0000000..6e4cec1 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_playing_3.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_1.png b/bytedesk_kefu/assets/images/chat/voice_volume_1.png new file mode 100644 index 0000000..8ed01f5 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_1.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_2.png b/bytedesk_kefu/assets/images/chat/voice_volume_2.png new file mode 100644 index 0000000..701f614 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_2.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_3.png b/bytedesk_kefu/assets/images/chat/voice_volume_3.png new file mode 100644 index 0000000..2b57dc2 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_3.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_4.png b/bytedesk_kefu/assets/images/chat/voice_volume_4.png new file mode 100644 index 0000000..7eabb92 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_4.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_5.png b/bytedesk_kefu/assets/images/chat/voice_volume_5.png new file mode 100644 index 0000000..3ee78cf Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_5.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_6.png b/bytedesk_kefu/assets/images/chat/voice_volume_6.png new file mode 100644 index 0000000..74a1cf6 Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_6.png differ diff --git a/bytedesk_kefu/assets/images/chat/voice_volume_7.png b/bytedesk_kefu/assets/images/chat/voice_volume_7.png new file mode 100644 index 0000000..e33ebbb Binary files /dev/null and b/bytedesk_kefu/assets/images/chat/voice_volume_7.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_camera.webp b/bytedesk_kefu/example/assets/images/chat/extra_camera.webp new file mode 100644 index 0000000..fe83d04 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_camera.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_card.webp b/bytedesk_kefu/example/assets/images/chat/extra_card.webp new file mode 100644 index 0000000..d99a68b Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_card.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_favorite.webp b/bytedesk_kefu/example/assets/images/chat/extra_favorite.webp new file mode 100644 index 0000000..190ae40 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_favorite.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_file.webp b/bytedesk_kefu/example/assets/images/chat/extra_file.webp new file mode 100644 index 0000000..c8d75ab Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_file.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_localtion.webp b/bytedesk_kefu/example/assets/images/chat/extra_localtion.webp new file mode 100644 index 0000000..fcd628d Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_localtion.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_media.webp b/bytedesk_kefu/example/assets/images/chat/extra_media.webp new file mode 100644 index 0000000..2806229 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_media.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_photo.webp b/bytedesk_kefu/example/assets/images/chat/extra_photo.webp new file mode 100644 index 0000000..27607e2 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_photo.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_red.webp b/bytedesk_kefu/example/assets/images/chat/extra_red.webp new file mode 100644 index 0000000..5a0b66b Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_red.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_videocall.webp b/bytedesk_kefu/example/assets/images/chat/extra_videocall.webp new file mode 100644 index 0000000..44a6db6 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_videocall.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_voice.webp b/bytedesk_kefu/example/assets/images/chat/extra_voice.webp new file mode 100644 index 0000000..0ebfbe2 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_voice.webp differ diff --git a/bytedesk_kefu/example/assets/images/chat/extra_wallet.png b/bytedesk_kefu/example/assets/images/chat/extra_wallet.png new file mode 100755 index 0000000..972274f Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/extra_wallet.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/input_emoji.png b/bytedesk_kefu/example/assets/images/chat/input_emoji.png new file mode 100755 index 0000000..db61921 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/input_emoji.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/input_extra.png b/bytedesk_kefu/example/assets/images/chat/input_extra.png new file mode 100755 index 0000000..eddec89 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/input_extra.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/input_keyboard.png b/bytedesk_kefu/example/assets/images/chat/input_keyboard.png new file mode 100755 index 0000000..5c53793 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/input_keyboard.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/input_voice.png b/bytedesk_kefu/example/assets/images/chat/input_voice.png new file mode 100755 index 0000000..537bc29 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/input_voice.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_playing_1.png b/bytedesk_kefu/example/assets/images/chat/voice_playing_1.png new file mode 100755 index 0000000..009de38 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_playing_1.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_playing_2.png b/bytedesk_kefu/example/assets/images/chat/voice_playing_2.png new file mode 100755 index 0000000..3999990 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_playing_2.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_playing_3.png b/bytedesk_kefu/example/assets/images/chat/voice_playing_3.png new file mode 100755 index 0000000..6e4cec1 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_playing_3.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_1.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_1.png new file mode 100644 index 0000000..8ed01f5 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_1.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_2.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_2.png new file mode 100644 index 0000000..701f614 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_2.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_3.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_3.png new file mode 100644 index 0000000..2b57dc2 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_3.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_4.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_4.png new file mode 100644 index 0000000..7eabb92 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_4.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_5.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_5.png new file mode 100644 index 0000000..3ee78cf Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_5.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_6.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_6.png new file mode 100644 index 0000000..74a1cf6 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_6.png differ diff --git a/bytedesk_kefu/example/assets/images/chat/voice_volume_7.png b/bytedesk_kefu/example/assets/images/chat/voice_volume_7.png new file mode 100644 index 0000000..e33ebbb Binary files /dev/null and b/bytedesk_kefu/example/assets/images/chat/voice_volume_7.png differ diff --git a/bytedesk_kefu/example/ios/Podfile.lock b/bytedesk_kefu/example/ios/Podfile.lock index 274b3c0..9a7d1d5 100644 --- a/bytedesk_kefu/example/ios/Podfile.lock +++ b/bytedesk_kefu/example/ios/Podfile.lock @@ -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 diff --git a/bytedesk_kefu/example/lib/main.dart b/bytedesk_kefu/example/lib/main.dart index bd6f869..464786e 100644 --- a/bytedesk_kefu/example/lib/main.dart +++ b/bytedesk_kefu/example/lib/main.dart @@ -31,6 +31,7 @@ void main() { // 第一步:初始化 BytedeskKefu.init(_appKey, _subDomain); // 注:如果需要多平台统一用户(用于同步聊天记录等),可使用下列接口,其中:username只能包含数字或字母,不能含有汉字和特殊字符等,nickname可以使用汉字 + // 注:如需切换用户,请首先执行BytedeskKefu.logout() // BytedeskKefu.initWithUsernameAndNicknameAndAvatar('myflutterusername', '我是美女', 'https://bytedesk.oss-cn-shenzhen.aliyuncs.com/avatars/girl.png', _appKey, _subDomain); // BytedeskKefu.initWithUsername('myflutterusername', _appKey, _subDomain); // 其中:username为自定义用户名,可与开发者所在用户系统对接 // 如果还需要自定义昵称/头像,可以使用 initWithUsernameAndNickname或initWithUsernameAndNicknameAndAvatar, diff --git a/bytedesk_kefu/example/pubspec.yaml b/bytedesk_kefu/example/pubspec.yaml index fcd0f95..ab7ea6a 100644 --- a/bytedesk_kefu/example/pubspec.yaml +++ b/bytedesk_kefu/example/pubspec.yaml @@ -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 diff --git a/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart b/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart index d051bf9..201ce58 100755 --- a/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart +++ b/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart @@ -100,7 +100,7 @@ class MessageBloc extends Bloc { emit(SendMessageRestSuccess(jsonResult)); } catch (error) { BytedeskUtils.printLog(error); - emit(SendMessageRestError()); + emit(SendMessageRestError(event.json!)); } } diff --git a/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart b/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart index 666a3d7..a974b06 100755 --- a/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart +++ b/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart @@ -65,6 +65,13 @@ class UpLoadImageError extends MessageState { } class SendMessageRestError extends MessageState { + final String json; + + const SendMessageRestError(this.json); + + @override + List get props => [json]; + @override String toString() => 'SendMessageRestError'; } diff --git a/bytedesk_kefu/lib/http/bytedesk_user_api.dart b/bytedesk_kefu/lib/http/bytedesk_user_api.dart index d215d2e..6ebdd80 100755 --- a/bytedesk_kefu/lib/http/bytedesk_user_api.dart +++ b/bytedesk_kefu/lib/http/bytedesk_user_api.dart @@ -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 ?? ''); diff --git a/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart b/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart index 62add9c..52ccca7 100755 --- a/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart +++ b/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart @@ -984,7 +984,9 @@ class BytedeskMqtt { // 断开长连接 void disconnect() { - mqttClient.disconnect(); + if (mqttClient != null) { + mqttClient.disconnect(); + } } /// The subscribed callback diff --git a/bytedesk_kefu/lib/ui/chat/page/chat_kf_page.dart b/bytedesk_kefu/lib/ui/chat/page/chat_kf_page.dart index 8c60241..eb8dce5 100755 --- a/bytedesk_kefu/lib/ui/chat/page/chat_kf_page.dart +++ b/bytedesk_kefu/lib/ui/chat/page/chat_kf_page.dart @@ -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: 接通客服之前,在title显示loading // 客服关闭会话,或者 自动关闭会话,则禁止继续发送消息 @@ -87,6 +93,7 @@ class _ChatKFPageState extends State 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 Timer? _debounce; // 定时拉取聊天记录 Timer? _loadHistoryTimer; + // + Timer? _resendTimer; // 视频压缩 // final _flutterVideoCompress = FlutterVideoCompress(); bool _isRequestingThread = true; @@ -138,6 +147,41 @@ class _ChatKFPageState extends State // // 每隔 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) { + // 超时15秒,设置为消息状态为error + _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(context) // ..add(LoadUnreadVisitorMessagesEvent(page: 0, size: 10)); } @@ -401,6 +445,18 @@ class _ChatKFPageState extends State 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 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 _handleSendPressed(String content) async { + print('send: ${content}'); + _handleSubmitted(content); + return true; + } + + void _handleImageSelection() async { + print('_handleImageSelection'); + _pickImage(); + } + + void _handleFileSelection() async { + print('_handleFileSelection'); + } + + Future _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 @override bool get wantKeepAlive => true; - // + // 发送消息 void _handleSubmitted(String? text) { _textController.clear(); // 内容为空,直接返回 @@ -572,100 +696,175 @@ class _ChatKFPageState extends State } } - // http rest 接口发生文本消息 + // http rest 接口发生文本消息,长链接断开情况下调用 void sendTextMessageRest(String text) { // String? mid = BytedeskUuid.uuid(); - String? timestamp = BytedeskUtils.formatedDateNow(); - String? client = BytedeskUtils.getClient(); - String? type = BytedeskConstants.MESSAGE_TYPE_TEXT; - // - var jsonContent = { - "mid": mid, - "timestamp": timestamp, - "client": client, - "version": "1", - "type": type, - "status": "sending", - "user": { - "uid": this._currentUid, - "nickname": this._currentNickname, - "avatar": this._currentAvatar, - "extra": {"agent": false} - }, - "text": {"content": text}, - "thread": { - "tid": this._currentThread!.tid, - "type": this._currentThread!.type, - "content": text, - "nickname": this._currentThread!.nickname, - "avatar": this._currentThread!.avatar, - "topic": this._currentThread!.topic, - "timestamp": timestamp, - "unreadCount": 0 - } - }; - String? jsonString = json.encode(jsonContent); - BlocProvider.of(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); + sendMessageRest(mid, BytedeskConstants.MESSAGE_TYPE_TEXT, text); } - // http rest 接口发送图片消息 + // 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_IMAGE; // - var jsonContent = { - "mid": mid, - "timestamp": timestamp, - "client": client, - "version": "1", - "type": type, - "status": "sending", - "user": { - "uid": this._currentUid, - "nickname": this._currentNickname, - "avatar": this._currentAvatar, - "extra": {"agent": false} - }, - "image": {"imageUrl": imageUrl}, - "thread": { - "tid": this._currentThread!.tid, - "type": this._currentThread!.type, - "content": "[图片]", - "nickname": this._currentThread!.nickname, - "avatar": this._currentThread!.avatar, - "topic": this._currentThread!.topic, + var jsonContent; + + if (type == BytedeskConstants.MESSAGE_TYPE_TEXT) { + jsonContent = { + "mid": mid, "timestamp": timestamp, - "unreadCount": 0 - } - }; + "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} + }, + "text": {"content": 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 + } + }; + } else if (type == BytedeskConstants.MESSAGE_TYPE_IMAGE) { + jsonContent = { + "mid": mid, + "timestamp": timestamp, + "client": client, + "version": "1", + "type": type, + "status": "sending", + "user": { + "uid": this._currentUid, + "username": this._currentUsername, + "nickname": this._currentNickname, + "avatar": this._currentAvatar, + "extra": {"agent": false} + }, + "image": {"imageUrl": 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_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(context) ..add(SendMessageRestEvent(json: jsonString)); @@ -674,7 +873,7 @@ class _ChatKFPageState extends State 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 avatar: this._currentAvatar, nickname: this._currentNickname); // - message.imageUrl = imageUrl; + message.content = content; // 插入本地数据库 _messageProvider.insert(message); // @@ -1174,6 +1373,7 @@ class _ChatKFPageState extends State WidgetsBinding.instance!.removeObserver(this); _debounce?.cancel(); _loadHistoryTimer?.cancel(); + _resendTimer?.cancel(); // bytedeskEventBus.destroy(); // FIXME: 只能取消监听,不能destroy super.dispose(); } diff --git a/bytedesk_kefu/lib/ui/widget/chat_input.dart b/bytedesk_kefu/lib/ui/widget/chat_input.dart new file mode 100644 index 0000000..b76612a --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/chat_input.dart @@ -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 Function(types.PartialText) onSendPressed; + final Future 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 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().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(); + _inputFocusNode.dispose(); + _textController.dispose(); + super.dispose(); + } + + Future _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 _voiceBtnOnPressed(InputType type) async { + if (type == this.inputType) { + return; + } + if (type != InputType.text) { + hideSoftKey(); + } else { + showSoftKey(); + } + + setState(() { + this.inputType = type; + }); + } + + Future 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: [ + 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: [ + 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( + onInvoke: (SendMessageIntent intent) => + _handleSendPressed(), + ), + NewLineIntent: CallbackAction( + 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()), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/bytedesk_kefu/lib/ui/widget/emoji_picker_view.dart b/bytedesk_kefu/lib/ui/widget/emoji_picker_view.dart new file mode 100644 index 0000000..bef4401 --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/emoji_picker_view.dart @@ -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 + 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((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((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, + )); + } +} diff --git a/bytedesk_kefu/lib/ui/widget/extra_item.dart b/bytedesk_kefu/lib/ui/widget/extra_item.dart new file mode 100644 index 0000000..a2e5998 --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/extra_item.dart @@ -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 { + int _current = 0; + CarouselController _controller = CarouselController(); + + @override + Widget build(BuildContext context) { + var items = [ + Column( + children: [ + 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: [ + // 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: [ + 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 +} diff --git a/bytedesk_kefu/lib/ui/widget/image_button.dart b/bytedesk_kefu/lib/ui/widget/image_button.dart new file mode 100644 index 0000000..d67c00d --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/image_button.dart @@ -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 { + @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, + ), + ), + ); + } +} diff --git a/bytedesk_kefu/lib/ui/widget/send_button_visibility_mode.dart b/bytedesk_kefu/lib/ui/widget/send_button_visibility_mode.dart new file mode 100644 index 0000000..0b3e0c9 --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/send_button_visibility_mode.dart @@ -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, +} diff --git a/bytedesk_kefu/lib/ui/widget/voice_record/custom_overlay.dart b/bytedesk_kefu/lib/ui/widget/voice_record/custom_overlay.dart new file mode 100644 index 0000000..6a7af9a --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/voice_record/custom_overlay.dart @@ -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, + ), + ), + ), + ), + ); + } +} diff --git a/bytedesk_kefu/lib/ui/widget/voice_record/voice_animation.dart b/bytedesk_kefu/lib/ui/widget/voice_record/voice_animation.dart new file mode 100644 index 0000000..ddf0e91 --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/voice_record/voice_animation.dart @@ -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 createState() { + voiceAnimationImageState = VoiceAnimationState(); + return voiceAnimationImageState; + } + + start() { + voiceAnimationImageState.start(); + } + + stop() { + voiceAnimationImageState.stop(); + } +} + +class VoiceAnimationState extends State + with SingleTickerProviderStateMixin { + // 动画控制 + late Animation _animation; + late AnimationController _controller; + int interval = 200; + + List 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(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 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, + ); + } +} diff --git a/bytedesk_kefu/lib/ui/widget/voice_record/voice_widget.dart b/bytedesk_kefu/lib/ui/widget/voice_record/voice_widget.dart new file mode 100644 index 0000000..8c1271b --- /dev/null +++ b/bytedesk_kefu/lib/ui/widget/voice_record/voice_widget.dart @@ -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 { +// // 倒计时总时长 +// 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 _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: [ +// 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 _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 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 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 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(); +// } +// }); +// } +// } +// } diff --git a/bytedesk_kefu/lib/util/bytedesk_constants.dart b/bytedesk_kefu/lib/util/bytedesk_constants.dart index 20d265c..e9115a2 100755 --- a/bytedesk_kefu/lib/util/bytedesk_constants.dart +++ b/bytedesk_kefu/lib/util/bytedesk_constants.dart @@ -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; diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/emoji_picker_flutter.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/emoji_picker_flutter.dart new file mode 100644 index 0000000..5fba8eb --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/emoji_picker_flutter.dart @@ -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'; diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_emoji.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_emoji.dart new file mode 100644 index 0000000..4930316 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_emoji.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; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_icon.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_icon.dart new file mode 100644 index 0000000..18e4783 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_icon.dart @@ -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; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_icons.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_icons.dart new file mode 100644 index 0000000..3096680 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/category_icons.dart @@ -0,0 +1,49 @@ +import './category_icon.dart'; +import 'package:flutter/material.dart'; + +/// Class used to define all the [CategoryIcon] shown for each [Category] +/// +/// This allows the keyboard to be personalized by changing icons shown. +/// If a [CategoryIcon] is set as null or not defined during initialization, +/// the default icons will be used instead +class CategoryIcons { + /// Constructor + const CategoryIcons({ + this.recentIcon = Icons.access_time, + this.smileyIcon = Icons.tag_faces, + this.animalIcon = Icons.pets, + this.foodIcon = Icons.fastfood, + this.activityIcon = Icons.directions_run, + this.travelIcon = Icons.location_city, + this.objectIcon = Icons.lightbulb_outline, + this.symbolIcon = Icons.emoji_symbols, + this.flagIcon = Icons.flag, + }); + + /// Icon for [Category.RECENT] + final IconData recentIcon; + + /// Icon for [Category.SMILEYS] + final IconData smileyIcon; + + /// Icon for [Category.ANIMALS] + final IconData animalIcon; + + /// Icon for [Category.FOODS] + final IconData foodIcon; + + /// Icon for [Category.ACTIVITIES] + final IconData activityIcon; + + /// Icon for [Category.TRAVEL] + final IconData travelIcon; + + /// Icon for [Category.OBJECTS] + final IconData objectIcon; + + /// Icon for [Category.SYMBOLS] + final IconData symbolIcon; + + /// Icon for [Category.FLAGS] + final IconData flagIcon; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/config.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/config.dart new file mode 100644 index 0000000..de2e8be --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/config.dart @@ -0,0 +1,192 @@ +import 'dart:math'; + +import './category_icons.dart'; +import './emoji_picker.dart'; +import 'package:flutter/material.dart'; + +/// Default Widget if no recent is available +const DefaultNoRecentsWidget = Text( + 'No Recents', + style: TextStyle(fontSize: 20, color: Colors.black26), + textAlign: TextAlign.center, +); + +/// Config for customizations +class Config { + /// Constructor + const Config( + {this.columns = 7, + this.emojiSizeMax = 32.0, + this.verticalSpacing = 0, + this.horizontalSpacing = 0, + this.gridPadding = EdgeInsets.zero, + this.initCategory = Category.RECENT, + this.bgColor = const Color(0xFFEBEFF2), + this.indicatorColor = Colors.blue, + this.iconColor = Colors.grey, + this.iconColorSelected = Colors.blue, + this.progressIndicatorColor = Colors.blue, + this.backspaceColor = Colors.blue, + this.skinToneDialogBgColor = Colors.white, + this.skinToneIndicatorColor = Colors.grey, + this.enableSkinTones = true, + this.showRecentsTab = true, + this.recentsLimit = 28, + this.replaceEmojiOnLimitExceed = false, + this.noRecents = DefaultNoRecentsWidget, + this.tabIndicatorAnimDuration = kTabScrollDuration, + this.categoryIcons = const CategoryIcons(), + this.buttonMode = ButtonMode.MATERIAL}); + + /// Number of emojis per row + final int columns; + + /// Width and height the emoji will be maximal displayed + /// Can be smaller due to screen size and amount of columns + final double emojiSizeMax; + + /// Verical spacing between emojis + final double verticalSpacing; + + /// Horizontal spacing between emojis + final double horizontalSpacing; + + /// The initial [Category] that will be selected + /// This [Category] will have its button in the bottombar darkened + final Category initCategory; + + /// The background color of the Widget + final Color bgColor; + + /// The color of the category indicator + final Color indicatorColor; + + /// The color of the category icons + final Color iconColor; + + /// The color of the category icon when selected + final Color iconColorSelected; + + /// The color of the loading indicator during initalization + final Color progressIndicatorColor; + + /// The color of the backspace icon button + final Color backspaceColor; + + /// The background color of the skin tone dialog + final Color skinToneDialogBgColor; + + /// Color of the small triangle next to multiple skin tone emoji + final Color skinToneIndicatorColor; + + /// Enable feature to select a skin tone of certain emoji's + final bool enableSkinTones; + + /// Show extra tab with recently used emoji + final bool showRecentsTab; + + /// Limit of recently used emoji that will be saved + final int recentsLimit; + + /// A widget (usually [Text]) to be displayed if no recent emojis to display + final Widget noRecents; + + /// Duration of tab indicator to animate to next category + final Duration tabIndicatorAnimDuration; + + /// Determines the icon to display for each [Category] + final CategoryIcons categoryIcons; + + /// Change between Material and Cupertino button style + final ButtonMode buttonMode; + + /// The padding of GridView, default is [EdgeInsets.zero] + final EdgeInsets gridPadding; + + /// Replace latest emoji on recents list on limit exceed + final bool replaceEmojiOnLimitExceed; + + /// Get Emoji size based on properties and screen width + double getEmojiSize(double width) { + final maxSize = width / columns; + return min(maxSize, emojiSizeMax); + } + + /// Returns the icon for the category + IconData getIconForCategory(Category category) { + switch (category) { + case Category.RECENT: + return categoryIcons.recentIcon; + case Category.SMILEYS: + return categoryIcons.smileyIcon; + case Category.ANIMALS: + return categoryIcons.animalIcon; + case Category.FOODS: + return categoryIcons.foodIcon; + case Category.TRAVEL: + return categoryIcons.travelIcon; + case Category.ACTIVITIES: + return categoryIcons.activityIcon; + case Category.OBJECTS: + return categoryIcons.objectIcon; + case Category.SYMBOLS: + return categoryIcons.symbolIcon; + case Category.FLAGS: + return categoryIcons.flagIcon; + default: + throw Exception('Unsupported Category'); + } + } + + @override + bool operator ==(other) { + return (other is Config) && + other.columns == columns && + other.emojiSizeMax == emojiSizeMax && + other.verticalSpacing == verticalSpacing && + other.horizontalSpacing == horizontalSpacing && + other.initCategory == initCategory && + other.bgColor == bgColor && + other.indicatorColor == indicatorColor && + other.iconColor == iconColor && + other.iconColorSelected == iconColorSelected && + other.progressIndicatorColor == progressIndicatorColor && + other.backspaceColor == backspaceColor && + other.skinToneDialogBgColor == skinToneDialogBgColor && + other.skinToneIndicatorColor == skinToneIndicatorColor && + other.enableSkinTones == enableSkinTones && + other.showRecentsTab == showRecentsTab && + other.recentsLimit == recentsLimit && + other.noRecents == noRecents && + other.tabIndicatorAnimDuration == tabIndicatorAnimDuration && + other.categoryIcons == categoryIcons && + other.buttonMode == buttonMode && + other.gridPadding == gridPadding && + other.replaceEmojiOnLimitExceed == replaceEmojiOnLimitExceed; + } + + @override + int get hashCode => + columns.hashCode ^ + emojiSizeMax.hashCode ^ + verticalSpacing.hashCode ^ + horizontalSpacing.hashCode ^ + initCategory.hashCode ^ + bgColor.hashCode ^ + indicatorColor.hashCode ^ + iconColor.hashCode ^ + iconColorSelected.hashCode ^ + progressIndicatorColor.hashCode ^ + backspaceColor.hashCode ^ + skinToneDialogBgColor.hashCode ^ + skinToneIndicatorColor.hashCode ^ + enableSkinTones.hashCode ^ + showRecentsTab.hashCode ^ + recentsLimit.hashCode ^ + noRecents.hashCode ^ + tabIndicatorAnimDuration.hashCode ^ + categoryIcons.hashCode ^ + buttonMode.hashCode ^ + gridPadding.hashCode ^ + replaceEmojiOnLimitExceed.hashCode; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/default_emoji_picker_view.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/default_emoji_picker_view.dart new file mode 100644 index 0000000..5247ef8 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/default_emoji_picker_view.dart @@ -0,0 +1,375 @@ +import '../emoji_picker_flutter.dart'; +import './category_emoji.dart'; +import './emoji_picker_internal_utils.dart'; +import './emoji_skin_tones.dart'; +import './triangle_shape.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// Default EmojiPicker Implementation +class DefaultEmojiPickerView extends EmojiPickerBuilder { + /// Constructor + DefaultEmojiPickerView(Config config, EmojiViewState state) + : super(config, state); + + @override + _DefaultEmojiPickerViewState createState() => _DefaultEmojiPickerViewState(); +} + +class _DefaultEmojiPickerViewState extends State + with SingleTickerProviderStateMixin { + PageController? _pageController; + TabController? _tabController; + OverlayEntry? _overlay; + late final _scrollController = ScrollController(); + late final _utils = EmojiPickerInternalUtils(); + final int _skinToneCount = 6; + final double tabBarHeight = 46; + + @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) + ..addListener(_closeSkinToneDialog); + _scrollController.addListener(_closeSkinToneDialog); + super.initState(); + } + + @override + void dispose() { + _closeSkinToneDialog(); + super.dispose(); + } + + void _closeSkinToneDialog() { + _overlay?.remove(); + _overlay = null; + } + + void _openSkinToneDialog( + Emoji emoji, + double emojiSize, + CategoryEmoji categoryEmoji, + int index, + ) { + _overlay = _buildSkinToneOverlay( + emoji, + emojiSize, + categoryEmoji, + index, + ); + Overlay.of(context)?.insert(_overlay!); + } + + Widget _buildBackspaceButton() { + if (widget.state.onBackspacePressed != null) { + return Material( + type: MaterialType.transparency, + child: IconButton( + padding: const EdgeInsets.only(bottom: 2), + icon: Icon( + Icons.backspace, + color: widget.config.backspaceColor, + ), + onPressed: () { + widget.state.onBackspacePressed!(); + }), + ); + } + return Container(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final emojiSize = widget.config.getEmojiSize(constraints.maxWidth); + + return Container( + color: widget.config.bgColor, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: SizedBox( + height: tabBarHeight, + child: TabBar( + labelColor: widget.config.iconColorSelected, + indicatorColor: widget.config.indicatorColor, + unselectedLabelColor: widget.config.iconColor, + controller: _tabController, + labelPadding: EdgeInsets.zero, + onTap: (index) { + _closeSkinToneDialog(); + _pageController!.jumpToPage(index); + }, + tabs: widget.state.categoryEmoji + .asMap() + .entries + .map((item) => + _buildCategory(item.key, item.value.category)) + .toList(), + ), + ), + ), + _buildBackspaceButton(), + ], + ), + 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]), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildCategory(int index, Category category) { + return Tab( + icon: Icon( + widget.config.getIconForCategory(category), + ), + ); + } + + 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 GestureDetector( + onTap: _closeSkinToneDialog, + child: GridView.count( + scrollDirection: Axis.vertical, + physics: const ScrollPhysics(), + controller: _scrollController, + shrinkWrap: true, + primary: false, + padding: widget.config.gridPadding, + crossAxisCount: widget.config.columns, + mainAxisSpacing: widget.config.verticalSpacing, + crossAxisSpacing: widget.config.horizontalSpacing, + children: categoryEmoji.emoji.asMap().entries.map((item) { + final index = item.key; + final emoji = item.value; + final onPressed = () { + _closeSkinToneDialog(); + widget.state.onEmojiSelected(categoryEmoji.category, emoji); + }; + + final onLongPressed = () { + if (!emoji.hasSkinTone || !widget.config.enableSkinTones) { + _closeSkinToneDialog(); + return; + } + _closeSkinToneDialog(); + _openSkinToneDialog(emoji, emojiSize, categoryEmoji, index); + }; + + return _buildButtonWidget( + onPressed: onPressed, + onLongPressed: onLongPressed, + child: _buildEmoji( + emojiSize, + categoryEmoji, + emoji, + widget.config.enableSkinTones, + ), + ); + }).toList(), + ), + ); + } + + /// Build and display Emoji centered of its parent + Widget _buildEmoji( + double emojiSize, + CategoryEmoji categoryEmoji, + Emoji emoji, + bool showSkinToneIndicator, + ) { + // FittedBox needed for display, font scale settings + return FittedBox( + fit: BoxFit.fill, + child: Stack(children: [ + emoji.hasSkinTone && showSkinToneIndicator + ? Positioned( + bottom: 0, + right: 0, + child: CustomPaint( + size: const Size(8, 8), + painter: TriangleShape(widget.config.skinToneIndicatorColor), + ), + ) + : Container(), + Text( + emoji.emoji, + textScaleFactor: 1.0, + style: TextStyle( + fontSize: emojiSize, + backgroundColor: Colors.transparent, + ), + ), + ]), + ); + } + + /// Build different Button based on ButtonMode + Widget _buildButtonWidget({ + required VoidCallback onPressed, + required VoidCallback onLongPressed, + required Widget child, + }) { + if (widget.config.buttonMode == ButtonMode.MATERIAL) { + return TextButton( + onPressed: onPressed, + onLongPress: onLongPressed, + child: child, + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + minimumSize: MaterialStateProperty.all(Size.zero), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ); + } + return GestureDetector( + onLongPress: onLongPressed, + child: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + child: child, + ), + ); + } + + /// Build Widget for when no recent emoji are available + Widget _buildNoRecent() { + return Center( + child: widget.config.noRecents, + ); + } + + /// Overlay for SkinTone + OverlayEntry _buildSkinToneOverlay( + Emoji emoji, + double emojiSize, + CategoryEmoji categoryEmoji, + int index, + ) { + // Calculate position of emoji in the grid + final row = index ~/ widget.config.columns; + final column = index % widget.config.columns; + // Calculate position for skin tone dialog + final renderBox = context.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal(Offset.zero); + final emojiSpace = renderBox.size.width / widget.config.columns; + final topOffset = emojiSpace; + final leftOffset = _getLeftOffset(emojiSpace, column); + final left = offset.dx + column * emojiSpace + leftOffset; + final top = tabBarHeight + + offset.dy + + row * emojiSpace - + _scrollController.offset - + topOffset; + + // Generate other skintone options + final skinTonesEmoji = SkinTone.values + .map((skinTone) => _utils.applySkinTone(emoji, skinTone)) + .toList(); + + return OverlayEntry( + builder: (context) => Positioned( + left: left, + top: top, + child: Material( + elevation: 4.0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4.0), + color: widget.config.skinToneDialogBgColor, + child: Row( + children: [ + _buildSkinToneEmoji( + categoryEmoji, emoji, emojiSpace, emojiSize), + _buildSkinToneEmoji( + categoryEmoji, skinTonesEmoji[0], emojiSpace, emojiSize), + _buildSkinToneEmoji( + categoryEmoji, skinTonesEmoji[1], emojiSpace, emojiSize), + _buildSkinToneEmoji( + categoryEmoji, skinTonesEmoji[2], emojiSpace, emojiSize), + _buildSkinToneEmoji( + categoryEmoji, skinTonesEmoji[3], emojiSpace, emojiSize), + _buildSkinToneEmoji( + categoryEmoji, skinTonesEmoji[4], emojiSpace, emojiSize), + ], + ), + ), + ), + ), + ); + } + + // Build Emoji inside skin tone dialog + Widget _buildSkinToneEmoji( + CategoryEmoji categoryEmoji, + Emoji emoji, + double width, + double emojiSize, + ) { + return SizedBox( + width: width, + height: width, + child: _buildButtonWidget( + onPressed: () { + widget.state.onEmojiSelected(categoryEmoji.category, emoji); + _closeSkinToneDialog(); + }, + onLongPressed: () {}, + child: _buildEmoji(emojiSize, categoryEmoji, emoji, false), + ), + ); + } + + // Calucates the offset from the middle of selected emoji to the left side + // of the skin tone dialog + // Case 1: Selected Emoji is close to left border and offset needs to be + // reduced + // Case 2: Selected Emoji is close to right border and offset needs to be + // larger than half of the whole width + // Case 3: Enough space to left and right border and offset can be half + // of whole width + double _getLeftOffset(double emojiWidth, int column) { + var remainingColumns = + widget.config.columns - (column + 1 + (_skinToneCount ~/ 2)); + if (column >= 0 && column < 3) { + return -1 * column * emojiWidth; + } else if (remainingColumns < 0) { + return -1 * + ((_skinToneCount ~/ 2 - 1) + -1 * remainingColumns) * + emojiWidth; + } + return -1 * ((_skinToneCount ~/ 2) * emojiWidth) + emojiWidth / 2; + } +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji.dart new file mode 100644 index 0000000..125db19 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +/// A class to store data for each individual emoji +@immutable +class Emoji { + /// Emoji constructor + const Emoji(this.name, this.emoji, {this.hasSkinTone = false}); + + /// The name or description for this emoji + final String name; + + /// The unicode string for this emoji + /// + /// This is the string that should be displayed to view the emoji + final String emoji; + + /// Flag if emoji supports multiple skin tones + final bool hasSkinTone; + + @override + String toString() { + return 'Name: $name, Emoji: $emoji, HasSkinTone: $hasSkinTone'; + } + + /// Parse Emoji from json + static Emoji fromJson(Map json) { + return Emoji( + json['name'] as String, + json['emoji'] as String, + hasSkinTone: + json['hasSkinTone'] != null ? json['hasSkinTone'] as bool : false, + ); + } + + /// Encode Emoji to json + Map toJson() { + return { + 'name': name, + 'emoji': emoji, + 'hasSkinTone': hasSkinTone, + }; + } + + /// Copy method + Emoji copyWith({String? name, String? emoji, bool? hasSkinTone}) { + return Emoji( + name ?? this.name, + emoji ?? this.emoji, + hasSkinTone: hasSkinTone ?? this.hasSkinTone, + ); + } +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_lists.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_lists.dart new file mode 100644 index 0000000..dac3fa3 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_lists.dart @@ -0,0 +1,3409 @@ +// Copyright information +// File originally from https://github.com/JeffG05/emoji_picker + +/// Emoji Version +const int version = 2; + +/// Map of all possible emojis along with their names in [Category.SMILEYS] +final Map smileys = Map.fromIterables([ + 'Grinning Face', + 'Grinning Face With Big Eyes', + 'Grinning Face With Smiling Eyes', + 'Beaming Face With Smiling Eyes', + 'Grinning Squinting Face', + 'Grinning Face With Sweat', + 'Rolling on the Floor Laughing', + 'Face With Tears of Joy', + 'Slightly Smiling Face', + 'Upside-Down Face', + 'Winking Face', + 'Smiling Face With Smiling Eyes', + 'Smiling Face With Halo', + 'Smiling Face With Hearts', + 'Smiling Face With Heart-Eyes', + 'Star-Struck', + 'Face Blowing a Kiss', + 'Kissing Face', + 'Smiling Face', + 'Kissing Face With Closed Eyes', + 'Kissing Face With Smiling Eyes', + 'Face Savoring Food', + 'Face With Tongue', + 'Winking Face With Tongue', + 'Zany Face', + 'Squinting Face With Tongue', + 'Money-Mouth Face', + 'Hugging Face', + 'Face With Hand Over Mouth', + 'Shushing Face', + 'Thinking Face', + 'Zipper-Mouth Face', + 'Face With Raised Eyebrow', + 'Neutral Face', + 'Expressionless Face', + 'Face Without Mouth', + 'Smirking Face', + 'Unamused Face', + 'Face With Rolling Eyes', + 'Grimacing Face', + 'Lying Face', + 'Relieved Face', + 'Pensive Face', + 'Sleepy Face', + 'Drooling Face', + 'Sleeping Face', + 'Face With Medical Mask', + 'Face With Thermometer', + 'Face With Head-Bandage', + 'Nauseated Face', + 'Face Vomiting', + 'Sneezing Face', + 'Hot Face', + 'Cold Face', + 'Woozy Face', + 'Dizzy Face', + 'Exploding Head', + 'Cowboy Hat Face', + 'Partying Face', + 'Smiling Face With Sunglasses', + 'Nerd Face', + 'Face With Monocle', + 'Confused Face', + 'Worried Face', + 'Slightly Frowning Face', + 'Frowning Face', + 'Face With Open Mouth', + 'Hushed Face', + 'Astonished Face', + 'Flushed Face', + 'Pleading Face', + 'Frowning Face With Open Mouth', + 'Anguished Face', + 'Fearful Face', + 'Anxious Face With Sweat', + 'Sad but Relieved Face', + 'Crying Face', + 'Loudly Crying Face', + 'Face Screaming in Fear', + 'Confounded Face', + 'Persevering Face', + 'Disappointed Face', + 'Downcast Face With Sweat', + 'Weary Face', + 'Tired Face', + 'Face With Steam From Nose', + 'Pouting Face', + 'Angry Face', + 'Face With Symbols on Mouth', + 'Smiling Face With Horns', + 'Angry Face With Horns', + 'Skull', + 'Skull and Crossbones', + 'Pile of Poo', + 'Clown Face', + 'Ogre', + 'Goblin', + 'Ghost', + 'Alien', + 'Alien Monster', + 'Robot Face', + 'Grinning Cat Face', + 'Grinning Cat Face With Smiling Eyes', + 'Cat Face With Tears of Joy', + 'Smiling Cat Face With Heart-Eyes', + 'Cat Face With Wry Smile', + 'Kissing Cat Face', + 'Weary Cat Face', + 'Crying Cat Face', + 'Pouting Cat Face', + 'Kiss Mark', + 'Waving Hand', + 'Raised Back of Hand', + 'Hand With Fingers Splayed', + 'Raised Hand', + 'Vulcan Salute', + 'OK Hand', + 'Victory Hand', + 'Crossed Fingers', + 'Love-You Gesture', + 'Sign of the Horns', + 'Call Me Hand', + 'Backhand Index Pointing Left', + 'Backhand Index Pointing Right', + 'Backhand Index Pointing Up', + 'Middle Finger', + 'Backhand Index Pointing Down', + 'Index Pointing Up', + 'Thumbs Up', + 'Thumbs Down', + 'Raised Fist', + 'Oncoming Fist', + 'Left-Facing Fist', + 'Right-Facing Fist', + 'Clapping Hands', + 'Raising Hands', + 'Open Hands', + 'Palms Up Together', + 'Handshake', + 'Folded Hands', + 'Writing Hand', + 'Nail Polish', + 'Selfie', + 'Flexed Biceps', + 'Leg', + 'Foot', + 'Ear', + 'Nose', + 'Brain', + 'Tooth', + 'Bone', + 'Eyes', + 'Eye', + 'Tongue', + 'Mouth', + 'Baby', + 'Child', + 'Boy', + 'Girl', + 'Person', + 'Man', + 'Man: Beard', + 'Man: Blond Hair', + 'Man: Red Hair', + 'Man: Curly Hair', + 'Man: White Hair', + 'Man: Bald', + 'Woman', + 'Woman: Blond Hair', + 'Woman: Red Hair', + 'Woman: Curly Hair', + 'Woman: White Hair', + 'Woman: Bald', + 'Older Person', + 'Old Man', + 'Old Woman', + 'Man Frowning', + 'Woman Frowning', + 'Man Pouting', + 'Woman Pouting', + 'Man Gesturing No', + 'Woman Gesturing No', + 'Man Gesturing OK', + 'Woman Gesturing OK', + 'Man Tipping Hand', + 'Woman Tipping Hand', + 'Man Raising Hand', + 'Woman Raising Hand', + 'Man Bowing', + 'Woman Bowing', + 'Man Facepalming', + 'Woman Facepalming', + 'Man Shrugging', + 'Woman Shrugging', + 'Man Health Worker', + 'Woman Health Worker', + 'Man Student', + 'Woman Student', + 'Man Teacher', + 'Woman Teacher', + 'Man Judge', + 'Woman Judge', + 'Man Farmer', + 'Woman Farmer', + 'Man Cook', + 'Woman Cook', + 'Man Mechanic', + 'Woman Mechanic', + 'Man Factory Worker', + 'Woman Factory Worker', + 'Man Office Worker', + 'Woman Office Worker', + 'Man Scientist', + 'Woman Scientist', + 'Man Technologist', + 'Woman Technologist', + 'Man Singer', + 'Woman Singer', + 'Man Artist', + 'Woman Artist', + 'Man Pilot', + 'Woman Pilot', + 'Man Astronaut', + 'Woman Astronaut', + 'Man Firefighter', + 'Woman Firefighter', + 'Man Police Officer', + 'Woman Police Officer', + 'Man Detective', + 'Woman Detective', + 'Man Guard', + 'Woman Guard', + 'Man Construction Worker', + 'Woman Construction Worker', + 'Prince', + 'Princess', + 'Man Wearing Turban', + 'Woman Wearing Turban', + 'Man With Chinese Cap', + 'Woman With Headscarf', + 'Man in Tuxedo', + 'Bride With Veil', + 'Pregnant Woman', + 'Breast-Feeding', + 'Baby Angel', + 'Santa Claus', + 'Mrs. Claus', + 'Man Superhero', + 'Woman Superhero', + 'Man Supervillain', + 'Woman Supervillain', + 'Man Mage', + 'Woman Mage', + 'Man Fairy', + 'Woman Fairy', + 'Man Vampire', + 'Woman Vampire', + 'Merman', + 'Mermaid', + 'Man Elf', + 'Woman Elf', + 'Man Genie', + 'Woman Genie', + 'Man Zombie', + 'Woman Zombie', + 'Man Getting Massage', + 'Woman Getting Massage', + 'Man Getting Haircut', + 'Woman Getting Haircut', + 'Man Walking', + 'Woman Walking', + 'Man Running', + 'Woman Running', + 'Man Dancing', + 'Woman Dancing', + 'Man in Suit Levitating', + 'Men With Bunny Ears', + 'Women With Bunny Ears', + 'Man in Steamy Room', + 'Woman in Steamy Room', + 'Person in Lotus Position', + 'Women Holding Hands', + 'Woman and Man Holding Hands', + 'Men Holding Hands', + 'Kiss', + 'Kiss: Man, Man', + 'Kiss: Woman, Woman', + 'Couple With Heart', + 'Couple With Heart: Man, Man', + 'Couple With Heart: Woman, Woman', + 'Family', + 'Family: Man, Woman, Boy', + 'Family: Man, Woman, Girl', + 'Family: Man, Woman, Girl, Boy', + 'Family: Man, Woman, Boy, Boy', + 'Family: Man, Woman, Girl, Girl', + 'Family: Man, Man, Boy', + 'Family: Man, Man, Girl', + 'Family: Man, Man, Girl, Boy', + 'Family: Man, Man, Boy, Boy', + 'Family: Man, Man, Girl, Girl', + 'Family: Woman, Woman, Boy', + 'Family: Woman, Woman, Girl', + 'Family: Woman, Woman, Girl, Boy', + 'Family: Woman, Woman, Boy, Boy', + 'Family: Woman, Woman, Girl, Girl', + 'Family: Man, Boy', + 'Family: Man, Boy, Boy', + 'Family: Man, Girl', + 'Family: Man, Girl, Boy', + 'Family: Man, Girl, Girl', + 'Family: Woman, Boy', + 'Family: Woman, Boy, Boy', + 'Family: Woman, Girl', + 'Family: Woman, Girl, Boy', + 'Family: Woman, Girl, Girl', + 'Speaking Head', + 'Bust in Silhouette', + 'Busts in Silhouette', + 'Footprints', + 'Luggage', + 'Closed Umbrella', + 'Umbrella', + 'Thread', + 'Yarn', + 'Glasses', + 'Sunglasses', + 'Goggles', + 'Lab Coat', + 'Necktie', + 'T-Shirt', + 'Jeans', + 'Scarf', + 'Gloves', + 'Coat', + 'Socks', + 'Dress', + 'Kimono', + 'Bikini', + 'Woman’s Clothes', + 'Purse', + 'Handbag', + 'Clutch Bag', + 'Backpack', + 'Man’s Shoe', + 'Running Shoe', + 'Hiking Boot', + 'Flat Shoe', + 'High-Heeled Shoe', + 'Woman’s Sandal', + 'Woman’s Boot', + 'Crown', + 'Woman’s Hat', + 'Top Hat', + 'Graduation Cap', + 'Billed Cap', + 'Rescue Worker’s Helmet', + 'Lipstick', + 'Ring', + 'Briefcase' +], [ + '😀', + '😃', + '😄', + '😁', + '😆', + '😅', + '🤣', + '😂', + '🙂', + '🙃', + '😉', + '😊', + '😇', + '🥰', + '😍', + '🤩', + '😘', + '😗', + '☺', + '😚', + '😙', + '😋', + '😛', + '😜', + '🤪', + '😝', + '🤑', + '🤗', + '🤭', + '🤫', + '🤔', + '🤐', + '🤨', + '😐', + '😑', + '😶', + '😏', + '😒', + '🙄', + '😬', + '🤥', + '😌', + '😔', + '😪', + '🤤', + '😴', + '😷', + '🤒', + '🤕', + '🤢', + '🤮', + '🤧', + '🥵', + '🥶', + '🥴', + '😵', + '🤯', + '🤠', + '🥳', + '😎', + '🤓', + '🧐', + '😕', + '😟', + '🙁', + '☹', + '😮', + '😯', + '😲', + '😳', + '🥺', + '😦', + '😧', + '😨', + '😰', + '😥', + '😢', + '😭', + '😱', + '😖', + '😣', + '😞', + '😓', + '😩', + '😫', + '😤', + '😡', + '😠', + '🤬', + '😈', + '👿', + '💀', + '☠', + '💩', + '🤡', + '👹', + '👺', + '👻', + '👽', + '👾', + '🤖', + '😺', + '😸', + '😹', + '😻', + '😼', + '😽', + '🙀', + '😿', + '😾', + '💋', + '👋', + '🤚', + '🖐', + '✋', + '🖖', + '👌', + '✌', + '🤞', + '🤟', + '🤘', + '🤙', + '👈', + '👉', + '👆', + '🖕', + '👇', + '☝', + '👍', + '👎', + '✊', + '👊', + '🤛', + '🤜', + '👏', + '🙌', + '👐', + '🤲', + '🤝', + '🙏', + '✍', + '💅', + '🤳', + '💪', + '🦵', + '🦶', + '👂', + '👃', + '🧠', + '🦷', + '🦴', + '👀', + '👁', + '👅', + '👄', + '👶', + '🧒', + '👦', + '👧', + '🧑', + '👨', + '🧔', + '👱', + '👨‍🦰', + '👨‍🦱', + '👨‍🦳', + '👨‍🦲', + '👩', + '👱‍♀️', + '👩‍🦰', + '👩‍🦱', + '👩‍🦳', + '👩‍🦲', + '🧓', + '👴', + '👵', + '🙍‍♂️', + '🙍‍♀️', + '🙎‍♂️', + '🙎‍♀️', + '🙅‍♂️', + '🙅‍♀️', + '🙆‍♂️', + '🙆‍♀️', + '💁‍♂️', + '💁‍♀️', + '🙋‍♂️', + '🙋‍♀️', + '🙇‍♂️', + '🙇‍♀️', + '🤦‍♂️', + '🤦‍♀️', + '🤷‍♂️', + '🤷‍♀️', + '👨‍⚕️', + '👩‍⚕️', + '👨‍🎓', + '👩‍🎓', + '👨‍🏫', + '👩‍🏫', + '👨‍⚖️', + '👩‍⚖️', + '👨‍🌾', + '👩‍🌾', + '👨‍🍳', + '👩‍🍳', + '👨‍🔧', + '👩‍🔧', + '👨‍🏭', + '👩‍🏭', + '👨‍💼', + '👩‍💼', + '👨‍🔬', + '👩‍🔬', + '👨‍💻', + '👩‍💻', + '👨‍🎤', + '👩‍🎤', + '👨‍🎨', + '👩‍🎨', + '👨‍✈️', + '👩‍✈️', + '👨‍🚀', + '👩‍🚀', + '👨‍🚒', + '👩‍🚒', + '👮‍♂️', + '👮‍♀️', + '🕵️‍♂️', + '🕵️‍♀️', + '💂‍♂️', + '💂‍♀️', + '👷‍♂️', + '👷‍♀️', + '🤴', + '👸', + '👳‍♂️', + '👳‍♀️', + '👲', + '🧕', + '🤵', + '👰', + '🤰', + '🤱', + '👼', + '🎅', + '🤶', + '🦸‍♂️', + '🦸‍♀️', + '🦹‍♂️', + '🦹‍♀️', + '🧙‍♂️', + '🧙‍♀️', + '🧚‍♂️', + '🧚‍♀️', + '🧛‍♂️', + '🧛‍♀️', + '🧜‍♂️', + '🧜‍♀️', + '🧝‍♂️', + '🧝‍♀️', + '🧞‍♂️', + '🧞‍♀️', + '🧟‍♂️', + '🧟‍♀️', + '💆‍♂️', + '💆‍♀️', + '💇‍♂️', + '💇‍♀️', + '🚶‍♂️', + '🚶‍♀️', + '🏃‍♂️', + '🏃‍♀️', + '🕺', + '💃', + '🕴', + '👯‍♂️', + '👯‍♀️', + '🧖‍♂️', + '🧖‍♀️', + '🧘', + '👭', + '👫', + '👬', + '💏', + '👨‍❤️‍💋‍👨', + '👩‍❤️‍💋‍👩', + '💑', + '👨‍❤️‍👨', + '👩‍❤️‍👩', + '👪', + '👨‍👩‍👦', + '👨‍👩‍👧', + '👨‍👩‍👧‍👦', + '👨‍👩‍👦‍👦', + '👨‍👩‍👧‍👧', + '👨‍👨‍👦', + '👨‍👨‍👧', + '👨‍👨‍👧‍👦', + '👨‍👨‍👦‍👦', + '👨‍👨‍👧‍👧', + '👩‍👩‍👦', + '👩‍👩‍👧', + '👩‍👩‍👧‍👦', + '👩‍👩‍👦‍👦', + '👩‍👩‍👧‍👧', + '👨‍👦', + '👨‍👦‍👦', + '👨‍👧', + '👨‍👧‍👦', + '👨‍👧‍👧', + '👩‍👦', + '👩‍👦‍👦', + '👩‍👧', + '👩‍👧‍👦', + '👩‍👧‍👧', + '🗣', + '👤', + '👥', + '👣', + '🧳', + '🌂', + '☂', + '🧵', + '🧶', + '👓', + '🕶', + '🥽', + '🥼', + '👔', + '👕', + '👖', + '🧣', + '🧤', + '🧥', + '🧦', + '👗', + '👘', + '👙', + '👚', + '👛', + '👜', + '👝', + '🎒', + '👞', + '👟', + '🥾', + '🥿', + '👠', + '👡', + '👢', + '👑', + '👒', + '🎩', + '🎓', + '🧢', + '⛑', + '💄', + '💍', + '💼', +]); + +/// Map of all possible emojis along with their names in [Category.ANIMALS] +final Map animals = Map.fromIterables([ + 'Dog Face', + 'Cat Face', + 'Mouse Face', + 'Hamster Face', + 'Rabbit Face', + 'Fox Face', + 'Bear Face', + 'Panda Face', + 'Tiger Face', + 'Lion Face', + 'Cow Face', + 'Pig Face', + 'Pig Nose', + 'Frog Face', + 'Monkey Face', + 'See-No-Evil Monkey', + 'Hear-No-Evil Monkey', + 'Speak-No-Evil Monkey', + 'Monkey', + 'Collision', + 'Dizzy', + 'Sweat Droplets', + 'Dashing Away', + 'Gorilla', + 'Dog', + 'Poodle', + 'Wolf Face', + 'Raccoon', + 'Cat', + 'Tiger', + 'Leopard', + 'Horse Face', + 'Horse', + 'Unicorn Face', + 'Zebra', + 'Ox', + 'Water Buffalo', + 'Cow', + 'Pig', + 'Boar', + 'Ram', + 'Ewe', + 'Goat', + 'Camel', + 'Two-Hump Camel', + 'Llama', + 'Giraffe', + 'Elephant', + 'Rhinoceros', + 'Hippopotamus', + 'Mouse', + 'Rat', + 'Rabbit', + 'Chipmunk', + 'Hedgehog', + 'Bat', + 'Koala', + 'Kangaroo', + 'Badger', + 'Paw Prints', + 'Turkey', + 'Chicken', + 'Rooster', + 'Hatching Chick', + 'Baby Chick', + 'Front-Facing Baby Chick', + 'Bird', + 'Penguin', + 'Dove', + 'Eagle', + 'Duck', + 'Swan', + 'Owl', + 'Peacock', + 'Parrot', + 'Crocodile', + 'Turtle', + 'Lizard', + 'Snake', + 'Dragon Face', + 'Dragon', + 'Sauropod', + 'T-Rex', + 'Spouting Whale', + 'Whale', + 'Dolphin', + 'Fish', + 'Tropical Fish', + 'Blowfish', + 'Shark', + 'Octopus', + 'Spiral Shell', + 'Snail', + 'Butterfly', + 'Bug', + 'Ant', + 'Honeybee', + 'Lady Beetle', + 'Cricket', + 'Spider', + 'Spider Web', + 'Scorpion', + 'Mosquito', + 'Microbe', + 'Bouquet', + 'Cherry Blossom', + 'White Flower', + 'Rosette', + 'Rose', + 'Wilted Flower', + 'Hibiscus', + 'Sunflower', + 'Blossom', + 'Tulip', + 'Seedling', + 'Evergreen Tree', + 'Deciduous Tree', + 'Palm Tree', + 'Cactus', + 'Sheaf of Rice', + 'Herb', + 'Shamrock', + 'Four Leaf Clover', + 'Maple Leaf', + 'Fallen Leaf', + 'Leaf Fluttering in Wind', + 'Mushroom', + 'Chestnut', + 'Crab', + 'Lobster', + 'Shrimp', + 'Squid', + 'Globe Showing Europe-Africa', + 'Globe Showing Americas', + 'Globe Showing Asia-Australia', + 'Globe With Meridians', + 'New Moon', + 'Waxing Crescent Moon', + 'First Quarter Moon', + 'Waxing Gibbous Moon', + 'Full Moon', + 'Waning Gibbous Moon', + 'Last Quarter Moon', + 'Waning Crescent Moon', + 'Crescent Moon', + 'New Moon Face', + 'First Quarter Moon Face', + 'Last Quarter Moon Face', + 'Sun', + 'Full Moon Face', + 'Sun With Face', + 'Star', + 'Glowing Star', + 'Shooting Star', + 'Cloud', + 'Sun Behind Cloud', + 'Cloud With Lightning and Rain', + 'Sun Behind Small Cloud', + 'Sun Behind Large Cloud', + 'Sun Behind Rain Cloud', + 'Cloud With Rain', + 'Cloud With Snow', + 'Cloud With Lightning', + 'Tornado', + 'Fog', + 'Wind Face', + 'Rainbow', + 'Umbrella', + 'Umbrella With Rain Drops', + 'High Voltage', + 'Snowflake', + 'Snowman', + 'Snowman Without Snow', + 'Comet', + 'Fire', + 'Droplet', + 'Water Wave', + 'Christmas Tree', + 'Sparkles', + 'Tanabata Tree', + 'Pine Decoration' +], [ + '🐶', + '🐱', + '🐭', + '🐹', + '🐰', + '🦊', + '🐻', + '🐼', + '🐨', + '🐯', + '🦁', + '🐮', + '🐷', + '🐽', + '🐸', + '🐵', + '🙈', + '🙉', + '🙊', + '🐒', + '💥', + '💫', + '💦', + '💨', + '🦍', + '🐕', + '🐩', + '🐺', + '🦝', + '🐈', + '🐅', + '🐆', + '🐴', + '🐎', + '🦄', + '🦓', + '🐂', + '🐃', + '🐄', + '🐖', + '🐗', + '🐏', + '🐑', + '🐐', + '🐪', + '🐫', + '🦙', + '🦒', + '🐘', + '🦏', + '🦛', + '🐁', + '🐀', + '🐇', + '🐿', + '🦔', + '🦇', + '🦘', + '🦡', + '🐾', + '🦃', + '🐔', + '🐓', + '🐣', + '🐤', + '🐥', + '🐦', + '🐧', + '🕊', + '🦅', + '🦆', + '🦢', + '🦉', + '🦚', + '🦜', + '🐊', + '🐢', + '🦎', + '🐍', + '🐲', + '🐉', + '🦕', + '🦖', + '🐳', + '🐋', + '🐬', + '🐟', + '🐠', + '🐡', + '🦈', + '🐙', + '🐚', + '🐌', + '🦋', + '🐛', + '🐜', + '🐝', + '🐞', + '🦗', + '🕷', + '🕸', + '🦂', + '🦟', + '🦠', + '💐', + '🌸', + '💮', + '🏵', + '🌹', + '🥀', + '🌺', + '🌻', + '🌼', + '🌷', + '🌱', + '🌲', + '🌳', + '🌴', + '🌵', + '🌾', + '🌿', + '☘', + '🍀', + '🍁', + '🍂', + '🍃', + '🍄', + '🌰', + '🦀', + '🦞', + '🦐', + '🦑', + '🌍', + '🌎', + '🌏', + '🌐', + '🌑', + '🌒', + '🌓', + '🌔', + '🌕', + '🌖', + '🌗', + '🌘', + '🌙', + '🌚', + '🌛', + '🌜', + '☀', + '🌝', + '🌞', + '⭐', + '🌟', + '🌠', + '☁', + '⛅', + '⛈', + '🌤', + '🌥', + '🌦', + '🌧', + '🌨', + '🌩', + '🌪', + '🌫', + '🌬', + '🌈', + '☂', + '☔', + '⚡', + '❄', + '☃', + '⛄', + '☄', + '🔥', + '💧', + '🌊', + '🎄', + '✨', + '🎋', + '🎍' +]); + +/// Map of all possible emojis along with their names in [Category.FOODS] +final Map foods = Map.fromIterables([ + 'Grapes', + 'Melon', + 'Watermelon', + 'Tangerine', + 'Lemon', + 'Banana', + 'Pineapple', + 'Mango', + 'Red Apple', + 'Green Apple', + 'Pear', + 'Peach', + 'Cherries', + 'Strawberry', + 'Kiwi Fruit', + 'Tomato', + 'Coconut', + 'Avocado', + 'Eggplant', + 'Potato', + 'Carrot', + 'Ear of Corn', + 'Hot Pepper', + 'Cucumber', + 'Leafy Green', + 'Broccoli', + 'Mushroom', + 'Peanuts', + 'Chestnut', + 'Bread', + 'Croissant', + 'Baguette Bread', + 'Pretzel', + 'Bagel', + 'Pancakes', + 'Cheese Wedge', + 'Meat on Bone', + 'Poultry Leg', + 'Cut of Meat', + 'Bacon', + 'Hamburger', + 'French Fries', + 'Pizza', + 'Hot Dog', + 'Sandwich', + 'Taco', + 'Burrito', + 'Stuffed Flatbread', + 'Cooking', + 'Shallow Pan of Food', + 'Pot of Food', + 'Bowl With Spoon', + 'Green Salad', + 'Popcorn', + 'Salt', + 'Canned Food', + 'Bento Box', + 'Rice Cracker', + 'Rice Ball', + 'Cooked Rice', + 'Curry Rice', + 'Steaming Bowl', + 'Spaghetti', + 'Roasted Sweet Potato', + 'Oden', + 'Sushi', + 'Fried Shrimp', + 'Fish Cake With Swirl', + 'Moon Cake', + 'Dango', + 'Dumpling', + 'Fortune Cookie', + 'Takeout Box', + 'Soft Ice Cream', + 'Shaved Ice', + 'Ice Cream', + 'Doughnut', + 'Cookie', + 'Birthday Cake', + 'Shortcake', + 'Cupcake', + 'Pie', + 'Chocolate Bar', + 'Candy', + 'Lollipop', + 'Custard', + 'Honey Pot', + 'Baby Bottle', + 'Glass of Milk', + 'Hot Beverage', + 'Teacup Without Handle', + 'Mate Drink', + 'Sake', + 'Bottle With Popping Cork', + 'Wine Glass', + 'Cocktail Glass', + 'Tropical Drink', + 'Beer Mug', + 'Clinking Beer Mugs', + 'Clinking Glasses', + 'Tumbler Glass', + 'Cup With Straw', + 'Chopsticks', + 'Fork and Knife With Plate', + 'Fork and Knife', + 'Spoon' +], [ + '🍇', + '🍈', + '🍉', + '🍊', + '🍋', + '🍌', + '🍍', + '🥭', + '🍎', + '🍏', + '🍐', + '🍑', + '🍒', + '🍓', + '🥝', + '🍅', + '🥥', + '🥑', + '🍆', + '🥔', + '🥕', + '🌽', + '🌶', + '🥒', + '🥬', + '🥦', + '🍄', + '🥜', + '🌰', + '🍞', + '🥐', + '🥖', + '🥨', + '🥯', + '🥞', + '🧀', + '🍖', + '🍗', + '🥩', + '🥓', + '🍔', + '🍟', + '🍕', + '🌭', + '🥪', + '🌮', + '🌯', + '🥙', + '🍳', + '🥘', + '🍲', + '🥣', + '🥗', + '🍿', + '🧂', + '🥫', + '🍱', + '🍘', + '🍙', + '🍚', + '🍛', + '🍜', + '🍝', + '🍠', + '🍢', + '🍣', + '🍤', + '🍥', + '🥮', + '🍡', + '🥟', + '🥠', + '🥡', + '🍦', + '🍧', + '🍨', + '🍩', + '🍪', + '🎂', + '🍰', + '🧁', + '🥧', + '🍫', + '🍬', + '🍭', + '🍮', + '🍯', + '🍼', + '🥛', + '☕', + '🍵', + '🧉', + '🍶', + '🍾', + '🍷', + '🍸', + '🍹', + '🍺', + '🍻', + '🥂', + '🥃', + '🥤', + '🥢', + '🍽', + '🍴', + '🥄' +]); + +/// Map of all possible emojis along with their names in [Category.TRAVEL] +final Map travel = Map.fromIterables([ + 'Map of Japan', + 'Snow-Capped Mountain', + 'Mountain', + 'Volcano', + 'Mount Fuji', + 'Camping', + 'Beach With Umbrella', + 'Desert', + 'Desert Island', + 'National Park', + 'Stadium', + 'Classical Building', + 'Building Construction', + 'Houses', + 'Derelict House', + 'House', + 'House With Garden', + 'Office Building', + 'Japanese Post Office', + 'Post Office', + 'Hospital', + 'Bank', + 'Hotel', + 'Love Hotel', + 'Convenience Store', + 'School', + 'Department Store', + 'Factory', + 'Japanese Castle', + 'Castle', + 'Wedding', + 'Tokyo Tower', + 'Statue of Liberty', + 'Church', + 'Mosque', + 'Synagogue', + 'Shinto Shrine', + 'Kaaba', + 'Fountain', + 'Tent', + 'Foggy', + 'Night With Stars', + 'Cityscape', + 'Sunrise Over Mountains', + 'Sunrise', + 'Cityscape at Dusk', + 'Sunset', + 'Bridge at Night', + 'Carousel Horse', + 'Ferris Wheel', + 'Roller Coaster', + 'Locomotive', + 'Railway Car', + 'High-Speed Train', + 'Bullet Train', + 'Train', + 'Metro', + 'Light Rail', + 'Station', + 'Tram', + 'Monorail', + 'Mountain Railway', + 'Tram Car', + 'Bus', + 'Oncoming Bus', + 'Trolleybus', + 'Minibus', + 'Ambulance', + 'Fire Engine', + 'Police Car', + 'Oncoming Police Car', + 'Taxi', + 'Oncoming Taxi', + 'Automobile', + 'Oncoming Automobile', + 'Delivery Truck', + 'Articulated Lorry', + 'Tractor', + 'Racing Car', + 'Motorcycle', + 'Motor Scooter', + 'Bicycle', + 'Kick Scooter', + 'Bus Stop', + 'Railway Track', + 'Fuel Pump', + 'Police Car Light', + 'Horizontal Traffic Light', + 'Vertical Traffic Light', + 'Construction', + 'Anchor', + 'Sailboat', + 'Speedboat', + 'Passenger Ship', + 'Ferry', + 'Motor Boat', + 'Ship', + 'Airplane', + 'Small Airplane', + 'Airplane Departure', + 'Airplane Arrival', + 'Seat', + 'Helicopter', + 'Suspension Railway', + 'Mountain Cableway', + 'Aerial Tramway', + 'Satellite', + 'Rocket', + 'Flying Saucer', + 'Shooting Star', + 'Milky Way', + 'Umbrella on Ground', + 'Fireworks', + 'Sparkler', + 'Moon Viewing Ceremony', + 'Yen Banknote', + 'Dollar Banknote', + 'Euro Banknote', + 'Pound Banknote', + 'Moai', + 'Passport Control', + 'Customs', + 'Baggage Claim', + 'Left Luggage' +], [ + '🗾', + '🏔', + '⛰', + '🌋', + '🗻', + '🏕', + '🏖', + '🏜', + '🏝', + '🏞', + '🏟', + '🏛', + '🏗', + '🏘', + '🏚', + '🏠', + '🏡', + '🏢', + '🏣', + '🏤', + '🏥', + '🏦', + '🏨', + '🏩', + '🏪', + '🏫', + '🏬', + '🏭', + '🏯', + '🏰', + '💒', + '🗼', + '🗽', + '⛪', + '🕌', + '🕍', + '⛩', + '🕋', + '⛲', + '⛺', + '🌁', + '🌃', + '🏙', + '🌄', + '🌅', + '🌆', + '🌇', + '🌉', + '🎠', + '🎡', + '🎢', + '🚂', + '🚃', + '🚄', + '🚅', + '🚆', + '🚇', + '🚈', + '🚉', + '🚊', + '🚝', + '🚞', + '🚋', + '🚌', + '🚍', + '🚎', + '🚐', + '🚑', + '🚒', + '🚓', + '🚔', + '🚕', + '🚖', + '🚗', + '🚘', + '🚚', + '🚛', + '🚜', + '🏎', + '🏍', + '🛵', + '🚲', + '🛴', + '🚏', + '🛤', + '⛽', + '🚨', + '🚥', + '🚦', + '🚧', + '⚓', + '⛵', + '🚤', + '🛳', + '⛴', + '🛥', + '🚢', + '✈', + '🛩', + '🛫', + '🛬', + '💺', + '🚁', + '🚟', + '🚠', + '🚡', + '🛰', + '🚀', + '🛸', + '🌠', + '🌌', + '⛱', + '🎆', + '🎇', + '🎑', + '💴', + '💵', + '💶', + '💷', + '🗿', + '🛂', + '🛃', + '🛄', + '🛅' +]); + +/// Map of all possible emojis along with their names in [Category.ACTIVITIES] +final Map activities = Map.fromIterables([ + 'Man Climbing', + 'Woman Climbing', + 'Horse Racing', + 'Skier', + 'Snowboarder', + 'Man Golfing', + 'Woman Golfing', + 'Man Surfing', + 'Woman Surfing', + 'Man Rowing Boat', + 'Woman Rowing Boat', + 'Man Swimming', + 'Woman Swimming', + 'Man Bouncing Ball', + 'Woman Bouncing Ball', + 'Man Lifting Weights', + 'Woman Lifting Weights', + 'Man Biking', + 'Woman Biking', + 'Man Mountain Biking', + 'Woman Mountain Biking', + 'Man Cartwheeling', + 'Woman Cartwheeling', + 'Men Wrestling', + 'Women Wrestling', + 'Man Playing Water Polo', + 'Woman Playing Water Polo', + 'Man Playing Handball', + 'Woman Playing Handball', + 'Man Juggling', + 'Woman Juggling', + 'Man in Lotus Position', + 'Woman in Lotus Position', + 'Circus Tent', + 'Skateboard', + 'Reminder Ribbon', + 'Admission Tickets', + 'Ticket', + 'Military Medal', + 'Trophy', + 'Sports Medal', + '1st Place Medal', + '2nd Place Medal', + '3rd Place Medal', + 'Soccer Ball', + 'Baseball', + 'Softball', + 'Basketball', + 'Volleyball', + 'American Football', + 'Rugby Football', + 'Tennis', + 'Flying Disc', + 'Bowling', + 'Cricket Game', + 'Field Hockey', + 'Ice Hockey', + 'Lacrosse', + 'Ping Pong', + 'Badminton', + 'Boxing Glove', + 'Martial Arts Uniform', + 'Flag in Hole', + 'Ice Skate', + 'Fishing Pole', + 'Running Shirt', + 'Skis', + 'Sled', + 'Curling Stone', + 'Direct Hit', + 'Pool 8 Ball', + 'Video Game', + 'Slot Machine', + 'Game Die', + 'Jigsaw', + 'Chess Pawn', + 'Performing Arts', + 'Artist Palette', + 'Thread', + 'Yarn', + 'Musical Score', + 'Microphone', + 'Headphone', + 'Saxophone', + 'Guitar', + 'Musical Keyboard', + 'Trumpet', + 'Violin', + 'Drum', + 'Clapper Board', + 'Bow and Arrow' +], [ + '🧗‍♂️', + '🧗‍♀️', + '🏇', + '⛷', + '🏂', + '🏌️‍♂️', + '🏌️‍♀️', + '🏄‍♂️', + '🏄‍♀️', + '🚣‍♂️', + '🚣‍♀️', + '🏊‍♂️', + '🏊‍♀️', + '⛹️‍♂️', + '⛹️‍♀️', + '🏋️‍♂️', + '🏋️‍♀️', + '🚴‍♂️', + '🚴‍♀️', + '🚵‍♂️', + '🚵‍♀️', + '🤸‍♂️', + '🤸‍♀️', + '🤼‍♂️', + '🤼‍♀️', + '🤽‍♂️', + '🤽‍♀️', + '🤾‍♂️', + '🤾‍♀️', + '🤹‍♂️', + '🤹‍♀️', + '🧘‍♂️', + '🧘‍♀️', + '🎪', + '🛹', + '🎗', + '🎟', + '🎫', + '🎖', + '🏆', + '🏅', + '🥇', + '🥈', + '🥉', + '⚽', + '⚾', + '🥎', + '🏀', + '🏐', + '🏈', + '🏉', + '🎾', + '🥏', + '🎳', + '🏏', + '🏑', + '🏒', + '🥍', + '🏓', + '🏸', + '🥊', + '🥋', + '⛳', + '⛸', + '🎣', + '🎽', + '🎿', + '🛷', + '🥌', + '🎯', + '🎱', + '🎮', + '🎰', + '🎲', + '🧩', + '♟', + '🎭', + '🎨', + '🧵', + '🧶', + '🎼', + '🎤', + '🎧', + '🎷', + '🎸', + '🎹', + '🎺', + '🎻', + '🥁', + '🎬', + '🏹' +]); + +/// Map of all possible emojis along with their names in [Category.OBJECTS] +final Map objects = Map.fromIterables([ + 'Love Letter', + 'Hole', + 'Bomb', + 'Person Taking Bath', + 'Person in Bed', + 'Kitchen Knife', + 'Amphora', + 'World Map', + 'Compass', + 'Brick', + 'Barber Pole', + 'Oil Drum', + 'Bellhop Bell', + 'Luggage', + 'Hourglass Done', + 'Hourglass Not Done', + 'Watch', + 'Alarm Clock', + 'Stopwatch', + 'Timer Clock', + 'Mantelpiece Clock', + 'Thermometer', + 'Umbrella on Ground', + 'Firecracker', + 'Balloon', + 'Party Popper', + 'Confetti Ball', + 'Japanese Dolls', + 'Carp Streamer', + 'Wind Chime', + 'Red Envelope', + 'Ribbon', + 'Wrapped Gift', + 'Crystal Ball', + 'Nazar Amulet', + 'Joystick', + 'Teddy Bear', + 'Framed Picture', + 'Thread', + 'Yarn', + 'Shopping Bags', + 'Prayer Beads', + 'Gem Stone', + 'Postal Horn', + 'Studio Microphone', + 'Level Slider', + 'Control Knobs', + 'Radio', + 'Mobile Phone', + 'Mobile Phone With Arrow', + 'Telephone', + 'Telephone Receiver', + 'Pager', + 'Fax Machine', + 'Battery', + 'Electric Plug', + 'Laptop Computer', + 'Desktop Computer', + 'Printer', + 'Keyboard', + 'Computer Mouse', + 'Trackball', + 'Computer Disk', + 'Floppy Disk', + 'Optical Disk', + 'DVD', + 'Abacus', + 'Movie Camera', + 'Film Frames', + 'Film Projector', + 'Television', + 'Camera', + 'Camera With Flash', + 'Video Camera', + 'Videocassette', + 'Magnifying Glass Tilted Left', + 'Magnifying Glass Tilted Right', + 'Candle', + 'Light Bulb', + 'Flashlight', + 'Red Paper Lantern', + 'Notebook With Decorative Cover', + 'Closed Book', + 'Open Book', + 'Green Book', + 'Blue Book', + 'Orange Book', + 'Books', + 'Notebook', + 'Page With Curl', + 'Scroll', + 'Page Facing Up', + 'Newspaper', + 'Rolled-Up Newspaper', + 'Bookmark Tabs', + 'Bookmark', + 'Label', + 'Money Bag', + 'Yen Banknote', + 'Dollar Banknote', + 'Euro Banknote', + 'Pound Banknote', + 'Money With Wings', + 'Credit Card', + 'Receipt', + 'Envelope', + 'E-Mail', + 'Incoming Envelope', + 'Envelope With Arrow', + 'Outbox Tray', + 'Inbox Tray', + 'Package', + 'Closed Mailbox With Raised Flag', + 'Closed Mailbox With Lowered Flag', + 'Open Mailbox With Raised Flag', + 'Open Mailbox With Lowered Flag', + 'Postbox', + 'Ballot Box With Ballot', + 'Pencil', + 'Black Nib', + 'Fountain Pen', + 'Pen', + 'Paintbrush', + 'Crayon', + 'Memo', + 'File Folder', + 'Open File Folder', + 'Card Index Dividers', + 'Calendar', + 'Tear-Off Calendar', + 'Spiral Notepad', + 'Spiral Calendar', + 'Card Index', + 'Chart Increasing', + 'Chart Decreasing', + 'Bar Chart', + 'Clipboard', + 'Pushpin', + 'Round Pushpin', + 'Paperclip', + 'Linked Paperclips', + 'Straight Ruler', + 'Triangular Ruler', + 'Scissors', + 'Card File Box', + 'File Cabinet', + 'Wastebasket', + 'Locked', + 'Unlocked', + 'Locked With Pen', + 'Locked With Key', + 'Key', + 'Old Key', + 'Hammer', + 'Pick', + 'Hammer and Pick', + 'Hammer and Wrench', + 'Dagger', + 'Crossed Swords', + 'Pistol', + 'Shield', + 'Wrench', + 'Nut and Bolt', + 'Gear', + 'Clamp', + 'Balance Scale', + 'Link', + 'Chains', + 'Toolbox', + 'Magnet', + 'Alembic', + 'Test Tube', + 'Petri Dish', + 'DNA', + 'Microscope', + 'Telescope', + 'Satellite Antenna', + 'Syringe', + 'Pill', + 'Door', + 'Bed', + 'Couch and Lamp', + 'Toilet', + 'Shower', + 'Bathtub', + 'Lotion Bottle', + 'Safety Pin', + 'Broom', + 'Basket', + 'Roll of Paper', + 'Soap', + 'Sponge', + 'Fire Extinguisher', + 'Cigarette', + 'Coffin', + 'Funeral Urn', + 'Moai', + 'Potable Water' +], [ + '💌', + '🕳', + '💣', + '🛀', + '🛌', + '🔪', + '🏺', + '🗺', + '🧭', + '🧱', + '💈', + '🛢', + '🛎', + '🧳', + '⌛', + '⏳', + '⌚', + '⏰', + '⏱', + '⏲', + '🕰', + '🌡', + '⛱', + '🧨', + '🎈', + '🎉', + '🎊', + '🎎', + '🎏', + '🎐', + '🧧', + '🎀', + '🎁', + '🔮', + '🧿', + '🕹', + '🧸', + '🖼', + '🧵', + '🧶', + '🛍', + '📿', + '💎', + '📯', + '🎙', + '🎚', + '🎛', + '📻', + '📱', + '📲', + '☎', + '📞', + '📟', + '📠', + '🔋', + '🔌', + '💻', + '🖥', + '🖨', + '⌨', + '🖱', + '🖲', + '💽', + '💾', + '💿', + '📀', + '🧮', + '🎥', + '🎞', + '📽', + '📺', + '📷', + '📸', + '📹', + '📼', + '🔍', + '🔎', + '🕯', + '💡', + '🔦', + '🏮', + '📔', + '📕', + '📖', + '📗', + '📘', + '📙', + '📚', + '📓', + '📃', + '📜', + '📄', + '📰', + '🗞', + '📑', + '🔖', + '🏷', + '💰', + '💴', + '💵', + '💶', + '💷', + '💸', + '💳', + '🧾', + '✉', + '📧', + '📨', + '📩', + '📤', + '📥', + '📦', + '📫', + '📪', + '📬', + '📭', + '📮', + '🗳', + '✏', + '✒', + '🖋', + '🖊', + '🖌', + '🖍', + '📝', + '📁', + '📂', + '🗂', + '📅', + '📆', + '🗒', + '🗓', + '📇', + '📈', + '📉', + '📊', + '📋', + '📌', + '📍', + '📎', + '🖇', + '📏', + '📐', + '✂', + '🗃', + '🗄', + '🗑', + '🔒', + '🔓', + '🔏', + '🔐', + '🔑', + '🗝', + '🔨', + '⛏', + '⚒', + '🛠', + '🗡', + '⚔', + '🔫', + '🛡', + '🔧', + '🔩', + '⚙', + '🗜', + '⚖', + '🔗', + '⛓', + '🧰', + '🧲', + '⚗', + '🧪', + '🧫', + '🧬', + '🔬', + '🔭', + '📡', + '💉', + '💊', + '🚪', + '🛏', + '🛋', + '🚽', + '🚿', + '🛁', + '🧴', + '🧷', + '🧹', + '🧺', + '🧻', + '🧼', + '🧽', + '🧯', + '🚬', + '⚰', + '⚱', + '🗿', + '🚰' +]); + +/// Map of all possible emojis along with their names in [Category.SYMBOLS] +final Map symbols = Map.fromIterables([ + 'Heart With Arrow', + 'Heart With Ribbon', + 'Sparkling Heart', + 'Growing Heart', + 'Beating Heart', + 'Revolving Hearts', + 'Two Hearts', + 'Heart Decoration', + 'Heavy Heart Exclamation', + 'Broken Heart', + 'Red Heart', + 'Orange Heart', + 'Yellow Heart', + 'Green Heart', + 'Blue Heart', + 'Purple Heart', + 'Black Heart', + 'Hundred Points', + 'Anger Symbol', + 'Speech Balloon', + 'Eye in Speech Bubble', + 'Right Anger Bubble', + 'Thought Balloon', + 'Zzz', + 'White Flower', + 'Hot Springs', + 'Barber Pole', + 'Stop Sign', + 'Twelve O’Clock', + 'Twelve-Thirty', + 'One O’Clock', + 'One-Thirty', + 'Two O’Clock', + 'Two-Thirty', + 'Three O’Clock', + 'Three-Thirty', + 'Four O’Clock', + 'Four-Thirty', + 'Five O’Clock', + 'Five-Thirty', + 'Six O’Clock', + 'Six-Thirty', + 'Seven O’Clock', + 'Seven-Thirty', + 'Eight O’Clock', + 'Eight-Thirty', + 'Nine O’Clock', + 'Nine-Thirty', + 'Ten O’Clock', + 'Ten-Thirty', + 'Eleven O’Clock', + 'Eleven-Thirty', + 'Cyclone', + 'Spade Suit', + 'Heart Suit', + 'Diamond Suit', + 'Club Suit', + 'Joker', + 'Mahjong Red Dragon', + 'Flower Playing Cards', + 'Muted Speaker', + 'Speaker Low Volume', + 'Speaker Medium Volume', + 'Speaker High Volume', + 'Loudspeaker', + 'Megaphone', + 'Postal Horn', + 'Bell', + 'Bell With Slash', + 'Musical Note', + 'Musical Notes', + 'ATM Sign', + 'Litter in Bin Sign', + 'Potable Water', + 'Wheelchair Symbol', + 'Men’s Room', + 'Women’s Room', + 'Restroom', + 'Baby Symbol', + 'Water Closet', + 'Warning', + 'Children Crossing', + 'No Entry', + 'Prohibited', + 'No Bicycles', + 'No Smoking', + 'No Littering', + 'Non-Potable Water', + 'No Pedestrians', + 'No One Under Eighteen', + 'Radioactive', + 'Biohazard', + 'Up Arrow', + 'Up-Right Arrow', + 'Right Arrow', + 'Down-Right Arrow', + 'Down Arrow', + 'Down-Left Arrow', + 'Left Arrow', + 'Up-Left Arrow', + 'Up-Down Arrow', + 'Left-Right Arrow', + 'Right Arrow Curving Left', + 'Left Arrow Curving Right', + 'Right Arrow Curving Up', + 'Right Arrow Curving Down', + 'Clockwise Vertical Arrows', + 'Counterclockwise Arrows Button', + 'Back Arrow', + 'End Arrow', + 'On! Arrow', + 'Soon Arrow', + 'Top Arrow', + 'Place of Worship', + 'Atom Symbol', + 'Om', + 'Star of David', + 'Wheel of Dharma', + 'Yin Yang', + 'Latin Cross', + 'Orthodox Cross', + 'Star and Crescent', + 'Peace Symbol', + 'Menorah', + 'Dotted Six-Pointed Star', + 'Aries', + 'Taurus', + 'Gemini', + 'Cancer', + 'Leo', + 'Virgo', + 'Libra', + 'Scorpio', + 'Sagittarius', + 'Capricorn', + 'Aquarius', + 'Pisces', + 'Ophiuchus', + 'Shuffle Tracks Button', + 'Repeat Button', + 'Repeat Single Button', + 'Play Button', + 'Fast-Forward Button', + 'Reverse Button', + 'Fast Reverse Button', + 'Upwards Button', + 'Fast Up Button', + 'Downwards Button', + 'Fast Down Button', + 'Stop Button', + 'Eject Button', + 'Cinema', + 'Dim Button', + 'Bright Button', + 'Antenna Bars', + 'Vibration Mode', + 'Mobile Phone Off', + 'Infinity', + 'Recycling Symbol', + 'Trident Emblem', + 'Name Badge', + 'Japanese Symbol for Beginner', + 'Heavy Large Circle', + 'White Heavy Check Mark', + 'Ballot Box With Check', + 'Heavy Check Mark', + 'Heavy Multiplication X', + 'Cross Mark', + 'Cross Mark Button', + 'Heavy Plus Sign', + 'Heavy Minus Sign', + 'Heavy Division Sign', + 'Curly Loop', + 'Double Curly Loop', + 'Part Alternation Mark', + 'Eight-Spoked Asterisk', + 'Eight-Pointed Star', + 'Sparkle', + 'Double Exclamation Mark', + 'Exclamation Question Mark', + 'Question Mark', + 'White Question Mark', + 'White Exclamation Mark', + 'Exclamation Mark', + 'Copyright', + 'Registered', + 'Trade Mark', + 'Keycap Number Sign', + 'Keycap Digit Zero', + 'Keycap Digit One', + 'Keycap Digit Two', + 'Keycap Digit Three', + 'Keycap Digit Four', + 'Keycap Digit Five', + 'Keycap Digit Six', + 'Keycap Digit Seven', + 'Keycap Digit Eight', + 'Keycap Digit Nine', + 'Keycap: 10', + 'Input Latin Uppercase', + 'Input Latin Lowercase', + 'Input Numbers', + 'Input Symbols', + 'Input Latin Letters', + 'A Button (Blood Type)', + 'AB Button (Blood Type)', + 'B Button (Blood Type)', + 'CL Button', + 'Cool Button', + 'Free Button', + 'Information', + 'ID Button', + 'Circled M', + 'New Button', + 'NG Button', + 'O Button (Blood Type)', + 'OK Button', + 'P Button', + 'SOS Button', + 'Up! Button', + 'Vs Button', + 'Japanese “Here” Button', + 'Japanese “Service Charge” Button', + 'Japanese “Monthly Amount” Button', + 'Japanese “Not Free of Charge” Button', + 'Japanese “Reserved” Button', + 'Japanese “Bargain” Button', + 'Japanese “Discount” Button', + 'Japanese “Free of Charge” Button', + 'Japanese “Prohibited” Button', + 'Japanese “Acceptable” Button', + 'Japanese “Application” Button', + 'Japanese “Passing Grade” Button', + 'Japanese “Vacancy” Button', + 'Japanese “Congratulations” Button', + 'Japanese “Secret” Button', + 'Japanese “Open for Business” Button', + 'Japanese “No Vacancy” Button', + 'Red Circle', + 'Blue Circle', + 'Black Circle', + 'White Circle', + 'Black Large Square', + 'White Large Square', + 'Black Medium Square', + 'White Medium Square', + 'Black Medium-Small Square', + 'White Medium-Small Square', + 'Black Small Square', + 'White Small Square', + 'Large Orange Diamond', + 'Large Blue Diamond', + 'Small Orange Diamond', + 'Small Blue Diamond', + 'Red Triangle Pointed Up', + 'Red Triangle Pointed Down', + 'Diamond With a Dot', + 'White Square Button', + 'Black Square Button' +], [ + '💘', + '💝', + '💖', + '💗', + '💓', + '💞', + '💕', + '💟', + '❣', + '💔', + '❤', + '🧡', + '💛', + '💚', + '💙', + '💜', + '🖤', + '💯', + '💢', + '💬', + '👁‍🗨', + '🗯', + '💭', + '💤', + '💮', + '♨', + '💈', + '🛑', + '🕛', + '🕧', + '🕐', + '🕜', + '🕑', + '🕝', + '🕒', + '🕞', + '🕓', + '🕟', + '🕔', + '🕠', + '🕕', + '🕡', + '🕖', + '🕢', + '🕗', + '🕣', + '🕘', + '🕤', + '🕙', + '🕥', + '🕚', + '🕦', + '🌀', + '♠', + '♥', + '♦', + '♣', + '🃏', + '🀄', + '🎴', + '🔇', + '🔈', + '🔉', + '🔊', + '📢', + '📣', + '📯', + '🔔', + '🔕', + '🎵', + '🎶', + '🏧', + '🚮', + '🚰', + '♿', + '🚹', + '🚺', + '🚻', + '🚼', + '🚾', + '⚠', + '🚸', + '⛔', + '🚫', + '🚳', + '🚭', + '🚯', + '🚱', + '🚷', + '🔞', + '☢', + '☣', + '⬆', + '↗', + '➡', + '↘', + '⬇', + '↙', + '⬅', + '↖', + '↕', + '↔', + '↩', + '↪', + '⤴', + '⤵', + '🔃', + '🔄', + '🔙', + '🔚', + '🔛', + '🔜', + '🔝', + '🛐', + '⚛', + '🕉', + '✡', + '☸', + '☯', + '✝', + '☦', + '☪', + '☮', + '🕎', + '🔯', + '♈', + '♉', + '♊', + '♋', + '♌', + '♍', + '♎', + '♏', + '♐', + '♑', + '♒', + '♓', + '⛎', + '🔀', + '🔁', + '🔂', + '▶', + '⏩', + '◀', + '⏪', + '🔼', + '⏫', + '🔽', + '⏬', + '⏹', + '⏏', + '🎦', + '🔅', + '🔆', + '📶', + '📳', + '📴', + '♾', + '♻', + '🔱', + '📛', + '🔰', + '⭕', + '✅', + '☑', + '✔', + '✖', + '❌', + '❎', + '➕', + '➖', + '➗', + '➰', + '➿', + '〽', + '✳', + '✴', + '❇', + '‼', + '⁉', + '❓', + '❔', + '❕', + '❗', + '©', + '®', + '™', + '#️⃣', + '0️⃣', + '1️⃣', + '2️⃣', + '3️⃣', + '4️⃣', + '5️⃣', + '6️⃣', + '7️⃣', + '8️⃣', + '9️⃣', + '🔟', + '🔠', + '🔡', + '🔢', + '🔣', + '🔤', + '🅰', + '🆎', + '🅱', + '🆑', + '🆒', + '🆓', + 'ℹ', + '🆔', + 'Ⓜ', + '🆕', + '🆖', + '🅾', + '🆗', + '🅿', + '🆘', + '🆙', + '🆚', + '🈁', + '🈂', + '🈷', + '🈶', + '🈯', + '🉐', + '🈹', + '🈚', + '🈲', + '🉑', + '🈸', + '🈴', + '🈳', + '㊗', + '㊙', + '🈺', + '🈵', + '🔴', + '🔵', + '⚫', + '⚪', + '⬛', + '⬜', + '◼', + '◻', + '◾', + '◽', + '▪', + '▫', + '🔶', + '🔷', + '🔸', + '🔹', + '🔺', + '🔻', + '💠', + '🔳', + '🔲' +]); + +/// Map of all possible emojis along with their names in [Category.FLAGS] +final Map flags = Map.fromIterables([ + 'Chequered Flag', + 'Triangular Flag', + 'Crossed Flags', + 'Black Flag', + 'White Flag', + 'Rainbow Flag', + 'Pirate Flag', + 'Flag: Ascension Island', + 'Flag: Andorra', + 'Flag: United Arab Emirates', + 'Flag: Afghanistan', + 'Flag: Antigua & Barbuda', + 'Flag: Anguilla', + 'Flag: Albania', + 'Flag: Armenia', + 'Flag: Angola', + 'Flag: Antarctica', + 'Flag: Argentina', + 'Flag: American Samoa', + 'Flag: Austria', + 'Flag: Australia', + 'Flag: Aruba', + 'Flag: Åland Islands', + 'Flag: Azerbaijan', + 'Flag: Bosnia & Herzegovina', + 'Flag: Barbados', + 'Flag: Bangladesh', + 'Flag: Belgium', + 'Flag: Burkina Faso', + 'Flag: Bulgaria', + 'Flag: Bahrain', + 'Flag: Burundi', + 'Flag: Benin', + 'Flag: St. Barthélemy', + 'Flag: Bermuda', + 'Flag: Brunei', + 'Flag: Bolivia', + 'Flag: Caribbean Netherlands', + 'Flag: Brazil', + 'Flag: Bahamas', + 'Flag: Bhutan', + 'Flag: Bouvet Island', + 'Flag: Botswana', + 'Flag: Belarus', + 'Flag: Belize', + 'Flag: Canada', + 'Flag: Cocos (Keeling) Islands', + 'Flag: Congo - Kinshasa', + 'Flag: Central African Republic', + 'Flag: Congo - Brazzaville', + 'Flag: Switzerland', + 'Flag: Côte d’Ivoire', + 'Flag: Cook Islands', + 'Flag: Chile', + 'Flag: Cameroon', + 'Flag: China', + 'Flag: Colombia', + 'Flag: Clipperton Island', + 'Flag: Costa Rica', + 'Flag: Cuba', + 'Flag: Cape Verde', + 'Flag: Curaçao', + 'Flag: Christmas Island', + 'Flag: Cyprus', + 'Flag: Czechia', + 'Flag: Germany', + 'Flag: Diego Garcia', + 'Flag: Djibouti', + 'Flag: Denmark', + 'Flag: Dominica', + 'Flag: Dominican Republic', + 'Flag: Algeria', + 'Flag: Ceuta & Melilla', + 'Flag: Ecuador', + 'Flag: Estonia', + 'Flag: Egypt', + 'Flag: Western Sahara', + 'Flag: Eritrea', + 'Flag: Spain', + 'Flag: Ethiopia', + 'Flag: European Union', + 'Flag: Finland', + 'Flag: Fiji', + 'Flag: Falkland Islands', + 'Flag: Micronesia', + 'Flag: Faroe Islands', + 'Flag: France', + 'Flag: Gabon', + 'Flag: United Kingdom', + 'Flag: Grenada', + 'Flag: Georgia', + 'Flag: French Guiana', + 'Flag: Guernsey', + 'Flag: Ghana', + 'Flag: Gibraltar', + 'Flag: Greenland', + 'Flag: Gambia', + 'Flag: Guinea', + 'Flag: Guadeloupe', + 'Flag: Equatorial Guinea', + 'Flag: Greece', + 'Flag: South Georgia & South Sandwich Islands', + 'Flag: Guatemala', + 'Flag: Guam', + 'Flag: Guinea-Bissau', + 'Flag: Guyana', + 'Flag: Hong Kong SAR China', + 'Flag: Heard & McDonald Islands', + 'Flag: Honduras', + 'Flag: Croatia', + 'Flag: Haiti', + 'Flag: Hungary', + 'Flag: Canary Islands', + 'Flag: Indonesia', + 'Flag: Ireland', + 'Flag: Israel', + 'Flag: Isle of Man', + 'Flag: India', + 'Flag: British Indian Ocean Territory', + 'Flag: Iraq', + 'Flag: Iran', + 'Flag: Iceland', + 'Flag: Italy', + 'Flag: Jersey', + 'Flag: Jamaica', + 'Flag: Jordan', + 'Flag: Japan', + 'Flag: Kenya', + 'Flag: Kyrgyzstan', + 'Flag: Cambodia', + 'Flag: Kiribati', + 'Flag: Comoros', + 'Flag: St. Kitts & Nevis', + 'Flag: North Korea', + 'Flag: South Korea', + 'Flag: Kuwait', + 'Flag: Cayman Islands', + 'Flag: Kazakhstan', + 'Flag: Laos', + 'Flag: Lebanon', + 'Flag: St. Lucia', + 'Flag: Liechtenstein', + 'Flag: Sri Lanka', + 'Flag: Liberia', + 'Flag: Lesotho', + 'Flag: Lithuania', + 'Flag: Luxembourg', + 'Flag: Latvia', + 'Flag: Libya', + 'Flag: Morocco', + 'Flag: Monaco', + 'Flag: Moldova', + 'Flag: Montenegro', + 'Flag: St. Martin', + 'Flag: Madagascar', + 'Flag: Marshall Islands', + 'Flag: North Macedonia', + 'Flag: Mali', + 'Flag: Myanmar (Burma)', + 'Flag: Mongolia', + 'Flag: Macau Sar China', + 'Flag: Northern Mariana Islands', + 'Flag: Martinique', + 'Flag: Mauritania', + 'Flag: Montserrat', + 'Flag: Malta', + 'Flag: Mauritius', + 'Flag: Maldives', + 'Flag: Malawi', + 'Flag: Mexico', + 'Flag: Malaysia', + 'Flag: Mozambique', + 'Flag: Namibia', + 'Flag: New Caledonia', + 'Flag: Niger', + 'Flag: Norfolk Island', + 'Flag: Nigeria', + 'Flag: Nicaragua', + 'Flag: Netherlands', + 'Flag: Norway', + 'Flag: Nepal', + 'Flag: Nauru', + 'Flag: Niue', + 'Flag: New Zealand', + 'Flag: Oman', + 'Flag: Panama', + 'Flag: Peru', + 'Flag: French Polynesia', + 'Flag: Papua New Guinea', + 'Flag: Philippines', + 'Flag: Pakistan', + 'Flag: Poland', + 'Flag: St. Pierre & Miquelon', + 'Flag: Pitcairn Islands', + 'Flag: Puerto Rico', + 'Flag: Palestinian Territories', + 'Flag: Portugal', + 'Flag: Palau', + 'Flag: Paraguay', + 'Flag: Qatar', + 'Flag: Réunion', + 'Flag: Romania', + 'Flag: Serbia', + 'Flag: Russia', + 'Flag: Rwanda', + 'Flag: Saudi Arabia', + 'Flag: Solomon Islands', + 'Flag: Seychelles', + 'Flag: Sudan', + 'Flag: Sweden', + 'Flag: Singapore', + 'Flag: St. Helena', + 'Flag: Slovenia', + 'Flag: Svalbard & Jan Mayen', + 'Flag: Slovakia', + 'Flag: Sierra Leone', + 'Flag: San Marino', + 'Flag: Senegal', + 'Flag: Somalia', + 'Flag: Suriname', + 'Flag: South Sudan', + 'Flag: São Tomé & Príncipe', + 'Flag: El Salvador', + 'Flag: Sint Maarten', + 'Flag: Syria', + 'Flag: Swaziland', + 'Flag: Tristan Da Cunha', + 'Flag: Turks & Caicos Islands', + 'Flag: Chad', + 'Flag: French Southern Territories', + 'Flag: Togo', + 'Flag: Thailand', + 'Flag: Tajikistan', + 'Flag: Tokelau', + 'Flag: Timor-Leste', + 'Flag: Turkmenistan', + 'Flag: Tunisia', + 'Flag: Tonga', + 'Flag: Turkey', + 'Flag: Trinidad & Tobago', + 'Flag: Tuvalu', + 'Flag: Taiwan', + 'Flag: Tanzania', + 'Flag: Ukraine', + 'Flag: Uganda', + 'Flag: U.S. Outlying Islands', + 'Flag: United Nations', + 'Flag: United States', + 'Flag: Uruguay', + 'Flag: Uzbekistan', + 'Flag: Vatican City', + 'Flag: St. Vincent & Grenadines', + 'Flag: Venezuela', + 'Flag: British Virgin Islands', + 'Flag: U.S. Virgin Islands', + 'Flag: Vietnam', + 'Flag: Vanuatu', + 'Flag: Wallis & Futuna', + 'Flag: Samoa', + 'Flag: Kosovo', + 'Flag: Yemen', + 'Flag: Mayotte', + 'Flag: South Africa', + 'Flag: Zambia', + 'Flag: Zimbabwe' +], [ + '🏁', + '🚩', + '🎌', + '🏴', + '🏳', + '🏳️‍🌈', + '🏴‍☠️', + '🇦🇨', + '🇦🇩', + '🇦🇪', + '🇦🇫', + '🇦🇬', + '🇦🇮', + '🇦🇱', + '🇦🇲', + '🇦🇴', + '🇦🇶', + '🇦🇷', + '🇦🇸', + '🇦🇹', + '🇦🇺', + '🇦🇼', + '🇦🇽', + '🇦🇿', + '🇧🇦', + '🇧🇧', + '🇧🇩', + '🇧🇪', + '🇧🇫', + '🇧🇬', + '🇧🇭', + '🇧🇮', + '🇧🇯', + '🇧🇱', + '🇧🇲', + '🇧🇳', + '🇧🇴', + '🇧🇶', + '🇧🇷', + '🇧🇸', + '🇧🇹', + '🇧🇻', + '🇧🇼', + '🇧🇾', + '🇧🇿', + '🇨🇦', + '🇨🇨', + '🇨🇩', + '🇨🇫', + '🇨🇬', + '🇨🇭', + '🇨🇮', + '🇨🇰', + '🇨🇱', + '🇨🇲', + '🇨🇳', + '🇨🇴', + '🇨🇵', + '🇨🇷', + '🇨🇺', + '🇨🇻', + '🇨🇼', + '🇨🇽', + '🇨🇾', + '🇨🇿', + '🇩🇪', + '🇩🇬', + '🇩🇯', + '🇩🇰', + '🇩🇲', + '🇩🇴', + '🇩🇿', + '🇪🇦', + '🇪🇨', + '🇪🇪', + '🇪🇬', + '🇪🇭', + '🇪🇷', + '🇪🇸', + '🇪🇹', + '🇪🇺', + '🇫🇮', + '🇫🇯', + '🇫🇰', + '🇫🇲', + '🇫🇴', + '🇫🇷', + '🇬🇦', + '🇬🇧', + '🇬🇩', + '🇬🇪', + '🇬🇫', + '🇬🇬', + '🇬🇭', + '🇬🇮', + '🇬🇱', + '🇬🇲', + '🇬🇳', + '🇬🇵', + '🇬🇶', + '🇬🇷', + '🇬🇸', + '🇬🇹', + '🇬🇺', + '🇬🇼', + '🇬🇾', + '🇭🇰', + '🇭🇲', + '🇭🇳', + '🇭🇷', + '🇭🇹', + '🇭🇺', + '🇮🇨', + '🇮🇩', + '🇮🇪', + '🇮🇱', + '🇮🇲', + '🇮🇳', + '🇮🇴', + '🇮🇶', + '🇮🇷', + '🇮🇸', + '🇮🇹', + '🇯🇪', + '🇯🇲', + '🇯🇴', + '🇯🇵', + '🇰🇪', + '🇰🇬', + '🇰🇭', + '🇰🇮', + '🇰🇲', + '🇰🇳', + '🇰🇵', + '🇰🇷', + '🇰🇼', + '🇰🇾', + '🇰🇿', + '🇱🇦', + '🇱🇧', + '🇱🇨', + '🇱🇮', + '🇱🇰', + '🇱🇷', + '🇱🇸', + '🇱🇹', + '🇱🇺', + '🇱🇻', + '🇱🇾', + '🇲🇦', + '🇲🇨', + '🇲🇩', + '🇲🇪', + '🇲🇫', + '🇲🇬', + '🇲🇭', + '🇲🇰', + '🇲🇱', + '🇲🇲', + '🇲🇳', + '🇲🇴', + '🇲🇵', + '🇲🇶', + '🇲🇷', + '🇲🇸', + '🇲🇹', + '🇲🇺', + '🇲🇻', + '🇲🇼', + '🇲🇽', + '🇲🇾', + '🇲🇿', + '🇳🇦', + '🇳🇨', + '🇳🇪', + '🇳🇫', + '🇳🇬', + '🇳🇮', + '🇳🇱', + '🇳🇴', + '🇳🇵', + '🇳🇷', + '🇳🇺', + '🇳🇿', + '🇴🇲', + '🇵🇦', + '🇵🇪', + '🇵🇫', + '🇵🇬', + '🇵🇭', + '🇵🇰', + '🇵🇱', + '🇵🇲', + '🇵🇳', + '🇵🇷', + '🇵🇸', + '🇵🇹', + '🇵🇼', + '🇵🇾', + '🇶🇦', + '🇷🇪', + '🇷🇴', + '🇷🇸', + '🇷🇺', + '🇷🇼', + '🇸🇦', + '🇸🇧', + '🇸🇨', + '🇸🇩', + '🇸🇪', + '🇸🇬', + '🇸🇭', + '🇸🇮', + '🇸🇯', + '🇸🇰', + '🇸🇱', + '🇸🇲', + '🇸🇳', + '🇸🇴', + '🇸🇷', + '🇸🇸', + '🇸🇹', + '🇸🇻', + '🇸🇽', + '🇸🇾', + '🇸🇿', + '🇹🇦', + '🇹🇨', + '🇹🇩', + '🇹🇫', + '🇹🇬', + '🇹🇭', + '🇹🇯', + '🇹🇰', + '🇹🇱', + '🇹🇲', + '🇹🇳', + '🇹🇴', + '🇹🇷', + '🇹🇹', + '🇹🇻', + '🇹🇼', + '🇹🇿', + '🇺🇦', + '🇺🇬', + '🇺🇲', + '🇺🇳', + '🇺🇸', + '🇺🇾', + '🇺🇿', + '🇻🇦', + '🇻🇨', + '🇻🇪', + '🇻🇬', + '🇻🇮', + '🇻🇳', + '🇻🇺', + '🇼🇫', + '🇼🇸', + '🇽🇰', + '🇾🇪', + '🇾🇹', + '🇿🇦', + '🇿🇲', + '🇿🇼' +]); + +/// Set of emoji that support different skin tones +final supportSkinToneList = { + '👋', + '🤚', + '🖐', + '✋', + '🖖', + '👌', + '✌', + '🤞', + '🤟', + '🤘', + '🤙', + '👈', + '👉', + '👆', + '🖕', + '👇', + '☝', + '👍', + '👎', + '✊', + '👊', + '🤛', + '🤜', + '👏', + '🙌', + '👐', + '🤲', + '🤝', + '🙏', + '✍', + '💅', + '🤳', + '💪', + '🦵', + '🦶', + '👂', + '👃', + '👶', + '🧒', + '👦', + '👧', + '🧑', + '👨', + '🧔', + '👱', + '👨‍🦰', + '👨‍🦱', + '👨‍🦳', + '👨‍🦲', + '👩', + '👱‍♀️', + '👩‍🦰', + '👩‍🦱', + '👩‍🦳', + '👩‍🦲', + '🧓', + '👴', + '👵', + '🙍‍♂️', + '🙍‍♀️', + '🙎‍♂️', + '🙎‍♀️', + '🙅‍♂️', + '🙅‍♀️', + '🙆‍♂️', + '🙆‍♀️', + '💁‍♂️', + '💁‍♀️', + '🙋‍♂️', + '🙋‍♀️', + '🙇‍♂️', + '🙇‍♀️', + '🤦‍♂️', + '🤦‍♀️', + '🤷‍♂️', + '🤷‍♀️', + '👨‍⚕️', + '👩‍⚕️', + '👨‍🎓', + '👩‍🎓', + '👨‍🏫', + '👩‍🏫', + '👨‍⚖️', + '👩‍⚖️', + '👨‍🌾', + '👩‍🌾', + '👨‍🍳', + '👩‍🍳', + '👨‍🔧', + '👩‍🔧', + '👨‍🏭', + '👩‍🏭', + '👨‍💼', + '👩‍💼', + '👨‍🔬', + '👩‍🔬', + '👨‍💻', + '👩‍💻', + '👨‍🎤', + '👩‍🎤', + '👨‍🎨', + '👩‍🎨', + '👨‍✈️', + '👩‍✈️', + '👨‍🚀', + '👩‍🚀', + '👨‍🚒', + '👩‍🚒', + '👮‍♂️', + '👮‍♀️', + '🕵️‍♂️', + '🕵️‍♀️', + '💂‍♂️', + '💂‍♀️', + '👷‍♂️', + '👷‍♀️', + '🤴', + '👸', + '👳‍♂️', + '👳‍♀️', + '👲', + '🧕', + '🤵', + '👰', + '🤰', + '🤱', + '👼', + '🎅', + '🤶', + '🦸‍♂️', + '🦸‍♀️', + '🦹‍♂️', + '🦹‍♀️', + '🧙‍♂️', + '🧙‍♀️', + '🧚‍♂️', + '🧚‍♀️', + '🧛‍♂️', + '🧛‍♀️', + '🧜‍♂️', + '🧜‍♀️', + '🧝‍♂️', + '🧝‍♀️', + '💆‍♂️', + '💆‍♀️', + '💇‍♂️', + '💇‍♀️', + '🚶‍♂️', + '🚶‍♀️', + '🏃‍♂️', + '🏃‍♀️', + '🕺', + '💃', + '🕴', + '🧖‍♂️', + '🧖‍♀️', + '🧘', + '👭', + '👫', + '👬', + // Activities + '🧗‍♂️', + '🧗‍♀️', + '🏇', + '🏌️‍♂️', + '🏌️‍♀️', + '🏄‍♂️', + '🏄‍♀️', + '🚣‍♂️', + '🚣‍♀️', + '🏊‍♂️', + '🏊‍♀️', + '⛹️‍♂️', + '⛹️‍♀️', + '🏋️‍♂️', + '🏋️‍♀️', + '🚴‍♂️', + '🚴‍♀️', + '🚵‍♂️', + '🚵‍♀️', + '🤸‍♂️', + '🤸‍♀️', + '🤼‍♂️', + '🤼‍♀️', + '🤽‍♂️', + '🤽‍♀️', + '🤾‍♂️', + '🤾‍♀️', + '🤹‍♂️', + '🤹‍♀️', + '🧘‍♂️', + '🧘‍♀️', +}; diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker.dart new file mode 100644 index 0000000..33e0c79 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker.dart @@ -0,0 +1,253 @@ +import './category_emoji.dart'; +import './config.dart'; +import './default_emoji_picker_view.dart'; +import './emoji.dart'; +import './emoji_picker_internal_utils.dart'; +import './emoji_view_state.dart'; +import './recent_emoji.dart'; +import 'package:flutter/material.dart'; + +/// All the possible categories that [Emoji] can be put into +/// +/// All [Category] are shown in the category bar +enum Category { + /// Recent emojis + RECENT, + + /// Smiley emojis + SMILEYS, + + /// Animal emojis + ANIMALS, + + /// Food emojis + FOODS, + + /// Activity emojis + ACTIVITIES, + + /// Travel emojis + TRAVEL, + + /// Ojects emojis + OBJECTS, + + /// Sumbol emojis + SYMBOLS, + + /// Flag emojis + FLAGS, +} + +/// Extension on Category enum to get its name +extension CategoryExtension on Category { + /// Returns name of Category + String get name { + switch (this) { + case Category.RECENT: + return 'recent'; + case Category.SMILEYS: + return 'smileys'; + case Category.ANIMALS: + return 'animals'; + case Category.FOODS: + return 'foods'; + case Category.ACTIVITIES: + return 'activities'; + case Category.TRAVEL: + return 'travel'; + case Category.OBJECTS: + return 'objects'; + case Category.SYMBOLS: + return 'symbols'; + case Category.FLAGS: + return 'flags'; + } + } +} + +/// Enum to alter the keyboard button style +enum ButtonMode { + /// Android button style - gives the button a splash color with ripple effect + MATERIAL, + + /// iOS button style - gives the button a fade out effect when pressed + CUPERTINO +} + +/// Callback function for when emoji is selected +/// +/// The function returns the selected [Emoji] as well +/// as the [Category] from which it originated +typedef void OnEmojiSelected(Category category, Emoji emoji); + +/// Callback function for backspace button +typedef void OnBackspacePressed(); + +/// Callback function for custom view +typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state); + +/// The Emoji Keyboard widget +/// +/// This widget displays a grid of [Emoji] sorted by [Category] +/// which the user can horizontally scroll through. +/// +/// There is also a bottombar which displays all the possible [Category] +/// and allow the user to quickly switch to that [Category] +class EmojiPicker extends StatefulWidget { + /// EmojiPicker for flutter + const EmojiPicker({ + Key? key, + required this.onEmojiSelected, + this.onBackspacePressed, + this.config = const Config(), + this.customWidget, + }) : super(key: key); + + /// Custom widget + final EmojiViewBuilder? customWidget; + + /// The function called when the emoji is selected + final OnEmojiSelected onEmojiSelected; + + /// The function called when backspace button is pressed + final OnBackspacePressed? onBackspacePressed; + + /// Config for customizations + final Config config; + + @override + EmojiPickerState createState() => EmojiPickerState(); +} + +/// EmojiPickerState +class EmojiPickerState extends State { + final List _categoryEmoji = List.empty(growable: true); + List _recentEmoji = List.empty(growable: true); + late Future _updateEmojiFuture; + + // Prevent emojis to be reloaded with every build + bool _loaded = false; + + // Internal helper + final _emojiPickerInternalUtils = EmojiPickerInternalUtils(); + + /// Update recentEmoji list from outside using EmojiPickerUtils + void updateRecentEmoji(List recentEmoji) { + _recentEmoji = recentEmoji; + if (mounted) { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + _updateEmojiFuture = _updateEmojis(); + } + + @override + void didUpdateWidget(covariant EmojiPicker oldWidget) { + if (oldWidget.config != widget.config) { + // Config changed - rebuild EmojiPickerView completely + _loaded = false; + _updateEmojiFuture = _updateEmojis(); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + if (!_loaded) { + // Load emojis + _updateEmojiFuture.then( + (value) => WidgetsBinding.instance!.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + _loaded = true; + }); + }), + ); + + // Show loading indicator + return Container( + alignment: Alignment.center, + color: widget.config.bgColor, + child: const CircularProgressIndicator(), + ); + } + if (widget.config.showRecentsTab) { + _categoryEmoji[0].emoji = _recentEmoji.map((e) => e.emoji).toList(); + } + + var state = EmojiViewState( + _categoryEmoji, + _getOnEmojiListener(), + widget.onBackspacePressed, + ); + + // Build + return widget.customWidget == null + ? DefaultEmojiPickerView(widget.config, state) + : widget.customWidget!(widget.config, state); + } + + // Add recent emoji handling to tap listener + OnEmojiSelected _getOnEmojiListener() { + return (category, emoji) { + if (widget.config.showRecentsTab) { + _emojiPickerInternalUtils + .addEmojiToRecentlyUsed(emoji: emoji, config: widget.config) + .then((newRecentEmoji) => { + _recentEmoji = newRecentEmoji, + if (category != Category.RECENT && mounted) + setState(() { + // rebuild to update recent emoji tab + // when it is not current tab + }) + }); + } + widget.onEmojiSelected(category, emoji); + }; + } + + // Initialize emoji data + Future _updateEmojis() async { + _categoryEmoji.clear(); + if (widget.config.showRecentsTab) { + _recentEmoji = await _emojiPickerInternalUtils.getRecentEmojis(); + final recentEmojiMap = _recentEmoji.map((e) => e.emoji).toList(); + _categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap)); + } + + final availableCategoryEmoji = + await _emojiPickerInternalUtils.getAvailableCategoryEmoji(); + + availableCategoryEmoji.forEach((category, emojis) async { + _categoryEmoji.add( + CategoryEmoji( + category, + emojis.entries.map((emoji) { + var _emoji = Emoji(emoji.key, emoji.value); + // Emoji with skin tone are only in SMILEY & ACTIVITIES category + if (category == Category.SMILEYS || + category == Category.ACTIVITIES) { + return _updateSkinToneSupport(_emoji); + } else + return _emoji; + }).toList()), + ); + }); + + // Update emoji list version once all categories were cached + _emojiPickerInternalUtils.updateEmojiVersion(); + } + + // Set [hasSkinTone] to true for emoji that support skin tones + Emoji _updateSkinToneSupport(Emoji emoji) { + if (_emojiPickerInternalUtils.hasSkinTone(emoji)) { + return emoji.copyWith(hasSkinTone: true); + } + return emoji; + } +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_builder.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_builder.dart new file mode 100644 index 0000000..2018b31 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_builder.dart @@ -0,0 +1,20 @@ +import './config.dart'; +import './emoji_view_state.dart'; +import 'package:flutter/material.dart'; + +/// Template class for custom implementation +/// Inhert this class to create your own EmojiPicker +abstract class EmojiPickerBuilder extends StatefulWidget { + /// Constructor + EmojiPickerBuilder( + this.config, + this.state, { + Key? key, + }) : super(key: key); + + /// Config for customizations + final Config config; + + /// State that holds current emoji data + final EmojiViewState state; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_internal_utils.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_internal_utils.dart new file mode 100644 index 0000000..370bc3b --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_internal_utils.dart @@ -0,0 +1,198 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import '../emoji_picker_flutter.dart'; +import './emoji_skin_tones.dart'; +import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'emoji_lists.dart' as emoji_list; +import 'recent_emoji.dart'; + +/// Helper class that provides internal usage +class EmojiPickerInternalUtils { + // Establish communication with native + static const _platform = MethodChannel('emoji_picker_flutter'); + static const _emojiVersion = 'emoji_version'; + + /// Returns true when local emoji list is outdated + Future isEmojiUpdateAvailable() async { + final prefs = await SharedPreferences.getInstance(); + var emojiVersion = prefs.getInt(_emojiVersion) ?? 0; + // != to support downgrading of emoji_picker versions + return emoji_list.version != emojiVersion; + } + + /// Updates local emoji version with current version + Future updateEmojiVersion() async { + final prefs = await SharedPreferences.getInstance(); + prefs.setInt(_emojiVersion, emoji_list.version); + } + + /// Restore locally cached emoji + Future?> _restoreFilteredEmojis(String title) async { + final prefs = await SharedPreferences.getInstance(); + var emojiJson = prefs.getString(title); + if (emojiJson == null) { + return null; + } + var emojis = + Map.from(jsonDecode(emojiJson) as Map); + return emojis; + } + + // Get available emoji for given category title + Future> _getAvailableEmojis(Map map, + {required String title}) async { + Map? newMap; + + // Get Emojis cached locally if available + if (await isEmojiUpdateAvailable() == false) { + newMap = await _restoreFilteredEmojis(title); + } + + if (newMap == null) { + // Check if emoji is available on this platform + newMap = await _getPlatformAvailableEmoji(map); + // Save available Emojis to local storage for faster loading next time + if (newMap != null) { + await _cacheFilteredEmojis(title, newMap); + } + } + + return newMap ?? {}; + } + + /// Returns map of all the available category emojis + Future>> getAvailableCategoryEmoji() async { + final allCategoryEmoji = Map.fromIterables([ + Category.SMILEYS, + Category.ANIMALS, + Category.FOODS, + Category.ACTIVITIES, + Category.TRAVEL, + Category.OBJECTS, + Category.SYMBOLS, + Category.FLAGS + ], [ + emoji_list.smileys, + emoji_list.animals, + emoji_list.foods, + emoji_list.activities, + emoji_list.travel, + emoji_list.objects, + emoji_list.symbols, + emoji_list.flags, + ]); + + final futures = allCategoryEmoji.entries + .map((e) => _getAvailableEmojis(e.value, title: e.key.name)); + + final allAvailableEmojis = await Future.wait(futures); + + return Map.fromIterables(allCategoryEmoji.keys, allAvailableEmojis); + } + + /// Stores filtered emoji locally for faster access next time + Future _cacheFilteredEmojis( + String title, Map emojis) async { + var emojiJson = jsonEncode(emojis); + final prefs = await SharedPreferences.getInstance(); + prefs.setString(title, emojiJson); + } + + /// Check if emoji is available on current platform + Future?> _getPlatformAvailableEmoji( + Map emoji) async { + if (Platform.isAndroid) { + Map? filtered = {}; + var delimiter = '|'; + try { + var entries = emoji.values.join(delimiter); + var keys = emoji.keys.join(delimiter); + var result = (await _platform.invokeMethod('checkAvailability', + {'emojiKeys': keys, 'emojiEntries': entries})) as String; + var resultKeys = result.split(delimiter); + for (var i = 0; i < resultKeys.length; i++) { + filtered[resultKeys[i]] = emoji[resultKeys[i]]!; + } + } on PlatformException catch (_) { + filtered = null; + } + return filtered; + } else { + return emoji; + } + } + + /// Returns list of recently used emoji from cache + Future> getRecentEmojis() async { + final prefs = await SharedPreferences.getInstance(); + var emojiJson = prefs.getString('recent'); + if (emojiJson == null) { + return []; + } + var json = jsonDecode(emojiJson) as List; + return json.map(RecentEmoji.fromJson).toList(); + } + + /// Add an emoji to recently used list or increase its counter + Future> addEmojiToRecentlyUsed( + {required Emoji emoji, Config config = const Config()}) async { + // Remove emoji's skin tone in Recent-Category + if (emoji.hasSkinTone) { + emoji = removeSkinTone(emoji); + } + var recentEmoji = await getRecentEmojis(); + var recentEmojiIndex = + recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji); + if (recentEmojiIndex != -1) { + // Already exist in recent list + // Just update counter + recentEmoji[recentEmojiIndex].counter++; + } else if (recentEmoji.length == config.recentsLimit && + config.replaceEmojiOnLimitExceed) { + // Replace latest emoji with the fresh one + recentEmoji[recentEmoji.length - 1] = RecentEmoji(emoji, 1); + } else { + recentEmoji.add(RecentEmoji(emoji, 1)); + } + // Sort by counter desc + recentEmoji.sort((a, b) => b.counter - a.counter); + // Limit entries to recentsLimit + recentEmoji = + recentEmoji.sublist(0, min(config.recentsLimit, recentEmoji.length)); + // save locally + final prefs = await SharedPreferences.getInstance(); + prefs.setString('recent', jsonEncode(recentEmoji)); + + return recentEmoji; + } + + /// Returns true when the emoji support multiple skin colors + bool hasSkinTone(Emoji emoji) { + return emoji_list.supportSkinToneList.contains(emoji.emoji); + } + + /// Applies skin tone to given emoji + Emoji applySkinTone(Emoji emoji, String color) { + final codeUnits = emoji.emoji.codeUnits; + var result = List.empty(growable: true) + ..addAll(codeUnits.sublist(0, min(codeUnits.length, 2))) + ..addAll(color.codeUnits); + if (codeUnits.length >= 2) { + result.addAll(codeUnits.sublist(2)); + } + return emoji.copyWith(emoji: String.fromCharCodes(result)); + } + + /// Remove skin tone from given emoji + Emoji removeSkinTone(Emoji emoji) { + return emoji.copyWith( + emoji: emoji.emoji.replaceFirst( + RegExp('${SkinTone.values.join('|')}'), + '', + ), + ); + } +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_utils.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_utils.dart new file mode 100644 index 0000000..59d689d --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_picker_utils.dart @@ -0,0 +1,59 @@ +import '../emoji_picker_flutter.dart'; +import './recent_emoji.dart'; +import 'package:flutter/material.dart'; +import 'emoji_picker_internal_utils.dart'; + +/// Helper class that provides extended usage +class EmojiPickerUtils { + /// Singleton Constructor + factory EmojiPickerUtils() { + return _singleton; + } + + EmojiPickerUtils._internal(); + + static final EmojiPickerUtils _singleton = EmojiPickerUtils._internal(); + final List _allAvailableEmojiEntities = []; + + /// Returns list of recently used emoji from cache + Future> getRecentEmojis() async { + return EmojiPickerInternalUtils().getRecentEmojis(); + } + + /// Search for related emoticons based on keywords + Future> searchEmoji(String keyword) async { + if (keyword.isEmpty) return []; + + if (_allAvailableEmojiEntities.isEmpty) { + final emojiPickerInternalUtils = EmojiPickerInternalUtils(); + + final availableCategoryEmoji = + await emojiPickerInternalUtils.getAvailableCategoryEmoji(); + + // Set all the emoji entities + availableCategoryEmoji.forEach((_, emojis) { + final emojiEntities = + emojis.entries.map((emoji) => Emoji(emoji.key, emoji.value)); + _allAvailableEmojiEntities.addAll(emojiEntities); + }); + } + + return _allAvailableEmojiEntities + .where( + (emoji) => emoji.name.toLowerCase().contains(keyword.toLowerCase()), + ) + .toList(); + } + + /// Add an emoji to recently used list or increase its counter + Future addEmojiToRecentlyUsed({ + required GlobalKey key, + required Emoji emoji, + Config config = const Config(), + }) async { + return EmojiPickerInternalUtils() + .addEmojiToRecentlyUsed(emoji: emoji, config: config) + .then((recentEmojiList) => + key.currentState?.updateRecentEmoji(recentEmojiList)); + } +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_skin_tones.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_skin_tones.dart new file mode 100644 index 0000000..9d20d79 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_skin_tones.dart @@ -0,0 +1,22 @@ +/// Alternative skin tones of Emoji +class SkinTone { + SkinTone._(); + + /// Light Skin Tone + static const String light = '🏻'; + + /// Medium-Light Skin Tone + static const String mediumLight = '🏼'; + + /// Medium Skin Tone + static const String medium = '🏽'; + + /// Medium-Dark Skin Tone + static const String mediumDark = '🏾'; + + /// Dark Skin Tone + static const String dark = '🏿'; + + /// Return all values as Array + static const values = [light, mediumLight, medium, mediumDark, dark]; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_view_state.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_view_state.dart new file mode 100644 index 0000000..f58aee8 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/emoji_view_state.dart @@ -0,0 +1,21 @@ +import '../emoji_picker_flutter.dart'; +import './category_emoji.dart'; + +/// State that holds current emoji data +class EmojiViewState { + /// Constructor + EmojiViewState( + this.categoryEmoji, + this.onEmojiSelected, + this.onBackspacePressed, + ); + + /// List of all category including their emoji + final List categoryEmoji; + + /// Callback when pressed on emoji + final OnEmojiSelected onEmojiSelected; + + /// Callback when pressed on backspace + final OnBackspacePressed? onBackspacePressed; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/recent_emoji.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/recent_emoji.dart new file mode 100644 index 0000000..cd02388 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/recent_emoji.dart @@ -0,0 +1,30 @@ +import './emoji.dart'; + +/// Class that holds an recent emoji +/// Recent Emoji has an instance of the emoji +/// And a counter, which counts how often this emoji +/// has been used before +class RecentEmoji { + /// Constructor + RecentEmoji(this.emoji, this.counter); + + /// Emoji instance + final Emoji emoji; + + /// Counter how often emoji has been used before + int counter = 0; + + /// Parse RecentEmoji from json + static RecentEmoji fromJson(dynamic json) { + return RecentEmoji( + Emoji.fromJson(json['emoji'] as Map), + json['counter'] as int, + ); + } + + /// Encode RecentEmoji to json + Map toJson() => { + 'emoji': emoji, + 'counter': counter, + }; +} diff --git a/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/triangle_shape.dart b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/triangle_shape.dart new file mode 100644 index 0000000..6254061 --- /dev/null +++ b/bytedesk_kefu/lib/vendors/emoji_picker_flutter/src/triangle_shape.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +/// Triangle shape used as skin tone indicator +class TriangleShape extends CustomPainter { + /// Constructor + /// Expects color that the triangle will be filled with + TriangleShape(Color color) { + _painter = Paint() + ..color = color + ..style = PaintingStyle.fill; + } + + late final Paint _painter; + + @override + void paint(Canvas canvas, Size size) { + var path = Path() + ..moveTo(size.width, 0) + ..lineTo(0, size.height) + ..lineTo(size.width, size.height) + ..lineTo(size.width, 0) + ..close(); + + canvas.drawPath(path, _painter); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} diff --git a/bytedesk_kefu/pubspec.yaml b/bytedesk_kefu/pubspec.yaml index 1623d0f..e38f313 100644 --- a/bytedesk_kefu/pubspec.yaml +++ b/bytedesk_kefu/pubspec.yaml @@ -1,6 +1,6 @@ name: bytedesk_kefu description: the best app chat sdk in china, you can chat with the agent freely at anytime. the agent can chat with the visitor by web/pc/mac/ios/android client. -version: 1.3.3 +version: 1.4.0 homepage: https://www.weikefu.net environment: @@ -84,6 +84,20 @@ dependencies: # 保存图片到相册 # https://pub.flutter-io.cn/packages/image_gallery_saver image_gallery_saver: ^1.7.1 + # https://pub.dev/packages/emoji_picker_flutter + # emoji_picker_flutter: ^1.1.2 + # emoji_picker_flutter: + # ./vendors/emoji_picker_flutter + shared_preferences: ^2.0.6 + # # https://pub.dev/packages/carousel_slider + carousel_slider: ^4.1.1 + # # https://pub.dev/packages/flutter_chat_ui + flutter_chat_ui: ^1.5.8 + # 录音 + # flutter_sound: ^9.2.12 + # flutter_sound_platform_interface: ^9.2.12 + # audio_session: ^0.1.6+1 + # permission_handler: ^9.2.0 # https://pub.dev/packages/feedback # feedback: ^2.0.0-beta.0 # feedback: ^1.2.2