├── .DS_Store
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── .DS_Store
├── anim
│ ├── voice_black.json
│ ├── voice_blue.json
│ ├── voice_record.json
│ └── voice_ripple.json
└── images
│ ├── ic_add.webp
│ ├── ic_add_emoji.webp
│ ├── ic_avatar_01.webp
│ ├── ic_avatar_02.webp
│ ├── ic_avatar_03.webp
│ ├── ic_avatar_04.webp
│ ├── ic_avatar_05.webp
│ ├── ic_avatar_06.webp
│ ├── ic_back.webp
│ ├── ic_del_face.webp
│ ├── ic_del_quote.webp
│ ├── ic_download_continue.webp
│ ├── ic_download_stop.webp
│ ├── ic_emoji.svg
│ ├── ic_emoji.webp
│ ├── ic_face_01.webp
│ ├── ic_face_02.webp
│ ├── ic_face_03.webp
│ ├── ic_face_04.webp
│ ├── ic_face_05.webp
│ ├── ic_face_06.webp
│ ├── ic_face_07.webp
│ ├── ic_face_08.webp
│ ├── ic_face_09.webp
│ ├── ic_face_10.webp
│ ├── ic_face_11.webp
│ ├── ic_face_12.webp
│ ├── ic_face_13.webp
│ ├── ic_face_14.webp
│ ├── ic_face_15.webp
│ ├── ic_face_16.webp
│ ├── ic_face_nor.webp
│ ├── ic_face_sel.webp
│ ├── ic_favorite_emoji_nor.webp
│ ├── ic_favorite_emoji_sel.webp
│ ├── ic_favorite_nor.webp
│ ├── ic_favorite_sel.webp
│ ├── ic_file.webp
│ ├── ic_file_grey.webp
│ ├── ic_keyboard.svg
│ ├── ic_keyboard.webp
│ ├── ic_load_error.webp
│ ├── ic_menu_add_emoji.webp
│ ├── ic_menu_copy.webp
│ ├── ic_menu_del.webp
│ ├── ic_menu_download.webp
│ ├── ic_menu_forward.webp
│ ├── ic_menu_multichoice.webp
│ ├── ic_menu_reply.webp
│ ├── ic_menu_revoke.webp
│ ├── ic_menu_translation.webp
│ ├── ic_multi_tool_del.webp
│ ├── ic_multi_tool_merge_forward.webp
│ ├── ic_not_disturb.webp
│ ├── ic_radio_msg_nor.webp
│ ├── ic_radio_msg_sel.webp
│ ├── ic_search.webp
│ ├── ic_send_failed.webp
│ ├── ic_speak.svg
│ ├── ic_speak.webp
│ ├── ic_tools.svg
│ ├── ic_tools.webp
│ ├── ic_tools_album.webp
│ ├── ic_tools_camera.webp
│ ├── ic_tools_carte.webp
│ ├── ic_tools_file.webp
│ ├── ic_tools_location.webp
│ ├── ic_tools_video_call.webp
│ ├── ic_tools_voice_input.webp
│ ├── ic_video_close.webp
│ ├── ic_video_download.webp
│ ├── ic_video_play.webp
│ ├── ic_video_play_small.webp
│ ├── ic_voice_black.webp
│ ├── ic_voice_blue.webp
│ ├── ic_voice_cancel.webp
│ ├── ic_voice_confirm.webp
│ ├── ic_voice_convert_fail.webp
│ ├── ic_voice_convert_suc.webp
│ ├── ic_voice_input_nor.webp
│ ├── ic_voice_record_bg1.webp
│ ├── ic_voice_record_bg2.webp
│ ├── ic_voice_record_cancel_grey.webp
│ ├── ic_voice_record_cancel_white.webp
│ ├── ic_voice_record_speaker.webp
│ ├── ic_voice_record_zi_grey.webp
│ └── ic_voice_record_zi_white.webp
├── lib
├── flutter_openim_widget.dart
└── src
│ ├── at_special_text_span_builder.dart
│ ├── chat_at_text.dart
│ ├── chat_avatar_view.dart
│ ├── chat_bubble.dart
│ ├── chat_carte_view.dart
│ ├── chat_custom_emoji_view.dart
│ ├── chat_emoji_view.dart
│ ├── chat_file_preview.dart
│ ├── chat_file_view.dart
│ ├── chat_inputbox_view.dart
│ ├── chat_item_group_view.dart
│ ├── chat_item_single_view.dart
│ ├── chat_itemview.dart
│ ├── chat_linear_progress_indicator.dart
│ ├── chat_listview.dart
│ ├── chat_location_view.dart
│ ├── chat_longpress_ripple.dart
│ ├── chat_menu.dart
│ ├── chat_merge_view.dart
│ ├── chat_multi_toolbox.dart
│ ├── chat_picture_preview.dart
│ ├── chat_picture_view.dart
│ ├── chat_quote_view.dart
│ ├── chat_radio_view.dart
│ ├── chat_send_failed_view.dart
│ ├── chat_send_progress_view.dart
│ ├── chat_textfield.dart
│ ├── chat_tools_view.dart
│ ├── chat_video_player_view.dart
│ ├── chat_video_view.dart
│ ├── chat_voice_record_bar.dart
│ ├── chat_voice_record_layout.dart
│ ├── chat_voice_record_view.dart
│ ├── chat_voice_view.dart
│ ├── chat_webview_map.dart
│ ├── conversation_itemview.dart
│ ├── custom_chawie_controls.dart
│ ├── custom_focus_detector.dart
│ ├── custom_pop_up_menu.dart
│ ├── fadein_image.dart
│ ├── favorite_emoji_listview.dart
│ ├── overlay_widget.dart
│ ├── pop_button.dart
│ ├── text_selection_controls.dart
│ ├── timing_view.dart
│ ├── title_bar.dart
│ ├── unread_count_view.dart
│ └── util
│ ├── common_util.dart
│ ├── custom_ext.dart
│ ├── image_util.dart
│ ├── permission_util.dart
│ ├── ui_locallizations.dart
│ └── voice_record.dart
├── pubspec.lock
└── pubspec.yaml
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/.DS_Store
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.3.2
2 |
3 | * 新增视频缓存
4 |
5 | ## 2.3.1
6 |
7 | * 更新各插件版本
8 |
9 | ## 2.3.0
10 |
11 | * 修复长按菜单位置显示问题
12 | * 修复消息过长不能触发已读事件问题
13 | * 修复其他问题以及UI显示调整
14 | * 更新IM sdk 版本:V2.3.0
15 |
16 | ## 2.1.0
17 |
18 | * Fix bug
19 |
20 | ## 2.0.9+1
21 |
22 | * Fix bug
23 | * Adapt to flutter 3.0
24 |
25 | ## 2.0.9
26 |
27 | * Fix burning after reading bug
28 | * Fix the bug of audio playback interruption
29 | * Fix other bug
30 | * Fix menu occlude bug
31 |
32 | ## 2.0.8
33 |
34 | * Update im sdk to 2.0.8
35 |
36 | ## 0.0.11
37 |
38 | * Update im sdk to v2.0.0+6
39 |
40 | ## 0.0.10
41 |
42 | * Update im sdk to v2.0.0+5
43 |
44 | ## 0.0.9
45 |
46 | * Update im sdk to v2.0.0+4
47 | * Replace better_player package to video_player
48 |
49 | ## 0.0.8
50 |
51 | * Update im sdk to v2.0.0+3
52 |
53 | ## 0.0.7
54 |
55 | * Update im sdk to v2.0.0+1
56 |
57 | ## 0.0.6
58 |
59 | * Update im sdk to v2.0.0
60 |
61 | ## 0.0.5
62 |
63 | * Update sdk maven url
64 |
65 | ## 0.0.4
66 |
67 | * Replace fijkplayer package to better_player
68 |
69 | ## 0.0.3
70 |
71 | * Fix ios TextField custom filter bug
72 |
73 | ## 0.0.2
74 |
75 | * Fix repository missing
76 |
77 | ## 0.0.1
78 |
79 | * TODO: Describe initial release.
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 hrxiang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://pub.flutter-io.cn/packages/flutter_openim_widget)
2 | [](https://pub.dev/packages/flutter_openim_widget)
3 | [](https://github.com/hrxiang/flutter_openim_widget/blob/master/LICENSE)
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/.DS_Store
--------------------------------------------------------------------------------
/assets/anim/voice_black.json:
--------------------------------------------------------------------------------
1 | {"v":"5.6.10","fr":24,"ip":0,"op":48,"w":16,"h":16,"nm":"语音播放","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"#first","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[8,8,0],"ix":2},"a":{"a":0,"k":[8,8,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.172,-0.173],[0.062,-0.148],[0,-0.161],[-0.062,-0.149],[-0.114,-0.113],[-0.239,-0.048],[-0.226,0.093],[-0.136,0.203],[0,0.244],[0.136,0.202],[0.225,0.092],[0.239,-0.048]],"o":[[-0.114,0.114],[-0.062,0.149],[0,0.161],[0.062,0.149],[0.172,0.174],[0.239,0.049],[0.225,-0.092],[0.136,-0.202],[0,-0.244],[-0.136,-0.203],[-0.226,-0.093],[-0.239,0.048]],"v":[[-0.872,-0.867],[-1.139,-0.47],[-1.233,0],[-1.139,0.469],[-0.872,0.866],[-0.242,1.206],[0.471,1.138],[1.025,0.684],[1.233,0],[1.025,-0.685],[0.471,-1.138],[-0.242,-1.207]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[3.843,8.02],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"#second","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[1]},{"t":16,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[8,8,0],"ix":2},"a":{"a":0,"k":[8,8,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.113,1.148],[0.181,0],[0.136,-0.121],[0,-0.183],[-0.123,-0.134],[0,-1.237],[0.872,-0.877],[0,-0.181],[-0.121,-0.134],[-0.183,0],[-0.134,0.123],[0,1.599]],"o":[[-0.135,-0.121],[-0.182,0],[-0.123,0.134],[0,0.182],[0.872,0.876],[0,1.237],[-0.121,0.135],[0,0.182],[0.134,0.123],[0.182,0],[1.113,-1.148],[0,-1.599]],"v":[[-0.28,-4.286],[-0.772,-4.473],[-1.265,-4.286],[-1.456,-3.792],[-1.265,-3.301],[0.096,-0.001],[-1.265,3.298],[-1.453,3.79],[-1.265,4.281],[-0.772,4.473],[-0.28,4.281],[1.456,-0.001]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[7.584,8.08],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"#third","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[1]},{"t":32,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[8,8,0],"ix":2},"a":{"a":0,"k":[8,8,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.784,1.797],[0.089,0.037],[0.096,0],[0.089,-0.038],[0.067,-0.069],[0,-0.183],[-0.125,-0.133],[0,-2.154],[1.522,-1.526],[0,-0.195],[-0.138,-0.138],[-0.196,0],[-0.139,0.138],[0.002,2.532]],"o":[[-0.067,-0.069],[-0.089,-0.038],[-0.096,0],[-0.089,0.037],[-0.125,0.133],[0,0.182],[1.522,1.525],[0,2.155],[-0.138,0.137],[0,0.195],[0.138,0.138],[0.196,0],[1.782,-1.799],[-0.002,-2.533]],"v":[[-0.77,-6.759],[-1.006,-6.921],[-1.287,-6.978],[-1.568,-6.921],[-1.804,-6.759],[-1.999,-6.266],[-1.804,-5.774],[0.573,-0.028],[-1.804,5.719],[-2.02,6.24],[-1.804,6.762],[-1.282,6.978],[-0.76,6.762],[2.018,0]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.314,7.964],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0}],"markers":[]}
--------------------------------------------------------------------------------
/assets/anim/voice_ripple.json:
--------------------------------------------------------------------------------
1 | {"v":"4.10.1","fr":29.9700012207031,"ip":0,"op":62.0000025253118,"w":400,"h":400,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"voice Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[270,270,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.022,0],[0,0],[0,0.023],[0,0],[-0.021,0.002],[0,0],[0,7.956],[0,0],[0.023,0],[0,0],[0,-0.023],[0,0],[7.542,-0.351],[0,7.94],[0,0],[0.022,0],[0,0],[0,-0.023],[0,0],[-7.895,-0.994],[0,0],[0,-0.021],[0,0],[0.023,0],[0,0],[0,-0.023],[0,0],[-0.023,0],[0,0],[0,0.022],[0,0]],"o":[[0,0],[-0.023,0],[0,0],[0,-0.021],[0,0],[7.893,-0.994],[0,0],[0,-0.023],[0,0],[-0.023,0],[0,0],[0,7.55],[-8.022,0.374],[0,0],[0,-0.023],[0,0],[-0.023,0],[0,0],[0,7.958],[0,0],[0.021,0.002],[0,0],[0,0.023],[0,0],[-0.023,0],[0,0],[0,0.022],[0,0],[0.022,0],[0,0],[0,-0.023]],"v":[[10.752,11.56],[0.917,11.56],[0.876,11.518],[0.876,6.398],[0.912,6.358],[1.936,6.228],[15.75,-9.435],[15.75,-13.268],[15.708,-13.31],[14.042,-13.31],[14,-13.268],[14,-9.841],[0.668,4.585],[-14,-9.398],[-14,-13.268],[-14.041,-13.31],[-15.708,-13.31],[-15.75,-13.268],[-15.75,-9.438],[-1.933,6.228],[-0.912,6.358],[-0.875,6.398],[-0.875,11.518],[-0.917,11.56],[-10.749,11.56],[-10.791,11.603],[-10.791,13.27],[-10.749,13.31],[10.752,13.31],[10.793,13.27],[10.793,11.603]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.051,0.455,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[270,284.189],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.442,0],[0,5.439],[0,0],[5.439,0],[0,-5.439],[0,0]],"o":[[5.439,0],[0,0],[0,-5.439],[-5.442,0],[0,0],[0,5.439]],"v":[[0.001,20.946],[9.866,11.081],[9.866,-11.08],[0.001,-20.946],[-9.866,-11.08],[-9.866,11.081]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.051,0.455,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[270,263.447],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-39.488],[39.488,0],[0,39.488],[-39.488,0]],"o":[[0,39.488],[-39.488,0],[0,-39.488],[39.488,0]],"v":[[71.5,0],[0,71.5],[-71.5,0],[0,-71.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.051,0.455,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[270,270],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Base Layer 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":20,"s":[49],"e":[0]},{"t":45.0000018328876}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.029,200.234,0],"ix":2},"a":{"a":0,"k":[9.953,-2.402,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[31.532,31.532,100],"e":[69.368,69.368,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":20,"s":[69.368,69.368,100],"e":[84.882,84.882,100]},{"t":45.0000018328876}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[331.855,331.855],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.051,0.455,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.051,0.455,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.178,-2.697],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.195,97.195],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":62.0000025253118,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Base Layer 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":34,"s":[27],"e":[0]},{"t":59.0000024031193}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200.971,199.766,0],"ix":2},"a":{"a":0,"k":[12.553,-3.029,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[31.532,31.532,100],"e":[69.368,69.368,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":34,"s":[69.368,69.368,100],"e":[84.882,84.882,100]},{"t":59.0000024031193}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[331.855,331.855],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.051,0.455,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.051,0.455,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.178,-2.697],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.195,97.195],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":62.0000025253118,"st":0,"bm":0}]}
--------------------------------------------------------------------------------
/assets/images/ic_add.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_add.webp
--------------------------------------------------------------------------------
/assets/images/ic_add_emoji.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_add_emoji.webp
--------------------------------------------------------------------------------
/assets/images/ic_avatar_01.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_avatar_01.webp
--------------------------------------------------------------------------------
/assets/images/ic_avatar_02.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_avatar_02.webp
--------------------------------------------------------------------------------
/assets/images/ic_avatar_03.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_avatar_03.webp
--------------------------------------------------------------------------------
/assets/images/ic_avatar_04.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_avatar_04.webp
--------------------------------------------------------------------------------
/assets/images/ic_avatar_05.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_avatar_05.webp
--------------------------------------------------------------------------------
/assets/images/ic_avatar_06.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_avatar_06.webp
--------------------------------------------------------------------------------
/assets/images/ic_back.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_back.webp
--------------------------------------------------------------------------------
/assets/images/ic_del_face.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_del_face.webp
--------------------------------------------------------------------------------
/assets/images/ic_del_quote.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_del_quote.webp
--------------------------------------------------------------------------------
/assets/images/ic_download_continue.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_download_continue.webp
--------------------------------------------------------------------------------
/assets/images/ic_download_stop.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_download_stop.webp
--------------------------------------------------------------------------------
/assets/images/ic_emoji.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/ic_emoji.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_emoji.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_01.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_01.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_02.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_02.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_03.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_03.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_04.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_04.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_05.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_05.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_06.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_06.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_07.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_07.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_08.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_08.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_09.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_09.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_10.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_10.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_11.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_11.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_12.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_12.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_13.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_13.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_14.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_14.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_15.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_15.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_16.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_16.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_nor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_nor.webp
--------------------------------------------------------------------------------
/assets/images/ic_face_sel.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_face_sel.webp
--------------------------------------------------------------------------------
/assets/images/ic_favorite_emoji_nor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_favorite_emoji_nor.webp
--------------------------------------------------------------------------------
/assets/images/ic_favorite_emoji_sel.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_favorite_emoji_sel.webp
--------------------------------------------------------------------------------
/assets/images/ic_favorite_nor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_favorite_nor.webp
--------------------------------------------------------------------------------
/assets/images/ic_favorite_sel.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_favorite_sel.webp
--------------------------------------------------------------------------------
/assets/images/ic_file.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_file.webp
--------------------------------------------------------------------------------
/assets/images/ic_file_grey.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_file_grey.webp
--------------------------------------------------------------------------------
/assets/images/ic_keyboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/ic_keyboard.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_keyboard.webp
--------------------------------------------------------------------------------
/assets/images/ic_load_error.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_load_error.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_add_emoji.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_add_emoji.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_copy.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_copy.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_del.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_del.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_download.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_download.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_forward.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_forward.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_multichoice.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_multichoice.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_reply.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_reply.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_revoke.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_revoke.webp
--------------------------------------------------------------------------------
/assets/images/ic_menu_translation.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_menu_translation.webp
--------------------------------------------------------------------------------
/assets/images/ic_multi_tool_del.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_multi_tool_del.webp
--------------------------------------------------------------------------------
/assets/images/ic_multi_tool_merge_forward.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_multi_tool_merge_forward.webp
--------------------------------------------------------------------------------
/assets/images/ic_not_disturb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_not_disturb.webp
--------------------------------------------------------------------------------
/assets/images/ic_radio_msg_nor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_radio_msg_nor.webp
--------------------------------------------------------------------------------
/assets/images/ic_radio_msg_sel.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_radio_msg_sel.webp
--------------------------------------------------------------------------------
/assets/images/ic_search.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_search.webp
--------------------------------------------------------------------------------
/assets/images/ic_send_failed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_send_failed.webp
--------------------------------------------------------------------------------
/assets/images/ic_speak.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/ic_speak.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_speak.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/ic_tools.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_album.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_album.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_camera.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_camera.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_carte.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_carte.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_file.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_file.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_location.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_location.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_video_call.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_video_call.webp
--------------------------------------------------------------------------------
/assets/images/ic_tools_voice_input.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_tools_voice_input.webp
--------------------------------------------------------------------------------
/assets/images/ic_video_close.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_video_close.webp
--------------------------------------------------------------------------------
/assets/images/ic_video_download.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_video_download.webp
--------------------------------------------------------------------------------
/assets/images/ic_video_play.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_video_play.webp
--------------------------------------------------------------------------------
/assets/images/ic_video_play_small.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_video_play_small.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_black.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_black.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_blue.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_blue.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_cancel.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_cancel.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_confirm.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_confirm.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_convert_fail.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_convert_fail.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_convert_suc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_convert_suc.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_input_nor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_input_nor.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_bg1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_bg1.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_bg2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_bg2.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_cancel_grey.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_cancel_grey.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_cancel_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_cancel_white.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_speaker.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_speaker.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_zi_grey.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_zi_grey.webp
--------------------------------------------------------------------------------
/assets/images/ic_voice_record_zi_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hrxiang/flutter_openim_widget/833672de27fe272705a42a7c4b909d6e8514dd2b/assets/images/ic_voice_record_zi_white.webp
--------------------------------------------------------------------------------
/lib/flutter_openim_widget.dart:
--------------------------------------------------------------------------------
1 | library flutter_openim_widget;
2 |
3 | export 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
4 | export 'package:flutter_slidable/flutter_slidable.dart';
5 |
6 | export 'src/at_special_text_span_builder.dart';
7 | export 'src/chat_at_text.dart';
8 | export 'src/chat_avatar_view.dart';
9 | export 'src/chat_bubble.dart';
10 | export 'src/chat_carte_view.dart';
11 | export 'src/chat_custom_emoji_view.dart';
12 | export 'src/chat_emoji_view.dart';
13 | export 'src/chat_file_preview.dart';
14 | export 'src/chat_file_view.dart';
15 | export 'src/chat_inputbox_view.dart';
16 | export 'src/chat_item_group_view.dart';
17 | export 'src/chat_item_single_view.dart';
18 | export 'src/chat_itemview.dart';
19 | export 'src/chat_linear_progress_indicator.dart';
20 | export 'src/chat_listview.dart';
21 | export 'src/chat_location_view.dart';
22 | export 'src/chat_longpress_ripple.dart';
23 | export 'src/chat_menu.dart';
24 | export 'src/chat_merge_view.dart';
25 | export 'src/chat_multi_toolbox.dart';
26 | export 'src/chat_picture_preview.dart';
27 | export 'src/chat_picture_view.dart';
28 | export 'src/chat_quote_view.dart';
29 | export 'src/chat_radio_view.dart';
30 | export 'src/chat_send_failed_view.dart';
31 | export 'src/chat_send_progress_view.dart';
32 | export 'src/chat_textfield.dart';
33 | export 'src/chat_tools_view.dart';
34 | export 'src/chat_video_player_view.dart';
35 | export 'src/chat_video_view.dart';
36 | export 'src/chat_voice_record_bar.dart';
37 | export 'src/chat_voice_record_layout.dart';
38 | export 'src/chat_voice_record_view.dart';
39 | export 'src/chat_voice_view.dart';
40 | export 'src/chat_webview_map.dart';
41 | export 'src/conversation_itemview.dart';
42 | export 'src/custom_chawie_controls.dart';
43 | export 'src/custom_focus_detector.dart';
44 | export 'src/custom_pop_up_menu.dart';
45 | export 'src/favorite_emoji_listview.dart';
46 | export 'src/overlay_widget.dart';
47 | export 'src/pop_button.dart';
48 | export 'src/timing_view.dart';
49 | export 'src/title_bar.dart';
50 | export 'src/unread_count_view.dart';
51 | export 'src/util/common_util.dart';
52 | export 'src/util/custom_ext.dart';
53 | export 'src/util/image_util.dart';
54 | export 'src/util/permission_util.dart';
55 | export 'src/util/ui_locallizations.dart';
56 | export 'src/util/voice_record.dart';
57 |
--------------------------------------------------------------------------------
/lib/src/at_special_text_span_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:extended_text_field/extended_text_field.dart';
2 | import 'package:flutter/foundation.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 |
6 | typedef AtTextCallback = Function(String showText, String actualText);
7 |
8 | class AtSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
9 | final AtTextCallback? atCallback;
10 |
11 | /// key:userid
12 | /// value:username
13 | final Map allAtMap;
14 | final TextStyle? atStyle;
15 |
16 | AtSpecialTextSpanBuilder({
17 | this.atCallback,
18 | this.atStyle,
19 | this.allAtMap = const {},
20 | });
21 |
22 | @override
23 | TextSpan build(
24 | String data, {
25 | TextStyle? textStyle,
26 | SpecialTextGestureTapCallback? onTap,
27 | }) {
28 | StringBuffer buffer = StringBuffer();
29 | if (kIsWeb) {
30 | return TextSpan(text: data, style: textStyle);
31 | }
32 | // if (allAtMap.isEmpty) {
33 | // return TextSpan(text: data, style: textStyle);
34 | // }
35 | final List children = [];
36 |
37 | var regexEmoji = emojiFaces.keys
38 | .toList()
39 | .join('|')
40 | .replaceAll('[', '\\[')
41 | .replaceAll(']', '\\]');
42 |
43 | final list = [regexAt, regexEmoji];
44 | final pattern = '(${list.toList().join('|')})';
45 | final atReg = RegExp(regexAt);
46 | final emojiReg = RegExp(regexEmoji);
47 |
48 | data.splitMapJoin(
49 | RegExp(pattern),
50 | // RegExp(r"(@\S+\s)"),
51 | onMatch: (Match m) {
52 | late InlineSpan inlineSpan;
53 | String value = m.group(0)!;
54 | try {
55 | if (atReg.hasMatch(value)) {
56 | String id = value.replaceFirst("@", "").trim();
57 | if (allAtMap.containsKey(id)) {
58 | var name = allAtMap[id]!;
59 | // inlineSpan = ExtendedWidgetSpan(
60 | // child: Text('@$name ', style: atStyle),
61 | // style: atStyle,
62 | // actualText: '$value',
63 | // start: m.start,
64 | // );
65 | inlineSpan = SpecialTextSpan(
66 | text: '@$name ',
67 | actualText: '$value',
68 | start: m.start,
69 | style: atStyle,
70 | );
71 | buffer.write('@$name ');
72 | } else {
73 | inlineSpan = TextSpan(text: '$value', style: textStyle);
74 | buffer.write('$value');
75 | }
76 | } else if (emojiReg.hasMatch(value)) {
77 | inlineSpan = ImageSpan(
78 | ImageUtil.emojiImage(value),
79 | imageWidth: atStyle!.fontSize!,
80 | imageHeight: atStyle!.fontSize!,
81 | start: m.start,
82 | actualText: '$value',
83 | );
84 | }
85 | /*String id = value.replaceAll("@", "").trim();
86 | if (allAtMap.containsKey(id)) {
87 | var name = allAtMap[id]!;
88 | inlineSpan = ExtendedWidgetSpan(
89 | child: Text('@$name ', style: atStyle),
90 | style: atStyle,
91 | actualText: '$value',
92 | start: m.start,
93 | );
94 | buffer.write('@$name ');
95 | }*/
96 | else {
97 | /* inlineSpan = SpecialTextSpan(
98 | text: '${m.group(0)}',
99 | style: TextStyle(color: Colors.blue),
100 | start: m.start,
101 | );*/
102 | inlineSpan = TextSpan(text: '$value', style: textStyle);
103 | buffer.write('$value');
104 | }
105 | } catch (e) {
106 | print('error: $e');
107 | }
108 | children.add(inlineSpan);
109 | return "";
110 | },
111 | onNonMatch: (text) {
112 | children.add(TextSpan(text: text, style: textStyle));
113 | buffer.write(text);
114 | return '';
115 | },
116 | );
117 | if (null != atCallback) atCallback!(buffer.toString(), data);
118 | return TextSpan(children: children, style: textStyle);
119 | }
120 |
121 | @override
122 | SpecialText? createSpecialText(
123 | String flag, {
124 | TextStyle? textStyle,
125 | SpecialTextGestureTapCallback? onTap,
126 | required int index,
127 | }) {
128 | return null;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/lib/src/chat_at_text.dart:
--------------------------------------------------------------------------------
1 | import 'package:extended_text/extended_text.dart';
2 | import 'package:extended_text_field/extended_text_field.dart';
3 | import 'package:flutter/gestures.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
6 | import 'package:flutter_screenutil/flutter_screenutil.dart';
7 |
8 | /// message content: @uid1 @uid2 xxxxxxx
9 | ///
10 |
11 | enum ChatTextModel { match, normal }
12 |
13 | class ChatAtText extends StatelessWidget {
14 | final String text;
15 | final TextStyle? textStyle;
16 | final InlineSpan? prefixSpan;
17 |
18 | /// isReceived ? TextAlign.left : TextAlign.right
19 | final TextAlign textAlign;
20 | final TextOverflow overflow;
21 | final int? maxLines;
22 | final double textScaleFactor;
23 |
24 | /// all user info
25 | /// key:userid
26 | /// value:username
27 | final Map allAtMap;
28 | final List patterns;
29 | final ChatTextModel model;
30 | final Function(String? text)? onVisibleTrulyText;
31 |
32 | // final TextAlign textAlign;
33 | const ChatAtText({
34 | Key? key,
35 | required this.text,
36 | this.allAtMap = const {},
37 | this.prefixSpan,
38 | this.patterns = const [],
39 | this.textAlign = TextAlign.left,
40 | this.overflow = TextOverflow.clip,
41 | this.textStyle,
42 | this.maxLines,
43 | this.textScaleFactor = 1.0,
44 | this.model = ChatTextModel.match,
45 | this.onVisibleTrulyText,
46 | }) : super(key: key);
47 |
48 | static var _textStyle = TextStyle(
49 | fontSize: 14.sp,
50 | color: Color(0xFF333333),
51 | );
52 |
53 | @override
54 | Widget build(BuildContext context) {
55 | final List children = [];
56 |
57 | if (prefixSpan != null) children.add(prefixSpan!);
58 |
59 | if (model == ChatTextModel.normal) {
60 | _normalModel(children);
61 | } else {
62 | _matchModel(children);
63 | }
64 |
65 | // 复制@消息直接使用不在重复解析
66 | final textSpan = TextSpan(children: children);
67 | onVisibleTrulyText?.call(textSpan.toPlainText());
68 |
69 | return Container(
70 | constraints: BoxConstraints(maxWidth: 0.5.sw),
71 | child: RichText(
72 | textAlign: textAlign,
73 | overflow: overflow,
74 | maxLines: maxLines,
75 | textScaleFactor: textScaleFactor,
76 | text: textSpan,
77 | ),
78 | );
79 | }
80 |
81 | _normalModel(List children) {
82 | var style = textStyle ?? _textStyle;
83 | children.add(TextSpan(text: text, style: style));
84 | }
85 |
86 | _matchModel(List children) {
87 | var style = textStyle ?? _textStyle;
88 |
89 | final _mapping = Map();
90 |
91 | patterns.forEach((e) {
92 | if (e.type == PatternType.AT) {
93 | _mapping[regexAt] = e;
94 | } else if (e.type == PatternType.EMAIL) {
95 | _mapping[regexEmail] = e;
96 | } else if (e.type == PatternType.MOBILE) {
97 | _mapping[regexMobile] = e;
98 | } else if (e.type == PatternType.TEL) {
99 | _mapping[regexTel] = e;
100 | } else if (e.type == PatternType.URL) {
101 | _mapping[regexUrl] = e;
102 | } else {
103 | _mapping[e.pattern!] = e;
104 | }
105 | });
106 |
107 | var regexEmoji = emojiFaces.keys
108 | .toList()
109 | .join('|')
110 | .replaceAll('[', '\\[')
111 | .replaceAll(']', '\\]');
112 |
113 | _mapping[regexEmoji] = MatchPattern(type: PatternType.EMOJI);
114 |
115 | final pattern;
116 |
117 | if (_mapping.length > 1) {
118 | pattern = '(${_mapping.keys.toList().join('|')})';
119 | } else {
120 | pattern = regexEmoji;
121 | }
122 |
123 | // match text
124 | stripHtmlIfNeeded(text).splitMapJoin(
125 | RegExp(pattern),
126 | onMatch: (Match match) {
127 | var matchText = match[0]!;
128 | var inlineSpan;
129 | final mapping = _mapping[matchText] ??
130 | _mapping[_mapping.keys.firstWhere((element) {
131 | final reg = RegExp(element);
132 | return reg.hasMatch(matchText);
133 | }, orElse: () {
134 | return '';
135 | })];
136 | if (mapping != null) {
137 | if (mapping.type == PatternType.AT) {
138 | String userID = matchText.replaceFirst("@", "").trim();
139 | if (allAtMap.containsKey(userID)) {
140 | matchText = '@${allAtMap[userID]} ';
141 | inlineSpan = TextSpan(
142 | text: "$matchText",
143 | style: mapping.style != null ? mapping.style : style,
144 | recognizer: mapping.onTap == null
145 | ? null
146 | : (TapGestureRecognizer()
147 | ..onTap = () => mapping.onTap!(
148 | _getUrl(userID, mapping.type), mapping.type)),
149 | );
150 | } else {
151 | inlineSpan = TextSpan(text: matchText, style: style);
152 | }
153 | } else if (mapping.type == PatternType.EMOJI) {
154 | inlineSpan = ImageSpan(
155 | ImageUtil.emojiImage(matchText),
156 | imageWidth: style.fontSize! * 1.4,
157 | imageHeight: style.fontSize! * 1.4,
158 | );
159 | } else {
160 | inlineSpan = TextSpan(
161 | text: matchText,
162 | style: mapping.style != null ? mapping.style : style,
163 | recognizer: mapping.onTap == null
164 | ? null
165 | : (TapGestureRecognizer()
166 | ..onTap = () => mapping.onTap!(
167 | _getUrl(matchText, mapping.type), mapping.type)),
168 | );
169 | }
170 | } else {
171 | inlineSpan = TextSpan(text: matchText, style: style);
172 | }
173 | children.add(inlineSpan);
174 | return '';
175 | },
176 | onNonMatch: (text) {
177 | children.add(TextSpan(text: text, style: style));
178 | return '';
179 | },
180 | );
181 | }
182 |
183 | _getUrl(String text, PatternType type) {
184 | switch (type) {
185 | case PatternType.URL:
186 | return text.substring(0, 4) == 'http' ? text : 'http://$text';
187 | case PatternType.EMAIL:
188 | return text.substring(0, 7) == 'mailto:' ? text : 'mailto:$text';
189 | case PatternType.TEL:
190 | case PatternType.MOBILE:
191 | return text.substring(0, 4) == 'tel:' ? text : 'tel:$text';
192 | // case PatternType.PHONE:
193 | // return text.substring(0, 4) == 'tel:' ? text : 'tel:$text';
194 | default:
195 | return text;
196 | }
197 | }
198 |
199 | static String stripHtmlIfNeeded(String text) {
200 | return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
201 | }
202 | }
203 |
204 | class MatchPattern {
205 | PatternType type;
206 |
207 | String? pattern;
208 |
209 | TextStyle? style;
210 |
211 | Function(String link, PatternType? type)? onTap;
212 |
213 | MatchPattern({required this.type, this.pattern, this.style, this.onTap});
214 | }
215 |
216 | enum PatternType { AT, EMAIL, MOBILE, TEL, URL, EMOJI, CUSTOM }
217 |
218 | /// 空格@uid空格
219 | const regexAt = r"(@\S+\s)";
220 | // const regexAt = r"(\s@\S+\s)";
221 |
222 | /// Email Regex - A predefined type for handling email matching
223 | const regexEmail = r"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b";
224 |
225 | /// URL Regex - A predefined type for handling URL matching
226 | const regexUrl =
227 | r"[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:._\+-~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:_\+.~#?&\/\/=]*)";
228 |
229 | /// Phone Regex - A predefined type for handling phone matching
230 | // const regexMobile =
231 | // r"(\+?( |-|\.)?\d{1,2}( |-|\.)?)?(\(?\d{3}\)?|\d{3})( |-|\.)?(\d{3}( |-|\.)?\d{4})";
232 |
233 | /// Regex of exact mobile.
234 | const String regexMobile =
235 | '^(\\+?86)?((13[0-9])|(14[57])|(15[0-35-9])|(16[2567])|(17[01235-8])|(18[0-9])|(19[1589]))\\d{8}\$';
236 |
237 | /// Regex of telephone number.
238 | const String regexTel = '^0\\d{2,3}[-]?\\d{7,8}';
239 |
--------------------------------------------------------------------------------
/lib/src/chat_avatar_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 | import 'package:flutter_screenutil/flutter_screenutil.dart';
6 | import 'package:font_awesome_flutter/font_awesome_flutter.dart';
7 |
8 | List get indexAvatarList => [
9 | 'ic_avatar_01',
10 | 'ic_avatar_02',
11 | 'ic_avatar_03',
12 | 'ic_avatar_04',
13 | 'ic_avatar_05',
14 | 'ic_avatar_06',
15 | ];
16 |
17 | typedef CustomAvatarBuilder = Widget? Function();
18 |
19 | class ChatAvatarView extends StatelessWidget {
20 | const ChatAvatarView({
21 | Key? key,
22 | this.visible = true,
23 | this.size,
24 | this.onTap,
25 | this.url,
26 | this.builder,
27 | this.onLongPress,
28 | this.isCircle = false,
29 | this.borderRadius,
30 | this.text,
31 | this.textStyle,
32 | this.lowMemory = false,
33 | this.isNineGrid = false,
34 | this.nineGridUrls = const [],
35 | this.isUserGroup = false,
36 | this.color,
37 | }) : super(key: key);
38 | final bool visible;
39 | final double? size;
40 | final Function()? onTap;
41 | final Function()? onLongPress;
42 | final String? url;
43 | final CustomAvatarBuilder? builder;
44 | final bool isCircle;
45 | final BorderRadius? borderRadius;
46 | final String? text;
47 | final TextStyle? textStyle;
48 | final Color? color;
49 | final bool lowMemory;
50 | final List nineGridUrls;
51 |
52 | /// 九宫格
53 | final bool isNineGrid;
54 |
55 | /// 群头像
56 | final bool isUserGroup;
57 |
58 | double get _size => size ?? 42.h;
59 |
60 | TextStyle get _style =>
61 | textStyle ?? TextStyle(fontSize: 14.sp, color: Colors.white);
62 |
63 | Color get _defaultAvatarBgColor => color ?? const Color(0xFF5496EB);
64 |
65 | bool _isIndexAvatar() => indexAvatarList.contains(url);
66 |
67 | static int? _calculateCacheWidth(double? width) {
68 | final maxW = 1.sw * .3;
69 | return (width == null ? maxW : (width < maxW ? width : maxW)).toInt();
70 | }
71 |
72 | double get _minSize {
73 | final maxW = 1.sw * .3;
74 | return min(maxW, _size);
75 | }
76 |
77 | @override
78 | Widget build(BuildContext context) {
79 | var child = InkWell(
80 | onTap: onTap,
81 | onLongPress: onLongPress,
82 | child:
83 | builder?.call() ?? (isNineGrid ? _nineGridAvatar() : _normalAvatar()),
84 | );
85 | return Visibility(
86 | visible: visible,
87 | child: isCircle
88 | ? ClipOval(child: child)
89 | : ClipRRect(
90 | child: child,
91 | borderRadius: borderRadius ?? BorderRadius.circular(6),
92 | ),
93 | );
94 | }
95 |
96 | Widget _normalAvatar() => _avatarView();
97 |
98 | Widget _avatarView() => null == url || url!.isEmpty
99 | ? _defaultAvatar()
100 | : (_isIndexAvatar() ? _indexAvatar() : _networkImage());
101 |
102 | Widget _indexAvatar() => Container(
103 | width: _size,
104 | height: _size,
105 | child: ImageUtil.assetImage(url!, width: size, height: size),
106 | );
107 |
108 | Widget _defaultAvatar() => Container(
109 | color: _defaultAvatarBgColor,
110 | child: isUserGroup
111 | ? FaIcon(
112 | FontAwesomeIcons.userGroup,
113 | color: Colors.white,
114 | size: _size - (_size / 2),
115 | )
116 | : null == text
117 | ? FaIcon(
118 | FontAwesomeIcons.solidUser,
119 | color: Colors.white,
120 | size: _size - (_size / 2),
121 | )
122 | : Text(text!, style: _style),
123 | width: _size,
124 | height: _size,
125 | alignment: Alignment.center,
126 | );
127 |
128 | Widget _networkImage() => lowMemory
129 | ? ImageUtil.lowMemoryNetworkImage(
130 | url: url!,
131 | width: _minSize,
132 | height: _minSize,
133 | fit: BoxFit.cover,
134 | loadProgress: false,
135 | cacheWidth: _calculateCacheWidth(_size),
136 | )
137 | : ImageUtil.networkImage(
138 | url: url!,
139 | width: _minSize,
140 | height: _minSize,
141 | fit: BoxFit.cover,
142 | loadProgress: false,
143 | cacheWidth: _calculateCacheWidth(_size),
144 | );
145 |
146 | Widget _nineGridAvatar() => Container(
147 | width: _size,
148 | height: _size,
149 | color: Colors.grey[300],
150 | padding: EdgeInsets.all(2.0),
151 | alignment: Alignment.center,
152 | child: _nineGridColumn(),
153 | );
154 |
155 | /// 9宫格列
156 | Widget _nineGridColumn() {
157 | var width;
158 | var margin = 2.0;
159 | var row1Length;
160 | var row2Length;
161 | var row3Length;
162 | var list = [];
163 | switch (nineGridUrls.length) {
164 | case 1:
165 | width = _size;
166 | row1Length = 1;
167 | break;
168 | case 2:
169 | width = _size / 2;
170 | row1Length = 2;
171 | break;
172 | case 3:
173 | width = _size / 2;
174 | row1Length = 1;
175 | row2Length = 2;
176 | break;
177 | case 4:
178 | width = _size / 2;
179 | row1Length = 2;
180 | row2Length = 2;
181 | break;
182 | case 5:
183 | width = _size / 3;
184 | row1Length = 2;
185 | row2Length = 3;
186 | break;
187 | case 6:
188 | width = _size / 3;
189 | row1Length = 3;
190 | row2Length = 3;
191 | break;
192 | case 7:
193 | width = _size / 3;
194 | row1Length = 1;
195 | row2Length = 3;
196 | row3Length = 3;
197 | break;
198 | case 8:
199 | width = _size / 3;
200 | row1Length = 2;
201 | row2Length = 3;
202 | row3Length = 3;
203 | break;
204 | case 9:
205 | width = _size / 3;
206 | row1Length = 3;
207 | row2Length = 3;
208 | row3Length = 3;
209 | break;
210 | }
211 | if (row1Length > 0) {
212 | list.add(_nineGridRow(
213 | length: row1Length,
214 | start: 0,
215 | size: width,
216 | margin: margin,
217 | ));
218 | }
219 | if (row2Length > 0) {
220 | list.add(_nineGridRow(
221 | length: row2Length,
222 | start: row1Length,
223 | size: width,
224 | margin: margin,
225 | ));
226 | }
227 | if (row3Length > 0) {
228 | list.add(_nineGridRow(
229 | length: row3Length,
230 | start: row1Length + row2Length,
231 | size: width,
232 | margin: margin,
233 | ));
234 | }
235 | return Column(
236 | children: list,
237 | );
238 | }
239 |
240 | /// 9宫格行
241 | Widget _nineGridRow({
242 | required int length,
243 | required int start,
244 | required double size,
245 | required double margin,
246 | }) {
247 | var list = [];
248 | for (var i = 0; i < length; i++) {
249 | start += i;
250 | list.add(_nineGridImage(nineGridUrls.elementAt(start), size));
251 | if (i != length - 1) {
252 | list.add(_nineGridLine(width: margin, height: size));
253 | }
254 | }
255 | return Row(
256 | mainAxisAlignment: MainAxisAlignment.center,
257 | children: list,
258 | );
259 | }
260 |
261 | Widget _nineGridImage(String? url, double size) => _avatarView();
262 |
263 | Widget _nineGridLine({
264 | double? width,
265 | double? height,
266 | }) =>
267 | Container(height: height, width: width, color: Colors.white);
268 | }
269 |
--------------------------------------------------------------------------------
/lib/src/chat_bubble.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_screenutil/flutter_screenutil.dart';
3 |
4 | enum BubbleType {
5 | send,
6 | receiver,
7 | }
8 |
9 | class ChatBubble extends StatelessWidget {
10 | const ChatBubble({
11 | Key? key,
12 | this.constraints,
13 | this.backgroundColor,
14 | this.child,
15 | required this.bubbleType,
16 | }) : super(key: key);
17 | final BoxConstraints? constraints;
18 | final Color? backgroundColor;
19 | final Widget? child;
20 | final BubbleType bubbleType;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Container(
25 | constraints: constraints,
26 | margin: EdgeInsets.only(right: 10.w, left: 10.w, bottom: 2.h),
27 | padding: EdgeInsets.symmetric(
28 | horizontal: 7.w,
29 | vertical: 7.h,
30 | ),
31 | alignment: Alignment.center,
32 | decoration: BoxDecoration(
33 | color: backgroundColor,
34 | borderRadius: BorderRadius.only(
35 | topLeft: Radius.circular(bubbleType == BubbleType.send ? 8 : 1),
36 | topRight: Radius.circular(bubbleType == BubbleType.send ? 1 : 8),
37 | bottomLeft: Radius.circular(8),
38 | bottomRight: Radius.circular(8),
39 | ),
40 | ),
41 | child: child,
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/src/chat_carte_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 |
5 | class ChatCarteView extends StatelessWidget {
6 | const ChatCarteView({
7 | Key? key,
8 | required this.name,
9 | this.url,
10 | }) : super(key: key);
11 | final String name;
12 | final String? url;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | return Container(
17 | // padding: EdgeInsets.only(top: 12.h, bottom: 4.h),
18 | width: 200.w,
19 | decoration: BoxDecoration(
20 | color: Colors.white,
21 | borderRadius: BorderRadius.circular(6),
22 | // border: Border.all(
23 | // color: Color(0xFFECECEC),
24 | // width: 0.5,
25 | // ),
26 | boxShadow: [
27 | BoxShadow(
28 | color: Color(0xFF000000).withOpacity(0.1),
29 | offset: Offset(0, 2.h),
30 | blurRadius: 4,
31 | ),
32 | ],
33 | ),
34 | child: Column(
35 | crossAxisAlignment: CrossAxisAlignment.start,
36 | children: [
37 | Padding(
38 | padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
39 | child: Row(
40 | crossAxisAlignment: CrossAxisAlignment.center,
41 | children: [
42 | ChatAvatarView(
43 | size: 40.h,
44 | url: url,
45 | ),
46 | SizedBox(
47 | width: 10.w,
48 | ),
49 | Expanded(
50 | child: Text(
51 | name,
52 | overflow: TextOverflow.ellipsis,
53 | style: TextStyle(
54 | color: Color(0xFF333333),
55 | fontSize: 16.sp,
56 | ),
57 | ),
58 | ),
59 | ],
60 | ),
61 | ),
62 | Container(
63 | color: Color(0xFFE9E9E9),
64 | height: 1.h,
65 | ),
66 | Padding(
67 | padding: EdgeInsets.only(top: 3.h, bottom: 4.h, left: 25.w),
68 | child: Text(
69 | UILocalizations.carte,
70 | overflow: TextOverflow.ellipsis,
71 | style: TextStyle(
72 | color: Color(0xFF999999),
73 | fontSize: 11.sp,
74 | ),
75 | ),
76 | )
77 | ],
78 | ),
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/src/chat_custom_emoji_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 |
6 | class ChatCustomEmojiView extends StatelessWidget {
7 | const ChatCustomEmojiView({
8 | Key? key,
9 | this.index,
10 | this.data,
11 | this.widgetWidth = 100,
12 | }) : super(key: key);
13 |
14 | /// 内置表情包,按位置显示
15 | final int? index;
16 |
17 | /// 收藏的表情包以加载url的方式
18 | /// {"url:"", "width":0, "height":0 }
19 | final String? data;
20 |
21 | final double widgetWidth;
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | // 收藏的url表情
26 | try {
27 | if (data != null) {
28 | var map = json.decode(data!);
29 | var url = map['url'];
30 | var w = map['width'] ?? 1.0;
31 | var h = map['height'] ?? 1.0;
32 | if (w is int) {
33 | w = w.toDouble();
34 | }
35 | if (h is int) {
36 | h = h.toDouble();
37 | }
38 | var trulyWidth;
39 | var trulyHeight;
40 | if (widgetWidth < w) {
41 | trulyWidth = widgetWidth;
42 | trulyHeight = trulyWidth * h / w;
43 | } else {
44 | trulyWidth = w;
45 | trulyHeight = h;
46 | }
47 |
48 | return ImageUtil.networkImage(
49 | url: url,
50 | width: trulyWidth,
51 | height: trulyHeight,
52 | // cacheWidth: trulyWidth,
53 | );
54 | }
55 | } catch (e) {
56 | print('e:$e');
57 | }
58 | // 位置表情
59 | return Container();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/src/chat_file_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 | import 'package:font_awesome_flutter/font_awesome_flutter.dart';
5 |
6 | /*
7 | class ChatFileView extends StatefulWidget {
8 | const ChatFileView({
9 | Key? key,
10 | required this.msgId,
11 | required this.fileName,
12 | required this.bytes,
13 | required this.index,
14 | required this.filePath,
15 | required this.url,
16 | this.clickStream,
17 | this.width = 158,
18 | this.initProgress = 100,
19 | this.uploadStream,
20 | }) : super(key: key);
21 | final String msgId;
22 | final String fileName;
23 | final String filePath;
24 | final String url;
25 | final int index;
26 | final Stream? clickStream;
27 | final int bytes;
28 | final int initProgress;
29 | final Stream>? uploadStream;
30 | final double width;
31 |
32 |
33 | @override
34 | _ChatFileViewState createState() => _ChatFileViewState();
35 | }
36 |
37 | class _ChatFileViewState extends State {
38 | @override
39 | void initState() {
40 | */
41 | /* widget.clickStream?.listen((i) {
42 | if (!mounted) return;
43 | if (_isClickedLocation(i)) {
44 | if (null != widget.onClickFile) {
45 | widget.onClickFile!(widget.url, widget.filePath);
46 | }
47 | }
48 | });*/ /*
49 |
50 | super.initState();
51 | }
52 |
53 | bool _isClickedLocation(i) => i == widget.index;
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | return Container(
58 | width: widget.width,
59 | // height: 70.h,
60 | child: Column(
61 | children: [
62 | Row(
63 | children: [
64 | Expanded(
65 | child: Column(
66 | crossAxisAlignment: CrossAxisAlignment.start,
67 | children: [
68 | Text(
69 | widget.fileName,
70 | style: TextStyle(
71 | fontSize: 12.sp,
72 | color: Color(0xFF333333),
73 | ),
74 | ),
75 | SizedBox(
76 | height: 4.w,
77 | ),
78 | Text(
79 | CommonUtil.formatBytes(widget.bytes),
80 | style: TextStyle(
81 | fontSize: 10.sp,
82 | color: Color(0xFF777777),
83 | ),
84 | )
85 | ],
86 | ),
87 | ),
88 | SizedBox(
89 | width: 10.w,
90 | ),
91 | ChatIcon.file(),
92 | ],
93 | ),
94 | ChatLinearProgressView(
95 | width: widget.width,
96 | height: 10.h,
97 | msgId: widget.msgId,
98 | initProgress: widget.initProgress,
99 | stream: widget.uploadStream,
100 | ),
101 | ],
102 | ),
103 | );
104 | }
105 | }
106 | */
107 |
108 | class ChatFileView extends StatelessWidget {
109 | const ChatFileView({
110 | Key? key,
111 | required this.msgId,
112 | required this.fileName,
113 | required this.bytes,
114 | required this.index,
115 | // required this.filePath,
116 | // required this.url,
117 | this.clickStream,
118 | this.width = 158,
119 | this.initProgress = 100,
120 | this.uploadStream,
121 | }) : super(key: key);
122 | final String msgId;
123 | final String fileName;
124 | final int bytes;
125 | final int initProgress;
126 | final Stream>? uploadStream;
127 | final double width;
128 | // final String filePath;
129 | // final String url;
130 | final int index;
131 | final Stream? clickStream;
132 |
133 | @override
134 | Widget build(BuildContext context) {
135 | return Container(
136 | // width: width,
137 | // height: 70.h,
138 | constraints: BoxConstraints(maxWidth: 140.w),
139 | child: Column(
140 | children: [
141 | Row(
142 | crossAxisAlignment: CrossAxisAlignment.start,
143 | children: [
144 | Expanded(
145 | child: Column(
146 | crossAxisAlignment: CrossAxisAlignment.start,
147 | children: [
148 | Text(
149 | fileName,
150 | style: TextStyle(
151 | fontSize: 14.sp,
152 | color: Color(0xFF333333),
153 | ),
154 | ),
155 | SizedBox(
156 | height: 4.w,
157 | ),
158 | Text(
159 | CommonUtil.formatBytes(bytes),
160 | style: TextStyle(
161 | fontSize: 11.sp,
162 | color: Color(0xFF666666),
163 | ),
164 | )
165 | ],
166 | ),
167 | ),
168 | SizedBox(
169 | width: 10.w,
170 | ),
171 | // ImageUtil.file(),
172 | FaIcon(
173 | CommonUtil.fileIcon(fileName),
174 | size: 28,
175 | color: Color(0xFF1b6bed),
176 | )
177 | ],
178 | ),
179 | ChatLinearProgressView(
180 | width: width,
181 | height: 10.h,
182 | msgId: msgId,
183 | initProgress: initProgress,
184 | stream: uploadStream,
185 | ),
186 | ],
187 | ),
188 | );
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/lib/src/chat_item_group_view.dart:
--------------------------------------------------------------------------------
1 | // import 'package:bubble/bubble.dart';
2 | // import 'package:flutter/material.dart';
3 | // import 'package:flutter_openim_widget/flutter_openim_widget.dart';
4 | // import 'package:flutter_screenutil/flutter_screenutil.dart';
5 | //
6 | // class ChatGroupLayout extends StatelessWidget {
7 | // const ChatGroupLayout({
8 | // Key? key,
9 | // required this.child,
10 | // required this.msgId,
11 | // required this.index,
12 | // required this.menuBuilder,
13 | // required this.clickSink,
14 | // required this.popupCtrl,
15 | // required this.isReceived,
16 | // required this.rightAvatar,
17 | // // required this.rightName,
18 | // required this.leftAvatar,
19 | // required this.leftName,
20 | // this.avatarSize = 42.0,
21 | // this.leftBubbleColor = const Color(0xFFF0F0F0),
22 | // this.rightBubbleColor = const Color(0xFFDCEBFE),
23 | // this.onLongPressRightAvatar,
24 | // this.onTapRightAvatar,
25 | // this.onLongPressLeftAvatar,
26 | // this.onTapLeftAvatar,
27 | // this.sendStatusStream,
28 | // this.isSendFailed = false,
29 | // this.timeView,
30 | // }) : super(key: key);
31 | //
32 | // final CustomPopupMenuController popupCtrl;
33 | // final Widget child;
34 | // final String msgId;
35 | // final int index;
36 | // final Sink clickSink;
37 | // final Widget Function() menuBuilder;
38 | // final Function()? onTapLeftAvatar;
39 | // final Function()? onLongPressLeftAvatar;
40 | // final Function()? onTapRightAvatar;
41 | // final Function()? onLongPressRightAvatar;
42 | // final String leftAvatar;
43 | // final String leftName;
44 | // final String rightAvatar;
45 | // final double avatarSize;
46 | //
47 | // // final String rightName;
48 | // final bool isReceived;
49 | // final Color leftBubbleColor;
50 | // final Color rightBubbleColor;
51 | // final Stream>? sendStatusStream;
52 | // final bool isSendFailed;
53 | // final Widget? timeView;
54 | //
55 | // @override
56 | // Widget build(BuildContext context) {
57 | // return Column(
58 | // children: [
59 | // if (timeView != null) timeView!,
60 | // Row(
61 | // mainAxisAlignment: _layoutAlignment(),
62 | // crossAxisAlignment: CrossAxisAlignment.start,
63 | // children: [
64 | // _buildAvatar(
65 | // leftAvatar,
66 | // isReceived,
67 | // onTap: onTapLeftAvatar,
68 | // onLongPress: onLongPressLeftAvatar,
69 | // ),
70 | // Column(
71 | // crossAxisAlignment: CrossAxisAlignment.start,
72 | // children: [
73 | // Container(
74 | // margin: EdgeInsets.only(bottom: 2.h, left: 10.w),
75 | // child: Visibility(
76 | // child: Text(
77 | // isReceived ? leftName : '',
78 | // style: TextStyle(
79 | // color: Color(0xFF666666),
80 | // fontSize: 10.sp,
81 | // ),
82 | // ),
83 | // ),
84 | // ),
85 | // Row(
86 | // mainAxisSize: MainAxisSize.min,
87 | // crossAxisAlignment: CrossAxisAlignment.center,
88 | // children: [
89 | // ChatSendFailedView(
90 | // msgId: msgId,
91 | // isReceived: isReceived,
92 | // stream: sendStatusStream,
93 | // isSendFailed: isSendFailed,
94 | // ),
95 | // CopyCustomPopupMenu(
96 | // controller: popupCtrl,
97 | // barrierColor: Colors.transparent,
98 | // arrowColor: Color(0xFF666666),
99 | // verticalMargin: 0,
100 | // // horizontalMargin: 0,
101 | // child: Bubble(
102 | // margin: BubbleEdges.only(
103 | // left: isReceived ? 4.w : 0,
104 | // right: isReceived ? 0 : 4.w,
105 | // ),
106 | // // alignment: Alignment.topRight,
107 | // nip: _nip(),
108 | // color: _bubbleColor(),
109 | // child: InkWell(
110 | // child: child,
111 | // onTap: () => _onItemClick?.add(index),
112 | // ),
113 | // ),
114 | // menuBuilder: menuBuilder,
115 | // pressType: PressType.longPress,
116 | // ),
117 | // ],
118 | // )
119 | // ],
120 | // ),
121 | // _buildAvatar(
122 | // rightAvatar,
123 | // !isReceived,
124 | // onTap: onTapRightAvatar,
125 | // onLongPress: onLongPressRightAvatar,
126 | // ),
127 | // ],
128 | // ),
129 | // ],
130 | // );
131 | // }
132 | //
133 | // Sink? get _onItemClick => clickSink;
134 | //
135 | // MainAxisAlignment _layoutAlignment() =>
136 | // isReceived ? MainAxisAlignment.start : MainAxisAlignment.end;
137 | //
138 | // BubbleNip _nip() => isReceived ? BubbleNip.leftTop : BubbleNip.rightTop;
139 | //
140 | // Color _bubbleColor() => isReceived ? leftBubbleColor : rightBubbleColor;
141 | //
142 | // Widget _buildAvatar(
143 | // String? url,
144 | // bool show, {
145 | // final Function()? onTap,
146 | // final Function()? onLongPress,
147 | // }) =>
148 | // ChatAvatarView(
149 | // url: url,
150 | // visible: show,
151 | // onTap: onTap,
152 | // onLongPress: onLongPress,
153 | // size: avatarSize,
154 | // );
155 | // }
156 |
--------------------------------------------------------------------------------
/lib/src/chat_linear_progress_indicator.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 |
6 | class ChatLinearProgressView extends StatefulWidget {
7 | /// this.show = message.status == MessageStatus.sending;
8 | const ChatLinearProgressView({
9 | Key? key,
10 | required this.width,
11 | this.height = 6,
12 | required this.msgId,
13 | this.stream,
14 | this.initProgress = 100,
15 | }) : super(key: key);
16 |
17 | final double width;
18 | final double height;
19 | final int initProgress;
20 | final String msgId;
21 | final Stream>? stream;
22 |
23 | @override
24 | State createState() => _ChatFileUploadProgressViewState();
25 | }
26 |
27 | class _ChatFileUploadProgressViewState extends State {
28 | int _progress = 0;
29 | StreamSubscription? _progressSubs;
30 |
31 | @override
32 | void initState() {
33 | _progress = widget.initProgress;
34 | _progressSubs = widget.stream?.listen((event) {
35 | if (!mounted) return;
36 | if (widget.msgId == event.msgId) {
37 | setState(() {
38 | _progress = event.value;
39 | });
40 | }
41 | });
42 | super.initState();
43 | }
44 |
45 | @override
46 | void dispose() {
47 | _progressSubs?.cancel();
48 | super.dispose();
49 | }
50 |
51 | @override
52 | Widget build(BuildContext context) {
53 | return Visibility(
54 | visible: _progress != 100,
55 | child: Container(
56 | height: widget.height,
57 | width: widget.width,
58 | alignment: Alignment.center,
59 | child: LinearProgressIndicator(
60 | value: _progress / 100,
61 | backgroundColor: Colors.grey[400], // 背景色
62 | valueColor: AlwaysStoppedAnimation(
63 | Color(0xFFDCEBFE), // 进度条颜色
64 | ),
65 | ),
66 | ),
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/src/chat_listview.dart:
--------------------------------------------------------------------------------
1 | library chat_listview;
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter/scheduler.dart';
6 |
7 | extension ScrollControllerExt on ScrollController {
8 | /// 滚动到底部
9 | Future scrollToBottom() async {
10 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
11 | while (position.pixels != position.maxScrollExtent) {
12 | jumpTo(position.maxScrollExtent);
13 | await SchedulerBinding.instance.endOfFrame;
14 | }
15 | });
16 | }
17 |
18 | /// 滚动到顶部
19 | Future scrollToTop() async {
20 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
21 | while (position.pixels != position.minScrollExtent) {
22 | jumpTo(position.minScrollExtent);
23 | await SchedulerBinding.instance.endOfFrame;
24 | }
25 | });
26 | }
27 | }
28 |
29 | class CustomChatListViewController {
30 | /// 添加数据 insert(0,T) 或 insertAll(0,[])
31 | /// UI上index:length-1 -> 0
32 | final _topList = [];
33 |
34 | /// 添加数据 add(T) 或 addAll([])
35 | /// UI上index:0->length-1
36 | final _bottomList = [];
37 |
38 | List get topList => _topList;
39 |
40 | List get bottomList => _bottomList;
41 |
42 | List get list => _topList + _bottomList;
43 |
44 | int get length => list.length;
45 |
46 | CustomChatListViewController(List list) {
47 | _bottomList.addAll(list);
48 | }
49 |
50 | void insertToTop(E data) {
51 | _topList.insert(0, data);
52 | }
53 |
54 | void insertAllToTop(Iterable iterable) {
55 | _topList.insertAll(0, iterable);
56 | }
57 |
58 | void insertToBottom(E data) {
59 | _bottomList.add(data);
60 | }
61 |
62 | void insertAllToBottom(Iterable iterable) {
63 | _bottomList.addAll(iterable);
64 | }
65 |
66 | /// [position] 使用 [CustomChatListViewItemBuilder]的position
67 | E elementAt(int position) => list.elementAt(position);
68 |
69 | /// [position] 使用 [CustomChatListViewItemBuilder]的position
70 | E removeAt(int position) => list.removeAt(position);
71 |
72 | bool remove(Object? value) => list.remove(value);
73 |
74 | /// max = pageNo * pageSize
75 | bool bottomHasMore({required int max}) => _bottomList.length < max;
76 |
77 | /// max = pageNo * pageSize
78 | bool topHasMore({required int max}) => _topList.length < max;
79 | }
80 |
81 | /// [index] 在上下列表实际的index
82 | /// [position] 真个界面上的position
83 | /// [data] 数据
84 | typedef CustomChatListViewItemBuilder = Widget Function(
85 | BuildContext context,
86 | int index,
87 | int position,
88 | T data,
89 | );
90 |
91 | /// 使用实例: https://github.com/hrxiang/chat_listview
92 | class CustomChatListView extends StatefulWidget {
93 | const CustomChatListView({
94 | Key? key,
95 | required this.itemBuilder,
96 | required this.controller,
97 | // this.topList = const [],
98 | // this.bottomList = const [],
99 | this.scrollController,
100 | this.onScrollToTopLoad,
101 | this.onScrollToBottomLoad,
102 | this.enabledBottomLoad = false,
103 | this.enabledTopLoad = false,
104 | this.indicatorColor,
105 | }) : super(key: key);
106 |
107 | /// index: topList/bottomList的下标
108 | /// position: 整个列表的下标
109 | final CustomChatListViewItemBuilder itemBuilder;
110 |
111 | /// 添加数据 insert(0,T) 或 insertAll(0,[])
112 | /// UI上index:length-1 -> 0
113 | // final List topList;
114 |
115 | /// 添加数据 add(T) 或 addAll([])
116 | /// UI上index:0->length-1
117 | // final List bottomList;
118 |
119 | final CustomChatListViewController controller;
120 |
121 | ///
122 | final ScrollController? scrollController;
123 |
124 | /// 滚动到顶部加载,返回ture:还存在未加载完的数据。false:已经没有更多的数据了
125 | final Future Function()? onScrollToTopLoad;
126 |
127 | /// 滚动到底部加载,返回ture:还存在未加载完的数据。false:已经没有更多的数据了
128 | final Future Function()? onScrollToBottomLoad;
129 |
130 | /// 启用顶部加载
131 | final bool enabledTopLoad;
132 |
133 | /// 启动底部加载
134 | final bool enabledBottomLoad;
135 |
136 | final Color? indicatorColor;
137 |
138 | @override
139 | State createState() => _ChatListViewState();
140 | }
141 |
142 | class _ChatListViewState extends State {
143 | final Key centerKey = const ValueKey('second-sliver-list');
144 |
145 | var _bottomHasMore = true;
146 |
147 | var _topHasMore = true;
148 |
149 | @override
150 | void initState() {
151 | widget.scrollController?.addListener(() {
152 | if (widget.enabledBottomLoad && _isBottom && _bottomHasMore) {
153 | _onScrollToBottomLoadMore();
154 | } else if (widget.enabledTopLoad && _isTop && _topHasMore) {
155 | _onScrollToTopLoadMore();
156 | }
157 | });
158 | super.initState();
159 | }
160 |
161 | bool get _isBottom =>
162 | widget.scrollController!.offset ==
163 | widget.scrollController!.position.maxScrollExtent;
164 |
165 | bool get _isTop =>
166 | widget.scrollController!.offset ==
167 | widget.scrollController!.position.minScrollExtent;
168 |
169 | void _onScrollToBottomLoadMore() {
170 | widget.onScrollToBottomLoad?.call().then((hasMore) {
171 | if (!mounted) return;
172 | setState(() {
173 | _bottomHasMore = hasMore;
174 | });
175 | });
176 | }
177 |
178 | void _onScrollToTopLoadMore() {
179 | widget.onScrollToTopLoad?.call().then((hasMore) {
180 | if (!mounted) return;
181 | setState(() {
182 | _topHasMore = hasMore;
183 | });
184 | });
185 | }
186 |
187 | Widget _buildLoadMoreView() => Container(
188 | alignment: Alignment.center,
189 | height: 44,
190 | child: CupertinoActivityIndicator(
191 | color: widget.indicatorColor ?? Colors.blueAccent,
192 | ),
193 | );
194 |
195 | @override
196 | Widget build(BuildContext context) {
197 | return CustomScrollView(
198 | center: centerKey,
199 | controller: widget.scrollController,
200 | physics: const AlwaysScrollableScrollPhysics(),
201 | // reverse: true,
202 | // shrinkWrap: false,
203 | slivers: [
204 | if (_topHasMore && widget.enabledTopLoad)
205 | SliverToBoxAdapter(child: _buildLoadMoreView()),
206 | SliverList(
207 | delegate: SliverChildBuilderDelegate(
208 | (_, index) {
209 | return widget.itemBuilder(
210 | context,
211 | index,
212 | widget.controller.topList.length - index - 1,
213 | widget.controller.topList.elementAt(index),
214 | );
215 | },
216 | childCount: widget.controller.topList.length,
217 | ),
218 | ),
219 | SliverList(
220 | key: centerKey,
221 | delegate: SliverChildBuilderDelegate(
222 | (_, index) {
223 | return widget.itemBuilder(
224 | context,
225 | index,
226 | widget.controller.topList.length + index,
227 | widget.controller.bottomList.elementAt(index),
228 | );
229 | },
230 | childCount: widget.controller.bottomList.length,
231 | ),
232 | ),
233 | if (_bottomHasMore && widget.enabledBottomLoad)
234 | SliverToBoxAdapter(child: _buildLoadMoreView()),
235 | ],
236 | );
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/lib/src/chat_location_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 | import 'package:flutter_screenutil/flutter_screenutil.dart';
6 |
7 | class ChatLocationView extends StatelessWidget {
8 | ChatLocationView({
9 | Key? key,
10 | required this.description,
11 | required this.latitude,
12 | required this.longitude,
13 | }) : super(key: key);
14 | final String description;
15 | final double latitude;
16 | final double longitude;
17 | final int zoom = 15;
18 | final _decoder = JsonDecoder();
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | var url;
23 | var name;
24 | var addr;
25 | try {
26 | var map = _decoder.convert(description);
27 | url = map['url'];
28 | name = map['name'];
29 | addr = map['addr'];
30 | return Container(
31 | width: 200.w,
32 | // height: 124.h,
33 | color: Color(0xFFF0F0F0),
34 | padding: EdgeInsets.symmetric(horizontal: 1, vertical: 1),
35 | child: Column(
36 | children: [
37 | Row(
38 | children: [
39 | Expanded(
40 | child: Container(
41 | color: Colors.white,
42 | padding: EdgeInsets.symmetric(
43 | horizontal: 6.w,
44 | vertical: 2.h,
45 | ),
46 | child: Column(
47 | crossAxisAlignment: CrossAxisAlignment.start,
48 | children: [
49 | Text(
50 | name,
51 | maxLines: 1,
52 | overflow: TextOverflow.ellipsis,
53 | style: TextStyle(
54 | fontSize: 14.sp,
55 | color: Color(0xFF333333),
56 | ),
57 | ),
58 | Text(
59 | addr,
60 | maxLines: 1,
61 | overflow: TextOverflow.ellipsis,
62 | style: TextStyle(
63 | fontSize: 12.sp,
64 | color: Color(0xFF999999),
65 | ),
66 | ),
67 | ],
68 | ),
69 | ),
70 | )
71 | ],
72 | ),
73 | Row(
74 | children: [
75 | Expanded(
76 | child: ImageUtil.networkImage(
77 | url: url,
78 | height: 100.h,
79 | fit: BoxFit.cover,
80 | cacheWidth: (1.sw).toInt(),
81 | ),
82 | )
83 | ],
84 | )
85 | ],
86 | ),
87 | );
88 | } catch (e) {}
89 | return Container();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/lib/src/chat_longpress_ripple.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class LongPressRippleAnimation extends StatefulWidget {
5 | const LongPressRippleAnimation({
6 | Key? key,
7 | required this.child,
8 | required this.radius,
9 | this.onStart,
10 | this.onStop,
11 | }) : super(key: key);
12 | final Widget child;
13 | final double radius;
14 | final Function()? onStart;
15 | final Function()? onStop;
16 |
17 | @override
18 | _LongPressRippleAnimationState createState() =>
19 | _LongPressRippleAnimationState();
20 | }
21 |
22 | class _LongPressRippleAnimationState extends State
23 | with TickerProviderStateMixin {
24 | late AnimationController _rippleController;
25 | late Animation _rippleAnimation;
26 |
27 | late AnimationController _inflateController;
28 | late Animation _inflateAnimation;
29 | bool _a1 = true;
30 | bool _start = false;
31 |
32 | @override
33 | void dispose() {
34 | _rippleController.dispose();
35 | _inflateController.dispose();
36 | super.dispose();
37 | }
38 |
39 | @override
40 | void initState() {
41 | _rippleController = AnimationController(
42 | duration: Duration(milliseconds: 200),
43 | vsync: this,
44 | )..addStatusListener((status) {
45 | if (status == AnimationStatus.completed) {
46 | _rippleController.reverse();
47 | } else if (status == AnimationStatus.dismissed) {
48 | _rippleController.forward();
49 | }
50 | });
51 | _rippleAnimation = Tween(begin: 1.0, end: .8).animate(_rippleController)
52 | ..addListener(() {
53 | setState(() {});
54 | });
55 |
56 | _inflateController = AnimationController(
57 | duration: Duration(milliseconds: 200),
58 | vsync: this,
59 | )..addStatusListener((status) {
60 | if (status == AnimationStatus.completed) {
61 | setState(() {
62 | _a1 = false;
63 | });
64 | _inflateController.stop();
65 | _rippleController.forward();
66 | } else if (status == AnimationStatus.dismissed) {
67 | } else if (status == AnimationStatus.reverse) {
68 | setState(() {
69 | _start = false;
70 | });
71 | } else if (status == AnimationStatus.forward) {
72 | setState(() {
73 | _start = true;
74 | });
75 | }
76 | });
77 | _inflateAnimation = Tween(begin: 0.0, end: 1.0).animate(_inflateController)
78 | ..addListener(() {
79 | setState(() {});
80 | });
81 | super.initState();
82 | }
83 |
84 | double get size2 =>
85 | _rippleAnimation.value * (widget.radius) + widget.radius * 2;
86 |
87 | double get size1 =>
88 | _inflateAnimation.value * (widget.radius) + widget.radius * 2;
89 |
90 | @override
91 | Widget build(BuildContext context) {
92 | return GestureDetector(
93 | onLongPressStart: (detail) {
94 | _inflateController.forward();
95 | widget.onStart?.call();
96 | },
97 | onLongPressEnd: (detail) {
98 | setState(() {
99 | _a1 = true;
100 | });
101 | _rippleController.stop();
102 | _inflateController.reverse();
103 | widget.onStop?.call();
104 | },
105 | child: Container(
106 | alignment: Alignment.center,
107 | width: _a1 ? size1 : size2,
108 | height: _a1 ? size1 : size2,
109 | decoration: (!_start && _inflateAnimation.isDismissed)
110 | ? null
111 | : BoxDecoration(
112 | color: const Color(0xFF1d6bed).withOpacity(0.4),
113 | shape: BoxShape.circle,
114 | ),
115 | child: Center(
116 | child: widget.child,
117 | ),
118 | ),
119 | );
120 | }
121 | }
122 |
123 | /// You can use whatever widget as a [child], when you don't need to provide any
124 | /// [child], just provide an empty Container().
125 | /// [delay] is using a [Timer] for delaying the animation, it's zero by default.
126 | /// You can set [repeat] to true for making a paulsing effect.
127 | /*
128 | class LongPressRippleAnimation extends StatefulWidget {
129 | final Widget child;
130 | final Duration delay;
131 | final double minRadius;
132 | final Color color;
133 |
134 | final int ripplesCount;
135 | final Duration duration;
136 | final bool repeat;
137 |
138 | LongPressRippleAnimation({
139 | Key? key,
140 | required this.child,
141 | this.color = Colors.black,
142 | this.delay = const Duration(milliseconds: 0),
143 | this.repeat = false,
144 | this.minRadius = 60,
145 | this.ripplesCount = 5,
146 | this.duration = const Duration(milliseconds: 0),
147 | }) : super(key: key);
148 |
149 | @override
150 | _RippleAnimationState createState() => _RippleAnimationState();
151 | }
152 |
153 | class _RippleAnimationState extends State
154 | with TickerProviderStateMixin {
155 | AnimationController? _controller;
156 |
157 | @override
158 | void initState() {
159 | _controller = AnimationController(
160 | duration: widget.duration,
161 | vsync: this,
162 | );
163 |
164 | // repeating or just forwarding the animation once.
165 | // Timer(widget.delay, () {
166 | // widget.repeat ? _controller?.repeat() : _controller?.forward();
167 | // });
168 |
169 | super.initState();
170 | }
171 |
172 | @override
173 | Widget build(BuildContext context) {
174 | return GestureDetector(
175 | // behavior: HitTestBehavior.translucent,
176 | onLongPressStart: (detail) {
177 | _controller?.repeat();
178 | },
179 | onLongPressEnd: (detail) {
180 | _controller?.reset();
181 | },
182 | child: CustomPaint(
183 | painter: CirclePainter(
184 | _controller,
185 | color: widget.color,
186 | minRadius: widget.minRadius,
187 | wavesCount: widget.ripplesCount,
188 | ),
189 | child: widget.child,
190 | ),
191 | );
192 | }
193 |
194 | @override
195 | void dispose() {
196 | _controller!.dispose();
197 | super.dispose();
198 | }
199 | }
200 |
201 | // Creating a Circular painter for clipping the rects and creating circle shapes
202 | class CirclePainter extends CustomPainter {
203 | CirclePainter(
204 | this._animation, {
205 | this.minRadius,
206 | this.wavesCount,
207 | required this.color,
208 | }) : super(repaint: _animation);
209 | final Color color;
210 | final double? minRadius;
211 | final wavesCount;
212 | final Animation? _animation;
213 |
214 | @override
215 | void paint(Canvas canvas, Size size) {
216 | final Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
217 | for (int wave = 0; wave <= wavesCount; wave++) {
218 | circle(canvas, rect, minRadius, wave, _animation!.value, wavesCount);
219 | }
220 | }
221 |
222 | // animating the opacity according to min radius and waves count.
223 | void circle(Canvas canvas, Rect rect, double? minRadius, int wave,
224 | double value, int? length) {
225 | Color _color;
226 | double r;
227 | if (wave != 0) {
228 | double opacity = (1 - ((wave - 1) / length!) - value).clamp(0.0, 1.0);
229 | _color = color.withOpacity(opacity);
230 |
231 | r = minRadius! * (1 + ((wave * value))) * value;
232 | final Paint paint = Paint()..color = _color;
233 | canvas.drawCircle(rect.center, r, paint);
234 | }
235 | }
236 |
237 | @override
238 | bool shouldRepaint(CirclePainter oldDelegate) => true;
239 | }
240 | */
241 |
--------------------------------------------------------------------------------
/lib/src/chat_menu.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/widgets.dart';
3 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
4 | import 'package:flutter_screenutil/flutter_screenutil.dart';
5 |
6 | class MenuInfo {
7 | Widget icon;
8 | String text;
9 | TextStyle? textStyle;
10 | Function()? onTap;
11 | bool enabled;
12 |
13 | MenuInfo({
14 | required this.icon,
15 | required this.text,
16 | this.textStyle,
17 | this.onTap,
18 | this.enabled = true,
19 | });
20 | }
21 |
22 | class PopMenuStyle {
23 | final int crossAxisCount;
24 | final double mainAxisSpacing;
25 | final double crossAxisSpacing;
26 | final Color background;
27 | final double radius;
28 |
29 | PopMenuStyle({
30 | required this.crossAxisCount,
31 | required this.mainAxisSpacing,
32 | required this.crossAxisSpacing,
33 | required this.background,
34 | required this.radius,
35 | });
36 |
37 | const PopMenuStyle.base()
38 | : crossAxisCount = 4,
39 | mainAxisSpacing = 10,
40 | crossAxisSpacing = 10,
41 | background = const Color(0xFF666666),
42 | radius = 4;
43 | }
44 |
45 | class ChatLongPressMenu extends StatelessWidget {
46 | final CustomPopupMenuController controller;
47 | final List menus;
48 | final PopMenuStyle menuStyle;
49 |
50 | const ChatLongPressMenu({
51 | Key? key,
52 | required this.controller,
53 | required this.menus,
54 | this.menuStyle = const PopMenuStyle.base(),
55 | }) : super(key: key);
56 |
57 | @override
58 | Widget build(BuildContext context) {
59 | return Container(
60 | decoration: BoxDecoration(
61 | color: menuStyle.background,
62 | borderRadius: BorderRadius.circular(menuStyle.radius),
63 | ),
64 | child: Column(
65 | mainAxisSize: MainAxisSize.min,
66 | crossAxisAlignment: CrossAxisAlignment.start,
67 | children: _children(),
68 | ),
69 | );
70 | }
71 |
72 | List _children() {
73 | var widgets = [];
74 | menus.removeWhere((element) => !element.enabled);
75 | var rows = menus.length ~/ menuStyle.crossAxisCount;
76 | if (menus.length % menuStyle.crossAxisCount != 0) {
77 | rows++;
78 | }
79 | for (var i = 0; i < rows; i++) {
80 | var start = i * menuStyle.crossAxisCount;
81 | var end = (i + 1) * menuStyle.crossAxisCount;
82 | if (end > menus.length) {
83 | end = menus.length;
84 | }
85 | var subList = menus.sublist(start, end);
86 | widgets.add(Row(
87 | mainAxisSize: MainAxisSize.min,
88 | // mainAxisAlignment: MainAxisAlignment.start,
89 | crossAxisAlignment: CrossAxisAlignment.start,
90 | children: subList
91 | .map((e) => _menuItem(
92 | icon: e.icon,
93 | label: e.text,
94 | onTap: e.onTap,
95 | style: e.textStyle ??
96 | TextStyle(fontSize: 10.sp, color: Color(0xFFFFFFFF)),
97 | ))
98 | .toList(),
99 | ));
100 | }
101 | return widgets;
102 | }
103 |
104 | Widget _menuItem({
105 | required Widget icon,
106 | required String label,
107 | TextStyle? style,
108 | Function()? onTap,
109 | }) =>
110 | GestureDetector(
111 | onTap: () {
112 | controller.hideMenu();
113 | if (null != onTap) onTap();
114 | },
115 | behavior: HitTestBehavior.translucent,
116 | child: Container(
117 | // width: 50.w,
118 | // constraints: BoxConstraints(maxWidth: 35.w, minWidth: 30.w),
119 | padding: EdgeInsets.symmetric(
120 | horizontal: menuStyle.crossAxisSpacing,
121 | vertical: menuStyle.mainAxisSpacing / 2,
122 | ),
123 | child: _ItemView(icon: icon, label: label, style: style),
124 | ),
125 | );
126 | }
127 |
128 | class _ItemView extends StatelessWidget {
129 | const _ItemView({
130 | Key? key,
131 | required this.icon,
132 | required this.label,
133 | this.style,
134 | }) : super(key: key);
135 | final Widget icon;
136 | final String label;
137 | final TextStyle? style;
138 |
139 | @override
140 | Widget build(BuildContext context) {
141 | return Container(
142 | child: Column(
143 | mainAxisSize: MainAxisSize.min,
144 | children: [
145 | Container(
146 | height: 20.w,
147 | child: icon,
148 | ),
149 | Text(
150 | label,
151 | maxLines: 1,
152 | overflow: TextOverflow.ellipsis,
153 | style: style,
154 | ),
155 | ],
156 | ),
157 | );
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/lib/src/chat_merge_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 |
5 | class ChatMergeMsgView extends StatelessWidget {
6 | const ChatMergeMsgView(
7 | {Key? key, required this.title, required this.summaryList})
8 | : super(key: key);
9 | final String title;
10 | final List summaryList;
11 |
12 | List _children() {
13 | var list = [];
14 | list
15 | ..add(Text(
16 | title,
17 | style: TextStyle(
18 | color: Color(0xFF333333),
19 | fontSize: 15.sp,
20 | ),
21 | ))
22 | ..add(Padding(
23 | padding: EdgeInsets.only(bottom: 8.h, top: 8.h),
24 | child: Divider(
25 | color: Color(0xFFD8D8D8),
26 | height: 0.5.h,
27 | ),
28 | ));
29 | for (var s in summaryList) {
30 | list.add(ChatAtText(
31 | text: s.trim(),
32 | textStyle: TextStyle(
33 | color: Color(0xFF666666),
34 | fontSize: 11.sp,
35 | ),
36 | ));
37 | }
38 | return list;
39 | }
40 |
41 | @override
42 | Widget build(BuildContext context) {
43 | return Container(
44 | constraints: BoxConstraints(maxWidth: 200.w),
45 | child: Column(
46 | crossAxisAlignment: CrossAxisAlignment.start,
47 | mainAxisSize: MainAxisSize.min,
48 | children: _children(),
49 | ),
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/src/chat_multi_toolbox.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 |
5 | class ChatMultiSelToolbox extends StatelessWidget {
6 | const ChatMultiSelToolbox({Key? key, this.onDelete, this.onMergeForward})
7 | : super(key: key);
8 | final Function()? onDelete;
9 | final Function()? onMergeForward;
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Container(
14 | constraints: BoxConstraints(minHeight: 40.h),
15 | decoration: BoxDecoration(color: Color(0xFFF6F6F6), boxShadow: [
16 | BoxShadow(color: Color(0xFF000000).withOpacity(0.2), blurRadius: 6)
17 | ]),
18 | padding: EdgeInsets.symmetric(horizontal: 12.w /*, vertical: 4.h*/),
19 | child: Row(
20 | children: [
21 | GestureDetector(
22 | onTap: onDelete,
23 | behavior: HitTestBehavior.translucent,
24 | child: Container(
25 | padding: EdgeInsets.all(12.h),
26 | child: ImageUtil.assetImage(
27 | 'ic_multi_tool_del',
28 | width: 20.w,
29 | height: 22.h,
30 | ),
31 | ),
32 | ),
33 | GestureDetector(
34 | onTap: onMergeForward,
35 | behavior: HitTestBehavior.translucent,
36 | child: Container(
37 | padding: EdgeInsets.all(4),
38 | child: ImageUtil.assetImage(
39 | 'ic_multi_tool_merge_forward',
40 | width: 19.w,
41 | height: 19.h,
42 | ),
43 | ),
44 | )
45 | ],
46 | ),
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/src/chat_picture_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
6 | import 'package:flutter_screenutil/flutter_screenutil.dart';
7 |
8 | class ChatPictureView extends StatefulWidget {
9 | const ChatPictureView({
10 | Key? key,
11 | required this.msgId,
12 | required this.index,
13 | this.clickStream,
14 | required this.isReceived,
15 | this.snapshotPath,
16 | this.snapshotUrl,
17 | this.sourcePath,
18 | this.sourceUrl,
19 | this.width,
20 | this.height,
21 | this.widgetWidth = 100,
22 | this.msgSenProgressStream,
23 | this.initMsgSendProgress = 100,
24 | }) : super(key: key);
25 | final int index;
26 | final Stream? clickStream;
27 | final String? sourcePath;
28 | final String? sourceUrl;
29 | final String? snapshotPath;
30 | final String? snapshotUrl;
31 | final double? width;
32 | final double? height;
33 | final double widgetWidth;
34 | final String msgId;
35 | final Stream>? msgSenProgressStream;
36 | final int initMsgSendProgress;
37 | final bool isReceived;
38 |
39 | @override
40 | _ChatPictureViewState createState() => _ChatPictureViewState();
41 | }
42 |
43 | class _ChatPictureViewState extends State {
44 | String? _sourcePath;
45 | String? _sourceUrl;
46 |
47 | // String? _snapshotPath;
48 | String? _snapshotUrl;
49 | late double _trulyWidth;
50 | late double _trulyHeight;
51 |
52 | @override
53 | void initState() {
54 | _sourcePath = widget.sourcePath;
55 | _sourceUrl = widget.sourceUrl;
56 | _snapshotUrl = widget.snapshotUrl;
57 | // _snapshotPath = widget.snapshotPath;
58 | var w = widget.width ?? 1.0;
59 | var h = widget.height ?? 1.0;
60 |
61 | // _trulyWidth = widget.widgetWidth;
62 | // _trulyHeight = _trulyWidth * h / w;
63 | if (widget.widgetWidth > w) {
64 | _trulyWidth = w;
65 | _trulyHeight = h;
66 | } else {
67 | _trulyWidth = widget.widgetWidth;
68 | _trulyHeight = _trulyWidth * h / w;
69 | }
70 |
71 | final widgetHeight = widget.widgetWidth * 1.sh / 1.sw;
72 |
73 | // 超长图显示为方形
74 | if (_trulyHeight > 2 * widgetHeight) {
75 | _trulyHeight = _trulyWidth;
76 | }
77 |
78 | //
79 | /*if (!_isNotNull(_snapshotPath) && _isNotNull(_sourcePath)) {
80 | CommonUtil.createThumbnail(
81 | path: _sourcePath!,
82 | minWidth: _trulyWidth,
83 | minHeight: _trulyHeight,
84 | ).then((path) {
85 | if (!mounted) return;
86 | if (null != path) {
87 | setState(() {
88 | _snapshotPath = path;
89 | });
90 | }
91 | });
92 | }*/
93 | //
94 | /* widget.clickStream?.listen((i) {
95 | if (!mounted) return;
96 | if (_isClickedLocation(i)) {
97 | if (null != widget.onClickPic) {
98 | widget.onClickPic!(_sourceUrl, _sourcePath, _tag);
99 | } else {
100 | Navigator.of(context).push(
101 | MaterialPageRoute(
102 | builder: (BuildContext context) {
103 | return PicturePreview(
104 | url: _sourceUrl!,
105 | tag: _tag!,
106 | localizations: widget.localizations,
107 | );
108 | },
109 | ),
110 | );
111 | }
112 | }
113 | });*/
114 | super.initState();
115 | }
116 |
117 | bool _isClickedLocation(i) => i == widget.index;
118 |
119 | Widget _urlView({required String url}) => ImageUtil.networkImage(
120 | url: url,
121 | height: _trulyHeight,
122 | width: _trulyWidth,
123 | fit: BoxFit.fitWidth,
124 | );
125 |
126 | Widget _pathView({required String path}) => Stack(
127 | children: [
128 | Image(
129 | image: FileImage(File(path)),
130 | height: _trulyHeight,
131 | width: _trulyWidth,
132 | fit: BoxFit.fitWidth,
133 | errorBuilder: (_, error, stack) => _errorIcon(),
134 | ),
135 | ChatSendProgressView(
136 | height: _trulyHeight,
137 | width: _trulyWidth,
138 | msgId: widget.msgId,
139 | stream: widget.msgSenProgressStream,
140 | initProgress: widget.initMsgSendProgress,
141 | ),
142 | ],
143 | );
144 |
145 | Widget _buildChildView() {
146 | Widget? child;
147 | // if (_isNotNull(_snapshotUrl)) {
148 | // child = _urlView(url: _snapshotUrl!);
149 | // } else if (_isNotNull(_sourceUrl)) {
150 | // child = _urlView(url: _sourceUrl!);
151 | // } else if (_isNotNull(_snapshotPath) && File(_snapshotPath!).existsSync()) {
152 | // child = _pathView(path: _snapshotPath!);
153 | // } else if (_isNotNull(_sourcePath) && File(_sourcePath!).existsSync()) {
154 | // child = _pathView(path: _sourcePath!);
155 | // }
156 | if (widget.isReceived) {
157 | if (_isNotNull(_snapshotUrl)) {
158 | child = _urlView(url: _snapshotUrl!);
159 | } else if (_isNotNull(_sourceUrl)) {
160 | child = _urlView(url: _sourceUrl!);
161 | }
162 | } else {
163 | /*if (_isNotNull(_snapshotPath) && File(_snapshotPath!).existsSync()) {
164 | child = _pathView(path: _snapshotPath!);
165 | } else*/
166 | if (_isNotNull(_sourcePath) && File(_sourcePath!).existsSync()) {
167 | child = _pathView(path: _sourcePath!);
168 | } else if (_isNotNull(_snapshotUrl)) {
169 | child = _urlView(url: _snapshotUrl!);
170 | } else if (_isNotNull(_sourceUrl)) {
171 | child = _urlView(url: _sourceUrl!);
172 | }
173 | }
174 | return Container(child: child ?? _errorIcon());
175 | }
176 |
177 | @override
178 | Widget build(BuildContext context) {
179 | var child = _buildChildView();
180 | // return child;
181 | return Hero(tag: widget.msgId, child: child);
182 | }
183 |
184 | Widget _errorIcon() =>
185 | ImageUtil.error(width: _trulyWidth, height: _trulyHeight);
186 |
187 | static bool _isNotNull(String? value) =>
188 | null != value && value.trim().isNotEmpty;
189 | }
190 |
--------------------------------------------------------------------------------
/lib/src/chat_quote_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:io';
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
6 | import 'package:flutter_screenutil/flutter_screenutil.dart';
7 |
8 | class ChatQuoteView extends StatelessWidget {
9 | ChatQuoteView({Key? key, required this.message, this.onTap})
10 | : super(key: key);
11 | final Message message;
12 | final Function()? onTap;
13 | final _decoder = JsonDecoder();
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | var child;
18 | var name;
19 | var content;
20 | name = message.senderNickname;
21 | if (message.contentType == MessageType.text) {
22 | content = message.content;
23 | } else if (message.contentType == MessageType.picture) {
24 | var url1 = message.pictureElem?.snapshotPicture?.url;
25 | var url2 = message.pictureElem?.sourcePicture?.url;
26 | var path = message.pictureElem?.sourcePath;
27 | if (url1 != null && url1.isNotEmpty) {
28 | child = ImageUtil.networkImage(
29 | url: url1,
30 | width: 42.h,
31 | height: 42.h,
32 | fit: BoxFit.fill,
33 | );
34 | } else if (url2 != null && url2.isNotEmpty) {
35 | child = ImageUtil.networkImage(
36 | url: url2,
37 | width: 42.h,
38 | height: 42.h,
39 | fit: BoxFit.fill,
40 | );
41 | } else if (path != null && path.isNotEmpty) {
42 | child = Image(
43 | image: FileImage(File(path)),
44 | height: 42.h,
45 | width: 42.h,
46 | fit: BoxFit.fill,
47 | );
48 | }
49 | } else if (message.contentType == MessageType.video) {
50 | var url = message.videoElem?.snapshotUrl;
51 | var path = message.videoElem?.snapshotPath;
52 | if (url != null && url.isNotEmpty) {
53 | child = _playIcon(
54 | child: ImageUtil.networkImage(
55 | url: url,
56 | width: 42.h,
57 | height: 42.h,
58 | fit: BoxFit.fill,
59 | ),
60 | );
61 | } else if (path != null && path.isNotEmpty) {
62 | child = _playIcon(
63 | child: Image(
64 | image: FileImage(File(path)),
65 | height: 42.h,
66 | width: 42.h,
67 | fit: BoxFit.fill,
68 | ),
69 | );
70 | }
71 | } else if (message.contentType == MessageType.location) {
72 | var location = message.locationElem;
73 | if (null != location) {
74 | var map = _decoder.convert(location.description!);
75 | var url = map['url'];
76 | var name = map['name'];
77 | var addr = map['addr'];
78 | content = '$name($addr)';
79 | child = ImageUtil.networkImage(
80 | url: url,
81 | width: 42.h,
82 | height: 42.h,
83 | fit: BoxFit.fill,
84 | );
85 | }
86 | } else if (message.contentType == MessageType.file) {}
87 |
88 | return GestureDetector(
89 | behavior: HitTestBehavior.translucent,
90 | onTap: onTap,
91 | child: Container(
92 | padding: EdgeInsets.all(6),
93 | child: Row(
94 | crossAxisAlignment: CrossAxisAlignment.start,
95 | children: [
96 | Container(
97 | constraints: BoxConstraints(maxWidth: 150.w),
98 | child: ChatAtText(
99 | text: '$name:${content ?? ''}',
100 | textStyle: TextStyle(
101 | fontSize: 12.sp,
102 | color: Color(0xFF666666),
103 | ),
104 | maxLines: 3,
105 | overflow: TextOverflow.ellipsis,
106 | ),
107 | // child: Text(
108 | // '$name:${content ?? ''}',
109 | // style: TextStyle(
110 | // fontSize: 12.sp,
111 | // color: Color(0xFF666666),
112 | // ),
113 | // ),
114 | ),
115 | Container(
116 | child: child,
117 | )
118 | ],
119 | ),
120 | ),
121 | );
122 | }
123 |
124 | Widget _playIcon({required Widget child}) => Stack(
125 | alignment: Alignment.center,
126 | children: [
127 | child,
128 | ImageUtil.assetImage(
129 | 'ic_video_play_small',
130 | width: 15.w,
131 | height: 15.h,
132 | ),
133 | ],
134 | );
135 | }
136 |
137 | /*
138 | class ChatQuoteView extends StatelessWidget {
139 | ChatQuoteView({Key? key, required this.message, this.onTap})
140 | : super(key: key);
141 | final Message message;
142 | final Function()? onTap;
143 | final _decoder = JsonDecoder();
144 |
145 | @override
146 | Widget build(BuildContext context) {
147 | var child;
148 | var name;
149 | var content;
150 | if (message.contentType == MessageType.quote) {
151 | var message = message.quoteElem?.message;
152 | if (null != message) {
153 | name = message.senderNickname;
154 | if (message.contentType == MessageType.text) {
155 | content = message.content;
156 | } else if (message.contentType == MessageType.picture) {
157 | var url1 = message.pictureElem?.snapshotPicture?.url;
158 | var url2 = message.pictureElem?.sourcePicture?.url;
159 | var path = message.pictureElem?.sourcePath;
160 | if (url1 != null && url1.isNotEmpty) {
161 | child = ImageUtil.networkImage(
162 | url: url1,
163 | width: 42.h,
164 | height: 42.h,
165 | fit: BoxFit.fill,
166 | );
167 | } else if (url2 != null && url2.isNotEmpty) {
168 | child = ImageUtil.networkImage(
169 | url: url2,
170 | width: 42.h,
171 | height: 42.h,
172 | fit: BoxFit.fill,
173 | );
174 | } else if (path != null && path.isNotEmpty) {
175 | child = Image(
176 | image: FileImage(File(path)),
177 | height: 42.h,
178 | width: 42.h,
179 | fit: BoxFit.fill,
180 | );
181 | }
182 | } else if (message.contentType == MessageType.video) {
183 | var url = message.videoElem?.snapshotUrl;
184 | var path = message.videoElem?.snapshotPath;
185 | if (url != null && url.isNotEmpty) {
186 | child = _playIcon(
187 | child: ImageUtil.networkImage(
188 | url: url,
189 | width: 42.h,
190 | height: 42.h,
191 | fit: BoxFit.fill,
192 | ),
193 | );
194 | } else if (path != null && path.isNotEmpty) {
195 | child = _playIcon(
196 | child: Image(
197 | image: FileImage(File(path)),
198 | height: 42.h,
199 | width: 42.h,
200 | fit: BoxFit.fill,
201 | ),
202 | );
203 | }
204 | } else if (message.contentType == MessageType.location) {
205 | var location = message.locationElem;
206 | if (null != location) {
207 | var map = _decoder.convert(location.description!);
208 | var url = map['url'];
209 | var name = map['name'];
210 | var addr = map['addr'];
211 | content = '$name($addr)';
212 | child = ImageUtil.networkImage(
213 | url: url,
214 | width: 42.h,
215 | height: 42.h,
216 | fit: BoxFit.fill,
217 | );
218 | }
219 | } else if (message.contentType == MessageType.file) {}
220 | }
221 | }
222 |
223 | return GestureDetector(
224 | behavior: HitTestBehavior.translucent,
225 | onTap: onTap,
226 | child: Container(
227 | padding: EdgeInsets.all(6),
228 | child: Row(
229 | crossAxisAlignment: CrossAxisAlignment.start,
230 | children: [
231 | Container(
232 | constraints: BoxConstraints(maxWidth: 150.w),
233 | child: Text(
234 | '$name:${content ?? ''}',
235 | style: TextStyle(
236 | fontSize: 12.sp,
237 | color: Color(0xFF666666),
238 | ),
239 | ),
240 | ),
241 | Container(
242 | child: child,
243 | )
244 | ],
245 | ),
246 | ),
247 | );
248 | }
249 |
250 | Widget _playIcon({required Widget child}) => Stack(
251 | alignment: Alignment.center,
252 | children: [
253 | child,
254 | ImageUtil.assetImage(
255 | 'ic_video_play_small',
256 | width: 15.w,
257 | height: 15.h,
258 | ),
259 | ],
260 | );
261 | }*/
262 |
--------------------------------------------------------------------------------
/lib/src/chat_radio_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 |
5 | class ChatRadio extends StatelessWidget {
6 | const ChatRadio({
7 | Key? key,
8 | this.checked = false,
9 | this.showRadio = false,
10 | }) : super(key: key);
11 | final bool checked;
12 | final bool showRadio;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | return Visibility(
17 | visible: showRadio,
18 | child: GestureDetector(
19 | behavior: HitTestBehavior.translucent,
20 | child: Container(
21 | margin: EdgeInsets.symmetric(horizontal: 12.w),
22 | child: ImageUtil.assetImage(
23 | checked ? 'ic_radio_msg_sel' : 'ic_radio_msg_nor',
24 | width: 22.w,
25 | height: 22.h,
26 | ),
27 | ),
28 | ),
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/src/chat_send_failed_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 |
6 | class ChatSendFailedView extends StatefulWidget {
7 | final String msgId;
8 | final bool isReceived;
9 | final Stream>? stream;
10 | final bool isSendFailed;
11 | final Function()? onFailedResend;
12 |
13 | const ChatSendFailedView({
14 | Key? key,
15 | required this.msgId,
16 | required this.isReceived,
17 | this.isSendFailed = false,
18 | this.stream,
19 | this.onFailedResend,
20 | }) : super(key: key);
21 |
22 | @override
23 | _ChatSendFailedViewState createState() => _ChatSendFailedViewState();
24 | }
25 |
26 | class _ChatSendFailedViewState extends State {
27 | late bool _failed;
28 | StreamSubscription? _statusSubs;
29 |
30 | @override
31 | void initState() {
32 | _failed = widget.isSendFailed;
33 | _statusSubs = widget.stream?.listen((event) {
34 | if (!mounted) return;
35 | if (widget.msgId == event.msgId) {
36 | setState(() {
37 | _failed = !event.value;
38 | });
39 | }
40 | });
41 | super.initState();
42 | }
43 |
44 | @override
45 | void dispose() {
46 | _statusSubs?.cancel();
47 | super.dispose();
48 | }
49 |
50 | @override
51 | Widget build(BuildContext context) {
52 | return Visibility(
53 | visible: !widget.isReceived && _failed,
54 | child: GestureDetector(
55 | behavior: HitTestBehavior.translucent,
56 | onTap: widget.onFailedResend,
57 | child: ImageUtil.sendFailed(),
58 | ),
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/src/chat_send_progress_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 |
6 | class ChatSendProgressView extends StatefulWidget {
7 | /// this.show = message.status == MessageStatus.sending;
8 | const ChatSendProgressView({
9 | Key? key,
10 | required this.width,
11 | required this.height,
12 | required this.msgId,
13 | this.stream,
14 | this.initProgress = 100,
15 | }) : super(key: key);
16 |
17 | final double width;
18 | final double height;
19 | final int initProgress;
20 | final String msgId;
21 | final Stream>? stream;
22 |
23 | @override
24 | State createState() => _ChatSendProgressViewState();
25 | }
26 |
27 | class _ChatSendProgressViewState extends State {
28 | int _progress = 0;
29 | StreamSubscription? _progressSubs;
30 |
31 | @override
32 | void initState() {
33 | _progress = widget.initProgress;
34 | _progressSubs = widget.stream?.listen((event) {
35 | if (!mounted) return;
36 | if (widget.msgId == event.msgId) {
37 | setState(() {
38 | _progress = event.value;
39 | });
40 | }
41 | });
42 | super.initState();
43 | }
44 |
45 | @override
46 | void dispose() {
47 | _progressSubs?.cancel();
48 | super.dispose();
49 | }
50 |
51 | @override
52 | Widget build(BuildContext context) {
53 | return Visibility(
54 | visible: _progress != 100,
55 | child: Container(
56 | height: widget.height,
57 | width: widget.width,
58 | color: Colors.black.withOpacity(0.5),
59 | alignment: Alignment.center,
60 | child: Text(
61 | '$_progress%',
62 | style: TextStyle(color: Colors.white),
63 | ),
64 | ),
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/src/chat_textfield.dart:
--------------------------------------------------------------------------------
1 | import 'package:extended_text_field/extended_text_field.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 | import 'package:flutter_screenutil/flutter_screenutil.dart';
6 |
7 | class ChatTextField extends StatelessWidget {
8 | final AtTextCallback? atCallback;
9 | final Map allAtMap;
10 | final FocusNode? focusNode;
11 | final TextEditingController? controller;
12 | final ValueChanged? onSubmitted;
13 | final TextStyle? style;
14 | final TextStyle? atStyle;
15 | final List? inputFormatters;
16 | final bool enabled;
17 |
18 | const ChatTextField({
19 | Key? key,
20 | this.allAtMap = const {},
21 | this.atCallback,
22 | this.focusNode,
23 | this.controller,
24 | this.onSubmitted,
25 | this.style,
26 | this.atStyle,
27 | this.inputFormatters,
28 | this.enabled = true,
29 | }) : super(key: key);
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | return ExtendedTextField(
34 | style: style,
35 | specialTextSpanBuilder: AtSpecialTextSpanBuilder(
36 | atCallback: atCallback,
37 | allAtMap: allAtMap,
38 | atStyle: atStyle,
39 | ),
40 | focusNode: focusNode,
41 | controller: controller,
42 | keyboardType: TextInputType.multiline,
43 | enabled: enabled,
44 | autofocus: false,
45 | minLines: 1,
46 | maxLines: 4,
47 | textInputAction: TextInputAction.newline,
48 | // onSubmitted: onSubmitted,
49 | decoration: InputDecoration(
50 | border: InputBorder.none,
51 | isDense: true,
52 | // contentPadding: EdgeInsets.zero,
53 | contentPadding: EdgeInsets.symmetric(
54 | horizontal: 4.w,
55 | vertical: 8.h,
56 | ),
57 | ),
58 | inputFormatters: inputFormatters,
59 | );
60 | }
61 | }
62 |
63 | class AtTextInputFormatter extends TextInputFormatter {
64 | final String? Function()? onTap;
65 |
66 | AtTextInputFormatter(this.onTap);
67 |
68 | @override
69 | TextEditingValue formatEditUpdate(
70 | TextEditingValue oldValue, TextEditingValue newValue) {
71 | int end = newValue.selection.end;
72 | int start = oldValue.selection.baseOffset;
73 | if (oldValue.text.length <= newValue.text.length) {
74 | var newChar = newValue.text.substring(start, end);
75 | if (newChar == '@') {
76 | var result = onTap?.call();
77 | if (result != null) {
78 | var v1 = newValue.text.replaceRange(start, end, result);
79 | var offset = start + result.length;
80 | return TextEditingValue(
81 | text: v1,
82 | selection: TextSelection.collapsed(offset: offset),
83 | );
84 | }
85 | }
86 | }
87 | return newValue;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/lib/src/chat_video_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/cupertino.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
6 |
7 | class ChatVideoView extends StatefulWidget {
8 | final String? videoPath;
9 | final String? videoUrl;
10 | final String? snapshotUrl;
11 | final String? snapshotPath;
12 | final double? width;
13 | final double? height;
14 | final double widgetWidth;
15 | final bool isReceived;
16 | final String msgId;
17 | final Stream>? msgSenProgressStream;
18 | final int initMsgSendProgress;
19 | final int index;
20 | final Stream? clickStream;
21 |
22 | const ChatVideoView({
23 | Key? key,
24 | required this.msgId,
25 | required this.isReceived,
26 | required this.index,
27 | this.clickStream,
28 | this.snapshotPath,
29 | this.snapshotUrl,
30 | this.videoPath,
31 | this.videoUrl,
32 | this.width,
33 | this.height,
34 | this.widgetWidth = 100,
35 | this.msgSenProgressStream,
36 | this.initMsgSendProgress = 100,
37 | }) : super(key: key);
38 |
39 | @override
40 | _ChatVideoViewState createState() => _ChatVideoViewState();
41 | }
42 |
43 | class _ChatVideoViewState extends State {
44 | late double _trulyWidth;
45 | late double _trulyHeight;
46 | String? snapshotUrl;
47 | String? snapshotPath;
48 | String? url;
49 | String? path;
50 |
51 | @override
52 | void initState() {
53 | path = widget.videoPath;
54 | url = widget.videoUrl;
55 | snapshotUrl = widget.snapshotUrl;
56 | snapshotPath = widget.snapshotPath;
57 |
58 | var w = widget.width ?? 1.0;
59 | var h = widget.height ?? 1.0;
60 |
61 | _trulyWidth = widget.widgetWidth;
62 | _trulyHeight = _trulyWidth * h / w;
63 | /*if (widget.widgetWidth > w) {
64 | _trulyWidth = w;
65 | _trulyHeight = h;
66 | } else {
67 | _trulyWidth = widget.widgetWidth;
68 | _trulyHeight = _trulyWidth * h / w;
69 | }*/
70 |
71 | /*widget.clickStream?.listen((i) {
72 | if (!mounted) return;
73 | if (_isClickedLocation(i)) {
74 | if (null != widget.onClickVideo) {
75 | widget.onClickVideo!(url, path);
76 | }
77 | }
78 | });*/
79 | super.initState();
80 | }
81 |
82 | bool _isClickedLocation(i) => i == widget.index;
83 |
84 | Widget _buildThumbView() {
85 | if (widget.isReceived) {
86 | if (null != snapshotUrl && snapshotUrl!.isNotEmpty) {
87 | return ImageUtil.networkImage(
88 | url: snapshotUrl!,
89 | width: _trulyWidth,
90 | height: _trulyHeight,
91 | fit: BoxFit.fitWidth,
92 | );
93 | }
94 | } else {
95 | if (null != snapshotPath &&
96 | snapshotPath!.isNotEmpty &&
97 | File(snapshotPath!).existsSync()) {
98 | return Image(
99 | image: FileImage(File(snapshotPath!)),
100 | height: _trulyHeight,
101 | width: _trulyWidth,
102 | fit: BoxFit.fitWidth,
103 | errorBuilder: (_, error, stack) => _errorIcon(),
104 | );
105 | } else {
106 | if (null != snapshotUrl && snapshotUrl!.isNotEmpty) {
107 | return ImageUtil.networkImage(
108 | url: snapshotUrl!,
109 | width: _trulyWidth,
110 | height: _trulyHeight,
111 | fit: BoxFit.fitWidth,
112 | );
113 | }
114 | }
115 | }
116 | return Container(width: _trulyWidth, height: _trulyHeight);
117 | }
118 |
119 | @override
120 | Widget build(BuildContext context) {
121 | return Hero(
122 | tag: widget.msgId,
123 | child: Container(
124 | width: _trulyWidth,
125 | height: _trulyHeight,
126 | // color: Color(0xFFB3D7FF),
127 | child: Stack(
128 | alignment: Alignment.center,
129 | children: [
130 | _buildThumbView(),
131 | ImageUtil.play(),
132 | ChatSendProgressView(
133 | height: _trulyHeight,
134 | width: _trulyWidth,
135 | msgId: widget.msgId,
136 | stream: widget.msgSenProgressStream,
137 | initProgress: widget.initMsgSendProgress,
138 | ),
139 | ],
140 | ),
141 | ));
142 | }
143 |
144 | Widget _errorIcon() =>
145 | ImageUtil.error(width: _trulyWidth, height: _trulyHeight);
146 | }
147 |
--------------------------------------------------------------------------------
/lib/src/chat_voice_record_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/services.dart';
3 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
4 | import 'package:flutter_screenutil/flutter_screenutil.dart';
5 |
6 | class ChatVoiceRecordBar extends StatefulWidget {
7 | const ChatVoiceRecordBar({
8 | Key? key,
9 | required this.onLongPressStart,
10 | required this.onLongPressEnd,
11 | required this.onLongPressMoveUpdate,
12 | this.speakBarColor,
13 | this.speakTextStyle,
14 | this.resetStatusStream,
15 | }) : super(key: key);
16 | final Function(LongPressStartDetails details) onLongPressStart;
17 | final Function(LongPressEndDetails details) onLongPressEnd;
18 | final Function(LongPressMoveUpdateDetails details) onLongPressMoveUpdate;
19 | final Color? speakBarColor;
20 | final TextStyle? speakTextStyle;
21 | final Stream? resetStatusStream;
22 |
23 | @override
24 | _ChatVoiceRecordBarState createState() => _ChatVoiceRecordBarState();
25 | }
26 |
27 | class _ChatVoiceRecordBarState extends State {
28 | bool _pressing = false;
29 |
30 | @override
31 | void initState() {
32 | widget.resetStatusStream?.listen(_reset);
33 | super.initState();
34 | }
35 |
36 | @override
37 | void dispose() {
38 | super.dispose();
39 | }
40 |
41 | _reset(rest) {
42 | if (!mounted) return;
43 | if (rest) {
44 | setState(() {
45 | _pressing = false;
46 | });
47 | }
48 | }
49 |
50 | @override
51 | Widget build(BuildContext context) {
52 | return GestureDetector(
53 | behavior: HitTestBehavior.translucent,
54 | onTapDown: (details) {
55 | setState(() {
56 | _pressing = true;
57 | });
58 | },
59 | onTapUp: (details) {
60 | setState(() {
61 | _pressing = false;
62 | });
63 | },
64 | onTapCancel: () {
65 | setState(() {
66 | _pressing = false;
67 | });
68 | },
69 | onLongPressStart: (details) {
70 | HapticFeedback.heavyImpact();
71 | widget.onLongPressStart(details);
72 | setState(() {
73 | _pressing = true;
74 | });
75 | },
76 | onLongPressEnd: (details) {
77 | widget.onLongPressEnd(details);
78 | setState(() {
79 | _pressing = false;
80 | });
81 | },
82 | onLongPressMoveUpdate: (details) {
83 | widget.onLongPressMoveUpdate(details);
84 | // Offset global = details.globalPosition;
85 | // Offset local = details.localPosition;
86 | // print('global:$global');
87 | // print('local:$local');
88 | },
89 | child: Container(
90 | // constraints: BoxConstraints(minHeight: 40.h),
91 | height: kVoiceRecordBarHeight,
92 | alignment: Alignment.center,
93 | decoration: BoxDecoration(
94 | color: (widget.speakBarColor ?? const Color(0xFF1D6BED))
95 | .withOpacity(_pressing ? 0.3 : 1),
96 | borderRadius: BorderRadius.circular(4),
97 | boxShadow: [
98 | BoxShadow(
99 | color: Color(0xFF000000).withOpacity(0.12),
100 | offset: Offset(0, -1),
101 | blurRadius: 4,
102 | spreadRadius: 0,
103 | ),
104 | ],
105 | ),
106 | child: Text(
107 | _pressing ? UILocalizations.releaseSend : UILocalizations.pressSpeak,
108 | style: widget.speakTextStyle ??
109 | TextStyle(
110 | fontSize: 12.sp,
111 | color: const Color(0xFFFFFFFF),
112 | ),
113 | ),
114 | ),
115 | );
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/lib/src/chat_voice_record_layout.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
5 | import 'package:flutter_screenutil/flutter_screenutil.dart';
6 | import 'package:rxdart/rxdart.dart';
7 |
8 | typedef SpeakViewChildBuilder = Widget Function(ChatVoiceRecordBar recordBar);
9 |
10 | class ChatVoiceRecordLayout extends StatefulWidget {
11 | const ChatVoiceRecordLayout({
12 | Key? key,
13 | required this.builder,
14 | this.locale,
15 | this.onCompleted,
16 | this.speakTextStyle,
17 | this.speakBarColor,
18 | this.maxRecordSec = 60,
19 | this.onLongPressStart,
20 | }) : super(key: key);
21 |
22 | final SpeakViewChildBuilder builder;
23 | final Locale? locale;
24 | final Function(int sec, String path)? onCompleted;
25 | final Color? speakBarColor;
26 | final TextStyle? speakTextStyle;
27 | final Function()? onLongPressStart;
28 |
29 | /// 最大记录时长s
30 | final int maxRecordSec;
31 |
32 | @override
33 | _ChatVoiceRecordLayoutState createState() => _ChatVoiceRecordLayoutState();
34 | }
35 |
36 | class _ChatVoiceRecordLayoutState extends State {
37 | var _selectedCancelArea = false;
38 | var _selectedSoundToWordArea = false;
39 | var _selectedPressArea = true;
40 | var _showVoiceRecordView = false;
41 | var _showSpeechRecognizing = false;
42 | var _showRecognizeFailed = false;
43 | Timer? _timer;
44 | late VoiceRecord _record;
45 | String? _path;
46 | int _sec = 0;
47 | var _isInterrupt = false;
48 |
49 | /// 被其他事件(如:音视频通话)中断,重置状态。
50 | final _resetSpeakBarStatusSub = PublishSubject();
51 |
52 | @override
53 | void initState() {
54 | UILocalizations.set(widget.locale);
55 | super.initState();
56 | }
57 |
58 | void callback(int sec, String path) {
59 | _sec = sec;
60 | _path = path;
61 | }
62 |
63 | @override
64 | void dispose() {
65 | _resetSpeakBarStatusSub.close();
66 | if (null != _timer) {
67 | _timer?.cancel();
68 | _timer = null;
69 | }
70 | super.dispose();
71 | }
72 |
73 | ChatVoiceRecordBar _createSpeakBar() =>
74 | ChatVoiceRecordBar(
75 | speakBarColor: widget.speakBarColor,
76 | speakTextStyle: widget.speakTextStyle,
77 | resetStatusStream: _resetSpeakBarStatusSub,
78 | onLongPressMoveUpdate: (details) {
79 | Offset global = details.globalPosition;
80 | setState(() {
81 | _selectedPressArea = global.dy >= 683.h;
82 | _selectedCancelArea = /*global.dy >= 563.h &&*/
83 | global.dy < 683.h && global.dx < 172.w;
84 | _selectedSoundToWordArea = global.dy < 683.h && global.dx >= 172.w;
85 | });
86 | },
87 | onLongPressEnd: (details) async {
88 | if (!_isInterrupt) _stop();
89 | },
90 | onLongPressStart: (details) {
91 | _start();
92 | widget.onLongPressStart?.call();
93 | },
94 | );
95 |
96 | @override
97 | Widget build(BuildContext context) {
98 | return FocusDetector(
99 | onVisibilityLost: () {
100 | setState(() {
101 | _showVoiceRecordView = false;
102 | _selectedCancelArea = false;
103 | _selectedSoundToWordArea = false;
104 | _selectedPressArea = false;
105 | _showVoiceRecordView = false;
106 | _showSpeechRecognizing = false;
107 | _showRecognizeFailed = false;
108 | _resetSpeakBarStatusSub.add(true);
109 | });
110 | },
111 | child: Stack(
112 | children: [
113 | widget.builder(_createSpeakBar()),
114 | IgnorePointer(
115 | ignoring: !_showRecognizeFailed,
116 | child: Visibility(
117 | visible: _showVoiceRecordView,
118 | child: ChatRecordVoiceView(
119 | selectedCancelArea: _selectedCancelArea,
120 | selectedSoundToWordArea: _selectedSoundToWordArea,
121 | selectedPressArea: _selectedPressArea,
122 | showSpeechRecognizing: _showSpeechRecognizing,
123 | showRecognizeFailed: _showRecognizeFailed,
124 | onCancel: () {
125 | setState(() {
126 | _selectedCancelArea = false;
127 | _selectedSoundToWordArea = false;
128 | _selectedPressArea = true;
129 | _showVoiceRecordView = false;
130 | _showSpeechRecognizing = false;
131 | _showRecognizeFailed = false;
132 | });
133 | },
134 | onConfirm: () {
135 | setState(() {
136 | _callback();
137 | _selectedCancelArea = false;
138 | _selectedSoundToWordArea = false;
139 | _selectedPressArea = true;
140 | _showVoiceRecordView = false;
141 | _showSpeechRecognizing = false;
142 | _showRecognizeFailed = false;
143 | });
144 | },
145 | ),
146 | ),
147 | ),
148 | ],
149 | ),
150 | );
151 | }
152 |
153 | void _callback() {
154 | if (_sec > 0 && null != _path) {
155 | widget.onCompleted?.call(_sec, _path!);
156 | }
157 | }
158 |
159 | void _stop() async {
160 | if (!_isInterrupt) await _record.stop();
161 | // 停止记录
162 | setState(() {
163 | if (_selectedPressArea) {
164 | _callback();
165 | }
166 | if (_selectedSoundToWordArea) {
167 | if (null != _timer) {
168 | _timer?.cancel();
169 | _timer = null;
170 | }
171 | _timer = new Timer(Duration(seconds: 1), () {
172 | setState(() {
173 | _showRecognizeFailed = true;
174 | _showSpeechRecognizing = false;
175 | });
176 | });
177 | _showSpeechRecognizing = true;
178 | _showVoiceRecordView = true;
179 | _selectedPressArea = false;
180 | _selectedCancelArea = false;
181 | _selectedSoundToWordArea = false;
182 | } else {
183 | _showVoiceRecordView = false;
184 | _selectedPressArea = false;
185 | _selectedCancelArea = false;
186 | _selectedSoundToWordArea = false;
187 | }
188 | });
189 | }
190 |
191 | void _start() {
192 | setState(() {
193 | // 开始记录
194 | _isInterrupt = false;
195 | _record = VoiceRecord(
196 | onFinished: (sec, path) {
197 | callback.call(sec, path);
198 | },
199 | onInterrupt: (sec, path) {
200 | _isInterrupt = true;
201 | callback.call(sec, path);
202 | _stop();
203 | },
204 | maxRecordSec: widget.maxRecordSec,
205 | );
206 | _record.start();
207 | _selectedPressArea = true;
208 | _showVoiceRecordView = true;
209 | });
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/lib/src/chat_voice_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_screenutil/flutter_screenutil.dart';
5 | import 'package:lottie/lottie.dart';
6 |
7 | // class ChatVoiceView extends StatefulWidget {
8 | // final int index;
9 | // final Stream? clickStream;
10 | // final bool isReceived;
11 | // final String? soundPath;
12 | // final String? soundUrl;
13 | // final int? duration;
14 | //
15 | // const ChatVoiceView({
16 | // Key? key,
17 | // required this.index,
18 | // required this.clickStream,
19 | // required this.isReceived,
20 | // this.soundPath,
21 | // this.soundUrl,
22 | // this.duration,
23 | // }) : super(key: key);
24 | //
25 | // @override
26 | // _ChatVoiceViewState createState() => _ChatVoiceViewState();
27 | // }
28 | //
29 | // class _ChatVoiceViewState extends State {
30 | // bool _isPlaying = false;
31 | // bool _isExistSource = false;
32 | // var _voicePlayer = AudioPlayer();
33 | // StreamSubscription? _clickSubs;
34 | //
35 | // @override
36 | // void initState() {
37 | // _voicePlayer.playerStateStream.listen((state) {
38 | // if (!mounted) return;
39 | // switch (state.processingState) {
40 | // case ProcessingState.idle:
41 | // case ProcessingState.loading:
42 | // case ProcessingState.buffering:
43 | // case ProcessingState.ready:
44 | // break;
45 | // case ProcessingState.completed:
46 | // setState(() {
47 | // if (_isPlaying) {
48 | // _isPlaying = false;
49 | // // _voicePlayer.stop();
50 | // }
51 | // });
52 | // break;
53 | // }
54 | // });
55 | // _initSource();
56 | // _clickSubs = widget.clickStream?.listen((i) {
57 | // if (!mounted) return;
58 | // print('click:$i $_isExistSource');
59 | // if (_isExistSource) {
60 | // print('sound click:$i');
61 | // if (_isClickedLocation(i)) {
62 | // setState(() {
63 | // if (_isPlaying) {
64 | // print('sound stop:$i');
65 | // _isPlaying = false;
66 | // _voicePlayer.stop();
67 | // } else {
68 | // print('sound start:$i');
69 | // _isPlaying = true;
70 | // _voicePlayer.seek(Duration.zero);
71 | // _voicePlayer.play();
72 | // }
73 | // });
74 | // } else {
75 | // if (_isPlaying) {
76 | // setState(() {
77 | // print('sound stop:$i');
78 | // _isPlaying = false;
79 | // _voicePlayer.stop();
80 | // });
81 | // }
82 | // }
83 | // }
84 | // });
85 | // super.initState();
86 | // }
87 | //
88 | // void _initSource() async {
89 | // String? path = widget.soundPath;
90 | // String? url = widget.soundUrl;
91 | // if (widget.isReceived) {
92 | // if (null != url && url.trim().isNotEmpty) {
93 | // _isExistSource = true;
94 | // _voicePlayer.setUrl(url);
95 | // }
96 | // } else {
97 | // var _existFile = false;
98 | // if (path != null && path.trim().isNotEmpty) {
99 | // var file = File(path);
100 | // _existFile = await file.exists();
101 | // }
102 | // if (_existFile) {
103 | // _isExistSource = true;
104 | // _voicePlayer.setFilePath(path!);
105 | // } else if (null != url && url.trim().isNotEmpty) {
106 | // _isExistSource = true;
107 | // _voicePlayer.setUrl(url);
108 | // }
109 | // }
110 | // }
111 | //
112 | // @override
113 | // void dispose() {
114 | // _voicePlayer.dispose();
115 | // _clickSubs?.cancel();
116 | // super.dispose();
117 | // }
118 | //
119 | // bool _isClickedLocation(i) => i == widget.index;
120 | //
121 | // Widget _buildVoiceAnimView() {
122 | // var anim;
123 | // var png;
124 | // var turns;
125 | // if (widget.isReceived) {
126 | // anim = 'assets/anim/voice_black.json';
127 | // png = 'assets/images/ic_voice_black.webp';
128 | // turns = 0;
129 | // } else {
130 | // anim = 'assets/anim/voice_blue.json';
131 | // png = 'assets/images/ic_voice_blue.webp';
132 | // turns = 90;
133 | // }
134 | // return Row(
135 | // children: [
136 | // Visibility(
137 | // visible: !widget.isReceived,
138 | // child: Text(
139 | // '${widget.duration ?? 0}``',
140 | // style: TextStyle(
141 | // fontSize: 14.sp,
142 | // color: Color(0xFF333333),
143 | // ),
144 | // ),
145 | // ),
146 | // _isPlaying
147 | // ? RotatedBox(
148 | // quarterTurns: turns,
149 | // child: Lottie.asset(
150 | // anim,
151 | // height: 19.h,
152 | // width: 18.w,
153 | // package: 'flutter_openim_widget',
154 | // ),
155 | // )
156 | // : Image.asset(
157 | // png,
158 | // height: 19.h,
159 | // width: 18.w,
160 | // package: 'flutter_openim_widget',
161 | // ),
162 | // Visibility(
163 | // visible: widget.isReceived,
164 | // child: Text(
165 | // '${widget.duration ?? 0}``',
166 | // style: TextStyle(
167 | // fontSize: 14.sp,
168 | // color: Color(0xFF333333),
169 | // ),
170 | // ),
171 | // ),
172 | // ],
173 | // );
174 | // }
175 | //
176 | // @override
177 | // Widget build(BuildContext context) {
178 | // return Container(
179 | // margin: EdgeInsets.only(
180 | // left: widget.isReceived ? 0 : _margin,
181 | // right: widget.isReceived ? _margin : 0,
182 | // ),
183 | // child: _buildVoiceAnimView(),
184 | // );
185 | // }
186 | //
187 | // double get _margin {
188 | // double diff = ((widget.duration ?? 0) / 5) * 6.w;
189 | // return diff > 60.w ? 60.w : diff;
190 | // }
191 | // }
192 |
193 | /// 去掉语音播放功能
194 | class ChatVoiceView extends StatefulWidget {
195 | final int index;
196 | final Stream? clickStream;
197 | final bool isReceived;
198 | final String? soundPath;
199 | final String? soundUrl;
200 | final int? duration;
201 | final bool isPlaying;
202 |
203 | const ChatVoiceView({
204 | Key? key,
205 | required this.index,
206 | required this.clickStream,
207 | required this.isReceived,
208 | this.soundPath,
209 | this.soundUrl,
210 | this.duration,
211 | this.isPlaying = false,
212 | }) : super(key: key);
213 |
214 | @override
215 | _ChatVoiceViewState createState() => _ChatVoiceViewState();
216 | }
217 |
218 | class _ChatVoiceViewState extends State {
219 | @override
220 | void initState() {
221 | super.initState();
222 | }
223 |
224 | @override
225 | void dispose() {
226 | super.dispose();
227 | }
228 |
229 | Widget _buildVoiceAnimView() {
230 | var anim;
231 | var png;
232 | var turns;
233 | if (widget.isReceived) {
234 | anim = 'assets/anim/voice_black.json';
235 | png = 'assets/images/ic_voice_black.webp';
236 | turns = 0;
237 | } else {
238 | anim = 'assets/anim/voice_blue.json';
239 | png = 'assets/images/ic_voice_blue.webp';
240 | turns = 90;
241 | }
242 | return Row(
243 | children: [
244 | Visibility(
245 | visible: !widget.isReceived,
246 | child: Text(
247 | '${widget.duration ?? 0}``',
248 | style: TextStyle(
249 | fontSize: 14.sp,
250 | color: Color(0xFF333333),
251 | ),
252 | ),
253 | ),
254 | widget.isPlaying
255 | ? RotatedBox(
256 | quarterTurns: turns,
257 | child: Lottie.asset(
258 | anim,
259 | height: 19.h,
260 | width: 18.w,
261 | package: 'flutter_openim_widget',
262 | ),
263 | )
264 | : Image.asset(
265 | png,
266 | height: 19.h,
267 | width: 18.w,
268 | package: 'flutter_openim_widget',
269 | ),
270 | Visibility(
271 | visible: widget.isReceived,
272 | child: Text(
273 | '${widget.duration ?? 0}``',
274 | style: TextStyle(
275 | fontSize: 14.sp,
276 | color: Color(0xFF333333),
277 | ),
278 | ),
279 | ),
280 | ],
281 | );
282 | }
283 |
284 | @override
285 | Widget build(BuildContext context) {
286 | return Container(
287 | margin: EdgeInsets.only(
288 | left: widget.isReceived ? 0 : _margin,
289 | right: widget.isReceived ? _margin : 0,
290 | ),
291 | child: _buildVoiceAnimView(),
292 | );
293 | }
294 |
295 | double get _margin {
296 | // 60 100.w
297 | // duration x
298 | final maxWidth = 100.w;
299 | final maxDuration = 60;
300 | double diff = (widget.duration ?? 0) * maxWidth / maxDuration;
301 | return diff > maxWidth ? maxWidth : diff;
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/lib/src/chat_webview_map.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 | import 'dart:convert';
3 | import 'dart:io';
4 |
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter_inappwebview/flutter_inappwebview.dart';
7 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
8 | import 'package:flutter_screenutil/flutter_screenutil.dart';
9 | import 'package:sprintf/sprintf.dart';
10 |
11 | /// 腾讯h5地图
12 | class ChatWebViewMap extends StatefulWidget {
13 | const ChatWebViewMap({
14 | Key? key,
15 | this.mapAppKey = "",
16 | this.mapThumbnailSize = "1200*600",
17 | this.mapBackUrl = "http://callback",
18 | }) : super(key: key);
19 |
20 | final String mapAppKey;
21 | final String mapThumbnailSize;
22 | final String mapBackUrl;
23 |
24 | @override
25 | _ChatWebViewMapState createState() => _ChatWebViewMapState();
26 | }
27 |
28 | class _ChatWebViewMapState extends State {
29 | final GlobalKey webViewKey = GlobalKey();
30 | InAppWebViewController? webViewController;
31 | InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
32 | crossPlatform: InAppWebViewOptions(
33 | useShouldOverrideUrlLoading: true,
34 | mediaPlaybackRequiresUserGesture: false,
35 | ),
36 | android: AndroidInAppWebViewOptions(
37 | useHybridComposition: true,
38 | domStorageEnabled: true,
39 | geolocationEnabled: true,
40 | ),
41 | ios: IOSInAppWebViewOptions(
42 | allowsInlineMediaPlayback: true,
43 | ));
44 |
45 | late PullToRefreshController pullToRefreshController;
46 |
47 | String url = "";
48 | double progress = 0;
49 | double? latitude;
50 | double? longitude;
51 | String? description;
52 |
53 | late String mapUrl;
54 | late String mapThumbnailUrl;
55 |
56 | @override
57 | void initState() {
58 | super.initState();
59 | mapUrl =
60 | "https://apis.map.qq.com/tools/locpicker?search=1&type=0&backurl=${widget.mapBackUrl}&key=${widget.mapAppKey}&referer=myapp&policy=1";
61 | mapThumbnailUrl =
62 | "https://apis.map.qq.com/ws/staticmap/v2/?center=%s&zoom=18&size=${widget.mapThumbnailSize}&maptype=roadmap&markers=size:large|color:0xFFCCFF|label:k|%s&key=${widget.mapAppKey}";
63 | pullToRefreshController = PullToRefreshController(
64 | options: PullToRefreshOptions(
65 | color: Colors.blue,
66 | ),
67 | onRefresh: () async {
68 | if (Platform.isAndroid) {
69 | webViewController?.reload();
70 | } else if (Platform.isIOS) {
71 | webViewController?.loadUrl(
72 | urlRequest: URLRequest(url: await webViewController?.getUrl()));
73 | }
74 | },
75 | );
76 | }
77 |
78 | @override
79 | void dispose() {
80 | super.dispose();
81 | }
82 |
83 | @override
84 | Widget build(BuildContext context) {
85 | return Scaffold(
86 | backgroundColor: Colors.white,
87 | appBar: TitleBar.back(
88 | context,
89 | height: 49.h,
90 | title: UILocalizations.location,
91 | textStyle: TextStyle(
92 | fontSize: 18.sp,
93 | color: Color(0xFF333333),
94 | ),
95 | right: GestureDetector(
96 | behavior: HitTestBehavior.translucent,
97 | onTap: () {
98 | if (null == latitude || null == longitude) {
99 | showDialog(
100 | context: context,
101 | builder: (_) => AlertDialog(
102 | title: Text(
103 | '请选择一个位置',
104 | style: TextStyle(
105 | fontSize: 18.sp,
106 | color: Color(0xFF333333),
107 | ),
108 | ),
109 | actions: [
110 | GestureDetector(
111 | onTap: () => Navigator.pop(context),
112 | behavior: HitTestBehavior.translucent,
113 | child: Container(
114 | padding: EdgeInsets.symmetric(
115 | horizontal: 20,
116 | vertical: 10,
117 | ),
118 | child: Text(
119 | '确定',
120 | style: TextStyle(
121 | fontSize: 16.sp,
122 | color: Color(0xFF1B72EC),
123 | ),
124 | ),
125 | ),
126 | ),
127 | ],
128 | ),
129 | );
130 | return;
131 | }
132 | Navigator.pop(context, {
133 | 'latitude': latitude,
134 | 'longitude': longitude,
135 | 'description': description,
136 | });
137 | },
138 | child: Padding(
139 | padding: EdgeInsets.symmetric(horizontal: 22.w),
140 | child: Text(
141 | UILocalizations.confirm,
142 | style: TextStyle(
143 | fontSize: 18.sp,
144 | color: Color(0xFF333333),
145 | ),
146 | ),
147 | ),
148 | ),
149 | ),
150 | body: SafeArea(
151 | child: Stack(
152 | children: [
153 | InAppWebView(
154 | key: webViewKey,
155 | // contextMenu: contextMenu,
156 | initialUrlRequest: URLRequest(url: Uri.parse(mapUrl)),
157 | // initialFile: "assets/index.html",
158 | initialUserScripts: UnmodifiableListView([]),
159 | initialOptions: options,
160 | pullToRefreshController: pullToRefreshController,
161 | onWebViewCreated: (controller) {
162 | webViewController = controller;
163 | },
164 | onLoadStart: (controller, url) {},
165 | androidOnGeolocationPermissionsShowPrompt:
166 | (controller, origin) async {
167 | return GeolocationPermissionShowPromptResponse(
168 | origin: origin, allow: true, retain: true);
169 | },
170 | androidOnPermissionRequest: (ctrl, origin, res) async {
171 | return PermissionRequestResponse(
172 | resources: res,
173 | action: PermissionRequestResponseAction.GRANT,
174 | );
175 | },
176 | shouldOverrideUrlLoading: (controller, navigationAction) async {
177 | var uri = navigationAction.request.url!;
178 | if (uri.toString().startsWith(widget.mapBackUrl)) {
179 | try {
180 | print('${uri.queryParameters}');
181 | var _result = {};
182 | _result.addAll(uri.queryParameters);
183 | var lat = _result['latng'];
184 | //latitude, longitude
185 | var list = lat!.split(",");
186 | _result['latitude'] = list[0];
187 | _result['longitude'] = list[1];
188 | _result['url'] = sprintf(mapThumbnailUrl, [lat, lat]);
189 | print('${_result['url']}');
190 | // log('--url:${_result['url']}');
191 | latitude = double.tryParse(_result['latitude']!);
192 | longitude = double.tryParse(_result['longitude']!);
193 | description = jsonEncode(_result);
194 | } catch (e) {
195 | print('e:$e');
196 | }
197 | return NavigationActionPolicy.CANCEL;
198 | }
199 | return NavigationActionPolicy.ALLOW;
200 | },
201 | onLoadStop: (controller, url) async {
202 | pullToRefreshController.endRefreshing();
203 | this.url = url.toString();
204 | },
205 | onLoadError: (controller, url, code, message) {
206 | pullToRefreshController.endRefreshing();
207 | },
208 | onProgressChanged: (controller, progress) {
209 | if (progress == 100) {
210 | pullToRefreshController.endRefreshing();
211 | }
212 | setState(() {
213 | this.progress = progress / 100;
214 | });
215 | },
216 | onUpdateVisitedHistory: (controller, url, androidIsReload) {
217 | this.url = url.toString();
218 | },
219 | onConsoleMessage: (controller, consoleMessage) {
220 | print(consoleMessage);
221 | },
222 | ),
223 | progress < 1.0
224 | ? LinearProgressIndicator(value: progress)
225 | : Container(),
226 | ],
227 | ),
228 | ),
229 | );
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/lib/src/custom_focus_detector.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:flutter/widgets.dart';
4 | import 'package:visibility_detector/visibility_detector.dart';
5 |
6 | /// Fires callbacks every time the widget appears or disappears from the screen.
7 | /// Adapter to flutter 3.0
8 | class FocusDetector extends StatefulWidget {
9 | const FocusDetector({
10 | required this.child,
11 | this.onFocusGained,
12 | this.onFocusLost,
13 | this.onVisibilityGained,
14 | this.onVisibilityLost,
15 | this.onForegroundGained,
16 | this.onForegroundLost,
17 | Key? key,
18 | }) : super(key: key);
19 |
20 | /// Called when the widget becomes visible or enters foreground while visible.
21 | final VoidCallback? onFocusGained;
22 |
23 | /// Called when the widget becomes invisible or enters background while visible.
24 | final VoidCallback? onFocusLost;
25 |
26 | /// Called when the widget becomes visible.
27 | final VoidCallback? onVisibilityGained;
28 |
29 | /// Called when the widget becomes invisible.
30 | final VoidCallback? onVisibilityLost;
31 |
32 | /// Called when the app entered the foreground while the widget is visible.
33 | final VoidCallback? onForegroundGained;
34 |
35 | /// Called when the app is sent to background while the widget was visible.
36 | final VoidCallback? onForegroundLost;
37 |
38 | /// The widget below this widget in the tree.
39 | final Widget child;
40 |
41 | @override
42 | _FocusDetectorState createState() => _FocusDetectorState();
43 | }
44 |
45 | class _FocusDetectorState extends State
46 | with WidgetsBindingObserver {
47 | final _visibilityDetectorKey = UniqueKey();
48 |
49 | /// Whether this widget is currently visible within the app.
50 | bool _isWidgetVisible = false;
51 |
52 | /// Whether the app is in the foreground.
53 | bool _isAppInForeground = true;
54 |
55 | @override
56 | void initState() {
57 | WidgetsBinding.instance.addObserver(this);
58 | super.initState();
59 | }
60 |
61 | @override
62 | void didChangeAppLifecycleState(AppLifecycleState state) {
63 | _notifyPlaneTransition(state);
64 | }
65 |
66 | /// Notifies app's transitions to/from the foreground.
67 | void _notifyPlaneTransition(AppLifecycleState state) {
68 | if (!_isWidgetVisible) {
69 | return;
70 | }
71 |
72 | final isAppResumed = state == AppLifecycleState.resumed;
73 | final wasResumed = _isAppInForeground;
74 | if (isAppResumed && !wasResumed) {
75 | _isAppInForeground = true;
76 | _notifyFocusGain();
77 | _notifyForegroundGain();
78 | return;
79 | }
80 |
81 | final isAppPaused = state == AppLifecycleState.paused;
82 | if (isAppPaused && wasResumed) {
83 | _isAppInForeground = false;
84 | _notifyFocusLoss();
85 | _notifyForegroundLoss();
86 | }
87 | }
88 |
89 | @override
90 | Widget build(BuildContext context) => VisibilityDetector(
91 | key: _visibilityDetectorKey,
92 | onVisibilityChanged: (visibilityInfo) {
93 | try {
94 | // 当widget高度超过一屏时visibilityInfo.visibleFraction的值达不到1
95 | final visibleBoundsBottom = visibilityInfo.visibleBounds.bottom;
96 | final height = visibilityInfo.size.height;
97 | final fraction = visibleBoundsBottom / height;
98 | _notifyVisibilityStatusChange(fraction);
99 | } catch (_) {
100 | final visibleFraction = visibilityInfo.visibleFraction;
101 | _notifyVisibilityStatusChange(visibleFraction);
102 | }
103 | },
104 | child: widget.child,
105 | );
106 |
107 | /// Notifies changes in the widget's visibility.
108 | void _notifyVisibilityStatusChange(double newVisibleFraction) {
109 | if (!_isAppInForeground) {
110 | return;
111 | }
112 |
113 | final wasFullyVisible = _isWidgetVisible;
114 | final isFullyVisible = newVisibleFraction == 1;
115 | if (!wasFullyVisible && isFullyVisible) {
116 | _isWidgetVisible = true;
117 | _notifyFocusGain();
118 | _notifyVisibilityGain();
119 | }
120 |
121 | final isFullyInvisible = newVisibleFraction == 0;
122 | if (wasFullyVisible && isFullyInvisible) {
123 | _isWidgetVisible = false;
124 | _notifyFocusLoss();
125 | _notifyVisibilityLoss();
126 | }
127 | }
128 |
129 | void _notifyFocusGain() {
130 | final onFocusGained = widget.onFocusGained;
131 | if (onFocusGained != null) {
132 | onFocusGained();
133 | }
134 | }
135 |
136 | void _notifyFocusLoss() {
137 | final onFocusLost = widget.onFocusLost;
138 | if (onFocusLost != null) {
139 | onFocusLost();
140 | }
141 | }
142 |
143 | void _notifyVisibilityGain() {
144 | final onVisibilityGained = widget.onVisibilityGained;
145 | if (onVisibilityGained != null) {
146 | onVisibilityGained();
147 | }
148 | }
149 |
150 | void _notifyVisibilityLoss() {
151 | final onVisibilityLost = widget.onVisibilityLost;
152 | if (onVisibilityLost != null) {
153 | onVisibilityLost();
154 | }
155 | }
156 |
157 | void _notifyForegroundGain() {
158 | final onForegroundGained = widget.onForegroundGained;
159 | if (onForegroundGained != null) {
160 | onForegroundGained();
161 | }
162 | }
163 |
164 | void _notifyForegroundLoss() {
165 | final onForegroundLost = widget.onForegroundLost;
166 | if (onForegroundLost != null) {
167 | onForegroundLost();
168 | }
169 | }
170 |
171 | @override
172 | void dispose() {
173 | WidgetsBinding.instance.removeObserver(this);
174 | super.dispose();
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/lib/src/fadein_image.dart:
--------------------------------------------------------------------------------
1 | // import 'package:extended_image/extended_image.dart';
2 | // import 'package:flutter/material.dart';
3 | // import 'package:flutter_openim_widget/flutter_openim_widget.dart';
4 | // import 'package:flutter_screenutil/flutter_screenutil.dart';
5 | //
6 | // class FadeInExtendedImage extends StatefulWidget {
7 | // const FadeInExtendedImage({
8 | // Key? key,
9 | // required this.url,
10 | // this.width,
11 | // this.height,
12 | // this.cacheWidth,
13 | // this.cacheHeight,
14 | // this.fit,
15 | // this.clearMemoryCacheWhenDispose = true,
16 | // this.lowMemory = true,
17 | // this.loadProgress = true,
18 | // this.errorWidget,
19 | // }) : super(key: key);
20 | //
21 | // final String url;
22 | // final double? width;
23 | // final double? height;
24 | // final int? cacheWidth;
25 | // final int? cacheHeight;
26 | // final BoxFit? fit;
27 | // final bool loadProgress;
28 | // final bool clearMemoryCacheWhenDispose;
29 | // final bool lowMemory;
30 | // final Widget? errorWidget;
31 | //
32 | // @override
33 | // State createState() => _FadeInExtendedImageState();
34 | // }
35 | //
36 | // class _FadeInExtendedImageState extends State
37 | // with SingleTickerProviderStateMixin {
38 | // late AnimationController _controller;
39 | //
40 | // @override
41 | // void initState() {
42 | // _controller = AnimationController(
43 | // vsync: this,
44 | // duration: const Duration(seconds: 1),
45 | // lowerBound: 0.0,
46 | // upperBound: 1.0);
47 | // super.initState();
48 | // }
49 | //
50 | // @override
51 | // void dispose() {
52 | // _controller.dispose();
53 | // super.dispose();
54 | // }
55 | //
56 | // @override
57 | // Widget build(BuildContext context) {
58 | // return ExtendedImage.network(
59 | // widget.url,
60 | // width: widget.width,
61 | // height: widget.height,
62 | // fit: widget.fit,
63 | // cacheWidth:
64 | // widget.lowMemory ? widget.cacheWidth ?? (1.sw * .75).toInt() : null,
65 | // cacheHeight: widget.lowMemory ? widget.cacheHeight : null,
66 | // cache: true,
67 | // clearMemoryCacheWhenDispose: widget.clearMemoryCacheWhenDispose,
68 | // loadStateChanged: (ExtendedImageState state) {
69 | // switch (state.extendedImageLoadState) {
70 | // case LoadState.loading:
71 | // {
72 | // _controller.reset();
73 | // final ImageChunkEvent? loadingProgress = state.loadingProgress;
74 | // final double? progress =
75 | // loadingProgress?.expectedTotalBytes != null
76 | // ? loadingProgress!.cumulativeBytesLoaded /
77 | // loadingProgress.expectedTotalBytes!
78 | // : null;
79 | // // CupertinoActivityIndicator()
80 | // return Container(
81 | // width: 15.0,
82 | // height: 15.0,
83 | // child: widget.loadProgress
84 | // ? Center(
85 | // child: CircularProgressIndicator(
86 | // strokeWidth: 1.5,
87 | // value: progress ?? 0,
88 | // ),
89 | // )
90 | // : null,
91 | // );
92 | // }
93 | // case LoadState.completed:
94 | // _controller.forward();
95 | //
96 | // ///if you don't want override completed widget
97 | // ///please return null or state.completedWidget
98 | // //return null;
99 | // //return state.completedWidget;
100 | // return FadeTransition(
101 | // opacity: _controller,
102 | // child: ExtendedRawImage(
103 | // image: state.extendedImageInfo?.image,
104 | // width: widget.width,
105 | // height: widget.height,
106 | // ),
107 | // );
108 | // case LoadState.failed:
109 | // _controller.reset();
110 | // // remove memory cached
111 | // state.imageProvider.evict();
112 | // return widget.errorWidget ??
113 | // ImageUtil.error(width: widget.width, height: widget.height);
114 | // }
115 | // },
116 | // );
117 | // }
118 | // }
119 |
--------------------------------------------------------------------------------
/lib/src/favorite_emoji_listview.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 |
5 | class FavoriteEmojiListView extends StatelessWidget {
6 | const FavoriteEmojiListView({
7 | Key? key,
8 | required this.emojiList,
9 | this.selectedEmojiList = const [],
10 | this.onAddFavoriteEmoji,
11 | this.onChangedSelectedStatus,
12 | this.enabled = false,
13 | }) : super(key: key);
14 |
15 | final List emojiList;
16 | final List selectedEmojiList;
17 | final Function(String url, bool selected)? onChangedSelectedStatus;
18 | final Function()? onAddFavoriteEmoji;
19 | final bool enabled;
20 |
21 | bool _isChecked(url) => selectedEmojiList.contains(url);
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return GridView.builder(
26 | padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
27 | itemCount: emojiList.length + 1,
28 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
29 | crossAxisCount: 4,
30 | childAspectRatio: 1,
31 | mainAxisSpacing: 20.h,
32 | crossAxisSpacing: 37.w,
33 | ),
34 | itemBuilder: (BuildContext context, int index) {
35 | if (index == 0) {
36 | return GestureDetector(
37 | onTap: onAddFavoriteEmoji,
38 | child: Center(
39 | child: ImageUtil.assetImage('ic_add_emoji'),
40 | ),
41 | );
42 | }
43 | var url = emojiList.elementAt(index - 1);
44 | return _buildItemView(url: url);
45 | },
46 | );
47 | }
48 |
49 | Widget _buildItemView({required String url}) => GestureDetector(
50 | onTap: enabled
51 | ? () => onChangedSelectedStatus?.call(url, !_isChecked(url))
52 | : null,
53 | child: Container(
54 | child: Stack(
55 | children: [
56 | Center(
57 | child: ImageUtil.lowMemoryNetworkImage(
58 | url: '$url?imageView2/1/w/${60.w}/h/${60.w}/rq/80',
59 | width: 60.w,
60 | cacheWidth: 60.w.toInt(),
61 | fit: BoxFit.cover,
62 | ),
63 | ),
64 | if (enabled)
65 | Positioned(
66 | top: 4.h,
67 | left: 4.w,
68 | child: ImageUtil.assetImage(
69 | _isChecked(url)
70 | ? 'ic_favorite_emoji_sel'
71 | : 'ic_favorite_emoji_nor',
72 | width: 18.w,
73 | ),
74 | ),
75 | ],
76 | ),
77 | ),
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/lib/src/pop_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 | import 'package:flutter_screenutil/flutter_screenutil.dart';
4 |
5 | // typedef PopupMenuItemBuilder = Widget Function(PopMenuInfo info);
6 |
7 | class PopMenuInfo {
8 | final String? icon;
9 | final String text;
10 | final Function()? onTap;
11 |
12 | PopMenuInfo({
13 | this.icon,
14 | required this.text,
15 | this.onTap,
16 | });
17 | }
18 |
19 | class PopButton extends StatelessWidget {
20 | final List menus;
21 | final Widget child;
22 |
23 | final CustomPopupMenuController? popCtrl;
24 |
25 | // final PopupMenuItemBuilder builder;
26 | final PressType pressType;
27 | final bool showArrow;
28 | final Color arrowColor;
29 | final Color barrierColor;
30 | final double horizontalMargin;
31 | final double verticalMargin;
32 | final double arrowSize;
33 |
34 | final Color menuBgColor;
35 | final double menuBgRadius;
36 | final Color? menuBgShadowColor;
37 | final Offset? menuBgShadowOffset;
38 | final double? menuBgShadowBlurRadius;
39 | final double? menuBgShadowSpreadRadius;
40 |
41 | final double? menuItemHeight;
42 | final double? menuItemWidth;
43 | final EdgeInsetsGeometry? menuItemPadding;
44 | final TextStyle menuItemTextStyle;
45 | final double menuItemIconSize;
46 | final Color dividingLineColor;
47 | final double dividingLineWidth;
48 |
49 | PopButton({
50 | Key? key,
51 | required this.menus,
52 | required this.child,
53 | // required this.builder,
54 | this.popCtrl,
55 | this.arrowColor = const Color(0xFF1B72EC),
56 | this.showArrow = true,
57 | this.barrierColor = Colors.transparent,
58 | this.arrowSize = 10.0,
59 | this.horizontalMargin = 10.0,
60 | this.verticalMargin = 10.0,
61 | this.pressType = PressType.singleClick,
62 | this.menuBgColor = const Color(0xFF1B72EC),
63 | this.menuBgRadius = 10.0,
64 | this.menuBgShadowColor,
65 | this.menuBgShadowOffset,
66 | this.menuBgShadowBlurRadius,
67 | this.menuBgShadowSpreadRadius,
68 | this.menuItemHeight,
69 | this.menuItemWidth,
70 | this.menuItemTextStyle = const TextStyle(
71 | fontSize: 14,
72 | color: Colors.white,
73 | ),
74 | this.menuItemIconSize = 18.0,
75 | this.menuItemPadding,
76 | this.dividingLineColor = const Color(0xFFF0F0F0),
77 | this.dividingLineWidth = 1.0,
78 | }) : super(key: key);
79 |
80 | @override
81 | Widget build(BuildContext context) {
82 | return CopyCustomPopupMenu(
83 | controller: popCtrl,
84 | arrowColor: arrowColor,
85 | showArrow: showArrow,
86 | barrierColor: barrierColor,
87 | arrowSize: arrowSize,
88 | verticalMargin: verticalMargin,
89 | horizontalMargin: horizontalMargin,
90 | pressType: pressType,
91 | child: child,
92 | menuBuilder: () => _buildPopBgView(
93 | child: Column(
94 | crossAxisAlignment: CrossAxisAlignment.start,
95 | children: menus.map((e) => _buildPopItemView(e)).toList(),
96 | ),
97 | ),
98 | );
99 | }
100 |
101 | _clickArea(double dy) {
102 | for (var i = 0; i < menus.length; i++) {
103 | if (dy > i * menuItemHeight! && dy <= (i + 1) * menuItemHeight!) {
104 | menus.elementAt(i).onTap?.call();
105 | popCtrl?.hideMenu();
106 | }
107 | }
108 | }
109 |
110 | Widget _buildPopBgView({Widget? child}) => Container(
111 | child: child,
112 | // padding: EdgeInsets.symmetric(vertical: 4),
113 | decoration: BoxDecoration(
114 | color: menuBgColor,
115 | borderRadius: BorderRadius.circular(menuBgRadius),
116 | boxShadow: [
117 | BoxShadow(
118 | color: menuBgShadowColor ?? Color(0xFF000000).withOpacity(0.5),
119 | offset: menuBgShadowOffset ?? Offset(0, 2),
120 | blurRadius: menuBgShadowBlurRadius ?? 6,
121 | spreadRadius: menuBgShadowSpreadRadius ?? 0,
122 | )
123 | ],
124 | ),
125 | );
126 |
127 | Widget _buildPopItemView(PopMenuInfo info) => GestureDetector(
128 | onTap: () {
129 | popCtrl?.hideMenu();
130 | info.onTap?.call();
131 | },
132 | behavior: HitTestBehavior.translucent,
133 | child: Container(
134 | height: menuItemHeight,
135 | width: menuItemWidth,
136 | padding: menuItemPadding,
137 | // decoration: BoxDecoration(
138 | // border: BorderDirectional(
139 | // bottom: BorderSide(
140 | // color: dividingLineColor,
141 | // width: dividingLineWidth,
142 | // ),
143 | // ),
144 | // ),
145 | child: Row(
146 | mainAxisSize: MainAxisSize.min,
147 | children: [
148 | if (null != info.icon)
149 | Padding(
150 | padding: EdgeInsets.only(right: 10.w),
151 | child: Image.asset(
152 | info.icon!,
153 | width: menuItemIconSize,
154 | height: menuItemIconSize,
155 | ),
156 | ),
157 | Text(
158 | info.text,
159 | style: menuItemTextStyle,
160 | ),
161 | ],
162 | ),
163 | ),
164 | );
165 | }
166 |
--------------------------------------------------------------------------------
/lib/src/timing_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_screenutil/flutter_screenutil.dart';
5 |
6 | class TimingView extends StatefulWidget {
7 | const TimingView({
8 | Key? key,
9 | required this.sec,
10 | this.onFinished,
11 | }) : super(key: key);
12 | final int sec;
13 | final Function()? onFinished;
14 |
15 | @override
16 | _TimingViewState createState() => _TimingViewState();
17 | }
18 |
19 | class _TimingViewState extends State {
20 | Timer? _timer;
21 | late int _sec;
22 |
23 | @override
24 | void initState() {
25 | _sec = widget.sec;
26 | _timer = Timer.periodic(Duration(seconds: 1), (timer) {
27 | if (!mounted) return;
28 | --_sec;
29 | if (_sec <= 0) {
30 | _timer?.cancel();
31 | _timer = null;
32 | widget.onFinished?.call();
33 | }
34 | setState(() {
35 | if (_sec <= 0) {
36 | _sec = 0;
37 | }
38 | });
39 | });
40 | super.initState();
41 | }
42 |
43 | @override
44 | void dispose() {
45 | _timer?.cancel();
46 | super.dispose();
47 | }
48 |
49 | @override
50 | Widget build(BuildContext context) {
51 | return Container(
52 | margin: EdgeInsets.only(top: 2.h, right: 4.w),
53 | child: Text(
54 | '$_sec s',
55 | style: TextStyle(
56 | fontSize: 10.sp,
57 | color: Color(0xFF006AFF),
58 | ),
59 | ),
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/src/title_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_openim_widget/flutter_openim_widget.dart';
3 |
4 | class TitleBar extends StatelessWidget implements PreferredSizeWidget {
5 | @override
6 | Size get preferredSize => Size.fromHeight(height);
7 |
8 | const TitleBar({
9 | Key? key,
10 | this.result,
11 | this.height = 56.0,
12 | this.elevation = 0.0,
13 | this.left,
14 | this.center,
15 | this.right,
16 | this.shadowColor,
17 | this.backgroundColor,
18 | this.onBack,
19 | }) : super(key: key);
20 | final dynamic result;
21 | final double height;
22 | final double elevation;
23 | final Widget? left;
24 | final Widget? center;
25 | final Widget? right;
26 | final Color? shadowColor;
27 | final Color? backgroundColor;
28 | final Function()? onBack;
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return _appbar(
33 | data: result,
34 | height: height,
35 | elevation: elevation,
36 | shadowColor: shadowColor,
37 | backgroundColor: backgroundColor,
38 | onBack: onBack,
39 | left: left,
40 | center: center,
41 | right: right,
42 | );
43 | }
44 |
45 | TitleBar.back(
46 | BuildContext context, {
47 | String? title,
48 | TextStyle textStyle = const TextStyle(
49 | fontSize: 18.0,
50 | color: Color(0xFF333333),
51 | ),
52 | dynamic result,
53 | double height = 56.0,
54 | double elevation = 0.0,
55 | Widget? right,
56 | Widget? center,
57 | Color? shadowColor,
58 | Color backgroundColor = Colors.white,
59 | }) : result = result,
60 | height = height,
61 | elevation = elevation,
62 | backgroundColor = backgroundColor,
63 | shadowColor = shadowColor,
64 | onBack = null,
65 | left = GestureDetector(
66 | onTap: () => Navigator.pop(context, result),
67 | behavior: HitTestBehavior.translucent,
68 | child: Container(
69 | padding: EdgeInsets.symmetric(horizontal: 22),
70 | child: ImageUtil.back(),
71 | ),
72 | ),
73 | right = right,
74 | center = center ??
75 | Text(
76 | title ?? '',
77 | style: textStyle,
78 | );
79 |
80 | TitleBar.leftTitle({
81 | String? title,
82 | TextStyle textStyle = const TextStyle(
83 | fontSize: 18.0,
84 | color: Color(0xFF333333),
85 | ),
86 | dynamic result,
87 | double height = 56.0,
88 | double elevation = 0.0,
89 | Widget? right,
90 | Color? shadowColor,
91 | Color backgroundColor = Colors.white,
92 | }) : result = result,
93 | height = height,
94 | elevation = elevation,
95 | backgroundColor = backgroundColor,
96 | shadowColor = shadowColor,
97 | onBack = null,
98 | center = null,
99 | right = right,
100 | left = Container(
101 | padding: EdgeInsets.symmetric(horizontal: 22),
102 | child: Text(
103 | title ?? '',
104 | style: textStyle,
105 | ),
106 | );
107 |
108 | TitleBar.imTitle({
109 | String? title,
110 | TextStyle textStyle = const TextStyle(
111 | fontSize: 22.0,
112 | color: Color(0xFF1B72EC),
113 | ),
114 | dynamic result,
115 | double height = 56.0,
116 | double elevation = 0.0,
117 | Color? shadowColor,
118 | Color backgroundColor = Colors.white,
119 | Function()? onSearch,
120 | List menus = const [],
121 | }) : result = result,
122 | height = height,
123 | elevation = elevation,
124 | backgroundColor = backgroundColor,
125 | shadowColor = shadowColor,
126 | onBack = null,
127 | center = null,
128 | right = Container(
129 | padding: EdgeInsets.only(right: 12),
130 | child: Row(
131 | mainAxisSize: MainAxisSize.min,
132 | children: [
133 | Visibility(
134 | visible: null != onSearch,
135 | child: GestureDetector(
136 | onTap: onSearch,
137 | behavior: HitTestBehavior.translucent,
138 | child: Container(
139 | padding: EdgeInsets.only(left: 10, right: 10),
140 | child: ImageUtil.search(),
141 | ),
142 | ),
143 | ),
144 | Visibility(
145 | visible: menus.length > 0,
146 | child: PopButton(
147 | menus: menus,
148 | child: Container(
149 | padding: EdgeInsets.only(left: 10, right: 10),
150 | child: ImageUtil.add(),
151 | ),
152 | ),
153 | ),
154 | ],
155 | ),
156 | ),
157 | left = Container(
158 | padding: EdgeInsets.symmetric(horizontal: 22),
159 | child: Text(
160 | title ?? '',
161 | style: textStyle,
162 | ),
163 | );
164 |
165 | static AppBar _appbar({
166 | dynamic data,
167 | double height = 56,
168 | double elevation = 0,
169 | Color? shadowColor,
170 | Color? backgroundColor = Colors.white,
171 | Function()? onBack,
172 | Widget? left,
173 | Widget? center,
174 | Widget? right,
175 | }) {
176 | return AppBar(
177 | shadowColor: shadowColor,
178 | elevation: elevation,
179 | titleSpacing: 0,
180 | backgroundColor: backgroundColor,
181 | toolbarHeight: height,
182 | leading: null,
183 | automaticallyImplyLeading: false,
184 | title: Container(
185 | height: height,
186 | child: Stack(
187 | children: [
188 | Align(
189 | alignment: Alignment.centerLeft,
190 | child: left,
191 | ),
192 | Align(
193 | alignment: Alignment.center,
194 | child: center,
195 | ),
196 | Align(
197 | alignment: Alignment.centerRight,
198 | child: right,
199 | )
200 | ],
201 | ),
202 | ),
203 | );
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/lib/src/unread_count_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_screenutil/flutter_screenutil.dart';
3 |
4 | class UnreadCountView extends StatelessWidget {
5 | const UnreadCountView({
6 | Key? key,
7 | this.count = 0,
8 | this.size = 13,
9 | this.margin,
10 | }) : super(key: key);
11 | final int count;
12 | final double size;
13 | final EdgeInsetsGeometry? margin;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return Visibility(
18 | visible: count > 0,
19 | child: Container(
20 | alignment: Alignment.center,
21 | margin: margin,
22 | padding: count > 99 ? EdgeInsets.symmetric(horizontal: 4.w) : null,
23 | constraints: BoxConstraints(maxHeight: size, minWidth: size),
24 | decoration: _decoration,
25 | child: _text,
26 | ),
27 | );
28 | }
29 |
30 | Text get _text => Text(
31 | '${count > 99 ? '99+' : count}',
32 | style: TextStyle(
33 | fontSize: 8.sp,
34 | color: const Color(0xFFFFFFFF),
35 | ),
36 | textAlign: TextAlign.center,
37 | );
38 |
39 | Decoration get _decoration => BoxDecoration(
40 | color: Color(0xFFFF3366),
41 | shape: count > 99 ? BoxShape.rectangle : BoxShape.circle,
42 | borderRadius: count > 99 ? BorderRadius.circular(10.r) : null,
43 | boxShadow: [
44 | BoxShadow(
45 | color: Color(0x26C61B4A),
46 | offset: Offset(1.15.w, 1.15.h),
47 | blurRadius: 57.58.r,
48 | ),
49 | BoxShadow(
50 | color: Color(0x1AC61B4A),
51 | offset: Offset(2.3.w, 2.3.h),
52 | blurRadius: 11.52.r,
53 | ),
54 | BoxShadow(
55 | color: Color(0x0DC61B4A),
56 | offset: Offset(4.61.w, 4.61.h),
57 | blurRadius: 17.28.r,
58 | ),
59 | ],
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/lib/src/util/common_util.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_image_compress/flutter_image_compress.dart';
5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart';
6 | import 'package:mime_type/mime_type.dart';
7 | import 'package:path_provider/path_provider.dart';
8 | import 'package:sprintf/sprintf.dart';
9 |
10 | late String imCachePath;
11 |
12 | class CommonUtil {
13 | /// path: image path
14 | static Future createThumbnail({
15 | required String path,
16 | required double minWidth,
17 | required double minHeight,
18 | }) async {
19 | if (!(await File(path).exists())) {
20 | return null;
21 | }
22 | String thumbPath = await createTempPath(path, flag: 'im');
23 | File destFile = File(thumbPath);
24 | if (!(await destFile.exists())) {
25 | await destFile.create(recursive: true);
26 | } else {
27 | return thumbPath;
28 | }
29 | var compressFile = await compressImage(
30 | File(path),
31 | targetPath: thumbPath,
32 | minHeight: minHeight ~/ 1,
33 | minWidth: minWidth ~/ 1,
34 | );
35 | return compressFile?.path;
36 | }
37 |
38 | static Future createTempPath(
39 | String sourcePath, {
40 | String flag = "",
41 | String dir = 'pic',
42 | }) async {
43 | var path = (await getApplicationDocumentsDirectory()).path;
44 | var name =
45 | '${flag}_${sourcePath.substring(sourcePath.lastIndexOf('/') + 1)}';
46 | String dest = '$path/$dir/$name';
47 |
48 | return dest;
49 | }
50 |
51 | /// get Now Date Milliseconds.
52 | static int getNowDateMs() {
53 | return DateTime.now().millisecondsSinceEpoch;
54 | }
55 |
56 | /// compress file and get file.
57 | static Future compressImage(
58 | File? file, {
59 | required String targetPath,
60 | required int minWidth,
61 | required int minHeight,
62 | }) async {
63 | if (null == file) return null;
64 | var path = file.path;
65 | var name = path.substring(path.lastIndexOf("/"));
66 | // var ext = name.substring(name.lastIndexOf("."));
67 | CompressFormat format = CompressFormat.jpeg;
68 | if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
69 | format = CompressFormat.jpeg;
70 | } else if (name.endsWith(".png")) {
71 | format = CompressFormat.png;
72 | } else if (name.endsWith(".heic")) {
73 | format = CompressFormat.heic;
74 | } else if (name.endsWith(".webp")) {
75 | format = CompressFormat.webp;
76 | }
77 |
78 | var result = await FlutterImageCompress.compressAndGetFile(
79 | file.absolute.path,
80 | targetPath,
81 | quality: 70,
82 | inSampleSize: 2,
83 | minWidth: minWidth,
84 | minHeight: minHeight,
85 | format: format,
86 | );
87 | return result;
88 | }
89 |
90 | //fileExt 文件后缀名
91 | static String? getMediaType(final String filePath) {
92 | var fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
93 | var fileExt = fileName.substring(fileName.lastIndexOf("."));
94 | switch (fileExt.toLowerCase()) {
95 | case ".jpg":
96 | case ".jpeg":
97 | case ".jpe":
98 | return "image/jpeg";
99 | case ".png":
100 | return "image/png";
101 | case ".bmp":
102 | return "image/bmp";
103 | case ".gif":
104 | return "image/gif";
105 | case ".json":
106 | return "application/json";
107 | case ".svg":
108 | case ".svgz":
109 | return "image/svg+xml";
110 | case ".mp3":
111 | return "audio/mpeg";
112 | case ".mp4":
113 | return "video/mp4";
114 | case ".mov":
115 | return "video/mov";
116 | case ".htm":
117 | case ".html":
118 | return "text/html";
119 | case ".css":
120 | return "text/css";
121 | case ".csv":
122 | return "text/csv";
123 | case ".txt":
124 | case ".text":
125 | case ".conf":
126 | case ".def":
127 | case ".log":
128 | case ".in":
129 | return "text/plain";
130 | }
131 | return null;
132 | }
133 |
134 | /// 将字节数转化为MB
135 | static String formatBytes(int bytes) {
136 | int kb = 1024;
137 | int mb = kb * 1024;
138 | int gb = mb * 1024;
139 | if (bytes >= gb) {
140 | return sprintf("%.1f GB", [bytes / gb]);
141 | } else if (bytes >= mb) {
142 | double f = bytes / mb;
143 | return sprintf(f > 100 ? "%.0f MB" : "%.1f MB", [f]);
144 | } else if (bytes > kb) {
145 | double f = bytes / kb;
146 | return sprintf(f > 100 ? "%.0f KB" : "%.1f KB", [f]);
147 | } else {
148 | return sprintf("%d B", [bytes]);
149 | }
150 | }
151 |
152 | static IconData fileIcon(String fileName) {
153 | var mimeType = mime(fileName) ?? '';
154 | if (mimeType == 'application/pdf') {
155 | return FontAwesomeIcons.solidFilePdf;
156 | } else if (mimeType == 'application/msword' ||
157 | mimeType ==
158 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
159 | return FontAwesomeIcons.solidFileWord;
160 | } else if (mimeType == 'application/vnd.ms-excel' ||
161 | mimeType ==
162 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
163 | return FontAwesomeIcons.solidFileExcel;
164 | } else if (mimeType == 'application/vnd.ms-powerpoint') {
165 | return FontAwesomeIcons.solidFilePowerpoint;
166 | } else if (mimeType.startsWith('audio/')) {
167 | } else if (mimeType == 'application/zip' ||
168 | mimeType == 'application/x-rar-compressed') {
169 | return FontAwesomeIcons.solidFileZipper;
170 | } else if (mimeType.startsWith('audio/')) {
171 | return FontAwesomeIcons.solidFileAudio;
172 | } else if (mimeType.startsWith('video/')) {
173 | return FontAwesomeIcons.solidFileVideo;
174 | } else if (mimeType.startsWith('image/')) {
175 | return FontAwesomeIcons.solidFileImage;
176 | } else if (mimeType == 'text/plain') {
177 | return FontAwesomeIcons.solidFileCode;
178 | }
179 | return FontAwesomeIcons.solidFileLines;
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/lib/src/util/custom_ext.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:rxdart/rxdart.dart';
5 |
6 | extension SubjectExt on Subject {
7 | T addSafely(T data) {
8 | if (!isClosed) sink.add(data);
9 | // if (!isClosed) add(data);
10 | return data;
11 | }
12 | }
13 |
14 | /// 解决当输入框内容全为字母且长度超过63不能继续输入的bug
15 | ///
16 | extension TextEdCtrlExt on TextEditingController {
17 | void fixed63Length() {
18 | addListener(() {
19 | if (text.length == 63 && Platform.isAndroid) {
20 | text += " ";
21 | selection = TextSelection.fromPosition(
22 | TextPosition(
23 | affinity: TextAffinity.downstream,
24 | offset: text.length - 1,
25 | ),
26 | );
27 | }
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/src/util/permission_util.dart:
--------------------------------------------------------------------------------
1 | import 'package:permission_handler/permission_handler.dart';
2 |
3 | class PermissionUtil {
4 | PermissionUtil._();
5 |
6 | static void camera(Function() onGranted) async {
7 | if (await Permission.camera.request().isGranted) {
8 | // Either the permission was already granted before or the user just granted it.
9 | onGranted();
10 | }
11 | if (await Permission.camera.isPermanentlyDenied) {
12 | // The user opted to never again see the permission request dialog for this
13 | // app. The only way to change the permission's status now is to let the
14 | // user manually enable it in the system settings.
15 | }
16 | }
17 |
18 | static void storage(Function() onGranted) async {
19 | if (await Permission.storage.request().isGranted) {
20 | // Either the permission was already granted before or the user just granted it.
21 | onGranted();
22 | }
23 | if (await Permission.storage.isPermanentlyDenied) {
24 | // The user opted to never again see the permission request dialog for this
25 | // app. The only way to change the permission's status now is to let the
26 | // user manually enable it in the system settings.
27 | }
28 | }
29 |
30 | static void microphone(Function() onGranted) async {
31 | if (await Permission.microphone.request().isGranted) {
32 | // Either the permission was already granted before or the user just granted it.
33 | onGranted();
34 | }
35 | if (await Permission.microphone.isPermanentlyDenied) {
36 | // The user opted to never again see the permission request dialog for this
37 | // app. The only way to change the permission's status now is to let the
38 | // user manually enable it in the system settings.
39 | }
40 | }
41 |
42 | static void location(Function() onGranted) async {
43 | if (await Permission.location.request().isGranted) {
44 | // Either the permission was already granted before or the user just granted it.
45 | onGranted();
46 | }
47 | if (await Permission.location.isPermanentlyDenied) {
48 | // The user opted to never again see the permission request dialog for this
49 | // app. The only way to change the permission's status now is to let the
50 | // user manually enable it in the system settings.
51 | }
52 | }
53 |
54 | static void speech(Function() onGranted) async {
55 | if (await Permission.speech.request().isGranted) {
56 | // Either the permission was already granted before or the user just granted it.
57 | onGranted();
58 | }
59 | if (await Permission.speech.isPermanentlyDenied) {
60 | // The user opted to never again see the permission request dialog for this
61 | // app. The only way to change the permission's status now is to let the
62 | // user manually enable it in the system settings.
63 | }
64 | }
65 |
66 | static void photos(Function() onGranted) async {
67 | if (await Permission.photos.request().isGranted) {
68 | // Either the permission was already granted before or the user just granted it.
69 | onGranted();
70 | }
71 | if (await Permission.photos.isPermanentlyDenied) {
72 | // The user opted to never again see the permission request dialog for this
73 | // app. The only way to change the permission's status now is to let the
74 | // user manually enable it in the system settings.
75 | }
76 | }
77 |
78 | static Future