├── .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 | [![pub package](https://img.shields.io/pub/v/flutter_openim_widget.svg)](https://pub.flutter-io.cn/packages/flutter_openim_widget) 2 | [![Generic badge](https://img.shields.io/badge/platform-android%20|%20ios%20-blue.svg)](https://pub.dev/packages/flutter_openim_widget) 3 | [![GitHub license](https://img.shields.io/github/license/hrxiang/flutter_openim_widget)](https://github.com/hrxiang/flutter_openim_widget/blob/master/LICENSE) 4 | 5 | 6 | 1715854117862 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 | 3 | 表情 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | 3 | 键盘 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | 3 | 语音 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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> request( 79 | List permissions) async { 80 | // You can request multiple permissions at once. 81 | Map statuses = await permissions.request(); 82 | return statuses; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/util/ui_locallizations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class UILocalizations { 4 | UILocalizations._(); 5 | 6 | static void set(Locale? locale) { 7 | _locale = locale ?? const Locale('zh'); 8 | } 9 | 10 | static String _value({required String key}) => 11 | _localizedValues[_locale.languageCode]![key] ?? key; 12 | 13 | static Locale _locale = const Locale('zh'); 14 | 15 | static Map> _localizedValues = { 16 | 'en': { 17 | 'top': 'Stick to Top', 18 | 'cancelTop': 'Remove from Top', 19 | 'remove': 'Delete', 20 | 'markRead': 'Mark as Read', 21 | "album": "Album", 22 | "camera": "Camera", 23 | "videoCall": "Video Call", 24 | "picture": "Picture", 25 | "video": "Video", 26 | "voice": "Voice", 27 | "location": "Location", 28 | "file": "File", 29 | "carte": "Contact Card", 30 | "voiceInput": "Voice Input", 31 | 'haveRead': 'Have read', 32 | 'groupHaveRead': '%s people have read', 33 | 'unread': 'Unread', 34 | 'groupUnread': '%s unread', 35 | 'allRead': 'All read', 36 | 'copy': 'Copy', 37 | "delete": "Delete", 38 | "forward": "Forward", 39 | "reply": "Quote", 40 | "revoke": "Revoke", 41 | "multiChoice": "Choice", 42 | "translation": "Translate", 43 | "download": "Download", 44 | "pressSpeak": "Hold to Talk", 45 | "releaseSend": "Release to send", 46 | "releaseCancel": "Release to cancel", 47 | "soundToWord": "Convert", 48 | "converting": "Converting...", 49 | "cancelVoiceSend": "Cancel", 50 | "confirmVoiceSend": "Send Voice", 51 | "convertFailTips": "Unable to recognize words", 52 | "confirm": "Confirm", 53 | "you": "You", 54 | "revokeAMsg": "revoke a message", 55 | "picLoadError": "Image failed to load", 56 | "fileSize": "File size: %s", 57 | "fileUnavailable": "The file has expired or has been cleaned up", 58 | "send": 'Send', 59 | "unsupportedMessage": '[Message types not supported]', 60 | "add": 'Add', 61 | "youMuted": 'You have been muted', 62 | "groupMuted": 'Enable group mute', 63 | "inBlacklist": 'The other party has been blacklisted', 64 | "playSpeed": 'Play speed', 65 | "cancel": 'Cancel', 66 | "groupNotice": 'Group Notice', 67 | "groupOwnerOrAdminRevokeAMsg": "%s revoked %s' message", 68 | "recentlyUsed": "Recently Used", 69 | }, 70 | 'zh': { 71 | 'top': '置顶', 72 | 'cancelTop': '取消置顶', 73 | 'remove': '移除', 74 | 'markRead': '标记为已读', 75 | "album": "相册", 76 | "camera": "拍摄", 77 | "videoCall": "视频通话", 78 | "picture": "图片", 79 | "video": "视频", 80 | "voice": "语音", 81 | "location": "位置", 82 | "file": "文件", 83 | "carte": "名片", 84 | "voiceInput": "语音输入", 85 | 'haveRead': '已读', 86 | 'groupHaveRead': '%s人已读', 87 | 'unread': '未读', 88 | 'groupUnread': '%s人未读', 89 | 'allRead': '全部已读', 90 | 'copy': '复制', 91 | "delete": "删除", 92 | "forward": "转发", 93 | "reply": "回复", 94 | "revoke": "撤回", 95 | "multiChoice": "多选", 96 | "translation": "翻译", 97 | "download": "下载", 98 | "pressSpeak": "按住说话", 99 | "releaseSend": "松开发送", 100 | "releaseCancel": "松开取消", 101 | "soundToWord": "转文字", 102 | "converting": "转换中...", 103 | "cancelVoiceSend": "取消", 104 | "confirmVoiceSend": "发送原语音", 105 | "convertFailTips": "未识别到文字", 106 | "confirm": "确定", 107 | "you": "你", 108 | "revokeAMsg": "撤回了一条消息", 109 | "picLoadError": "图片加载失败", 110 | "fileSize": "文件大小:%s", 111 | "fileUnavailable": "文件已过期或已被清理", 112 | "send": '发送', 113 | "unsupportedMessage": '[暂不支持的消息类型]', 114 | "add": '添加', 115 | "youMuted": '你已被禁言', 116 | "groupMuted": '已开启群禁言', 117 | "inBlacklist": '对方已被拉入黑名单', 118 | "playSpeed": '播放速度', 119 | "cancel": '取消', 120 | "groupNotice": '群公告', 121 | "groupOwnerOrAdminRevokeAMsg": "%s 撤回了 %s 的消息", 122 | "recentlyUsed": "最近使用", 123 | }, 124 | }; 125 | 126 | static String get top => _value(key: 'top'); 127 | 128 | static String get cancelTop => _value(key: 'cancelTop'); 129 | 130 | static String get remove => _value(key: 'remove'); 131 | 132 | static String get markRead => _value(key: 'markRead'); 133 | 134 | static String get album => _value(key: 'album'); 135 | 136 | static String get camera => _value(key: 'camera'); 137 | 138 | static String get videoCall => _value(key: 'videoCall'); 139 | 140 | static String get picture => _value(key: 'picture'); 141 | 142 | static String get video => _value(key: 'video'); 143 | 144 | static String get voice => _value(key: 'voice'); 145 | 146 | static String get location => _value(key: 'location'); 147 | 148 | static String get file => _value(key: 'file'); 149 | 150 | static String get carte => _value(key: 'carte'); 151 | 152 | static String get voiceInput => _value(key: 'voiceInput'); 153 | 154 | static String get haveRead => _value(key: 'haveRead'); 155 | 156 | static String get unread => _value(key: 'unread'); 157 | 158 | static String get groupHaveRead => _value(key: 'groupHaveRead'); 159 | 160 | static String get groupUnread => _value(key: 'groupUnread'); 161 | 162 | static String get allRead => _value(key: 'allRead'); 163 | 164 | static String get copy => _value(key: 'copy'); 165 | 166 | static String get delete => _value(key: 'delete'); 167 | 168 | static String get forward => _value(key: 'forward'); 169 | 170 | static String get reply => _value(key: 'reply'); 171 | 172 | static String get revoke => _value(key: 'revoke'); 173 | 174 | static String get multiChoice => _value(key: 'multiChoice'); 175 | 176 | static String get translation => _value(key: 'translation'); 177 | 178 | static String get download => _value(key: 'download'); 179 | 180 | static String get pressSpeak => _value(key: 'pressSpeak'); 181 | 182 | static String get releaseSend => _value(key: 'releaseSend'); 183 | 184 | static String get releaseCancel => _value(key: 'releaseCancel'); 185 | 186 | static String get soundToWord => _value(key: 'soundToWord'); 187 | 188 | static String get converting => _value(key: 'converting'); 189 | 190 | static String get cancelVoiceSend => _value(key: 'cancelVoiceSend'); 191 | 192 | static String get confirmVoiceSend => _value(key: 'confirmVoiceSend'); 193 | 194 | static String get convertFailTips => _value(key: 'convertFailTips'); 195 | 196 | static String get confirm => _value(key: 'confirm'); 197 | 198 | static String get you => _value(key: 'you'); 199 | 200 | static String get revokeAMsg => _value(key: 'revokeAMsg'); 201 | 202 | static String get picLoadError => _value(key: 'picLoadError'); 203 | 204 | static String get fileSize => _value(key: 'fileSize'); 205 | 206 | static String get fileUnavailable => _value(key: 'fileUnavailable'); 207 | 208 | static String get acceptFriendHint => _value(key: 'acceptFriendHint'); 209 | 210 | static String get addFriendHint => _value(key: 'addFriendHint'); 211 | 212 | static String get send => _value(key: 'send'); 213 | 214 | static String get unsupportedMessage => _value(key: 'unsupportedMessage'); 215 | 216 | static String get youMuted => _value(key: 'youMuted'); 217 | 218 | static String get groupMuted => _value(key: 'groupMuted'); 219 | 220 | static String get add => _value(key: 'add'); 221 | 222 | static String get inBlacklist => _value(key: 'inBlacklist'); 223 | 224 | static String get playSpeed => _value(key: 'playSpeed'); 225 | 226 | static String get cancel => _value(key: 'cancel'); 227 | 228 | static String get groupNotice => _value(key: 'groupNotice'); 229 | 230 | static String get groupOwnerOrAdminRevokeAMsg => 231 | _value(key: 'groupOwnerOrAdminRevokeAMsg'); 232 | 233 | static String get recentlyUsed => _value(key: 'recentlyUsed'); 234 | } 235 | -------------------------------------------------------------------------------- /lib/src/util/voice_record.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:record/record.dart'; 6 | 7 | typedef RecordFc = Function(int sec, String path); 8 | 9 | class VoiceRecord { 10 | static const _dir = "voice"; 11 | static const _ext = ".m4a"; 12 | late String _path; 13 | int _startTimestamp = 0; 14 | late int _tag; 15 | final RecordFc onFinished; 16 | final RecordFc onInterrupt; 17 | final int maxRecordSec; 18 | final _audioRecorder = Record(); 19 | Timer? _timer; 20 | 21 | VoiceRecord({ 22 | required this.maxRecordSec, 23 | required this.onInterrupt, 24 | required this.onFinished, 25 | }) : _tag = _now(); 26 | 27 | start() async { 28 | if (await _audioRecorder.hasPermission()) { 29 | var path = (await getApplicationDocumentsDirectory()).path; 30 | _path = '$path/$_dir/$_tag$_ext'; 31 | File file = File(_path); 32 | if (!(await file.exists())) { 33 | await file.create(recursive: true); 34 | } 35 | await _audioRecorder.start(path: _path); 36 | _startTimestamp = _now(); 37 | _timer?.cancel(); 38 | _timer = null; 39 | _timer = Timer.periodic(Duration(seconds: 1), (timer) async { 40 | // _long = (_now() - _long) ~/ 1000; 41 | if (((_now() - _startTimestamp) ~/ 1000) >= maxRecordSec) { 42 | await stop(isInterrupt: true); 43 | onInterrupt(maxRecordSec, _path); 44 | } 45 | }); 46 | } 47 | } 48 | 49 | stop({bool isInterrupt = false}) async { 50 | _timer?.cancel(); 51 | _timer = null; 52 | if (await _audioRecorder.isRecording()) { 53 | await _audioRecorder.stop(); 54 | if (isInterrupt) return; 55 | onFinished((_now() - _startTimestamp) ~/ 1000, _path); 56 | } 57 | } 58 | 59 | static int _now() => DateTime.now().millisecondsSinceEpoch; 60 | } 61 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_openim_widget 2 | description: The public ui library is used with the openim demo, and you can directly use it for secondary development. 3 | version: 2.3.2 4 | homepage: https://github.com/hrxiang/flutter_openim_widget.git 5 | repository: https://github.com/hrxiang/flutter_openim_widget.git 6 | 7 | environment: 8 | sdk: ">=2.17.0 <3.0.0" 9 | flutter: ">=1.20.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | font_awesome_flutter: ^10.2.1 15 | rxdart: ^0.27.5 16 | flutter_screenutil: ^5.5.4 17 | flutter_slidable: ^2.0.0 18 | cached_network_image: ^3.2.2 19 | photo_view: ^0.14.0 20 | path_provider: ^2.0.11 21 | flutter_image_compress: ^1.1.3 22 | record: ^4.4.1 23 | just_audio: ^0.9.29 24 | lottie: ^1.4.3 25 | permission_handler: ^10.1.0 26 | extended_text_field: ^11.0.0 27 | extended_text: ^10.0.0 28 | # extended_image: ^7.0.2 29 | # extended_image: ^6.3.1 30 | # focus_detector: ^2.0.1 31 | visibility_detector: ^0.3.3 32 | sprintf: ^7.0.0 33 | flutter_inappwebview: ^5.4.4+3 34 | animate_do: ^2.1.0 35 | flutter_svg: ^1.1.5 36 | qq_badge: ^0.0.2 37 | # better_player: ^0.0.81 38 | chewie: ^1.3.5 39 | video_player: ^2.4.7 40 | flutter_cache_manager: ^3.3.0 41 | mime_type: ^1.0.0 42 | dio: ^4.0.6 43 | flutter_keyboard_visibility: ^5.3.0 44 | emoji_picker_flutter: ^1.4.0 45 | should_rebuild: ^1.0.1 46 | # flutter_openim_sdk: 2.3.5+2 47 | flutter_openim_sdk: 48 | # path: ../flutter_openim_sdk 49 | git: 50 | url: https://github.com/OpenIMSDK/Open-IM-SDK-Flutter.git 51 | ref: 2.3.5+3 52 | 53 | 54 | 55 | dev_dependencies: 56 | flutter_test: 57 | sdk: flutter 58 | 59 | #dependency_overrides: 60 | # video_player: 61 | # git: 62 | # url: https://github.com/999eagle/plugins.git 63 | # ref: feature/caching 64 | # path: packages/video_player 65 | # For information on the generic Dart part of this file, see the 66 | # following page: https://dart.dev/tools/pub/pubspec 67 | 68 | # The following section is specific to Flutter. 69 | flutter: 70 | 71 | # To add assets to your package, add an assets section, like this: 72 | assets: 73 | - assets/images/ 74 | - assets/anim/ 75 | 76 | # - images/a_dot_burr.jpeg 77 | # - images/a_dot_ham.jpeg 78 | # 79 | # For details regarding assets in packages, see 80 | # https://flutter.dev/assets-and-images/#from-packages 81 | # 82 | # An image asset can refer to one or more resolution-specific "variants", see 83 | # https://flutter.dev/assets-and-images/#resolution-aware. 84 | 85 | # To add custom fonts to your package, add a fonts section here, 86 | # in this "flutter" section. Each entry in this list should have a 87 | # "family" key with the font family name, and a "fonts" key with a 88 | # list giving the asset and other descriptors for the font. For 89 | # example: 90 | # fonts: 91 | # - family: Schyler 92 | # fonts: 93 | # - asset: fonts/Schyler-Regular.ttf 94 | # - asset: fonts/Schyler-Italic.ttf 95 | # style: italic 96 | # - family: Trajan Pro 97 | # fonts: 98 | # - asset: fonts/TrajanPro.ttf 99 | # - asset: fonts/TrajanPro_Bold.ttf 100 | # weight: 700 101 | # 102 | # For details regarding fonts in packages, see 103 | # https://flutter.dev/custom-fonts/#from-packages 104 | 105 | 106 | # flutter packages pub publish --dry-run 107 | # flutter packages pub publish 108 | # flutter packages pub publish --server=https://pub.dartlang.org --------------------------------------------------------------------------------